feat(t219): show pending-payment reconciliation

This commit is contained in:
QiuSW
2026-07-28 18:16:09 +08:00
parent c2ec98d5be
commit 78dc595815
20 changed files with 768 additions and 37 deletions
@@ -380,6 +380,9 @@ func (h *adminHandlers) taskDetail(ctx *gin.Context) {
"order_authorizations": orderAuthorizationResponses(
detail.OrderAuthorizations,
),
"order_submissions": adminOrderSubmissionResponses(
detail.OrderSubmissions,
),
"events": events,
"assets": []gin.H{
assetResponse(detail.Asset),
@@ -387,6 +390,50 @@ func (h *adminHandlers) taskDetail(ctx *gin.Context) {
})
}
func adminOrderSubmissionResponses(
submissions []domain.OrderSubmission,
) []gin.H {
result := make([]gin.H, 0, len(submissions))
for _, submission := range submissions {
result = append(result, adminOrderSubmissionResponse(submission))
}
return result
}
func adminOrderSubmissionResponse(
submission domain.OrderSubmission,
) gin.H {
return gin.H{
"id": submission.ID,
"authorization_id": submission.AuthorizationID,
"dry_run_id": submission.DryRunID,
"execution_id": submission.ExecutionID,
"status": submission.Status,
"expected_title": submission.ExpectedTitle,
"expected_sku": submission.ExpectedSKU,
"expected_quantity": submission.ExpectedQuantity,
"expected_unit_price_cents": submission.ExpectedUnitPriceCents,
"expected_total_price_cents": submission.ExpectedTotalPriceCents,
"platform_order_no": submission.PlatformOrderNo,
"platform_ordered_at": formatOptionalTime(
submission.PlatformOrderedAt,
),
"platform_order_status": submission.PlatformOrderStatus,
"reconciliation_evidence_asset_id": submission.
ReconciliationEvidenceAssetID,
"reconciliation_evidence_sha256": submission.
ReconciliationEvidenceSHA256,
"manual_reason_code": submission.ManualReasonCode,
"fenced_at": formatTime(submission.FencedAt),
"reconciled_at": formatOptionalTime(
submission.ReconciledAt,
),
"manual_review_at": formatOptionalTime(
submission.ManualReviewAt,
),
}
}
func (h *adminHandlers) createOrderAuthorization(ctx *gin.Context) {
var request struct {
ExecutionID string `json:"execution_id"`
@@ -197,9 +197,13 @@ func TestAdminAPIAssetAndTaskLifecycle(t *testing.T) {
var detail map[string]any
decodeResponse(t, detailResponse, &detail)
requirement, _ := detail["original_requirement"].(map[string]any)
orderSubmissions, ok := detail["order_submissions"].([]any)
if detailResponse.Code != http.StatusOK ||
requirement["sku"] != "BLACK-20L" ||
requirement["quantity"] != float64(2) {
requirement["quantity"] != float64(2) ||
!ok ||
len(orderSubmissions) != 0 ||
detailResponse.Header().Get("Cache-Control") != "no-store" {
t.Fatalf(
"task detail status/body = %d / %#v",
detailResponse.Code,
@@ -1314,6 +1314,71 @@ func TestDeviceOrderCommandDeliveryAndAcknowledgementAreRecoverable(
authorizationStatus,
)
}
adminDetail := performAdminRequest(
t,
fixture.adminRouter,
http.MethodGet,
"/api/v1/tasks/"+taskID,
"",
nil,
"",
)
requireAdminStatus(t, adminDetail, http.StatusOK)
if adminDetail.Header().Get("Cache-Control") != "no-store" {
t.Fatalf(
"admin detail cache control = %q",
adminDetail.Header().Get("Cache-Control"),
)
}
var adminDetailBody struct {
OrderSubmissions []struct {
ID string `json:"id"`
AuthorizationID string `json:"authorization_id"`
Status string `json:"status"`
ExpectedSKU string `json:"expected_sku"`
ExpectedQuantity int `json:"expected_quantity"`
ExpectedTotalCents int64 `json:"expected_total_price_cents"`
PlatformOrderNo string `json:"platform_order_no"`
PlatformOrderedAt string `json:"platform_ordered_at"`
PlatformOrderStatus string `json:"platform_order_status"`
EvidenceAssetID string `json:"reconciliation_evidence_asset_id"`
} `json:"order_submissions"`
}
decodeResponse(t, adminDetail, &adminDetailBody)
if len(adminDetailBody.OrderSubmissions) != 1 {
t.Fatalf(
"admin order submissions = %+v",
adminDetailBody.OrderSubmissions,
)
}
adminSubmission := adminDetailBody.OrderSubmissions[0]
if adminSubmission.ID != submissionResponse.Submission.ID ||
adminSubmission.AuthorizationID != command.ID ||
adminSubmission.Status != "RECONCILED" ||
adminSubmission.ExpectedSKU != command.OriginalSKU ||
adminSubmission.ExpectedQuantity != 2 ||
adminSubmission.ExpectedTotalCents != 4300 ||
adminSubmission.PlatformOrderNo != "12345678901234567890" ||
adminSubmission.PlatformOrderedAt == "" ||
adminSubmission.PlatformOrderStatus != "PENDING_PAYMENT" ||
adminSubmission.EvidenceAssetID != reconciliationEvidenceID {
t.Fatalf("admin submission = %+v", adminSubmission)
}
detailAfterReconcile, err := fixture.tasks.Get(
context.Background(),
localAdminSubject,
taskID,
)
if err != nil ||
len(detailAfterReconcile.OrderSubmissions) != 1 ||
detailAfterReconcile.OrderSubmissions[0].ID !=
submissionResponse.Submission.ID {
t.Fatalf(
"task detail submissions = %+v, error = %v",
detailAfterReconcile.OrderSubmissions,
err,
)
}
var deliveredEvents, acknowledgedEvents, dryRunStartedEvents,
dryRunReadyEvents, fencedEvents, manualEvents, reconciledEvents int
for eventType, target := range map[string]*int{
@@ -1617,11 +1682,12 @@ func TestDeviceCancelAcknowledgementFollowsAdminStopRequest(
}
type deviceHTTPFixture struct {
db *sql.DB
store *repository.Store
assets *usecase.AssetService
tasks *usecase.TaskService
router http.Handler
db *sql.DB
store *repository.Store
assets *usecase.AssetService
tasks *usecase.TaskService
router http.Handler
adminRouter http.Handler
taskSequence int
}
@@ -1678,6 +1744,14 @@ func newDeviceHTTPFixture(t *testing.T) *deviceHTTPFixture {
if err != nil {
t.Fatalf("usecase.NewExecutionResultService() error = %v", err)
}
authorizations, err := usecase.NewOrderAuthorizationService(
store,
clock,
ids,
)
if err != nil {
t.Fatalf("usecase.NewOrderAuthorizationService() error = %v", err)
}
commands, err := usecase.NewDeviceOrderCommandService(store, clock, ids)
if err != nil {
t.Fatalf("usecase.NewDeviceOrderCommandService() error = %v", err)
@@ -1716,12 +1790,37 @@ func newDeviceHTTPFixture(t *testing.T) *deviceHTTPFixture {
if err != nil {
t.Fatalf("NewRouter() error = %v", err)
}
adminRoutes, err := NewAdminRouteRegistrar(
AdminServices{
Assets: assets,
Tasks: tasks,
Results: results,
Authorizations: authorizations,
},
emptyAdminWeb{},
)
if err != nil {
t.Fatalf("NewAdminRouteRegistrar() error = %v", err)
}
adminRouter, err := NewRouter(RouterDependencies{
Database: db,
RegisterPublicRoutes: discardRoutes,
RegisterAdminRoutes: adminRoutes,
RegisterDeviceRoutes: discardRoutes,
AdminSessions: authenticator,
DeviceAccess: authenticator,
LogEvent: discardEvent,
})
if err != nil {
t.Fatalf("NewRouter(admin) error = %v", err)
}
return &deviceHTTPFixture{
db: db,
store: store,
assets: assets,
tasks: tasks,
router: router,
db: db,
store: store,
assets: assets,
tasks: tasks,
router: router,
adminRouter: adminRouter,
}
}