feat(t216): deliver recoverable device order commands
This commit is contained in:
@@ -0,0 +1,336 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user