feat(t217): verify authorized order dry runs
This commit is contained in:
@@ -383,6 +383,9 @@ func TestAuthMigrationCanRollbackWithoutRebuildingPurchaseTasks(
|
||||
if err != nil {
|
||||
t.Fatalf("migration.New() error = %v", err)
|
||||
}
|
||||
if err := runner.Down(context.Background()); err != nil {
|
||||
t.Fatalf("Down(v10) error = %v", err)
|
||||
}
|
||||
if err := runner.Down(context.Background()); err != nil {
|
||||
t.Fatalf("Down(v9) error = %v", err)
|
||||
}
|
||||
@@ -417,9 +420,9 @@ func TestAuthMigrationCanRollbackWithoutRebuildingPurchaseTasks(
|
||||
t.Fatal("purchase_tasks was lost during auth migration rollback")
|
||||
}
|
||||
if applied, err := runner.Up(context.Background()); err != nil {
|
||||
t.Fatalf("Up(v3-v9) error = %v", err)
|
||||
} else if applied != 7 {
|
||||
t.Fatalf("Up(v3-v9) applied = %d, want 7", applied)
|
||||
t.Fatalf("Up(v3-v10) error = %v", err)
|
||||
} else if applied != 8 {
|
||||
t.Fatalf("Up(v3-v10) applied = %d, want 8", applied)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -108,14 +108,15 @@ func (s *Store) PullDeviceOrderCommand(
|
||||
}
|
||||
command.AuthorizationStatus = domain.OrderAuthorizationDelivered
|
||||
case domain.OrderAuthorizationDelivered,
|
||||
domain.OrderAuthorizationAcknowledged:
|
||||
domain.OrderAuthorizationAcknowledged,
|
||||
domain.OrderAuthorizationExecuting:
|
||||
result, err := tx.ExecContext(
|
||||
ctx,
|
||||
`UPDATE order_authorizations
|
||||
SET last_delivered_at = ?,
|
||||
delivery_attempt_count = delivery_attempt_count + 1
|
||||
WHERE id = ?
|
||||
AND status IN ('DELIVERED', 'ACKNOWLEDGED')
|
||||
AND status IN ('DELIVERED', 'ACKNOWLEDGED', 'EXECUTING')
|
||||
AND command_sha256 = ?`,
|
||||
formatTimestamp(write.Now),
|
||||
authorization.ID,
|
||||
@@ -389,7 +390,8 @@ func findActiveDeviceOrderAuthorization(
|
||||
AND oa.status IN (
|
||||
'PENDING_DELIVERY',
|
||||
'DELIVERED',
|
||||
'ACKNOWLEDGED'
|
||||
'ACKNOWLEDGED',
|
||||
'EXECUTING'
|
||||
)
|
||||
ORDER BY oa.authorization_version DESC
|
||||
LIMIT 1`,
|
||||
|
||||
@@ -0,0 +1,521 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"math"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"cmroubao/backend-api/internal/domain"
|
||||
"cmroubao/backend-api/internal/usecase"
|
||||
)
|
||||
|
||||
func (s *Store) StartOrderDryRun(
|
||||
ctx context.Context,
|
||||
write usecase.StartOrderDryRunWrite,
|
||||
) (domain.OrderDryRun, bool, error) {
|
||||
tx, err := s.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return domain.OrderDryRun{}, false, repositoryFailure(err)
|
||||
}
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
if result, found, err := replayOrderDryRunRequest(
|
||||
ctx, tx, write.DeviceID, "START", write.IdempotencyKey,
|
||||
write.RequestSHA256, write.TaskID,
|
||||
); err != nil || found {
|
||||
return commitOrderDryRunReplay(tx, result, found, err)
|
||||
}
|
||||
if err := validateDeviceOrderCommandClaim(
|
||||
ctx, tx, write.UserID, write.DeviceID, write.TaskID,
|
||||
write.ExecutionID, write.ClaimGeneration, write.ClaimTokenHash,
|
||||
write.Now,
|
||||
); err != nil {
|
||||
return domain.OrderDryRun{}, false, err
|
||||
}
|
||||
authorization, err := getOrderAuthorization(
|
||||
ctx, tx, write.AuthorizationID,
|
||||
)
|
||||
if err != nil {
|
||||
return domain.OrderDryRun{}, false, err
|
||||
}
|
||||
if err := validateDryRunAuthorization(
|
||||
authorization, write.UserID, write.DeviceID, write.TaskID,
|
||||
write.ExecutionID, write.ClaimGeneration, write.CommandSHA256,
|
||||
); err != nil {
|
||||
return domain.OrderDryRun{}, false, err
|
||||
}
|
||||
dryRun, found, err := getOrderDryRunByAuthorization(
|
||||
ctx, tx, authorization.ID,
|
||||
)
|
||||
if err != nil {
|
||||
return domain.OrderDryRun{}, false, err
|
||||
}
|
||||
switch authorization.Status {
|
||||
case domain.OrderAuthorizationAcknowledged:
|
||||
if found {
|
||||
return domain.OrderDryRun{}, false, usecase.ErrRepositoryInvariant
|
||||
}
|
||||
result, err := tx.ExecContext(
|
||||
ctx,
|
||||
`UPDATE order_authorizations
|
||||
SET status = 'EXECUTING', execution_started_at = ?
|
||||
WHERE id = ? AND status = 'ACKNOWLEDGED'
|
||||
AND command_sha256 = ?`,
|
||||
formatTimestamp(write.Now),
|
||||
authorization.ID,
|
||||
write.CommandSHA256,
|
||||
)
|
||||
if err != nil {
|
||||
return domain.OrderDryRun{}, false, repositoryFailure(err)
|
||||
}
|
||||
if affected, err := result.RowsAffected(); err != nil || affected != 1 {
|
||||
if err != nil {
|
||||
return domain.OrderDryRun{}, false, repositoryFailure(err)
|
||||
}
|
||||
return domain.OrderDryRun{}, false, usecase.ErrTaskStateConflict
|
||||
}
|
||||
_, err = tx.ExecContext(
|
||||
ctx,
|
||||
`INSERT INTO order_dry_runs (
|
||||
id, authorization_id, task_id, execution_id,
|
||||
command_sha256, status, started_at
|
||||
) VALUES (?, ?, ?, ?, ?, 'PREPARING', ?)`,
|
||||
write.DryRunID,
|
||||
authorization.ID,
|
||||
write.TaskID,
|
||||
write.ExecutionID,
|
||||
write.CommandSHA256,
|
||||
formatTimestamp(write.Now),
|
||||
)
|
||||
if err != nil {
|
||||
return domain.OrderDryRun{}, false, repositoryFailure(err)
|
||||
}
|
||||
if err := insertTaskEvent(ctx, tx, write.Event); err != nil {
|
||||
return domain.OrderDryRun{}, false, err
|
||||
}
|
||||
dryRun, found, err = getOrderDryRunByAuthorization(
|
||||
ctx, tx, authorization.ID,
|
||||
)
|
||||
if err != nil || !found {
|
||||
if err != nil {
|
||||
return domain.OrderDryRun{}, false, err
|
||||
}
|
||||
return domain.OrderDryRun{}, false, usecase.ErrRepositoryInvariant
|
||||
}
|
||||
case domain.OrderAuthorizationExecuting:
|
||||
if !found || dryRun.CommandSHA256 != write.CommandSHA256 {
|
||||
return domain.OrderDryRun{}, false, usecase.ErrTaskStateConflict
|
||||
}
|
||||
default:
|
||||
return domain.OrderDryRun{}, false, usecase.ErrTaskStateConflict
|
||||
}
|
||||
if err := insertOrderDryRunRequest(
|
||||
ctx, tx, write.DeviceID, "START", write.IdempotencyKey,
|
||||
write.RequestSHA256, write.TaskID, dryRun.ID, write.Now,
|
||||
); err != nil {
|
||||
return domain.OrderDryRun{}, false, err
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return domain.OrderDryRun{}, false, repositoryFailure(err)
|
||||
}
|
||||
return dryRun, false, nil
|
||||
}
|
||||
|
||||
func (s *Store) ReadyOrderDryRun(
|
||||
ctx context.Context,
|
||||
write usecase.ReadyOrderDryRunWrite,
|
||||
) (domain.OrderDryRun, bool, error) {
|
||||
tx, err := s.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return domain.OrderDryRun{}, false, repositoryFailure(err)
|
||||
}
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
if result, found, err := replayOrderDryRunRequest(
|
||||
ctx, tx, write.DeviceID, "READY", write.IdempotencyKey,
|
||||
write.RequestSHA256, write.TaskID,
|
||||
); err != nil || found {
|
||||
return commitOrderDryRunReplay(tx, result, found, err)
|
||||
}
|
||||
if err := validateDeviceOrderCommandClaim(
|
||||
ctx, tx, write.UserID, write.DeviceID, write.TaskID,
|
||||
write.ExecutionID, write.ClaimGeneration, write.ClaimTokenHash,
|
||||
write.Now,
|
||||
); err != nil {
|
||||
return domain.OrderDryRun{}, false, err
|
||||
}
|
||||
authorization, err := getOrderAuthorization(
|
||||
ctx, tx, write.AuthorizationID,
|
||||
)
|
||||
if err != nil {
|
||||
return domain.OrderDryRun{}, false, err
|
||||
}
|
||||
if err := validateDryRunAuthorization(
|
||||
authorization, write.UserID, write.DeviceID, write.TaskID,
|
||||
write.ExecutionID, write.ClaimGeneration, write.CommandSHA256,
|
||||
); err != nil {
|
||||
return domain.OrderDryRun{}, false, err
|
||||
}
|
||||
if authorization.Status != domain.OrderAuthorizationExecuting {
|
||||
return domain.OrderDryRun{}, false, usecase.ErrTaskStateConflict
|
||||
}
|
||||
dryRun, found, err := getOrderDryRunByAuthorization(
|
||||
ctx, tx, authorization.ID,
|
||||
)
|
||||
if err != nil {
|
||||
return domain.OrderDryRun{}, false, err
|
||||
}
|
||||
if !found || dryRun.CommandSHA256 != write.CommandSHA256 {
|
||||
return domain.OrderDryRun{}, false, usecase.ErrTaskStateConflict
|
||||
}
|
||||
if dryRun.Status == domain.OrderDryRunReady {
|
||||
return domain.OrderDryRun{}, false, usecase.ErrTaskStateConflict
|
||||
}
|
||||
if write.Quantity != authorization.Quantity ||
|
||||
write.SelectedSKU != authorization.OriginalSKU ||
|
||||
(write.CardSignature != authorization.CardSignature &&
|
||||
write.DetailSignature != authorization.DetailSignature) {
|
||||
return domain.OrderDryRun{}, false, usecase.ErrTaskStateConflict
|
||||
}
|
||||
if write.UnitPriceCents > math.MaxInt64/int64(write.Quantity) ||
|
||||
write.TotalPriceCents !=
|
||||
write.UnitPriceCents*int64(write.Quantity) {
|
||||
return domain.OrderDryRun{}, false, usecase.ErrTaskStateConflict
|
||||
}
|
||||
var observedTitle string
|
||||
err = tx.QueryRowContext(
|
||||
ctx,
|
||||
`SELECT co.title
|
||||
FROM candidate_observation_identities coi
|
||||
JOIN candidate_observations co
|
||||
ON co.execution_id = coi.execution_id
|
||||
AND co.ordinal = coi.candidate_ordinal
|
||||
WHERE coi.candidate_key = ? AND coi.execution_id = ?`,
|
||||
authorization.CandidateKey,
|
||||
authorization.ExecutionID,
|
||||
).Scan(&observedTitle)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return domain.OrderDryRun{}, false, usecase.ErrRepositoryInvariant
|
||||
}
|
||||
if err != nil {
|
||||
return domain.OrderDryRun{}, false, repositoryFailure(err)
|
||||
}
|
||||
if normalizeDryRunTitle(observedTitle) !=
|
||||
normalizeDryRunTitle(write.ObservedTitle) {
|
||||
return domain.OrderDryRun{}, false, usecase.ErrTaskStateConflict
|
||||
}
|
||||
task, err := getClaimProtectedTask(ctx, tx, write.TaskID)
|
||||
if err != nil {
|
||||
return domain.OrderDryRun{}, false, err
|
||||
}
|
||||
if task.MaxBudgetCents != nil &&
|
||||
write.TotalPriceCents > *task.MaxBudgetCents {
|
||||
return domain.OrderDryRun{}, false, usecase.ErrTaskStateConflict
|
||||
}
|
||||
if task.MaxBudgetCents == nil {
|
||||
authorizedPrice, ok := parseDryRunPrice(
|
||||
authorization.CandidatePriceText,
|
||||
)
|
||||
if !ok || write.UnitPriceCents != authorizedPrice {
|
||||
return domain.OrderDryRun{}, false, usecase.ErrTaskStateConflict
|
||||
}
|
||||
}
|
||||
evidence, err := getExecutionEvidence(
|
||||
ctx, tx, write.EvidenceAssetID,
|
||||
)
|
||||
if err != nil {
|
||||
return domain.OrderDryRun{}, false, err
|
||||
}
|
||||
if evidence.TaskID != write.TaskID ||
|
||||
evidence.ExecutionID != write.ExecutionID ||
|
||||
evidence.MediaType != "image/jpeg" ||
|
||||
evidence.SHA256 != write.EvidenceSHA256 ||
|
||||
evidence.ReceivedAfterExecutionExpiry {
|
||||
return domain.OrderDryRun{}, false, usecase.ErrTaskStateConflict
|
||||
}
|
||||
result, err := tx.ExecContext(
|
||||
ctx,
|
||||
`UPDATE order_dry_runs
|
||||
SET status = 'READY',
|
||||
card_signature = ?,
|
||||
detail_signature = ?,
|
||||
observed_title = ?,
|
||||
selected_sku = ?,
|
||||
quantity = ?,
|
||||
unit_price_cents = ?,
|
||||
total_price_cents = ?,
|
||||
evidence_asset_id = ?,
|
||||
evidence_sha256 = ?,
|
||||
ready_at = ?
|
||||
WHERE id = ? AND status = 'PREPARING'`,
|
||||
write.CardSignature,
|
||||
write.DetailSignature,
|
||||
write.ObservedTitle,
|
||||
write.SelectedSKU,
|
||||
write.Quantity,
|
||||
write.UnitPriceCents,
|
||||
write.TotalPriceCents,
|
||||
write.EvidenceAssetID,
|
||||
write.EvidenceSHA256,
|
||||
formatTimestamp(write.Now),
|
||||
dryRun.ID,
|
||||
)
|
||||
if err != nil {
|
||||
return domain.OrderDryRun{}, false, repositoryFailure(err)
|
||||
}
|
||||
if affected, err := result.RowsAffected(); err != nil || affected != 1 {
|
||||
if err != nil {
|
||||
return domain.OrderDryRun{}, false, repositoryFailure(err)
|
||||
}
|
||||
return domain.OrderDryRun{}, false, usecase.ErrTaskStateConflict
|
||||
}
|
||||
if err := insertTaskEvent(ctx, tx, write.Event); err != nil {
|
||||
return domain.OrderDryRun{}, false, err
|
||||
}
|
||||
if err := insertOrderDryRunRequest(
|
||||
ctx, tx, write.DeviceID, "READY", write.IdempotencyKey,
|
||||
write.RequestSHA256, write.TaskID, dryRun.ID, write.Now,
|
||||
); err != nil {
|
||||
return domain.OrderDryRun{}, false, err
|
||||
}
|
||||
dryRun, found, err = getOrderDryRunByAuthorization(
|
||||
ctx, tx, authorization.ID,
|
||||
)
|
||||
if err != nil || !found {
|
||||
if err != nil {
|
||||
return domain.OrderDryRun{}, false, err
|
||||
}
|
||||
return domain.OrderDryRun{}, false, usecase.ErrRepositoryInvariant
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return domain.OrderDryRun{}, false, repositoryFailure(err)
|
||||
}
|
||||
return dryRun, false, nil
|
||||
}
|
||||
|
||||
func validateDryRunAuthorization(
|
||||
authorization domain.OrderAuthorization,
|
||||
userID, deviceID, taskID, executionID string,
|
||||
claimGeneration int64,
|
||||
commandSHA256 string,
|
||||
) error {
|
||||
if authorization.UserID != userID ||
|
||||
authorization.DeviceID != deviceID ||
|
||||
authorization.TaskID != taskID ||
|
||||
authorization.ExecutionID != executionID ||
|
||||
authorization.ClaimGeneration != claimGeneration ||
|
||||
authorization.CommandSHA256 == nil ||
|
||||
*authorization.CommandSHA256 != commandSHA256 {
|
||||
return usecase.ErrExecutionMismatch
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizeDryRunTitle(value string) string {
|
||||
return strings.Map(func(character rune) rune {
|
||||
if unicode.IsSpace(character) || unicode.IsPunct(character) {
|
||||
return -1
|
||||
}
|
||||
return unicode.ToLower(character)
|
||||
}, value)
|
||||
}
|
||||
|
||||
func parseDryRunPrice(value string) (int64, bool) {
|
||||
matches := dryRunPricePattern.FindStringSubmatch(strings.TrimSpace(value))
|
||||
if len(matches) != 3 {
|
||||
return 0, false
|
||||
}
|
||||
whole, err := strconv.ParseInt(matches[1], 10, 64)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
fractionText := matches[2]
|
||||
if len(fractionText) == 1 {
|
||||
fractionText += "0"
|
||||
}
|
||||
fraction := int64(0)
|
||||
if fractionText != "" {
|
||||
fraction, err = strconv.ParseInt(fractionText, 10, 64)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
cents := whole*100 + fraction
|
||||
return cents, cents > 0 && cents <= 100_000_000
|
||||
}
|
||||
|
||||
var dryRunPricePattern = regexp.MustCompile(
|
||||
`^(?:首件)?[¥¥]?(0|[1-9][0-9]{0,6})(?:\.([0-9]{1,2}))?$`,
|
||||
)
|
||||
|
||||
func getOrderDryRunByAuthorization(
|
||||
ctx context.Context,
|
||||
queryer queryRower,
|
||||
authorizationID string,
|
||||
) (domain.OrderDryRun, bool, error) {
|
||||
var result domain.OrderDryRun
|
||||
var card, detail, title, sku sql.NullString
|
||||
var quantity sql.NullInt64
|
||||
var unitPrice, totalPrice sql.NullInt64
|
||||
var evidenceID, evidenceSHA, readyAt sql.NullString
|
||||
var startedAt string
|
||||
err := queryer.QueryRowContext(
|
||||
ctx,
|
||||
`SELECT id, authorization_id, task_id, execution_id,
|
||||
command_sha256, status, card_signature, detail_signature,
|
||||
observed_title, selected_sku, quantity, unit_price_cents,
|
||||
total_price_cents, evidence_asset_id, evidence_sha256,
|
||||
started_at, ready_at
|
||||
FROM order_dry_runs WHERE authorization_id = ?`,
|
||||
authorizationID,
|
||||
).Scan(
|
||||
&result.ID,
|
||||
&result.AuthorizationID,
|
||||
&result.TaskID,
|
||||
&result.ExecutionID,
|
||||
&result.CommandSHA256,
|
||||
&result.Status,
|
||||
&card,
|
||||
&detail,
|
||||
&title,
|
||||
&sku,
|
||||
&quantity,
|
||||
&unitPrice,
|
||||
&totalPrice,
|
||||
&evidenceID,
|
||||
&evidenceSHA,
|
||||
&startedAt,
|
||||
&readyAt,
|
||||
)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return domain.OrderDryRun{}, false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return domain.OrderDryRun{}, false, repositoryFailure(err)
|
||||
}
|
||||
result.CardSignature = dryRunOptionalString(card)
|
||||
result.DetailSignature = dryRunOptionalString(detail)
|
||||
result.ObservedTitle = dryRunOptionalString(title)
|
||||
result.SelectedSKU = dryRunOptionalString(sku)
|
||||
if quantity.Valid {
|
||||
value := int(quantity.Int64)
|
||||
result.Quantity = &value
|
||||
}
|
||||
if unitPrice.Valid {
|
||||
result.UnitPriceCents = &unitPrice.Int64
|
||||
}
|
||||
if totalPrice.Valid {
|
||||
result.TotalPriceCents = &totalPrice.Int64
|
||||
}
|
||||
result.EvidenceAssetID = dryRunOptionalString(evidenceID)
|
||||
result.EvidenceSHA256 = dryRunOptionalString(evidenceSHA)
|
||||
parsed, err := parseTimestamp(startedAt)
|
||||
if err != nil {
|
||||
return domain.OrderDryRun{}, false, repositoryFailure(err)
|
||||
}
|
||||
result.StartedAt = parsed
|
||||
if readyAt.Valid {
|
||||
parsed, parseErr := parseTimestamp(readyAt.String)
|
||||
if parseErr != nil {
|
||||
return domain.OrderDryRun{}, false, repositoryFailure(parseErr)
|
||||
}
|
||||
result.ReadyAt = &parsed
|
||||
}
|
||||
return result, true, nil
|
||||
}
|
||||
|
||||
func dryRunOptionalString(value sql.NullString) *string {
|
||||
if !value.Valid {
|
||||
return nil
|
||||
}
|
||||
return &value.String
|
||||
}
|
||||
|
||||
func replayOrderDryRunRequest(
|
||||
ctx context.Context,
|
||||
tx *sql.Tx,
|
||||
deviceID, operation, idempotencyKey, requestSHA256, taskID string,
|
||||
) (domain.OrderDryRun, bool, error) {
|
||||
var knownHash, knownTask, dryRunID string
|
||||
err := tx.QueryRowContext(
|
||||
ctx,
|
||||
`SELECT request_sha256, task_id, dry_run_id
|
||||
FROM device_order_dry_run_requests
|
||||
WHERE device_id = ? AND operation = ? AND idempotency_key = ?`,
|
||||
deviceID,
|
||||
operation,
|
||||
idempotencyKey,
|
||||
).Scan(&knownHash, &knownTask, &dryRunID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return domain.OrderDryRun{}, false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return domain.OrderDryRun{}, false, repositoryFailure(err)
|
||||
}
|
||||
if knownHash != requestSHA256 || knownTask != taskID {
|
||||
return domain.OrderDryRun{}, false, usecase.ErrIdempotencyConflict
|
||||
}
|
||||
var authorizationID string
|
||||
if err := tx.QueryRowContext(
|
||||
ctx,
|
||||
`SELECT authorization_id FROM order_dry_runs WHERE id = ?`,
|
||||
dryRunID,
|
||||
).Scan(&authorizationID); err != nil {
|
||||
return domain.OrderDryRun{}, false, repositoryFailure(err)
|
||||
}
|
||||
result, found, err := getOrderDryRunByAuthorization(
|
||||
ctx, tx, authorizationID,
|
||||
)
|
||||
if err != nil || !found {
|
||||
if err != nil {
|
||||
return domain.OrderDryRun{}, false, err
|
||||
}
|
||||
return domain.OrderDryRun{}, false, usecase.ErrRepositoryInvariant
|
||||
}
|
||||
return result, true, nil
|
||||
}
|
||||
|
||||
func commitOrderDryRunReplay(
|
||||
tx *sql.Tx,
|
||||
result domain.OrderDryRun,
|
||||
found bool,
|
||||
err error,
|
||||
) (domain.OrderDryRun, bool, error) {
|
||||
if err != nil || !found {
|
||||
return domain.OrderDryRun{}, false, err
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return domain.OrderDryRun{}, false, repositoryFailure(err)
|
||||
}
|
||||
return result, true, nil
|
||||
}
|
||||
|
||||
func insertOrderDryRunRequest(
|
||||
ctx context.Context,
|
||||
tx *sql.Tx,
|
||||
deviceID, operation, idempotencyKey, requestSHA256, taskID, dryRunID string,
|
||||
now time.Time,
|
||||
) error {
|
||||
_, err := tx.ExecContext(
|
||||
ctx,
|
||||
`INSERT INTO device_order_dry_run_requests (
|
||||
device_id, operation, idempotency_key, request_sha256,
|
||||
task_id, dry_run_id, created_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
deviceID,
|
||||
operation,
|
||||
idempotencyKey,
|
||||
requestSHA256,
|
||||
taskID,
|
||||
dryRunID,
|
||||
formatTimestamp(now.UTC()),
|
||||
)
|
||||
if err != nil {
|
||||
return repositoryFailure(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user