|
|
|
@@ -0,0 +1,684 @@
|
|
|
|
|
package sqlite
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"database/sql"
|
|
|
|
|
"errors"
|
|
|
|
|
|
|
|
|
|
"cmroubao/backend-api/internal/domain"
|
|
|
|
|
"cmroubao/backend-api/internal/usecase"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func (s *Store) CreateOrderAuthorization(
|
|
|
|
|
ctx context.Context,
|
|
|
|
|
write usecase.CreateOrderAuthorizationWrite,
|
|
|
|
|
) (domain.OrderAuthorization, bool, error) {
|
|
|
|
|
tx, err := s.db.BeginTx(ctx, nil)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return domain.OrderAuthorization{}, false, repositoryFailure(err)
|
|
|
|
|
}
|
|
|
|
|
defer func() { _ = tx.Rollback() }()
|
|
|
|
|
|
|
|
|
|
existing, found, err := lookupOrderAuthorizationRequest(
|
|
|
|
|
ctx,
|
|
|
|
|
tx,
|
|
|
|
|
write.Command.ActorUserID,
|
|
|
|
|
write.Command.IdempotencyKey,
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return domain.OrderAuthorization{}, false, err
|
|
|
|
|
}
|
|
|
|
|
if found {
|
|
|
|
|
if existing.RequestSHA256 != write.RequestSHA256 ||
|
|
|
|
|
existing.TaskID != write.Command.TaskID {
|
|
|
|
|
return domain.OrderAuthorization{}, false, usecase.ErrIdempotencyConflict
|
|
|
|
|
}
|
|
|
|
|
authorization, err := getOrderAuthorization(
|
|
|
|
|
ctx,
|
|
|
|
|
tx,
|
|
|
|
|
existing.AuthorizationID,
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return domain.OrderAuthorization{}, false, err
|
|
|
|
|
}
|
|
|
|
|
if err := tx.Commit(); err != nil {
|
|
|
|
|
return domain.OrderAuthorization{}, false, repositoryFailure(err)
|
|
|
|
|
}
|
|
|
|
|
return authorization, true, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
task, err := getTaskByID(
|
|
|
|
|
ctx,
|
|
|
|
|
tx,
|
|
|
|
|
localAdminSubject,
|
|
|
|
|
write.Command.TaskID,
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return domain.OrderAuthorization{}, false, err
|
|
|
|
|
}
|
|
|
|
|
if task.Status != domain.TaskStatusWaitingConfirmation ||
|
|
|
|
|
task.Version != write.Command.ExpectedTaskVersion ||
|
|
|
|
|
task.CancelRequestedAt != nil ||
|
|
|
|
|
task.ClaimedByUserID == nil ||
|
|
|
|
|
task.ClaimedByDeviceID == nil ||
|
|
|
|
|
task.ClaimExpiresAt == nil ||
|
|
|
|
|
!write.Now.Before(*task.ClaimExpiresAt) ||
|
|
|
|
|
usecase.TaskContentSHA256(task) != write.Command.TaskContentSHA256 {
|
|
|
|
|
return domain.OrderAuthorization{}, false, usecase.ErrTaskStateConflict
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var executionUserID, executionDeviceID string
|
|
|
|
|
var executionClaimGeneration int64
|
|
|
|
|
var executionFinishedAt sql.NullString
|
|
|
|
|
err = tx.QueryRowContext(
|
|
|
|
|
ctx,
|
|
|
|
|
`SELECT user_id, device_id, claim_generation, finished_at
|
|
|
|
|
FROM task_executions
|
|
|
|
|
WHERE id = ? AND task_id = ?`,
|
|
|
|
|
write.Command.ExecutionID,
|
|
|
|
|
write.Command.TaskID,
|
|
|
|
|
).Scan(
|
|
|
|
|
&executionUserID,
|
|
|
|
|
&executionDeviceID,
|
|
|
|
|
&executionClaimGeneration,
|
|
|
|
|
&executionFinishedAt,
|
|
|
|
|
)
|
|
|
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
|
|
|
return domain.OrderAuthorization{}, false, usecase.ErrTaskStateConflict
|
|
|
|
|
}
|
|
|
|
|
if err != nil {
|
|
|
|
|
return domain.OrderAuthorization{}, false, repositoryFailure(err)
|
|
|
|
|
}
|
|
|
|
|
if executionFinishedAt.Valid ||
|
|
|
|
|
executionUserID != *task.ClaimedByUserID ||
|
|
|
|
|
executionDeviceID != *task.ClaimedByDeviceID ||
|
|
|
|
|
executionClaimGeneration != task.ClaimGeneration {
|
|
|
|
|
return domain.OrderAuthorization{}, false, usecase.ErrTaskStateConflict
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
observations, err := orderAuthorizationObservations(
|
|
|
|
|
ctx,
|
|
|
|
|
tx,
|
|
|
|
|
write.Command.TaskID,
|
|
|
|
|
write.Command.ExecutionID,
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return domain.OrderAuthorization{}, false, err
|
|
|
|
|
}
|
|
|
|
|
if len(observations) == 0 ||
|
|
|
|
|
len(observations) != len(write.Command.Items) {
|
|
|
|
|
return domain.OrderAuthorization{}, false, usecase.ErrTaskStateConflict
|
|
|
|
|
}
|
|
|
|
|
byKey := make(map[string]orderAuthorizationObservation, len(observations))
|
|
|
|
|
for _, observation := range observations {
|
|
|
|
|
byKey[observation.CandidateKey] = observation
|
|
|
|
|
}
|
|
|
|
|
selected, found := byKey[write.Command.CandidateKey]
|
|
|
|
|
if !found {
|
|
|
|
|
return domain.OrderAuthorization{}, false, usecase.ErrTaskStateConflict
|
|
|
|
|
}
|
|
|
|
|
for _, item := range write.Command.Items {
|
|
|
|
|
if _, found := byKey[item.CandidateKey]; !found {
|
|
|
|
|
return domain.OrderAuthorization{}, false, usecase.ErrTaskStateConflict
|
|
|
|
|
}
|
|
|
|
|
for _, reason := range item.ReasonCodes {
|
|
|
|
|
if task.MaxBudgetCents == nil &&
|
|
|
|
|
(reason == "PRICE_ACCEPTABLE" || reason == "PRICE_TOO_HIGH") {
|
|
|
|
|
return domain.OrderAuthorization{}, false, usecase.ErrTaskStateConflict
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
activeID, activeStatus, err := activeOrderAuthorization(
|
|
|
|
|
ctx,
|
|
|
|
|
tx,
|
|
|
|
|
write.Command.ExecutionID,
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return domain.OrderAuthorization{}, false, err
|
|
|
|
|
}
|
|
|
|
|
switch {
|
|
|
|
|
case activeID == nil && write.Command.SupersedesAuthorizationID != nil:
|
|
|
|
|
return domain.OrderAuthorization{}, false, usecase.ErrTaskStateConflict
|
|
|
|
|
case activeID != nil &&
|
|
|
|
|
(write.Command.SupersedesAuthorizationID == nil ||
|
|
|
|
|
*write.Command.SupersedesAuthorizationID != *activeID ||
|
|
|
|
|
activeStatus != domain.OrderAuthorizationPendingDelivery):
|
|
|
|
|
return domain.OrderAuthorization{}, false, usecase.ErrTaskStateConflict
|
|
|
|
|
}
|
|
|
|
|
if activeID != nil {
|
|
|
|
|
result, err := tx.ExecContext(
|
|
|
|
|
ctx,
|
|
|
|
|
`UPDATE order_authorizations
|
|
|
|
|
SET status = 'SUPERSEDED'
|
|
|
|
|
WHERE id = ? AND status = 'PENDING_DELIVERY'`,
|
|
|
|
|
*activeID,
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return domain.OrderAuthorization{}, false, repositoryFailure(err)
|
|
|
|
|
}
|
|
|
|
|
affected, err := result.RowsAffected()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return domain.OrderAuthorization{}, false, repositoryFailure(err)
|
|
|
|
|
}
|
|
|
|
|
if affected != 1 {
|
|
|
|
|
return domain.OrderAuthorization{}, false, usecase.ErrTaskStateConflict
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
latestReviewID, latestReviewVersion, err := latestCandidateHumanReview(
|
|
|
|
|
ctx,
|
|
|
|
|
tx,
|
|
|
|
|
write.Command.ExecutionID,
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return domain.OrderAuthorization{}, false, err
|
|
|
|
|
}
|
|
|
|
|
review := domain.CandidateHumanReview{
|
|
|
|
|
ID: write.ReviewID,
|
|
|
|
|
TaskID: write.Command.TaskID,
|
|
|
|
|
ExecutionID: write.Command.ExecutionID,
|
|
|
|
|
TaskContentSHA256: write.Command.TaskContentSHA256,
|
|
|
|
|
Version: latestReviewVersion + 1,
|
|
|
|
|
ReasonSchemaVersion: write.Command.ReasonSchemaVersion,
|
|
|
|
|
Outcome: "CANDIDATE_ACCEPTED",
|
|
|
|
|
SelectedCandidateOrdinal: &selected.Ordinal,
|
|
|
|
|
PrimaryReasonCode: write.Command.PrimaryReasonCode,
|
|
|
|
|
Note: write.Command.Note,
|
|
|
|
|
SupersedesReviewID: latestReviewID,
|
|
|
|
|
ActorUserID: write.Command.ActorUserID,
|
|
|
|
|
CreatedAt: write.Now,
|
|
|
|
|
Items: make([]domain.CandidateHumanReviewItem, 0, len(write.Command.Items)),
|
|
|
|
|
}
|
|
|
|
|
for _, item := range write.Command.Items {
|
|
|
|
|
observation := byKey[item.CandidateKey]
|
|
|
|
|
review.Items = append(review.Items, domain.CandidateHumanReviewItem{
|
|
|
|
|
CandidateOrdinal: observation.Ordinal,
|
|
|
|
|
Label: item.Label,
|
|
|
|
|
PrimaryReasonCode: item.PrimaryReasonCode,
|
|
|
|
|
ReasonCodes: append([]string(nil), item.ReasonCodes...),
|
|
|
|
|
Note: item.Note,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
if err := insertAdminCandidateReview(ctx, tx, review); err != nil {
|
|
|
|
|
return domain.OrderAuthorization{}, false, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
authorizationVersion, err := nextOrderAuthorizationVersion(
|
|
|
|
|
ctx,
|
|
|
|
|
tx,
|
|
|
|
|
write.Command.ExecutionID,
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return domain.OrderAuthorization{}, false, err
|
|
|
|
|
}
|
|
|
|
|
authorization := domain.OrderAuthorization{
|
|
|
|
|
ID: write.AuthorizationID,
|
|
|
|
|
TaskID: task.ID,
|
|
|
|
|
ExecutionID: write.Command.ExecutionID,
|
|
|
|
|
AuthorizationVersion: authorizationVersion,
|
|
|
|
|
CandidateKey: selected.CandidateKey,
|
|
|
|
|
TaskContentSHA256: write.Command.TaskContentSHA256,
|
|
|
|
|
TaskVersion: task.Version + 1,
|
|
|
|
|
ReviewID: review.ID,
|
|
|
|
|
ReviewVersion: review.Version,
|
|
|
|
|
UserID: executionUserID,
|
|
|
|
|
DeviceID: executionDeviceID,
|
|
|
|
|
ClaimGeneration: executionClaimGeneration,
|
|
|
|
|
OriginalSKU: task.SKU,
|
|
|
|
|
Quantity: task.Quantity,
|
|
|
|
|
CandidateSKUText: selected.SKUText,
|
|
|
|
|
CandidatePriceText: selected.PriceText,
|
|
|
|
|
CardSignature: selected.CardSignature,
|
|
|
|
|
DetailSignature: selected.DetailSignature,
|
|
|
|
|
DetailEvidenceSHA256: selected.DetailEvidenceSHA256,
|
|
|
|
|
SpecificationEvidenceSHA256: selected.SpecificationEvidenceSHA256,
|
|
|
|
|
Status: domain.OrderAuthorizationPendingDelivery,
|
|
|
|
|
SupersedesAuthorizationID: write.Command.SupersedesAuthorizationID,
|
|
|
|
|
CreatedByUserID: write.Command.ActorUserID,
|
|
|
|
|
CreatedAt: write.Now,
|
|
|
|
|
}
|
|
|
|
|
if err := insertOrderAuthorization(ctx, tx, authorization); err != nil {
|
|
|
|
|
return domain.OrderAuthorization{}, false, err
|
|
|
|
|
}
|
|
|
|
|
result, err := tx.ExecContext(
|
|
|
|
|
ctx,
|
|
|
|
|
`UPDATE purchase_tasks
|
|
|
|
|
SET version = version + 1, updated_at = ?
|
|
|
|
|
WHERE id = ? AND status = 'WAITING_CONFIRMATION'
|
|
|
|
|
AND version = ? AND cancel_requested_at IS NULL`,
|
|
|
|
|
formatTimestamp(write.Now),
|
|
|
|
|
task.ID,
|
|
|
|
|
task.Version,
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return domain.OrderAuthorization{}, false, repositoryFailure(err)
|
|
|
|
|
}
|
|
|
|
|
affected, err := result.RowsAffected()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return domain.OrderAuthorization{}, false, repositoryFailure(err)
|
|
|
|
|
}
|
|
|
|
|
if affected != 1 {
|
|
|
|
|
return domain.OrderAuthorization{}, false, usecase.ErrTaskStateConflict
|
|
|
|
|
}
|
|
|
|
|
if err := insertTaskEvent(ctx, tx, write.Event); err != nil {
|
|
|
|
|
return domain.OrderAuthorization{}, false, err
|
|
|
|
|
}
|
|
|
|
|
_, err = tx.ExecContext(
|
|
|
|
|
ctx,
|
|
|
|
|
`INSERT INTO admin_order_authorization_requests (
|
|
|
|
|
actor_user_id, idempotency_key, request_sha256,
|
|
|
|
|
task_id, authorization_id, created_at
|
|
|
|
|
) VALUES (?, ?, ?, ?, ?, ?)`,
|
|
|
|
|
write.Command.ActorUserID,
|
|
|
|
|
write.Command.IdempotencyKey,
|
|
|
|
|
write.RequestSHA256,
|
|
|
|
|
write.Command.TaskID,
|
|
|
|
|
authorization.ID,
|
|
|
|
|
formatTimestamp(write.Now),
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return domain.OrderAuthorization{}, false, repositoryFailure(err)
|
|
|
|
|
}
|
|
|
|
|
if err := tx.Commit(); err != nil {
|
|
|
|
|
return domain.OrderAuthorization{}, false, repositoryFailure(err)
|
|
|
|
|
}
|
|
|
|
|
return authorization, false, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type orderAuthorizationRequestRecord struct {
|
|
|
|
|
RequestSHA256 string
|
|
|
|
|
TaskID string
|
|
|
|
|
AuthorizationID string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func lookupOrderAuthorizationRequest(
|
|
|
|
|
ctx context.Context,
|
|
|
|
|
queryer queryRower,
|
|
|
|
|
actorUserID string,
|
|
|
|
|
idempotencyKey string,
|
|
|
|
|
) (orderAuthorizationRequestRecord, bool, error) {
|
|
|
|
|
var record orderAuthorizationRequestRecord
|
|
|
|
|
err := queryer.QueryRowContext(
|
|
|
|
|
ctx,
|
|
|
|
|
`SELECT request_sha256, task_id, authorization_id
|
|
|
|
|
FROM admin_order_authorization_requests
|
|
|
|
|
WHERE actor_user_id = ? AND idempotency_key = ?`,
|
|
|
|
|
actorUserID,
|
|
|
|
|
idempotencyKey,
|
|
|
|
|
).Scan(
|
|
|
|
|
&record.RequestSHA256,
|
|
|
|
|
&record.TaskID,
|
|
|
|
|
&record.AuthorizationID,
|
|
|
|
|
)
|
|
|
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
|
|
|
return orderAuthorizationRequestRecord{}, false, nil
|
|
|
|
|
}
|
|
|
|
|
if err != nil {
|
|
|
|
|
return orderAuthorizationRequestRecord{}, false, repositoryFailure(err)
|
|
|
|
|
}
|
|
|
|
|
return record, true, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type orderAuthorizationObservation struct {
|
|
|
|
|
CandidateKey string
|
|
|
|
|
Ordinal int
|
|
|
|
|
SKUText string
|
|
|
|
|
PriceText string
|
|
|
|
|
CardSignature string
|
|
|
|
|
DetailSignature string
|
|
|
|
|
DetailEvidenceSHA256 string
|
|
|
|
|
SpecificationEvidenceSHA256 string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func orderAuthorizationObservations(
|
|
|
|
|
ctx context.Context,
|
|
|
|
|
queryer queryer,
|
|
|
|
|
taskID string,
|
|
|
|
|
executionID string,
|
|
|
|
|
) ([]orderAuthorizationObservation, error) {
|
|
|
|
|
rows, err := queryer.QueryContext(
|
|
|
|
|
ctx,
|
|
|
|
|
`SELECT identity.candidate_key, observation.ordinal,
|
|
|
|
|
observation.sku_text, observation.price_text,
|
|
|
|
|
identity.card_signature, identity.detail_signature,
|
|
|
|
|
identity.detail_evidence_sha256,
|
|
|
|
|
identity.specification_evidence_sha256
|
|
|
|
|
FROM candidate_observations AS observation
|
|
|
|
|
JOIN candidate_observation_identities AS identity
|
|
|
|
|
ON identity.execution_id = observation.execution_id
|
|
|
|
|
AND identity.candidate_ordinal = observation.ordinal
|
|
|
|
|
WHERE observation.task_id = ? AND observation.execution_id = ?
|
|
|
|
|
ORDER BY observation.ordinal ASC`,
|
|
|
|
|
taskID,
|
|
|
|
|
executionID,
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, repositoryFailure(err)
|
|
|
|
|
}
|
|
|
|
|
defer rows.Close()
|
|
|
|
|
result := make([]orderAuthorizationObservation, 0, 5)
|
|
|
|
|
for rows.Next() {
|
|
|
|
|
var observation orderAuthorizationObservation
|
|
|
|
|
if err := rows.Scan(
|
|
|
|
|
&observation.CandidateKey,
|
|
|
|
|
&observation.Ordinal,
|
|
|
|
|
&observation.SKUText,
|
|
|
|
|
&observation.PriceText,
|
|
|
|
|
&observation.CardSignature,
|
|
|
|
|
&observation.DetailSignature,
|
|
|
|
|
&observation.DetailEvidenceSHA256,
|
|
|
|
|
&observation.SpecificationEvidenceSHA256,
|
|
|
|
|
); err != nil {
|
|
|
|
|
return nil, repositoryFailure(err)
|
|
|
|
|
}
|
|
|
|
|
result = append(result, observation)
|
|
|
|
|
}
|
|
|
|
|
if err := rows.Err(); err != nil {
|
|
|
|
|
return nil, repositoryFailure(err)
|
|
|
|
|
}
|
|
|
|
|
return result, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func activeOrderAuthorization(
|
|
|
|
|
ctx context.Context,
|
|
|
|
|
queryer queryRower,
|
|
|
|
|
executionID string,
|
|
|
|
|
) (*string, domain.OrderAuthorizationStatus, error) {
|
|
|
|
|
var id string
|
|
|
|
|
var status domain.OrderAuthorizationStatus
|
|
|
|
|
err := queryer.QueryRowContext(
|
|
|
|
|
ctx,
|
|
|
|
|
`SELECT id, status
|
|
|
|
|
FROM order_authorizations
|
|
|
|
|
WHERE execution_id = ?
|
|
|
|
|
AND status IN (
|
|
|
|
|
'PENDING_DELIVERY',
|
|
|
|
|
'DELIVERED',
|
|
|
|
|
'ACKNOWLEDGED',
|
|
|
|
|
'EXECUTING'
|
|
|
|
|
)
|
|
|
|
|
LIMIT 1`,
|
|
|
|
|
executionID,
|
|
|
|
|
).Scan(&id, &status)
|
|
|
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
|
|
|
return nil, "", nil
|
|
|
|
|
}
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, "", repositoryFailure(err)
|
|
|
|
|
}
|
|
|
|
|
return &id, status, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func nextOrderAuthorizationVersion(
|
|
|
|
|
ctx context.Context,
|
|
|
|
|
queryer queryRower,
|
|
|
|
|
executionID string,
|
|
|
|
|
) (int, error) {
|
|
|
|
|
var version int
|
|
|
|
|
err := queryer.QueryRowContext(
|
|
|
|
|
ctx,
|
|
|
|
|
`SELECT COALESCE(MAX(authorization_version), 0) + 1
|
|
|
|
|
FROM order_authorizations
|
|
|
|
|
WHERE execution_id = ?`,
|
|
|
|
|
executionID,
|
|
|
|
|
).Scan(&version)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return 0, repositoryFailure(err)
|
|
|
|
|
}
|
|
|
|
|
return version, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func insertAdminCandidateReview(
|
|
|
|
|
ctx context.Context,
|
|
|
|
|
tx *sql.Tx,
|
|
|
|
|
review domain.CandidateHumanReview,
|
|
|
|
|
) error {
|
|
|
|
|
_, err := tx.ExecContext(
|
|
|
|
|
ctx,
|
|
|
|
|
`INSERT INTO candidate_human_reviews (
|
|
|
|
|
id, execution_id, task_id, version, reason_schema_version, outcome,
|
|
|
|
|
selected_candidate_ordinal, primary_reason_code, note,
|
|
|
|
|
supersedes_review_id, actor_user_id, actor_device_id, created_at,
|
|
|
|
|
received_after_execution_expiry
|
|
|
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, 0)`,
|
|
|
|
|
review.ID,
|
|
|
|
|
review.ExecutionID,
|
|
|
|
|
review.TaskID,
|
|
|
|
|
review.Version,
|
|
|
|
|
review.ReasonSchemaVersion,
|
|
|
|
|
review.Outcome,
|
|
|
|
|
nullableInt(review.SelectedCandidateOrdinal),
|
|
|
|
|
review.PrimaryReasonCode,
|
|
|
|
|
review.Note,
|
|
|
|
|
nullableString(review.SupersedesReviewID),
|
|
|
|
|
review.ActorUserID,
|
|
|
|
|
formatTimestamp(review.CreatedAt),
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return repositoryFailure(err)
|
|
|
|
|
}
|
|
|
|
|
for _, item := range review.Items {
|
|
|
|
|
_, err = tx.ExecContext(
|
|
|
|
|
ctx,
|
|
|
|
|
`INSERT INTO candidate_human_review_items (
|
|
|
|
|
review_id, candidate_ordinal, label, primary_reason_code, note
|
|
|
|
|
) VALUES (?, ?, ?, ?, ?)`,
|
|
|
|
|
review.ID,
|
|
|
|
|
item.CandidateOrdinal,
|
|
|
|
|
item.Label,
|
|
|
|
|
item.PrimaryReasonCode,
|
|
|
|
|
item.Note,
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return repositoryFailure(err)
|
|
|
|
|
}
|
|
|
|
|
for _, reason := range item.ReasonCodes {
|
|
|
|
|
_, err = tx.ExecContext(
|
|
|
|
|
ctx,
|
|
|
|
|
`INSERT INTO candidate_human_review_reasons (
|
|
|
|
|
review_id, candidate_ordinal, reason_code
|
|
|
|
|
) VALUES (?, ?, ?)`,
|
|
|
|
|
review.ID,
|
|
|
|
|
item.CandidateOrdinal,
|
|
|
|
|
reason,
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return repositoryFailure(err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func insertOrderAuthorization(
|
|
|
|
|
ctx context.Context,
|
|
|
|
|
tx *sql.Tx,
|
|
|
|
|
authorization domain.OrderAuthorization,
|
|
|
|
|
) error {
|
|
|
|
|
_, err := tx.ExecContext(
|
|
|
|
|
ctx,
|
|
|
|
|
`INSERT INTO order_authorizations (
|
|
|
|
|
id, task_id, execution_id, authorization_version, candidate_key,
|
|
|
|
|
task_content_sha256, task_version, review_id, review_version,
|
|
|
|
|
user_id, device_id, claim_generation, original_sku, quantity,
|
|
|
|
|
candidate_sku_text, candidate_price_text, card_signature,
|
|
|
|
|
detail_signature, detail_evidence_sha256,
|
|
|
|
|
specification_evidence_sha256, status,
|
|
|
|
|
supersedes_authorization_id, created_by_user_id, created_at
|
|
|
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
|
|
|
|
?, ?, ?, ?)`,
|
|
|
|
|
authorization.ID,
|
|
|
|
|
authorization.TaskID,
|
|
|
|
|
authorization.ExecutionID,
|
|
|
|
|
authorization.AuthorizationVersion,
|
|
|
|
|
authorization.CandidateKey,
|
|
|
|
|
authorization.TaskContentSHA256,
|
|
|
|
|
authorization.TaskVersion,
|
|
|
|
|
authorization.ReviewID,
|
|
|
|
|
authorization.ReviewVersion,
|
|
|
|
|
authorization.UserID,
|
|
|
|
|
authorization.DeviceID,
|
|
|
|
|
authorization.ClaimGeneration,
|
|
|
|
|
authorization.OriginalSKU,
|
|
|
|
|
authorization.Quantity,
|
|
|
|
|
authorization.CandidateSKUText,
|
|
|
|
|
authorization.CandidatePriceText,
|
|
|
|
|
authorization.CardSignature,
|
|
|
|
|
authorization.DetailSignature,
|
|
|
|
|
authorization.DetailEvidenceSHA256,
|
|
|
|
|
authorization.SpecificationEvidenceSHA256,
|
|
|
|
|
authorization.Status,
|
|
|
|
|
nullableString(authorization.SupersedesAuthorizationID),
|
|
|
|
|
authorization.CreatedByUserID,
|
|
|
|
|
formatTimestamp(authorization.CreatedAt),
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return repositoryFailure(err)
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func getOrderAuthorization(
|
|
|
|
|
ctx context.Context,
|
|
|
|
|
queryer queryRower,
|
|
|
|
|
authorizationID string,
|
|
|
|
|
) (domain.OrderAuthorization, error) {
|
|
|
|
|
authorization, err := scanOrderAuthorization(queryer.QueryRowContext(
|
|
|
|
|
ctx,
|
|
|
|
|
orderAuthorizationSelect+` WHERE id = ?`,
|
|
|
|
|
authorizationID,
|
|
|
|
|
))
|
|
|
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
|
|
|
return domain.OrderAuthorization{}, usecase.ErrRepositoryNotFound
|
|
|
|
|
}
|
|
|
|
|
if err != nil {
|
|
|
|
|
return domain.OrderAuthorization{}, repositoryFailure(err)
|
|
|
|
|
}
|
|
|
|
|
return authorization, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func listOrderAuthorizations(
|
|
|
|
|
ctx context.Context,
|
|
|
|
|
queryer queryer,
|
|
|
|
|
taskID string,
|
|
|
|
|
) ([]domain.OrderAuthorization, error) {
|
|
|
|
|
rows, err := queryer.QueryContext(
|
|
|
|
|
ctx,
|
|
|
|
|
orderAuthorizationSelect+
|
|
|
|
|
` WHERE task_id = ?
|
|
|
|
|
ORDER BY authorization_version ASC`,
|
|
|
|
|
taskID,
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, repositoryFailure(err)
|
|
|
|
|
}
|
|
|
|
|
defer rows.Close()
|
|
|
|
|
result := make([]domain.OrderAuthorization, 0)
|
|
|
|
|
for rows.Next() {
|
|
|
|
|
authorization, err := scanOrderAuthorization(rows)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, repositoryFailure(err)
|
|
|
|
|
}
|
|
|
|
|
result = append(result, authorization)
|
|
|
|
|
}
|
|
|
|
|
if err := rows.Err(); err != nil {
|
|
|
|
|
return nil, repositoryFailure(err)
|
|
|
|
|
}
|
|
|
|
|
return result, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func scanOrderAuthorization(
|
|
|
|
|
scanner rowScanner,
|
|
|
|
|
) (domain.OrderAuthorization, error) {
|
|
|
|
|
var authorization domain.OrderAuthorization
|
|
|
|
|
var supersedes, deliveredAt, acknowledgedAt sql.NullString
|
|
|
|
|
var executionStartedAt, consumedAt, failedAt, revokedAt sql.NullString
|
|
|
|
|
var failureCode, failureMessage sql.NullString
|
|
|
|
|
var createdAt string
|
|
|
|
|
err := scanner.Scan(
|
|
|
|
|
&authorization.ID,
|
|
|
|
|
&authorization.TaskID,
|
|
|
|
|
&authorization.ExecutionID,
|
|
|
|
|
&authorization.AuthorizationVersion,
|
|
|
|
|
&authorization.CandidateKey,
|
|
|
|
|
&authorization.TaskContentSHA256,
|
|
|
|
|
&authorization.TaskVersion,
|
|
|
|
|
&authorization.ReviewID,
|
|
|
|
|
&authorization.ReviewVersion,
|
|
|
|
|
&authorization.UserID,
|
|
|
|
|
&authorization.DeviceID,
|
|
|
|
|
&authorization.ClaimGeneration,
|
|
|
|
|
&authorization.OriginalSKU,
|
|
|
|
|
&authorization.Quantity,
|
|
|
|
|
&authorization.CandidateSKUText,
|
|
|
|
|
&authorization.CandidatePriceText,
|
|
|
|
|
&authorization.CardSignature,
|
|
|
|
|
&authorization.DetailSignature,
|
|
|
|
|
&authorization.DetailEvidenceSHA256,
|
|
|
|
|
&authorization.SpecificationEvidenceSHA256,
|
|
|
|
|
&authorization.Status,
|
|
|
|
|
&supersedes,
|
|
|
|
|
&authorization.CreatedByUserID,
|
|
|
|
|
&createdAt,
|
|
|
|
|
&deliveredAt,
|
|
|
|
|
&acknowledgedAt,
|
|
|
|
|
&executionStartedAt,
|
|
|
|
|
&consumedAt,
|
|
|
|
|
&failedAt,
|
|
|
|
|
&revokedAt,
|
|
|
|
|
&failureCode,
|
|
|
|
|
&failureMessage,
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return domain.OrderAuthorization{}, err
|
|
|
|
|
}
|
|
|
|
|
if supersedes.Valid {
|
|
|
|
|
authorization.SupersedesAuthorizationID = &supersedes.String
|
|
|
|
|
}
|
|
|
|
|
authorization.CreatedAt, err = parseTimestamp(createdAt)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return domain.OrderAuthorization{}, err
|
|
|
|
|
}
|
|
|
|
|
if authorization.DeliveredAt, err = parseNullableTimestamp(deliveredAt); err != nil {
|
|
|
|
|
return domain.OrderAuthorization{}, err
|
|
|
|
|
}
|
|
|
|
|
if authorization.AcknowledgedAt, err = parseNullableTimestamp(acknowledgedAt); err != nil {
|
|
|
|
|
return domain.OrderAuthorization{}, err
|
|
|
|
|
}
|
|
|
|
|
if authorization.ExecutionStartedAt, err = parseNullableTimestamp(executionStartedAt); err != nil {
|
|
|
|
|
return domain.OrderAuthorization{}, err
|
|
|
|
|
}
|
|
|
|
|
if authorization.ConsumedAt, err = parseNullableTimestamp(consumedAt); err != nil {
|
|
|
|
|
return domain.OrderAuthorization{}, err
|
|
|
|
|
}
|
|
|
|
|
if authorization.FailedAt, err = parseNullableTimestamp(failedAt); err != nil {
|
|
|
|
|
return domain.OrderAuthorization{}, err
|
|
|
|
|
}
|
|
|
|
|
if authorization.RevokedAt, err = parseNullableTimestamp(revokedAt); err != nil {
|
|
|
|
|
return domain.OrderAuthorization{}, err
|
|
|
|
|
}
|
|
|
|
|
if failureCode.Valid {
|
|
|
|
|
authorization.FailureCode = &failureCode.String
|
|
|
|
|
}
|
|
|
|
|
if failureMessage.Valid {
|
|
|
|
|
authorization.FailureMessage = &failureMessage.String
|
|
|
|
|
}
|
|
|
|
|
return authorization, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const orderAuthorizationSelect = `SELECT
|
|
|
|
|
id, task_id, execution_id, authorization_version, candidate_key,
|
|
|
|
|
task_content_sha256, task_version, review_id, review_version,
|
|
|
|
|
user_id, device_id, claim_generation, original_sku, quantity,
|
|
|
|
|
candidate_sku_text, candidate_price_text, card_signature,
|
|
|
|
|
detail_signature, detail_evidence_sha256,
|
|
|
|
|
specification_evidence_sha256, status,
|
|
|
|
|
supersedes_authorization_id, created_by_user_id, created_at,
|
|
|
|
|
delivered_at, acknowledged_at, execution_started_at, consumed_at,
|
|
|
|
|
failed_at, revoked_at, failure_code, failure_message
|
|
|
|
|
FROM order_authorizations`
|
|
|
|
|
|
|
|
|
|
const localAdminSubject = "local-admin"
|
|
|
|
|
|
|
|
|
|
var _ usecase.OrderAuthorizationRepository = (*Store)(nil)
|