package usecase import ( "context" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "strings" "time" "cmroubao/backend-api/internal/domain" ) const ( deviceOrderCommandSchemaVersion = 1 deviceOrderCommandType = "CREATE_PENDING_ORDER" ) type PullDeviceOrderCommand struct { UserID string DeviceID string TaskID string ExecutionID string ClaimGeneration int64 ClaimToken string } type PullDeviceOrderCommandWrite struct { PullDeviceOrderCommand ClaimTokenHash string Now time.Time DeliveredEvent domain.TaskEvent } type AcknowledgeDeviceOrderCommand struct { UserID string DeviceID string TaskID string ExecutionID string AuthorizationID string ClaimGeneration int64 ClaimToken string CommandSHA256 string IdempotencyKey string } type AcknowledgeDeviceOrderCommandWrite struct { AcknowledgeDeviceOrderCommand ClaimTokenHash string RequestSHA256 string Now time.Time AcknowledgedEvent domain.TaskEvent } type AcknowledgeDeviceOrderCommandResult struct { Authorization domain.OrderAuthorization Replayed bool } type DeviceOrderCommandRepository interface { PullDeviceOrderCommand( context.Context, PullDeviceOrderCommandWrite, ) (*domain.DeviceOrderCommand, error) AcknowledgeDeviceOrderCommand( context.Context, AcknowledgeDeviceOrderCommandWrite, ) (domain.OrderAuthorization, bool, error) } type DeviceOrderCommandService struct { repository DeviceOrderCommandRepository clock Clock ids IDGenerator } func NewDeviceOrderCommandService( repository DeviceOrderCommandRepository, clock Clock, ids IDGenerator, ) (*DeviceOrderCommandService, error) { if repository == nil || clock == nil || ids == nil { return nil, errors.New("device order command service dependencies are required") } return &DeviceOrderCommandService{ repository: repository, clock: clock, ids: ids, }, nil } func (service *DeviceOrderCommandService) Pull( ctx context.Context, command PullDeviceOrderCommand, ) (*domain.DeviceOrderCommand, error) { command = normalizePullDeviceOrderCommand(command) fields := lifecycleClaimFields( command.UserID, command.DeviceID, command.TaskID, command.ClaimGeneration, command.ClaimToken, ) if !isUUID(command.ExecutionID) { fields["execution_id"] = "must be a UUID" } if len(fields) > 0 { return nil, invalidError( "ORDER_COMMAND_PULL_INVALID", "order command pull request is invalid", fields, ) } eventID, err := service.ids.NewID() if err != nil { return nil, internalLifecycleFailure(err) } now := service.clock.Now().UTC() userID, deviceID := command.UserID, command.DeviceID result, err := service.repository.PullDeviceOrderCommand( ctx, PullDeviceOrderCommandWrite{ PullDeviceOrderCommand: command, ClaimTokenHash: hashSecret(command.ClaimToken), Now: now, DeliveredEvent: domain.TaskEvent{ ID: eventID, TaskID: command.TaskID, ActorUserID: &userID, ActorDeviceID: &deviceID, Type: "ORDER_AUTHORIZATION_DELIVERED", Message: "order authorization delivered to device", OccurredAt: now, }, }, ) if err != nil { return nil, wrapLifecycleRepositoryError(err) } return result, nil } func (service *DeviceOrderCommandService) Acknowledge( ctx context.Context, command AcknowledgeDeviceOrderCommand, ) (AcknowledgeDeviceOrderCommandResult, error) { command = normalizeAcknowledgeDeviceOrderCommand(command) fields := lifecycleClaimFields( command.UserID, command.DeviceID, command.TaskID, command.ClaimGeneration, command.ClaimToken, ) if !isUUID(command.ExecutionID) { fields["execution_id"] = "must be a UUID" } if !isUUID(command.AuthorizationID) { fields["command_id"] = "must be a UUID" } if !sha256Pattern.MatchString(command.CommandSHA256) { fields["command_sha256"] = "must be lowercase SHA-256" } validateIdempotencyField(fields, command.IdempotencyKey) if len(fields) > 0 { return AcknowledgeDeviceOrderCommandResult{}, invalidError( "ORDER_COMMAND_ACK_INVALID", "order command acknowledgement is invalid", fields, ) } requestSHA256, err := lifecycleRequestHash(struct { UserID string `json:"user_id"` DeviceID string `json:"device_id"` TaskID string `json:"task_id"` ExecutionID string `json:"execution_id"` AuthorizationID string `json:"authorization_id"` ClaimGeneration int64 `json:"claim_generation"` ClaimTokenHash string `json:"claim_token_sha256"` CommandSHA256 string `json:"command_sha256"` }{ UserID: command.UserID, DeviceID: command.DeviceID, TaskID: command.TaskID, ExecutionID: command.ExecutionID, AuthorizationID: command.AuthorizationID, ClaimGeneration: command.ClaimGeneration, ClaimTokenHash: hashSecret(command.ClaimToken), CommandSHA256: command.CommandSHA256, }) if err != nil { return AcknowledgeDeviceOrderCommandResult{}, internalLifecycleFailure(err) } eventID, err := service.ids.NewID() if err != nil { return AcknowledgeDeviceOrderCommandResult{}, internalLifecycleFailure(err) } now := service.clock.Now().UTC() userID, deviceID := command.UserID, command.DeviceID authorization, replayed, err := service.repository.AcknowledgeDeviceOrderCommand( ctx, AcknowledgeDeviceOrderCommandWrite{ AcknowledgeDeviceOrderCommand: command, ClaimTokenHash: hashSecret(command.ClaimToken), RequestSHA256: requestSHA256, Now: now, AcknowledgedEvent: domain.TaskEvent{ ID: eventID, TaskID: command.TaskID, ActorUserID: &userID, ActorDeviceID: &deviceID, Type: "ORDER_AUTHORIZATION_ACKNOWLEDGED", Message: "order authorization persisted by device", OccurredAt: now, }, }, ) if err != nil { return AcknowledgeDeviceOrderCommandResult{}, wrapLifecycleRepositoryError(err) } return AcknowledgeDeviceOrderCommandResult{ Authorization: authorization, Replayed: replayed, }, nil } func DeviceOrderCommandSHA256( command domain.DeviceOrderCommand, ) (string, error) { payload := struct { ID string `json:"id"` SchemaVersion int `json:"schema_version"` Type string `json:"type"` AuthorizationVersion int `json:"authorization_version"` TaskID string `json:"task_id"` ExecutionID string `json:"execution_id"` TaskContentSHA256 string `json:"task_content_sha256"` OriginalSKU string `json:"original_sku"` Quantity int `json:"quantity"` Candidate struct { CandidateKey string `json:"candidate_key"` ObservedOrdinal int `json:"observed_ordinal"` Title string `json:"title"` SKUText string `json:"sku_text"` PriceText string `json:"price_text"` CardSignature string `json:"card_signature"` DetailSignature string `json:"detail_signature"` DetailEvidenceSHA256 string `json:"detail_evidence_sha256"` SpecificationEvidenceSHA256 string `json:"specification_evidence_sha256"` } `json:"candidate"` }{ ID: command.ID, SchemaVersion: command.SchemaVersion, Type: command.Type, AuthorizationVersion: command.AuthorizationVersion, TaskID: command.TaskID, ExecutionID: command.ExecutionID, TaskContentSHA256: command.TaskContentSHA256, OriginalSKU: command.OriginalSKU, Quantity: command.Quantity, } payload.Candidate.CandidateKey = command.CandidateKey payload.Candidate.ObservedOrdinal = command.ObservedOrdinal payload.Candidate.Title = command.CandidateTitle payload.Candidate.SKUText = command.CandidateSKUText payload.Candidate.PriceText = command.CandidatePriceText payload.Candidate.CardSignature = command.CardSignature payload.Candidate.DetailSignature = command.DetailSignature payload.Candidate.DetailEvidenceSHA256 = command.DetailEvidenceSHA256 payload.Candidate.SpecificationEvidenceSHA256 = command.SpecificationEvidenceSHA256 encoded, err := json.Marshal(payload) if err != nil { return "", err } sum := sha256.Sum256(encoded) return hex.EncodeToString(sum[:]), nil } func NewDeviceOrderCommand( authorization domain.OrderAuthorization, observedOrdinal int, candidateTitle string, ) domain.DeviceOrderCommand { return domain.DeviceOrderCommand{ ID: authorization.ID, SchemaVersion: deviceOrderCommandSchemaVersion, Type: deviceOrderCommandType, AuthorizationVersion: authorization.AuthorizationVersion, TaskID: authorization.TaskID, ExecutionID: authorization.ExecutionID, TaskContentSHA256: authorization.TaskContentSHA256, OriginalSKU: authorization.OriginalSKU, Quantity: authorization.Quantity, CandidateKey: authorization.CandidateKey, ObservedOrdinal: observedOrdinal, CandidateTitle: candidateTitle, CandidateSKUText: authorization.CandidateSKUText, CandidatePriceText: authorization.CandidatePriceText, CardSignature: authorization.CardSignature, DetailSignature: authorization.DetailSignature, DetailEvidenceSHA256: authorization.DetailEvidenceSHA256, SpecificationEvidenceSHA256: authorization.SpecificationEvidenceSHA256, AuthorizationStatus: authorization.Status, } } func normalizePullDeviceOrderCommand( command PullDeviceOrderCommand, ) PullDeviceOrderCommand { command.UserID = strings.TrimSpace(command.UserID) command.DeviceID = strings.TrimSpace(command.DeviceID) command.TaskID = strings.TrimSpace(command.TaskID) command.ExecutionID = strings.TrimSpace(command.ExecutionID) command.ClaimToken = strings.TrimSpace(command.ClaimToken) return command } func normalizeAcknowledgeDeviceOrderCommand( command AcknowledgeDeviceOrderCommand, ) AcknowledgeDeviceOrderCommand { command.UserID = strings.TrimSpace(command.UserID) command.DeviceID = strings.TrimSpace(command.DeviceID) command.TaskID = strings.TrimSpace(command.TaskID) command.ExecutionID = strings.TrimSpace(command.ExecutionID) command.AuthorizationID = strings.TrimSpace(command.AuthorizationID) command.ClaimToken = strings.TrimSpace(command.ClaimToken) command.CommandSHA256 = strings.TrimSpace(command.CommandSHA256) command.IdempotencyKey = strings.TrimSpace(command.IdempotencyKey) return command }