feat(t218): reconcile single order submissions
This commit is contained in:
@@ -17,17 +17,18 @@ import (
|
||||
const claimTokenHeader = "X-Claim-Token"
|
||||
|
||||
type DeviceServices struct {
|
||||
Lifecycle *usecase.LifecycleService
|
||||
Assets *usecase.AssetService
|
||||
Results *usecase.ExecutionResultService
|
||||
Commands *usecase.DeviceOrderCommandService
|
||||
DryRuns *usecase.OrderDryRunService
|
||||
Lifecycle *usecase.LifecycleService
|
||||
Assets *usecase.AssetService
|
||||
Results *usecase.ExecutionResultService
|
||||
Commands *usecase.DeviceOrderCommandService
|
||||
DryRuns *usecase.OrderDryRunService
|
||||
Submissions *usecase.OrderSubmissionService
|
||||
}
|
||||
|
||||
func (services DeviceServices) validate() error {
|
||||
if services.Lifecycle == nil || services.Assets == nil ||
|
||||
services.Results == nil || services.Commands == nil ||
|
||||
services.DryRuns == nil {
|
||||
services.DryRuns == nil || services.Submissions == nil {
|
||||
return errors.New("device services are required")
|
||||
}
|
||||
return nil
|
||||
@@ -93,12 +94,215 @@ func NewDeviceRouteRegistrar(
|
||||
"/api/v1/tasks/:id/order-dry-runs/:command_id/ready",
|
||||
handler.readyOrderDryRun,
|
||||
)
|
||||
routes.POST(
|
||||
"/api/v1/tasks/:id/order-submissions/start",
|
||||
handler.startOrderSubmission,
|
||||
)
|
||||
routes.POST(
|
||||
"/api/v1/tasks/:id/order-submissions/:submission_id/reconcile",
|
||||
handler.reconcileOrderSubmission,
|
||||
)
|
||||
routes.POST(
|
||||
"/api/v1/tasks/:id/order-submissions/:submission_id/manual-review",
|
||||
handler.manualReviewOrderSubmission,
|
||||
)
|
||||
routes.POST("/api/v1/tasks/:id/complete", handler.completeTask)
|
||||
routes.POST("/api/v1/tasks/:id/fail", handler.failTask)
|
||||
return nil
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (handler *deviceHandlers) startOrderSubmission(ctx *gin.Context) {
|
||||
principal, ok := devicePrincipal(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var request struct {
|
||||
DeviceID string `json:"device_id"`
|
||||
ExecutionID string `json:"execution_id"`
|
||||
ClaimGeneration int64 `json:"claim_generation"`
|
||||
CommandID string `json:"command_id"`
|
||||
CommandSHA256 string `json:"command_sha256"`
|
||||
DryRunID string `json:"dry_run_id"`
|
||||
DryRunEvidenceSHA256 string `json:"dry_run_evidence_sha256"`
|
||||
ObservedTitle string `json:"observed_title"`
|
||||
SelectedSKU string `json:"selected_sku"`
|
||||
Quantity int `json:"quantity"`
|
||||
UnitPriceCents int64 `json:"unit_price_cents"`
|
||||
TotalPriceCents int64 `json:"total_price_cents"`
|
||||
}
|
||||
if !decodeDeviceJSON(ctx, &request) ||
|
||||
!deviceIDMatches(ctx, request.DeviceID, principal.DeviceID) {
|
||||
return
|
||||
}
|
||||
result, err := handler.services.Submissions.Start(
|
||||
ctx.Request.Context(),
|
||||
usecase.StartOrderSubmissionCommand{
|
||||
UserID: principal.UserID,
|
||||
DeviceID: principal.DeviceID,
|
||||
TaskID: ctx.Param("id"),
|
||||
ExecutionID: request.ExecutionID,
|
||||
AuthorizationID: request.CommandID,
|
||||
ClaimGeneration: request.ClaimGeneration,
|
||||
ClaimToken: ctx.GetHeader(claimTokenHeader),
|
||||
CommandSHA256: request.CommandSHA256,
|
||||
DryRunID: request.DryRunID,
|
||||
DryRunEvidenceSHA256: request.DryRunEvidenceSHA256,
|
||||
ObservedTitle: request.ObservedTitle,
|
||||
SelectedSKU: request.SelectedSKU,
|
||||
Quantity: request.Quantity,
|
||||
UnitPriceCents: request.UnitPriceCents,
|
||||
TotalPriceCents: request.TotalPriceCents,
|
||||
IdempotencyKey: ctx.GetHeader("Idempotency-Key"),
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
writeUsecaseError(ctx, err)
|
||||
return
|
||||
}
|
||||
writeOrderSubmission(ctx, result)
|
||||
}
|
||||
|
||||
func (handler *deviceHandlers) reconcileOrderSubmission(ctx *gin.Context) {
|
||||
principal, ok := devicePrincipal(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var request struct {
|
||||
DeviceID string `json:"device_id"`
|
||||
ExecutionID string `json:"execution_id"`
|
||||
ClaimGeneration int64 `json:"claim_generation"`
|
||||
CommandID string `json:"command_id"`
|
||||
CommandSHA256 string `json:"command_sha256"`
|
||||
PlatformOrderNo string `json:"platform_order_no"`
|
||||
PlatformOrderedAt string `json:"platform_ordered_at"`
|
||||
PlatformOrderStatus string `json:"platform_order_status"`
|
||||
ObservedTitle string `json:"observed_title"`
|
||||
SelectedSKU string `json:"selected_sku"`
|
||||
Quantity int `json:"quantity"`
|
||||
TotalPriceCents int64 `json:"total_price_cents"`
|
||||
EvidenceAssetID string `json:"evidence_asset_id"`
|
||||
EvidenceSHA256 string `json:"evidence_sha256"`
|
||||
}
|
||||
if !decodeDeviceJSON(ctx, &request) ||
|
||||
!deviceIDMatches(ctx, request.DeviceID, principal.DeviceID) {
|
||||
return
|
||||
}
|
||||
result, err := handler.services.Submissions.Reconcile(
|
||||
ctx.Request.Context(),
|
||||
usecase.ReconcileOrderSubmissionCommand{
|
||||
UserID: principal.UserID,
|
||||
DeviceID: principal.DeviceID,
|
||||
TaskID: ctx.Param("id"),
|
||||
ExecutionID: request.ExecutionID,
|
||||
AuthorizationID: request.CommandID,
|
||||
ClaimGeneration: request.ClaimGeneration,
|
||||
ClaimToken: ctx.GetHeader(claimTokenHeader),
|
||||
CommandSHA256: request.CommandSHA256,
|
||||
SubmissionID: ctx.Param("submission_id"),
|
||||
PlatformOrderNo: request.PlatformOrderNo,
|
||||
PlatformOrderedAt: request.PlatformOrderedAt,
|
||||
PlatformOrderStatus: request.PlatformOrderStatus,
|
||||
ObservedTitle: request.ObservedTitle,
|
||||
SelectedSKU: request.SelectedSKU,
|
||||
Quantity: request.Quantity,
|
||||
TotalPriceCents: request.TotalPriceCents,
|
||||
EvidenceAssetID: request.EvidenceAssetID,
|
||||
EvidenceSHA256: request.EvidenceSHA256,
|
||||
IdempotencyKey: ctx.GetHeader("Idempotency-Key"),
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
writeUsecaseError(ctx, err)
|
||||
return
|
||||
}
|
||||
writeOrderSubmission(ctx, result)
|
||||
}
|
||||
|
||||
func (handler *deviceHandlers) manualReviewOrderSubmission(ctx *gin.Context) {
|
||||
principal, ok := devicePrincipal(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var request struct {
|
||||
DeviceID string `json:"device_id"`
|
||||
ExecutionID string `json:"execution_id"`
|
||||
ClaimGeneration int64 `json:"claim_generation"`
|
||||
CommandID string `json:"command_id"`
|
||||
CommandSHA256 string `json:"command_sha256"`
|
||||
ReasonCode string `json:"reason_code"`
|
||||
EvidenceAssetID string `json:"evidence_asset_id"`
|
||||
EvidenceSHA256 string `json:"evidence_sha256"`
|
||||
}
|
||||
if !decodeDeviceJSON(ctx, &request) ||
|
||||
!deviceIDMatches(ctx, request.DeviceID, principal.DeviceID) {
|
||||
return
|
||||
}
|
||||
result, err := handler.services.Submissions.ManualReview(
|
||||
ctx.Request.Context(),
|
||||
usecase.ManualReviewOrderSubmissionCommand{
|
||||
UserID: principal.UserID,
|
||||
DeviceID: principal.DeviceID,
|
||||
TaskID: ctx.Param("id"),
|
||||
ExecutionID: request.ExecutionID,
|
||||
AuthorizationID: request.CommandID,
|
||||
ClaimGeneration: request.ClaimGeneration,
|
||||
ClaimToken: ctx.GetHeader(claimTokenHeader),
|
||||
CommandSHA256: request.CommandSHA256,
|
||||
SubmissionID: ctx.Param("submission_id"),
|
||||
ReasonCode: request.ReasonCode,
|
||||
EvidenceAssetID: request.EvidenceAssetID,
|
||||
EvidenceSHA256: request.EvidenceSHA256,
|
||||
IdempotencyKey: ctx.GetHeader("Idempotency-Key"),
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
writeUsecaseError(ctx, err)
|
||||
return
|
||||
}
|
||||
writeOrderSubmission(ctx, result)
|
||||
}
|
||||
|
||||
func writeOrderSubmission(
|
||||
ctx *gin.Context,
|
||||
result usecase.OrderSubmissionResult,
|
||||
) {
|
||||
ctx.Header("Cache-Control", "no-store")
|
||||
ctx.JSON(http.StatusOK, gin.H{
|
||||
"submission": orderSubmissionResponse(result.Submission),
|
||||
"replayed": result.Replayed,
|
||||
})
|
||||
}
|
||||
|
||||
func orderSubmissionResponse(submission domain.OrderSubmission) gin.H {
|
||||
return gin.H{
|
||||
"id": submission.ID,
|
||||
"command_id": submission.AuthorizationID,
|
||||
"dry_run_id": submission.DryRunID,
|
||||
"task_id": submission.TaskID,
|
||||
"execution_id": submission.ExecutionID,
|
||||
"command_sha256": submission.CommandSHA256,
|
||||
"dry_run_evidence_sha256": submission.DryRunEvidenceSHA256,
|
||||
"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 (handler *deviceHandlers) startOrderDryRun(ctx *gin.Context) {
|
||||
principal, ok := devicePrincipal(ctx)
|
||||
if !ok {
|
||||
|
||||
Reference in New Issue
Block a user