feat(t223): generate tasks from freight items

This commit is contained in:
QiuSW
2026-07-28 23:51:59 +08:00
parent 6d052b0462
commit 433db45100
25 changed files with 2697 additions and 48 deletions
@@ -395,6 +395,199 @@ func TestAdminFreightAPIImportsAllItemsWithoutPII(t *testing.T) {
}
}
func TestAdminProcurementAPIProducesImmutablePendingTask(t *testing.T) {
fixture := newAdminIntegrationFixture(t)
createSync := performAdminRequest(
t,
fixture.router,
http.MethodPost,
"/api/v1/freight-syncs",
"application/json",
strings.NewReader(
`{"mode":"ORDER_NUMBER","order_number":"SOURCE-12"}`,
),
"procurement-freight-sync",
)
var syncBody struct {
Sync struct {
ID string `json:"id"`
} `json:"sync"`
}
decodeResponse(t, createSync, &syncBody)
succeeded := false
for attempt := 0; attempt < 50; attempt++ {
status := performAdminRequest(
t,
fixture.router,
http.MethodGet,
"/api/v1/freight-syncs/"+syncBody.Sync.ID,
"",
nil,
"",
)
if strings.Contains(status.Body.String(), `"status":"SUCCEEDED"`) {
succeeded = true
break
}
time.Sleep(10 * time.Millisecond)
}
if !succeeded {
t.Fatal("freight sync did not succeed")
}
orders := performAdminRequest(
t,
fixture.router,
http.MethodGet,
"/api/v1/freight-orders",
"",
nil,
"",
)
var orderBody struct {
Items []struct {
ID string `json:"id"`
} `json:"items"`
}
decodeResponse(t, orders, &orderBody)
detail := performAdminRequest(
t,
fixture.router,
http.MethodGet,
"/api/v1/freight-orders/"+orderBody.Items[0].ID,
"",
nil,
"",
)
var detailBody struct {
Items []struct {
ID string `json:"id"`
} `json:"items"`
}
decodeResponse(t, detail, &detailBody)
createRequest := performAdminRequest(
t,
fixture.router,
http.MethodPost,
"/api/v1/freight-items/"+detailBody.Items[0].ID+
"/procurement-request",
"application/json",
strings.NewReader(`{"confirm_procurement_needed":true}`),
"",
)
if createRequest.Code != http.StatusCreated ||
!strings.Contains(createRequest.Body.String(), `"status":"NEEDS_IMAGE"`) {
t.Fatalf(
"create request status/body = %d / %s",
createRequest.Code,
createRequest.Body,
)
}
var requestBody struct {
Request struct {
ID string `json:"id"`
} `json:"procurement_request"`
}
decodeResponse(t, createRequest, &requestBody)
imageBody, imageContentType := referenceUpload(t, "procurement-image")
upload := performAdminRequest(
t,
fixture.router,
http.MethodPost,
"/api/v1/assets",
imageContentType,
imageBody,
"procurement-image",
)
var assetBody struct {
ID string `json:"id"`
}
decodeResponse(t, upload, &assetBody)
bind := performAdminRequest(
t,
fixture.router,
http.MethodPut,
"/api/v1/procurement-requests/"+requestBody.Request.ID+
"/reference-asset",
"application/json",
strings.NewReader(
fmt.Sprintf(`{"image_asset_id":%q}`, assetBody.ID),
),
"",
)
if bind.Code != http.StatusOK ||
!strings.Contains(bind.Body.String(), `"status":"READY"`) {
t.Fatalf("bind status/body = %d / %s", bind.Code, bind.Body)
}
createTask := performAdminRequest(
t,
fixture.router,
http.MethodPost,
"/api/v1/procurement-requests/"+requestBody.Request.ID+
"/purchase-task",
"application/json",
strings.NewReader(`{}`),
"procurement-task-create",
)
if createTask.Code != http.StatusCreated ||
!strings.Contains(createTask.Body.String(), `"status":"PENDING"`) {
t.Fatalf(
"create task status/body = %d / %s",
createTask.Code,
createTask.Body,
)
}
var taskBody struct {
Task struct {
ID string `json:"id"`
} `json:"task"`
}
decodeResponse(t, createTask, &taskBody)
taskDetail := performAdminRequest(
t,
fixture.router,
http.MethodGet,
"/api/v1/tasks/"+taskBody.Task.ID,
"",
nil,
"",
)
for _, required := range []string{
`"title":"商品一"`,
`"sku":"BLACK-L"`,
`"quantity":1`,
`"image_asset_id":"` + assetBody.ID + `"`,
`"description":"ERP 规格:黑色,L"`,
} {
if !strings.Contains(taskDetail.Body.String(), required) {
t.Fatalf("task detail missing %q: %s", required, taskDetail.Body)
}
}
replay := performAdminRequest(
t,
fixture.router,
http.MethodPost,
"/api/v1/procurement-requests/"+requestBody.Request.ID+
"/purchase-task",
"application/json",
strings.NewReader(`{}`),
"procurement-task-replay",
)
if replay.Code != http.StatusCreated ||
!strings.Contains(replay.Body.String(), `"replayed":true`) ||
!strings.Contains(replay.Body.String(), taskBody.Task.ID) {
t.Fatalf("task replay status/body = %d / %s", replay.Code, replay.Body)
}
var sourceCount int
if err := fixture.db.QueryRow(
`SELECT count(*) FROM purchase_task_sources
WHERE task_id = ? AND procurement_request_id = ?`,
taskBody.Task.ID,
requestBody.Request.ID,
).Scan(&sourceCount); err != nil || sourceCount != 1 {
t.Fatalf("task source count = %d, error = %v", sourceCount, err)
}
}
func TestAdminOrderAuthorizationIsIdempotentAndRevisioned(t *testing.T) {
fixture := newAdminIntegrationFixture(t)
taskID, executionID, taskHash, firstKey, secondKey :=
@@ -515,6 +708,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("procurement migration down: %v", err)
}
if err := runner.Down(context.Background()); err != nil {
t.Fatalf("freight migration down: %v", err)
}
@@ -852,6 +1048,14 @@ func newAdminIntegrationFixture(t *testing.T) *adminIntegrationFixture {
if err != nil {
t.Fatalf("usecase.NewFreightService() error = %v", err)
}
procurement, err := usecase.NewProcurementService(
repositories,
clock,
ids,
)
if err != nil {
t.Fatalf("usecase.NewProcurementService() error = %v", err)
}
registrar, err := NewAdminRouteRegistrar(
AdminServices{
Assets: assets,
@@ -859,6 +1063,7 @@ func newAdminIntegrationFixture(t *testing.T) *adminIntegrationFixture {
Results: results,
Authorizations: authorizations,
Freight: freight,
Procurement: procurement,
},
emptyAdminWeb{},
)