package usecase import ( "context" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "strings" "time" "cmroubao/backend-api/internal/domain" ) type OrderAuthorizationItemInput struct { CandidateKey string `json:"candidate_key"` Label string `json:"label"` PrimaryReasonCode string `json:"primary_reason_code"` ReasonCodes []string `json:"reason_codes"` Note string `json:"note"` } type CreateOrderAuthorizationCommand struct { ActorUserID string `json:"-"` TaskID string `json:"-"` IdempotencyKey string `json:"-"` ExecutionID string `json:"execution_id"` TaskContentSHA256 string `json:"task_content_sha256"` ExpectedTaskVersion int64 `json:"expected_task_version"` CandidateKey string `json:"candidate_key"` ReasonSchemaVersion int `json:"reason_schema_version"` PrimaryReasonCode string `json:"primary_reason_code"` Note string `json:"note"` SupersedesAuthorizationID *string `json:"supersedes_authorization_id"` Items []OrderAuthorizationItemInput `json:"items"` } type CreateOrderAuthorizationWrite struct { AuthorizationID string ReviewID string RequestSHA256 string Now time.Time Event domain.TaskEvent Command CreateOrderAuthorizationCommand } type CreateOrderAuthorizationResult struct { Authorization domain.OrderAuthorization Replayed bool } type OrderAuthorizationRepository interface { CreateOrderAuthorization( context.Context, CreateOrderAuthorizationWrite, ) (domain.OrderAuthorization, bool, error) } type OrderAuthorizationService struct { repository OrderAuthorizationRepository clock Clock ids IDGenerator } func NewOrderAuthorizationService( repository OrderAuthorizationRepository, clock Clock, ids IDGenerator, ) (*OrderAuthorizationService, error) { if repository == nil || clock == nil || ids == nil { return nil, errors.New("order authorization service dependencies are required") } return &OrderAuthorizationService{ repository: repository, clock: clock, ids: ids, }, nil } func (service *OrderAuthorizationService) Create( ctx context.Context, command CreateOrderAuthorizationCommand, ) (CreateOrderAuthorizationResult, error) { command = normalizeOrderAuthorizationCommand(command) if err := validateOrderAuthorizationCommand(command); err != nil { return CreateOrderAuthorizationResult{}, err } requestSHA256, err := hashOrderAuthorizationCommand(command) if err != nil { return CreateOrderAuthorizationResult{}, internalExecutionResultFailure(err) } authorizationID, err := service.ids.NewID() if err != nil { return CreateOrderAuthorizationResult{}, internalExecutionResultFailure(err) } reviewID, err := service.ids.NewID() if err != nil { return CreateOrderAuthorizationResult{}, internalExecutionResultFailure(err) } eventID, err := service.ids.NewID() if err != nil { return CreateOrderAuthorizationResult{}, internalExecutionResultFailure(err) } now := service.clock.Now().UTC() actorUserID := command.ActorUserID authorization, replayed, err := service.repository.CreateOrderAuthorization( ctx, CreateOrderAuthorizationWrite{ AuthorizationID: authorizationID, ReviewID: reviewID, RequestSHA256: requestSHA256, Now: now, Event: domain.TaskEvent{ ID: eventID, TaskID: command.TaskID, ActorUserID: &actorUserID, Type: "ORDER_AUTHORIZATION_CREATED", Message: "order authorization created for candidate " + command.CandidateKey[:12], OccurredAt: now, }, Command: command, }, ) if err != nil { return CreateOrderAuthorizationResult{}, wrapRepositoryError(err) } return CreateOrderAuthorizationResult{ Authorization: authorization, Replayed: replayed, }, nil } func normalizeOrderAuthorizationCommand( command CreateOrderAuthorizationCommand, ) CreateOrderAuthorizationCommand { command.ActorUserID = strings.TrimSpace(command.ActorUserID) command.TaskID = strings.TrimSpace(command.TaskID) command.IdempotencyKey = strings.TrimSpace(command.IdempotencyKey) command.ExecutionID = strings.TrimSpace(command.ExecutionID) command.TaskContentSHA256 = strings.TrimSpace(command.TaskContentSHA256) command.CandidateKey = strings.TrimSpace(command.CandidateKey) command.PrimaryReasonCode = strings.TrimSpace(command.PrimaryReasonCode) command.Note = strings.TrimSpace(command.Note) if command.SupersedesAuthorizationID != nil { value := strings.TrimSpace(*command.SupersedesAuthorizationID) command.SupersedesAuthorizationID = &value } for index := range command.Items { item := &command.Items[index] item.CandidateKey = strings.TrimSpace(item.CandidateKey) item.Label = strings.TrimSpace(item.Label) item.PrimaryReasonCode = strings.TrimSpace(item.PrimaryReasonCode) item.Note = strings.TrimSpace(item.Note) for reasonIndex := range item.ReasonCodes { item.ReasonCodes[reasonIndex] = strings.TrimSpace(item.ReasonCodes[reasonIndex]) } } return command } func validateOrderAuthorizationCommand( command CreateOrderAuthorizationCommand, ) error { fields := make(map[string]string) if !isUUID(command.ActorUserID) { fields["actor_user_id"] = "must be a UUID" } if !isUUID(command.TaskID) { fields["task_id"] = "must be a UUID" } if !isUUID(command.ExecutionID) { fields["execution_id"] = "must be a UUID" } if len(command.IdempotencyKey) == 0 || len([]byte(command.IdempotencyKey)) > maxIdempotencyKeyBytes || !isPrintableASCII(command.IdempotencyKey) { fields["idempotency_key"] = "must be 1 to 128 printable ASCII bytes" } if !sha256Pattern.MatchString(command.TaskContentSHA256) { fields["task_content_sha256"] = "must be lowercase SHA-256" } if command.ExpectedTaskVersion < 1 { fields["expected_task_version"] = "must be positive" } if !sha256Pattern.MatchString(command.CandidateKey) { fields["candidate_key"] = "must be lowercase SHA-256" } if command.ReasonSchemaVersion != candidateReasonSchemaVersion { fields["reason_schema_version"] = "must be 1" } if !validReviewPrimaryReason("CANDIDATE_ACCEPTED", command.PrimaryReasonCode) || !validReviewNote(command.PrimaryReasonCode, command.Note) { fields["primary_reason_code"] = "is invalid" } if command.SupersedesAuthorizationID != nil && !isUUID(*command.SupersedesAuthorizationID) { fields["supersedes_authorization_id"] = "must be a UUID" } if len(command.Items) < 1 || len(command.Items) > 5 { fields["items"] = "must contain 1 to 5 items" } seen := make(map[string]struct{}, len(command.Items)) accepted := 0 for _, item := range command.Items { if !sha256Pattern.MatchString(item.CandidateKey) { fields["items"] = "candidate keys must be lowercase SHA-256" continue } if _, found := seen[item.CandidateKey]; found { fields["items"] = "candidate keys must be unique" continue } seen[item.CandidateKey] = struct{}{} if item.Label == "ACCEPT" { accepted++ } if item.Label != "ACCEPT" && item.Label != "REJECT" { fields["items"] = "labels must be ACCEPT or REJECT" continue } if !validOrderAuthorizationItem(item) { fields["items"] = "contains invalid reasons" } } _, found := seen[command.CandidateKey] if !found || accepted != 1 || !acceptedOrderAuthorizationItem( command.Items, command.CandidateKey, ) { fields["candidate_key"] = "must identify the single accepted item" } if len(fields) > 0 { return invalidError( "ORDER_AUTHORIZATION_INVALID", "order authorization request is invalid", fields, ) } return nil } func validOrderAuthorizationItem(item OrderAuthorizationItemInput) bool { if len(item.ReasonCodes) < 1 || len(item.ReasonCodes) > 8 { return false } seen := make(map[string]struct{}, len(item.ReasonCodes)) containsPrimary := false for _, reason := range item.ReasonCodes { if !validItemReason(item.Label, reason) { return false } if _, found := seen[reason]; found { return false } seen[reason] = struct{}{} containsPrimary = containsPrimary || reason == item.PrimaryReasonCode } return containsPrimary && validReviewNote(item.PrimaryReasonCode, item.Note) } func acceptedOrderAuthorizationItem( items []OrderAuthorizationItemInput, candidateKey string, ) bool { for _, item := range items { if item.CandidateKey == candidateKey { return item.Label == "ACCEPT" } } return false } func hashOrderAuthorizationCommand( command CreateOrderAuthorizationCommand, ) (string, error) { command.IdempotencyKey = "" payload := struct { ActorUserID string `json:"actor_user_id"` TaskID string `json:"task_id"` Command CreateOrderAuthorizationCommand `json:"command"` }{ ActorUserID: command.ActorUserID, TaskID: command.TaskID, Command: command, } encoded, err := json.Marshal(payload) if err != nil { return "", err } sum := sha256.Sum256(encoded) return hex.EncodeToString(sum[:]), nil }