337 lines
11 KiB
Go
337 lines
11 KiB
Go
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
|
||
|
|
}
|