Files

949 lines
25 KiB
Go

package sqlite
import (
"context"
"database/sql"
"errors"
"time"
"cmroubao/backend-api/internal/domain"
"cmroubao/backend-api/internal/usecase"
)
const createProcurementTaskOperation = "CREATE_PROCUREMENT_TASK"
func (store *Store) GetProcurementSourceItem(
ctx context.Context,
creatorSubject, itemID string,
) (domain.ProcurementSourceItem, error) {
item, err := scanFreightOrderItem(store.db.QueryRowContext(
ctx,
`SELECT
item.id, item.freight_order_id, item.external_item_id,
item.title, item.product_spec, item.sku, item.quantity,
item.product_thumb_ref, item.original_unit_price_minor,
item.original_currency, item.purchase_status,
item.canonical_sha256, item.revision, item.is_present,
item.first_sync_run_id, item.last_sync_run_id,
item.created_at, item.updated_at
FROM freight_order_items AS item
JOIN freight_orders AS freight
ON freight.id = item.freight_order_id
WHERE freight.creator_subject = ? AND item.id = ?
AND item.is_present = 1`,
creatorSubject,
itemID,
))
if errors.Is(err, sql.ErrNoRows) {
return domain.ProcurementSourceItem{}, usecase.ErrRepositoryNotFound
}
if err != nil {
return domain.ProcurementSourceItem{}, repositoryFailure(err)
}
var canceled sql.NullBool
if err := store.db.QueryRowContext(
ctx,
`SELECT freight.is_canceled
FROM freight_orders AS freight
JOIN freight_order_items AS item
ON item.freight_order_id = freight.id
WHERE freight.creator_subject = ? AND item.id = ?`,
creatorSubject,
itemID,
).Scan(&canceled); err != nil {
return domain.ProcurementSourceItem{}, repositoryFailure(err)
}
var isCanceled *bool
if canceled.Valid {
isCanceled = &canceled.Bool
}
return domain.ProcurementSourceItem{
Item: item,
IsCanceled: isCanceled,
}, nil
}
func (store *Store) CreateProcurementRequest(
ctx context.Context,
candidate domain.ProcurementRequest,
auto *usecase.AutoProcurementReference,
) (domain.ProcurementRequest, bool, bool, error) {
tx, err := store.db.BeginTx(ctx, nil)
if err != nil {
return domain.ProcurementRequest{}, false, false,
repositoryFailure(err)
}
defer tx.Rollback()
existing, err := getProcurementRequestBySource(
ctx,
tx,
candidate.CreatorSubject,
candidate.FreightOrderItemID,
candidate.SourceRevision,
)
if err == nil {
if auto != nil &&
existing.Status == domain.ProcurementNeedsImage {
updated, bindErr := bindAutomaticProcurementReference(
ctx,
tx,
existing,
*auto,
candidate.UpdatedAt,
)
if bindErr != nil {
return domain.ProcurementRequest{}, false, false, bindErr
}
if err := tx.Commit(); err != nil {
return domain.ProcurementRequest{}, false, false,
repositoryFailure(err)
}
return updated, false, true, nil
}
if err := tx.Commit(); err != nil {
return domain.ProcurementRequest{}, false, false,
repositoryFailure(err)
}
return existing, false, false, nil
}
if !errors.Is(err, usecase.ErrRepositoryNotFound) {
return domain.ProcurementRequest{}, false, false, err
}
if err := validateProcurementSource(ctx, tx, candidate); err != nil {
return domain.ProcurementRequest{}, false, false, err
}
referenceUsed := false
if auto != nil {
if candidate.Status != domain.ProcurementNeedsImage {
return domain.ProcurementRequest{}, false, false,
repositoryFailure(usecase.ErrRepositoryInvariant)
}
if err := validateAutomaticProcurementReference(
ctx,
tx,
candidate,
*auto,
); err != nil {
return domain.ProcurementRequest{}, false, false, err
}
if err := insertProcurementReferenceAsset(
ctx,
tx,
auto.Asset,
); err != nil {
return domain.ProcurementRequest{}, false, false, err
}
candidate.ReferenceAssetID = &auto.Asset.ID
candidate.Status = domain.ProcurementReady
referenceUsed = true
}
_, err = tx.ExecContext(
ctx,
`INSERT INTO procurement_requests (
id, creator_subject, freight_order_item_id, source_revision,
source_sha256, title, product_spec, sku, quantity,
source_purchase_status, source_is_canceled,
procurement_confirmed_by_user_id, procurement_confirmed_at,
status, blocking_code, reference_asset_id, purchase_task_id,
created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?)`,
candidate.ID,
candidate.CreatorSubject,
candidate.FreightOrderItemID,
candidate.SourceRevision,
candidate.SourceSHA256,
candidate.Title,
candidate.ProductSpec,
candidate.SKU,
nullableFreightQuantity(candidate.Quantity),
nullableString(candidate.SourcePurchaseStatus),
nullableBool(candidate.SourceIsCanceled),
candidate.ProcurementConfirmedByUserID,
formatTimestamp(candidate.ProcurementConfirmedAt),
candidate.Status,
nullableString(candidate.BlockingCode),
nullableString(candidate.ReferenceAssetID),
formatTimestamp(candidate.CreatedAt),
formatTimestamp(candidate.UpdatedAt),
)
if err != nil {
return domain.ProcurementRequest{}, false, false,
repositoryFailure(err)
}
if auto != nil {
if err := saveProcurementReferenceSource(
ctx,
tx,
candidate.ID,
domain.ProcurementReferenceSource{
Origin: domain.ProcurementReferenceERP,
ProductThumbRef: &auto.ProductThumbRef,
SourceImageSHA256: &auto.SourceImageSHA256,
BoundAt: candidate.UpdatedAt,
},
); err != nil {
return domain.ProcurementRequest{}, false, false, err
}
}
stored, err := getProcurementRequest(
ctx,
tx,
candidate.CreatorSubject,
candidate.ID,
)
if err != nil {
return domain.ProcurementRequest{}, false, false, err
}
if err := tx.Commit(); err != nil {
return domain.ProcurementRequest{}, false, false,
repositoryFailure(err)
}
return stored, true, referenceUsed, nil
}
func (store *Store) ListProcurementRequestsForOrder(
ctx context.Context,
creatorSubject, orderID string,
) ([]domain.ProcurementRequest, error) {
rows, err := store.db.QueryContext(
ctx,
procurementRequestSelect+`
WHERE request.creator_subject = ?
AND item.freight_order_id = ?
ORDER BY item.id, request.source_revision DESC, request.id DESC`,
creatorSubject,
orderID,
)
if err != nil {
return nil, repositoryFailure(err)
}
defer rows.Close()
requests := make([]domain.ProcurementRequest, 0)
for rows.Next() {
request, err := scanProcurementRequest(rows)
if err != nil {
return nil, repositoryFailure(err)
}
requests = append(requests, request)
}
if err := rows.Err(); err != nil {
return nil, repositoryFailure(err)
}
return requests, nil
}
func (store *Store) GetProcurementRequest(
ctx context.Context,
creatorSubject, requestID string,
) (domain.ProcurementRequest, error) {
return getProcurementRequest(ctx, store.db, creatorSubject, requestID)
}
func (store *Store) BindProcurementReference(
ctx context.Context,
creatorSubject, requestID, assetID string,
now time.Time,
) (domain.ProcurementRequest, *string, error) {
tx, err := store.db.BeginTx(ctx, nil)
if err != nil {
return domain.ProcurementRequest{}, nil, repositoryFailure(err)
}
defer tx.Rollback()
request, err := getProcurementRequest(
ctx,
tx,
creatorSubject,
requestID,
)
if err != nil {
return domain.ProcurementRequest{}, nil, err
}
if request.SourceChanged {
if request.PurchaseTaskID == nil {
if err := markProcurementSourceChanged(ctx, tx, request.ID, now); err != nil {
return domain.ProcurementRequest{}, nil, err
}
}
if err := tx.Commit(); err != nil {
return domain.ProcurementRequest{}, nil, repositoryFailure(err)
}
return domain.ProcurementRequest{}, nil,
usecase.ErrProcurementSourceChanged
}
if request.ReferenceAssetID != nil &&
*request.ReferenceAssetID == assetID &&
(request.Status == domain.ProcurementReady ||
request.Status == domain.ProcurementTaskCreated) {
if err := tx.Commit(); err != nil {
return domain.ProcurementRequest{}, nil, repositoryFailure(err)
}
return request, nil, nil
}
if request.Status != domain.ProcurementNeedsImage &&
request.Status != domain.ProcurementReady {
return domain.ProcurementRequest{}, nil,
usecase.ErrProcurementStateConflict
}
var available int
if err := tx.QueryRowContext(
ctx,
`SELECT EXISTS (
SELECT 1
FROM assets AS asset
WHERE asset.id = ? AND asset.creator_subject = ?
AND asset.purpose = 'TASK_REFERENCE'
AND NOT EXISTS (
SELECT 1 FROM purchase_tasks AS task
WHERE task.image_asset_id = asset.id
)
AND NOT EXISTS (
SELECT 1 FROM procurement_requests AS other
WHERE other.reference_asset_id = asset.id
AND other.id != ?
)
)`,
assetID,
creatorSubject,
requestID,
).Scan(&available); err != nil {
return domain.ProcurementRequest{}, nil, repositoryFailure(err)
}
if available != 1 {
return domain.ProcurementRequest{}, nil, usecase.ErrAssetUnavailable
}
var replacedStorageKey *string
var replacedAssetID *string
if request.ReferenceAssetID != nil &&
request.ReferenceOrigin != nil &&
*request.ReferenceOrigin == domain.ProcurementReferenceERP {
replaced, getErr := getAssetByID(
ctx,
tx,
creatorSubject,
*request.ReferenceAssetID,
)
if getErr != nil {
return domain.ProcurementRequest{}, nil, getErr
}
replacedStorageKey = &replaced.StorageKey
replacedAssetID = &replaced.ID
}
if _, err := tx.ExecContext(
ctx,
`UPDATE procurement_requests
SET reference_asset_id = ?, status = 'READY', blocking_code = NULL,
updated_at = ?
WHERE id = ? AND status IN ('NEEDS_IMAGE', 'READY')`,
assetID,
formatTimestamp(now),
requestID,
); err != nil {
return domain.ProcurementRequest{}, nil, repositoryFailure(err)
}
if err := saveProcurementReferenceSource(
ctx,
tx,
requestID,
domain.ProcurementReferenceSource{
Origin: domain.ProcurementReferenceManual,
BoundAt: now,
},
); err != nil {
return domain.ProcurementRequest{}, nil, err
}
if replacedAssetID != nil {
if _, err := tx.ExecContext(
ctx,
`DELETE FROM assets WHERE id = ?`,
*replacedAssetID,
); err != nil {
return domain.ProcurementRequest{}, nil, repositoryFailure(err)
}
}
updated, err := getProcurementRequest(ctx, tx, creatorSubject, requestID)
if err != nil {
return domain.ProcurementRequest{}, nil, err
}
if err := tx.Commit(); err != nil {
return domain.ProcurementRequest{}, nil, repositoryFailure(err)
}
return updated, replacedStorageKey, nil
}
func (store *Store) CreateProcurementTask(
ctx context.Context,
expected domain.ProcurementRequest,
task domain.PurchaseTask,
event domain.TaskEvent,
source domain.PurchaseTaskSource,
idempotencyKey, requestSHA256 string,
) (domain.PurchaseTask, bool, error) {
tx, err := store.db.BeginTx(ctx, nil)
if err != nil {
return domain.PurchaseTask{}, false, repositoryFailure(err)
}
defer tx.Rollback()
existingHash, resourceID, found, err := lookupIdempotency(
ctx,
tx,
task.CreatorSubject,
createProcurementTaskOperation,
idempotencyKey,
)
if err != nil {
return domain.PurchaseTask{}, false, err
}
if found {
if existingHash != requestSHA256 {
return domain.PurchaseTask{}, false, usecase.ErrIdempotencyConflict
}
existing, err := getTaskByID(
ctx,
tx,
task.CreatorSubject,
resourceID,
)
if err != nil {
return domain.PurchaseTask{}, false, err
}
if err := tx.Commit(); err != nil {
return domain.PurchaseTask{}, false, repositoryFailure(err)
}
return existing, false, nil
}
request, err := getProcurementRequest(
ctx,
tx,
task.CreatorSubject,
expected.ID,
)
if err != nil {
return domain.PurchaseTask{}, false, err
}
if request.Status == domain.ProcurementTaskCreated &&
request.PurchaseTaskID != nil {
existing, err := getTaskByID(
ctx,
tx,
task.CreatorSubject,
*request.PurchaseTaskID,
)
if err != nil {
return domain.PurchaseTask{}, false, err
}
if err := insertIdempotency(
ctx,
tx,
task.CreatorSubject,
createProcurementTaskOperation,
idempotencyKey,
requestSHA256,
"PURCHASE_TASK",
existing.ID,
task.CreatedAt,
); err != nil {
return domain.PurchaseTask{}, false, err
}
if err := tx.Commit(); err != nil {
return domain.PurchaseTask{}, false, repositoryFailure(err)
}
return existing, false, nil
}
if request.SourceChanged {
if request.PurchaseTaskID == nil {
if err := markProcurementSourceChanged(
ctx,
tx,
request.ID,
task.CreatedAt,
); err != nil {
return domain.PurchaseTask{}, false, err
}
}
if err := tx.Commit(); err != nil {
return domain.PurchaseTask{}, false, repositoryFailure(err)
}
return domain.PurchaseTask{}, false,
usecase.ErrProcurementSourceChanged
}
if request.Status != domain.ProcurementReady ||
request.ReferenceAssetID == nil ||
request.SourceSHA256 != expected.SourceSHA256 {
return domain.PurchaseTask{}, false,
usecase.ErrProcurementStateConflict
}
var assetAvailable int
if err := tx.QueryRowContext(
ctx,
`SELECT EXISTS (
SELECT 1 FROM assets AS asset
WHERE asset.id = ? AND asset.creator_subject = ?
AND asset.purpose = 'TASK_REFERENCE'
AND NOT EXISTS (
SELECT 1 FROM purchase_tasks AS other
WHERE other.image_asset_id = asset.id
)
)`,
*request.ReferenceAssetID,
task.CreatorSubject,
).Scan(&assetAvailable); err != nil {
return domain.PurchaseTask{}, false, repositoryFailure(err)
}
if assetAvailable != 1 {
return domain.PurchaseTask{}, false, usecase.ErrAssetUnavailable
}
_, err = tx.ExecContext(
ctx,
`INSERT INTO purchase_tasks (
id, creator_subject, created_by_user_id, source_ref, title,
description, sku, image_asset_id, quantity, max_budget_cents,
currency, status, version, cancel_reason, canceled_at,
created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, 'CNY', 'PENDING', 1,
NULL, NULL, ?, ?)`,
task.ID,
task.CreatorSubject,
nullableString(task.CreatedByUserID),
nullableString(task.SourceRef),
task.Title,
task.Description,
task.SKU,
task.ImageAssetID,
task.Quantity,
formatTimestamp(task.CreatedAt),
formatTimestamp(task.UpdatedAt),
)
if err != nil {
return domain.PurchaseTask{}, false, repositoryFailure(err)
}
if err := insertTaskEvent(ctx, tx, event); err != nil {
return domain.PurchaseTask{}, false, err
}
_, err = tx.ExecContext(
ctx,
`INSERT INTO purchase_task_sources (
task_id, procurement_request_id, freight_order_item_id,
source_revision, source_sha256, created_at
) VALUES (?, ?, ?, ?, ?, ?)`,
source.TaskID,
source.ProcurementRequestID,
source.FreightOrderItemID,
source.SourceRevision,
source.SourceSHA256,
formatTimestamp(source.CreatedAt),
)
if err != nil {
return domain.PurchaseTask{}, false, repositoryFailure(err)
}
result, err := tx.ExecContext(
ctx,
`UPDATE procurement_requests
SET status = 'TASK_CREATED', purchase_task_id = ?, updated_at = ?
WHERE id = ? AND status = 'READY'`,
task.ID,
formatTimestamp(task.CreatedAt),
request.ID,
)
if err != nil {
return domain.PurchaseTask{}, false, repositoryFailure(err)
}
changed, err := result.RowsAffected()
if err != nil {
return domain.PurchaseTask{}, false, repositoryFailure(err)
}
if changed != 1 {
return domain.PurchaseTask{}, false,
usecase.ErrProcurementStateConflict
}
if err := insertIdempotency(
ctx,
tx,
task.CreatorSubject,
createProcurementTaskOperation,
idempotencyKey,
requestSHA256,
"PURCHASE_TASK",
task.ID,
task.CreatedAt,
); err != nil {
return domain.PurchaseTask{}, false, err
}
if err := tx.Commit(); err != nil {
return domain.PurchaseTask{}, false, repositoryFailure(err)
}
return task, true, nil
}
const procurementRequestSelect = `SELECT
request.id, request.creator_subject, request.freight_order_item_id,
request.source_revision, request.source_sha256, request.title,
request.product_spec, request.sku, request.quantity,
request.source_purchase_status, request.source_is_canceled,
request.procurement_confirmed_by_user_id,
request.procurement_confirmed_at, request.status,
request.blocking_code, request.reference_asset_id,
reference_source.origin, reference_source.product_thumb_ref,
reference_source.source_image_sha256, reference_source.bound_at,
request.purchase_task_id, request.created_at, request.updated_at,
CASE
WHEN item.revision != request.source_revision
OR item.canonical_sha256 != request.source_sha256
OR item.is_present != 1
OR COALESCE(freight.is_canceled, -1)
!= COALESCE(request.source_is_canceled, -1)
THEN 1 ELSE 0
END
FROM procurement_requests AS request
JOIN freight_order_items AS item
ON item.id = request.freight_order_item_id
JOIN freight_orders AS freight
ON freight.id = item.freight_order_id
LEFT JOIN procurement_reference_sources AS reference_source
ON reference_source.procurement_request_id = request.id
`
func getProcurementRequest(
ctx context.Context,
query queryRower,
creatorSubject, requestID string,
) (domain.ProcurementRequest, error) {
request, err := scanProcurementRequest(query.QueryRowContext(
ctx,
procurementRequestSelect+`
WHERE request.creator_subject = ? AND request.id = ?`,
creatorSubject,
requestID,
))
if errors.Is(err, sql.ErrNoRows) {
return domain.ProcurementRequest{}, usecase.ErrRepositoryNotFound
}
if err != nil {
return domain.ProcurementRequest{}, repositoryFailure(err)
}
return request, nil
}
func getProcurementRequestBySource(
ctx context.Context,
query queryRower,
creatorSubject, itemID string,
revision int,
) (domain.ProcurementRequest, error) {
request, err := scanProcurementRequest(query.QueryRowContext(
ctx,
procurementRequestSelect+`
WHERE request.creator_subject = ?
AND request.freight_order_item_id = ?
AND request.source_revision = ?`,
creatorSubject,
itemID,
revision,
))
if errors.Is(err, sql.ErrNoRows) {
return domain.ProcurementRequest{}, usecase.ErrRepositoryNotFound
}
if err != nil {
return domain.ProcurementRequest{}, repositoryFailure(err)
}
return request, nil
}
func scanProcurementRequest(
scanner rowScanner,
) (domain.ProcurementRequest, error) {
var request domain.ProcurementRequest
var quantity sql.NullInt64
var purchaseStatus sql.NullString
var isCanceled sql.NullBool
var blockingCode, assetID, referenceOrigin sql.NullString
var referenceThumb, referenceSHA, referenceBoundAt sql.NullString
var taskID sql.NullString
var confirmedAt, createdAt, updatedAt string
var sourceChanged bool
err := scanner.Scan(
&request.ID,
&request.CreatorSubject,
&request.FreightOrderItemID,
&request.SourceRevision,
&request.SourceSHA256,
&request.Title,
&request.ProductSpec,
&request.SKU,
&quantity,
&purchaseStatus,
&isCanceled,
&request.ProcurementConfirmedByUserID,
&confirmedAt,
&request.Status,
&blockingCode,
&assetID,
&referenceOrigin,
&referenceThumb,
&referenceSHA,
&referenceBoundAt,
&taskID,
&createdAt,
&updatedAt,
&sourceChanged,
)
if err != nil {
return domain.ProcurementRequest{}, err
}
if quantity.Valid {
value := int(quantity.Int64)
request.Quantity = &value
}
request.SourcePurchaseStatus = optionalString(purchaseStatus)
if isCanceled.Valid {
request.SourceIsCanceled = &isCanceled.Bool
}
request.BlockingCode = optionalString(blockingCode)
request.ReferenceAssetID = optionalString(assetID)
if referenceOrigin.Valid {
origin := domain.ProcurementReferenceOrigin(referenceOrigin.String)
request.ReferenceOrigin = &origin
}
request.ReferenceProductThumbRef = optionalString(referenceThumb)
request.ReferenceSourceImageSHA256 = optionalString(referenceSHA)
if referenceBoundAt.Valid {
boundAt, parseErr := parseTimestamp(referenceBoundAt.String)
if parseErr != nil {
return domain.ProcurementRequest{}, parseErr
}
request.ReferenceBoundAt = &boundAt
}
request.PurchaseTaskID = optionalString(taskID)
request.SourceChanged = sourceChanged
request.ProcurementConfirmedAt, err = parseTimestamp(confirmedAt)
if err != nil {
return domain.ProcurementRequest{}, err
}
request.CreatedAt, err = parseTimestamp(createdAt)
if err != nil {
return domain.ProcurementRequest{}, err
}
request.UpdatedAt, err = parseTimestamp(updatedAt)
return request, err
}
func validateProcurementSource(
ctx context.Context,
tx *sql.Tx,
candidate domain.ProcurementRequest,
) error {
var revision int
var sourceSHA string
var present bool
err := tx.QueryRowContext(
ctx,
`SELECT item.revision, item.canonical_sha256, item.is_present
FROM freight_order_items AS item
JOIN freight_orders AS freight
ON freight.id = item.freight_order_id
WHERE freight.creator_subject = ? AND item.id = ?`,
candidate.CreatorSubject,
candidate.FreightOrderItemID,
).Scan(&revision, &sourceSHA, &present)
if errors.Is(err, sql.ErrNoRows) {
return usecase.ErrRepositoryNotFound
}
if err != nil {
return repositoryFailure(err)
}
if !present || revision != candidate.SourceRevision ||
sourceSHA != candidate.SourceSHA256 {
return usecase.ErrProcurementSourceChanged
}
return nil
}
func validateAutomaticProcurementReference(
ctx context.Context,
tx *sql.Tx,
candidate domain.ProcurementRequest,
auto usecase.AutoProcurementReference,
) error {
if auto.Asset.CreatorSubject != candidate.CreatorSubject ||
auto.Asset.Purpose != domain.AssetPurposeTaskReference ||
auto.Asset.MediaType != domain.NormalizedImageMediaType ||
auto.Asset.ID == "" || auto.Asset.StorageKey == "" ||
auto.Asset.SizeBytes <= 0 || len(auto.Asset.SHA256) != 64 {
return usecase.ErrRepositoryInvariant
}
var thumbRef, sourceSHA, storageKey string
err := tx.QueryRowContext(
ctx,
`SELECT image.product_thumb_ref, image.sha256, image.storage_key
FROM freight_item_images AS image
JOIN freight_order_items AS item
ON item.id = image.freight_order_item_id
JOIN freight_orders AS freight
ON freight.id = item.freight_order_id
WHERE freight.creator_subject = ?
AND item.id = ?
AND item.is_present = 1
AND item.product_thumb_ref = image.product_thumb_ref
AND image.status = 'READY'`,
candidate.CreatorSubject,
candidate.FreightOrderItemID,
).Scan(&thumbRef, &sourceSHA, &storageKey)
if errors.Is(err, sql.ErrNoRows) {
return usecase.ErrProcurementSourceChanged
}
if err != nil {
return repositoryFailure(err)
}
if thumbRef != auto.ProductThumbRef ||
sourceSHA != auto.SourceImageSHA256 ||
storageKey != auto.SourceImageStorageKey {
return usecase.ErrProcurementSourceChanged
}
return nil
}
func insertProcurementReferenceAsset(
ctx context.Context,
tx *sql.Tx,
asset domain.Asset,
) error {
_, err := tx.ExecContext(
ctx,
`INSERT INTO assets (
id, creator_subject, purpose, media_type, size_bytes,
sha256, storage_key, created_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
asset.ID,
asset.CreatorSubject,
asset.Purpose,
asset.MediaType,
asset.SizeBytes,
asset.SHA256,
asset.StorageKey,
formatTimestamp(asset.CreatedAt),
)
if err != nil {
return repositoryFailure(err)
}
return nil
}
func saveProcurementReferenceSource(
ctx context.Context,
tx *sql.Tx,
requestID string,
source domain.ProcurementReferenceSource,
) error {
_, err := tx.ExecContext(
ctx,
`INSERT INTO procurement_reference_sources (
procurement_request_id, origin, product_thumb_ref,
source_image_sha256, bound_at
) VALUES (?, ?, ?, ?, ?)
ON CONFLICT(procurement_request_id) DO UPDATE SET
origin = excluded.origin,
product_thumb_ref = excluded.product_thumb_ref,
source_image_sha256 = excluded.source_image_sha256,
bound_at = excluded.bound_at`,
requestID,
source.Origin,
nullableString(source.ProductThumbRef),
nullableString(source.SourceImageSHA256),
formatTimestamp(source.BoundAt),
)
if err != nil {
return repositoryFailure(err)
}
return nil
}
func bindAutomaticProcurementReference(
ctx context.Context,
tx *sql.Tx,
request domain.ProcurementRequest,
auto usecase.AutoProcurementReference,
now time.Time,
) (domain.ProcurementRequest, error) {
if request.SourceChanged {
return domain.ProcurementRequest{},
usecase.ErrProcurementSourceChanged
}
if err := validateProcurementSource(ctx, tx, request); err != nil {
return domain.ProcurementRequest{}, err
}
if err := validateAutomaticProcurementReference(
ctx,
tx,
request,
auto,
); err != nil {
return domain.ProcurementRequest{}, err
}
if err := insertProcurementReferenceAsset(ctx, tx, auto.Asset); err != nil {
return domain.ProcurementRequest{}, err
}
result, err := tx.ExecContext(
ctx,
`UPDATE procurement_requests
SET reference_asset_id = ?, status = 'READY',
blocking_code = NULL, updated_at = ?
WHERE id = ? AND status = 'NEEDS_IMAGE'
AND reference_asset_id IS NULL`,
auto.Asset.ID,
formatTimestamp(now),
request.ID,
)
if err != nil {
return domain.ProcurementRequest{}, repositoryFailure(err)
}
affected, err := result.RowsAffected()
if err != nil {
return domain.ProcurementRequest{}, repositoryFailure(err)
}
if affected != 1 {
return domain.ProcurementRequest{},
usecase.ErrProcurementStateConflict
}
if err := saveProcurementReferenceSource(
ctx,
tx,
request.ID,
domain.ProcurementReferenceSource{
Origin: domain.ProcurementReferenceERP,
ProductThumbRef: &auto.ProductThumbRef,
SourceImageSHA256: &auto.SourceImageSHA256,
BoundAt: now,
},
); err != nil {
return domain.ProcurementRequest{}, err
}
return getProcurementRequest(
ctx,
tx,
request.CreatorSubject,
request.ID,
)
}
func markProcurementSourceChanged(
ctx context.Context,
tx *sql.Tx,
requestID string,
now time.Time,
) error {
_, err := tx.ExecContext(
ctx,
`UPDATE procurement_requests
SET status = 'SOURCE_CHANGED', blocking_code = 'SOURCE_CHANGED',
updated_at = ?
WHERE id = ? AND purchase_task_id IS NULL`,
formatTimestamp(now),
requestID,
)
if err != nil {
return repositoryFailure(err)
}
return nil
}
var _ usecase.ProcurementRepository = (*Store)(nil)