feat(t245): use ERP images as procurement references

This commit is contained in:
QiuSW
2026-07-29 17:29:20 +08:00
parent a20590b4b7
commit 15074692a3
22 changed files with 1057 additions and 117 deletions
@@ -766,7 +766,11 @@ func TestAdminProcurementAPIProducesImmutablePendingTask(t *testing.T) {
"",
)
if createRequest.Code != http.StatusCreated ||
!strings.Contains(createRequest.Body.String(), `"status":"NEEDS_IMAGE"`) {
!strings.Contains(createRequest.Body.String(), `"status":"READY"`) ||
!strings.Contains(
createRequest.Body.String(),
`"reference_origin":"ERP_FREIGHT_IMAGE"`,
) {
t.Fatalf(
"create request status/body = %d / %s",
createRequest.Code,
@@ -806,7 +810,11 @@ func TestAdminProcurementAPIProducesImmutablePendingTask(t *testing.T) {
"",
)
if bind.Code != http.StatusOK ||
!strings.Contains(bind.Body.String(), `"status":"READY"`) {
!strings.Contains(bind.Body.String(), `"status":"READY"`) ||
!strings.Contains(
bind.Body.String(),
`"reference_origin":"MANUAL_UPLOAD"`,
) {
t.Fatalf("bind status/body = %d / %s", bind.Code, bind.Body)
}
deleteWithReference := performAdminRequest(
@@ -920,7 +928,7 @@ func TestAdminProcurementAPIProducesImmutablePendingTask(t *testing.T) {
}
func TestAdminFreightDeleteRemovesNeedsImageDraft(t *testing.T) {
fixture := newAdminIntegrationFixture(t)
fixture := newAdminIntegrationFixtureWithReferences(t, false)
createSync := performAdminRequest(
t,
fixture.router,
@@ -1123,6 +1131,9 @@ func TestAdminOrderAuthorizationIsIdempotentAndRevisioned(t *testing.T) {
if err != nil {
t.Fatalf("migration.New() error = %v", err)
}
if err := runner.Down(context.Background()); err != nil {
t.Fatalf("reference source migration down: %v", err)
}
if err := runner.Down(context.Background()); err != nil {
t.Fatalf("image migration down: %v", err)
}
@@ -1406,6 +1417,13 @@ type adminIntegrationFixture struct {
}
func newAdminIntegrationFixture(t *testing.T) *adminIntegrationFixture {
return newAdminIntegrationFixtureWithReferences(t, true)
}
func newAdminIntegrationFixtureWithReferences(
t *testing.T,
autoReferences bool,
) *adminIntegrationFixture {
t.Helper()
ctx := context.Background()
db, err := database.Open(ctx, filepath.Join(t.TempDir(), "admin.db"))
@@ -1482,10 +1500,18 @@ func newAdminIntegrationFixture(t *testing.T) *adminIntegrationFixture {
if err != nil {
t.Fatalf("usecase.NewFreightService() error = %v", err)
}
procurementOptions := make([]usecase.ProcurementServiceOption, 0, 1)
if autoReferences {
procurementOptions = append(
procurementOptions,
usecase.WithProcurementReferenceStore(files),
)
}
procurement, err := usecase.NewProcurementService(
repositories,
clock,
ids,
procurementOptions...,
)
if err != nil {
t.Fatalf("usecase.NewProcurementService() error = %v", err)
@@ -713,6 +713,9 @@ func TestDeviceExecutionResultsAreIdempotentAndAuditable(t *testing.T) {
if err != nil {
t.Fatalf("migration.New() after review error = %v", err)
}
if err := runner.Down(context.Background()); err != nil {
t.Fatalf("reference source migration down: %v", err)
}
if err := runner.Down(context.Background()); err != nil {
t.Fatalf("image migration down: %v", err)
}
@@ -742,8 +745,8 @@ func TestDeviceExecutionResultsAreIdempotentAndAuditable(t *testing.T) {
}
if applied, err := runner.Up(context.Background()); err != nil {
t.Fatalf("restore device command migration: %v", err)
} else if applied != 8 {
t.Fatalf("restored migrations = %d, want 8", applied)
} else if applied != 9 {
t.Fatalf("restored migrations = %d, want 9", applied)
}
completePayload := fmt.Sprintf(
@@ -1432,6 +1435,9 @@ func TestDeviceOrderCommandDeliveryAndAcknowledgementAreRecoverable(
if err != nil {
t.Fatalf("migration.New() error = %v", err)
}
if err := runner.Down(context.Background()); err != nil {
t.Fatalf("reference source migration down: %v", err)
}
if err := runner.Down(context.Background()); err != nil {
t.Fatalf("image migration down: %v", err)
}
@@ -160,6 +160,7 @@ func procurementRequestResponse(
"status": request.Status,
"blocking_code": request.BlockingCode,
"reference_asset_id": request.ReferenceAssetID,
"reference_origin": request.ReferenceOrigin,
"purchase_task_id": request.PurchaseTaskID,
"source_changed": request.SourceChanged,
"created_at": formatTime(request.CreatedAt),
@@ -1239,10 +1239,12 @@ func TestFreightDetailCreatesProcurementTaskWithCSRF(t *testing.T) {
ImageURL: "/api/v1/freight-items/" + itemID + "/image",
},
Request: &ProcurementRequest{
ID: itemID,
FreightOrderItemID: itemID,
Status: "READY",
StatusLabel: "可以生成任务",
ID: itemID,
FreightOrderItemID: itemID,
Status: "READY",
StatusLabel: "可以生成任务",
ReferenceOrigin: "ERP_FREIGHT_IMAGE",
ReferenceOriginLabel: "参考图来自 ERP 商品图",
},
}},
},
@@ -1263,6 +1265,8 @@ func TestFreightDetailCreatesProcurementTaskWithCSRF(t *testing.T) {
)
if detail.Code != http.StatusOK ||
!strings.Contains(detail.Body.String(), "生成采购任务") ||
!strings.Contains(detail.Body.String(), "更换参考图") ||
!strings.Contains(detail.Body.String(), "参考图来自 ERP 商品图") ||
!strings.Contains(detail.Body.String(), "可以生成任务") ||
!strings.Contains(detail.Body.String(), "TWD 129.50") ||
!strings.Contains(detail.Body.String(), `width="84" height="84"`) ||
@@ -1273,6 +1277,13 @@ func TestFreightDetailCreatesProcurementTaskWithCSRF(t *testing.T) {
}
cookie := csrfCookie(t, detail)
taskKey := hiddenValue(t, detail.Body.String(), "task_key")
if uploadKey := hiddenValue(
t,
detail.Body.String(),
"upload_key",
); !validToken(uploadKey) {
t.Fatalf("upload key = %q", uploadKey)
}
values := url.Values{
"csrf_token": {cookie.Value},
"order_id": {testTaskID},
@@ -103,6 +103,7 @@
<button class="button" type="submit">上传参考图</button>
</form>
{{else if eq .Request.Status "READY"}}
{{if .Request.ReferenceOriginLabel}}<span class="secondary">{{.Request.ReferenceOriginLabel}}</span>{{end}}
<form method="post"
action="/freight/procurement-requests/{{pathPart .Request.ID}}/purchase-task">
<input type="hidden" name="csrf_token" value="{{$.Page.CSRFToken}}">
@@ -110,6 +111,17 @@
<input type="hidden" name="task_key" value="{{.Request.TaskKey}}">
<button class="button primary" type="submit">生成采购任务</button>
</form>
<form method="post" enctype="multipart/form-data"
action="/freight/procurement-requests/{{pathPart .Request.ID}}/reference">
<input type="hidden" name="csrf_token" value="{{$.Page.CSRFToken}}">
<input type="hidden" name="order_id" value="{{$.Detail.Order.ID}}">
<input type="hidden" name="upload_key" value="{{.Request.UploadKey}}">
<label>
<span class="visually-hidden">更换参考图片</span>
<input name="image" type="file" accept="image/jpeg,image/png,image/webp" required>
</label>
<button class="button" type="submit">更换参考图</button>
</form>
{{else if .Request.PurchaseTaskID}}
<a class="detail-link" href="/tasks/{{pathPart .Request.PurchaseTaskID}}">查看采购任务</a>
{{end}}
+14 -12
View File
@@ -192,18 +192,20 @@ type FreightItemReview struct {
}
type ProcurementRequest struct {
ID string
FreightOrderItemID string
SourceRevision int
Status string
StatusLabel string
BlockingCode string
BlockingLabel string
ReferenceAssetID string
PurchaseTaskID string
SourceChanged bool
UploadKey string
TaskKey string
ID string
FreightOrderItemID string
SourceRevision int
Status string
StatusLabel string
BlockingCode string
BlockingLabel string
ReferenceAssetID string
ReferenceOrigin string
ReferenceOriginLabel string
PurchaseTaskID string
SourceChanged bool
UploadKey string
TaskKey string
}
type ListTasksInput struct {
@@ -292,9 +292,11 @@ func procurementRequestFrom(
BlockingLabel: procurementBlockingLabel(
stringValue(request.BlockingCode),
),
ReferenceAssetID: stringValue(request.ReferenceAssetID),
PurchaseTaskID: stringValue(request.PurchaseTaskID),
SourceChanged: request.SourceChanged,
ReferenceAssetID: stringValue(request.ReferenceAssetID),
ReferenceOrigin: procurementReferenceOrigin(request),
ReferenceOriginLabel: procurementReferenceOriginLabel(request),
PurchaseTaskID: stringValue(request.PurchaseTaskID),
SourceChanged: request.SourceChanged,
}
if request.SourceChanged {
result.StatusLabel = "来源已变化"
@@ -302,6 +304,28 @@ func procurementRequestFrom(
return result
}
func procurementReferenceOrigin(
request domain.ProcurementRequest,
) string {
if request.ReferenceOrigin == nil {
return ""
}
return string(*request.ReferenceOrigin)
}
func procurementReferenceOriginLabel(
request domain.ProcurementRequest,
) string {
switch procurementReferenceOrigin(request) {
case string(domain.ProcurementReferenceERP):
return "参考图来自 ERP 商品图"
case string(domain.ProcurementReferenceManual):
return "参考图由人工上传"
default:
return ""
}
}
func procurementStatusLabel(
status domain.ProcurementRequestStatus,
) string {