feat(t218): reconcile single order submissions

This commit is contained in:
QiuSW
2026-07-28 17:48:54 +08:00
parent 682cc7327c
commit a77d8be06d
32 changed files with 4306 additions and 79 deletions
@@ -0,0 +1,435 @@
-- +goose Up
CREATE TABLE order_submissions (
id TEXT PRIMARY KEY NOT NULL CHECK (length(id) = 36),
authorization_id TEXT NOT NULL UNIQUE
REFERENCES order_authorizations(id)
ON UPDATE RESTRICT ON DELETE RESTRICT,
dry_run_id TEXT NOT NULL UNIQUE
REFERENCES order_dry_runs(id)
ON UPDATE RESTRICT ON DELETE RESTRICT,
task_id TEXT NOT NULL
REFERENCES purchase_tasks(id) ON UPDATE RESTRICT ON DELETE RESTRICT,
execution_id TEXT NOT NULL
REFERENCES task_executions(id) ON UPDATE RESTRICT ON DELETE RESTRICT,
command_sha256 TEXT NOT NULL
CHECK (
length(command_sha256) = 64
AND command_sha256 NOT GLOB '*[^0-9a-f]*'
),
dry_run_evidence_sha256 TEXT NOT NULL
CHECK (
length(dry_run_evidence_sha256) = 64
AND dry_run_evidence_sha256 NOT GLOB '*[^0-9a-f]*'
),
status TEXT NOT NULL
CHECK (status IN ('FENCED', 'RECONCILED', 'MANUAL_REVIEW')),
expected_title TEXT NOT NULL
CHECK (
length(trim(expected_title)) > 0
AND length(CAST(expected_title AS BLOB)) <= 1024
),
expected_sku TEXT NOT NULL
CHECK (
length(trim(expected_sku)) > 0
AND length(CAST(expected_sku AS BLOB)) <= 512
),
expected_quantity INTEGER NOT NULL
CHECK (expected_quantity BETWEEN 1 AND 99),
expected_unit_price_cents INTEGER NOT NULL
CHECK (expected_unit_price_cents > 0),
expected_total_price_cents INTEGER NOT NULL
CHECK (expected_total_price_cents > 0),
platform_order_no TEXT UNIQUE
CHECK (
platform_order_no IS NULL
OR (
length(platform_order_no) BETWEEN 8 AND 40
AND platform_order_no NOT GLOB '*[^0-9]*'
)
),
platform_ordered_at TEXT,
platform_order_status TEXT
CHECK (
platform_order_status IS NULL
OR platform_order_status = 'PENDING_PAYMENT'
),
reconciliation_evidence_asset_id TEXT
REFERENCES execution_evidence_assets(id)
ON UPDATE RESTRICT ON DELETE RESTRICT,
reconciliation_evidence_sha256 TEXT
CHECK (
reconciliation_evidence_sha256 IS NULL
OR (
length(reconciliation_evidence_sha256) = 64
AND reconciliation_evidence_sha256 NOT GLOB '*[^0-9a-f]*'
)
),
manual_reason_code TEXT
CHECK (
manual_reason_code IS NULL
OR manual_reason_code IN (
'ORDER_NOT_FOUND',
'ORDER_AMBIGUOUS',
'ORDER_FIELDS_INCOMPLETE',
'ORDER_PAGE_UNKNOWN',
'RISK_OR_PAYMENT_BOUNDARY',
'EVIDENCE_UNAVAILABLE'
)
),
fenced_at TEXT NOT NULL,
reconciled_at TEXT,
manual_review_at TEXT,
CHECK (
expected_total_price_cents =
expected_unit_price_cents * expected_quantity
),
CHECK (
(status = 'FENCED'
AND platform_order_no IS NULL
AND platform_ordered_at IS NULL
AND platform_order_status IS NULL
AND reconciliation_evidence_asset_id IS NULL
AND reconciliation_evidence_sha256 IS NULL
AND manual_reason_code IS NULL
AND reconciled_at IS NULL
AND manual_review_at IS NULL)
OR
(status = 'RECONCILED'
AND platform_order_no IS NOT NULL
AND platform_ordered_at IS NOT NULL
AND platform_order_status = 'PENDING_PAYMENT'
AND reconciliation_evidence_asset_id IS NOT NULL
AND reconciliation_evidence_sha256 IS NOT NULL
AND manual_reason_code IS NULL
AND reconciled_at IS NOT NULL
AND manual_review_at IS NULL)
OR
(status = 'MANUAL_REVIEW'
AND platform_order_no IS NULL
AND platform_ordered_at IS NULL
AND platform_order_status IS NULL
AND manual_reason_code IS NOT NULL
AND reconciled_at IS NULL
AND manual_review_at IS NOT NULL)
)
);
CREATE INDEX order_submissions_task_fenced_idx
ON order_submissions (task_id, fenced_at DESC, id DESC);
CREATE TABLE device_order_submission_requests (
device_id TEXT NOT NULL
REFERENCES devices(id) ON UPDATE RESTRICT ON DELETE RESTRICT,
operation TEXT NOT NULL
CHECK (operation IN ('START', 'RECONCILE', 'MANUAL_REVIEW')),
idempotency_key TEXT NOT NULL
CHECK (
length(trim(idempotency_key)) > 0
AND length(CAST(idempotency_key AS BLOB)) <= 128
),
request_sha256 TEXT NOT NULL
CHECK (
length(request_sha256) = 64
AND request_sha256 NOT GLOB '*[^0-9a-f]*'
),
task_id TEXT NOT NULL
REFERENCES purchase_tasks(id) ON UPDATE RESTRICT ON DELETE RESTRICT,
submission_id TEXT NOT NULL
REFERENCES order_submissions(id) ON UPDATE RESTRICT ON DELETE RESTRICT,
created_at TEXT NOT NULL,
PRIMARY KEY (device_id, operation, idempotency_key)
);
ALTER TABLE execution_outcomes RENAME TO execution_outcomes_v10;
CREATE TABLE execution_outcomes (
execution_id TEXT PRIMARY KEY NOT NULL
REFERENCES task_executions(id) ON UPDATE RESTRICT ON DELETE CASCADE,
task_id TEXT NOT NULL
REFERENCES purchase_tasks(id) ON UPDATE RESTRICT ON DELETE CASCADE,
result_type TEXT NOT NULL
CHECK (result_type IN ('COMPLETE', 'FAIL')),
execution_mode TEXT
CHECK (execution_mode IS NULL OR execution_mode IN ('MANUAL_FIRST', 'AI_ASSISTED')),
task_content_sha256 TEXT
CHECK (
task_content_sha256 IS NULL
OR (
length(task_content_sha256) = 64
AND task_content_sha256 NOT GLOB '*[^0-9a-f]*'
)
),
outcome TEXT
CHECK (
outcome IS NULL
OR outcome IN (
'CANDIDATE_ACCEPTED',
'CANDIDATE_REJECTED',
'NO_MATCH',
'MANUAL_REQUIRED'
)
),
operator_reason TEXT
CHECK (
operator_reason IS NULL
OR length(CAST(operator_reason AS BLOB)) <= 1000
),
selected_candidate_json TEXT,
evidence_asset_ids_json TEXT,
error_code TEXT
CHECK (error_code IS NULL OR length(CAST(error_code AS BLOB)) <= 64),
error_message TEXT
CHECK (error_message IS NULL OR length(CAST(error_message AS BLOB)) <= 1000),
error_step TEXT
CHECK (error_step IS NULL OR length(CAST(error_step AS BLOB)) <= 64),
retryable INTEGER
CHECK (retryable IS NULL OR retryable IN (0, 1)),
order_submitted INTEGER NOT NULL DEFAULT 0
CHECK (order_submitted IN (0, 1)),
received_at TEXT NOT NULL,
received_after_execution_expiry INTEGER NOT NULL DEFAULT 0
CHECK (received_after_execution_expiry IN (0, 1)),
CHECK (
(result_type = 'COMPLETE'
AND execution_mode IS NOT NULL
AND task_content_sha256 IS NOT NULL
AND outcome IS NOT NULL
AND operator_reason IS NOT NULL
AND error_code IS NULL)
OR
(result_type = 'FAIL'
AND error_code IS NOT NULL
AND error_message IS NOT NULL
AND error_step IS NOT NULL
AND order_submitted = 0)
)
);
INSERT INTO execution_outcomes (
execution_id, task_id, result_type, execution_mode,
task_content_sha256, outcome, operator_reason,
selected_candidate_json, evidence_asset_ids_json,
error_code, error_message, error_step, retryable,
order_submitted, received_at, received_after_execution_expiry
)
SELECT
execution_id, task_id, result_type, execution_mode,
task_content_sha256, outcome, operator_reason,
selected_candidate_json, evidence_asset_ids_json,
error_code, error_message, error_step, retryable,
order_submitted, received_at, received_after_execution_expiry
FROM execution_outcomes_v10;
DROP TABLE execution_outcomes_v10;
ALTER TABLE task_events RENAME TO task_events_v10;
CREATE TABLE task_events (
id TEXT PRIMARY KEY NOT NULL CHECK (length(id) = 36),
task_id TEXT NOT NULL
REFERENCES purchase_tasks(id) ON UPDATE RESTRICT ON DELETE CASCADE,
event_type TEXT NOT NULL
CHECK (
event_type IN (
'TASK_CREATED',
'TASK_CLAIMED',
'TASK_RECLAIMED',
'TASK_RELEASED',
'TASK_STARTED',
'TASK_CANCEL_REQUESTED',
'TASK_CANCELED',
'CANDIDATES_READY',
'ORDER_AUTHORIZATION_CREATED',
'ORDER_AUTHORIZATION_DELIVERED',
'ORDER_AUTHORIZATION_ACKNOWLEDGED',
'ORDER_DRY_RUN_STARTED',
'ORDER_DRY_RUN_READY',
'ORDER_SUBMISSION_FENCED',
'ORDER_SUBMISSION_RECONCILED',
'ORDER_SUBMISSION_MANUAL_REVIEW'
)
),
message TEXT NOT NULL,
occurred_at TEXT NOT NULL,
actor_user_id TEXT
REFERENCES users(id) ON UPDATE RESTRICT ON DELETE RESTRICT,
actor_device_id TEXT
REFERENCES devices(id) ON UPDATE RESTRICT ON DELETE RESTRICT
);
INSERT INTO task_events (
id, task_id, event_type, message, occurred_at,
actor_user_id, actor_device_id
)
SELECT
id, task_id, event_type, message, occurred_at,
actor_user_id, actor_device_id
FROM task_events_v10;
DROP TABLE task_events_v10;
CREATE INDEX task_events_task_occurred_idx
ON task_events (task_id, occurred_at ASC, id ASC);
CREATE INDEX task_events_actor_user_idx
ON task_events (actor_user_id, occurred_at DESC, id DESC);
CREATE INDEX task_events_actor_device_idx
ON task_events (actor_device_id, occurred_at DESC, id DESC);
-- +goose Down
CREATE TEMP TABLE order_submissions_v11_down_guard (
allowed INTEGER NOT NULL CHECK (allowed = 1)
);
INSERT INTO order_submissions_v11_down_guard (allowed)
SELECT CASE
WHEN EXISTS (SELECT 1 FROM order_submissions)
OR EXISTS (SELECT 1 FROM device_order_submission_requests)
OR EXISTS (
SELECT 1 FROM task_events
WHERE event_type IN (
'ORDER_SUBMISSION_FENCED',
'ORDER_SUBMISSION_RECONCILED',
'ORDER_SUBMISSION_MANUAL_REVIEW'
)
)
OR EXISTS (
SELECT 1 FROM execution_outcomes WHERE order_submitted = 1
)
THEN 0
ELSE 1
END;
DROP TABLE order_submissions_v11_down_guard;
DROP TABLE device_order_submission_requests;
DROP TABLE order_submissions;
ALTER TABLE execution_outcomes RENAME TO execution_outcomes_v11;
CREATE TABLE execution_outcomes (
execution_id TEXT PRIMARY KEY NOT NULL
REFERENCES task_executions(id) ON UPDATE RESTRICT ON DELETE CASCADE,
task_id TEXT NOT NULL
REFERENCES purchase_tasks(id) ON UPDATE RESTRICT ON DELETE CASCADE,
result_type TEXT NOT NULL
CHECK (result_type IN ('COMPLETE', 'FAIL')),
execution_mode TEXT
CHECK (execution_mode IS NULL OR execution_mode IN ('MANUAL_FIRST', 'AI_ASSISTED')),
task_content_sha256 TEXT
CHECK (
task_content_sha256 IS NULL
OR (
length(task_content_sha256) = 64
AND task_content_sha256 NOT GLOB '*[^0-9a-f]*'
)
),
outcome TEXT
CHECK (
outcome IS NULL
OR outcome IN (
'CANDIDATE_ACCEPTED',
'CANDIDATE_REJECTED',
'NO_MATCH',
'MANUAL_REQUIRED'
)
),
operator_reason TEXT
CHECK (
operator_reason IS NULL
OR length(CAST(operator_reason AS BLOB)) <= 1000
),
selected_candidate_json TEXT,
evidence_asset_ids_json TEXT,
error_code TEXT
CHECK (error_code IS NULL OR length(CAST(error_code AS BLOB)) <= 64),
error_message TEXT
CHECK (error_message IS NULL OR length(CAST(error_message AS BLOB)) <= 1000),
error_step TEXT
CHECK (error_step IS NULL OR length(CAST(error_step AS BLOB)) <= 64),
retryable INTEGER
CHECK (retryable IS NULL OR retryable IN (0, 1)),
order_submitted INTEGER NOT NULL DEFAULT 0
CHECK (order_submitted = 0),
received_at TEXT NOT NULL,
received_after_execution_expiry INTEGER NOT NULL DEFAULT 0
CHECK (received_after_execution_expiry IN (0, 1)),
CHECK (
(result_type = 'COMPLETE'
AND execution_mode IS NOT NULL
AND task_content_sha256 IS NOT NULL
AND outcome IS NOT NULL
AND operator_reason IS NOT NULL
AND error_code IS NULL)
OR
(result_type = 'FAIL'
AND error_code IS NOT NULL
AND error_message IS NOT NULL
AND error_step IS NOT NULL)
)
);
INSERT INTO execution_outcomes (
execution_id, task_id, result_type, execution_mode,
task_content_sha256, outcome, operator_reason,
selected_candidate_json, evidence_asset_ids_json,
error_code, error_message, error_step, retryable,
order_submitted, received_at, received_after_execution_expiry
)
SELECT
execution_id, task_id, result_type, execution_mode,
task_content_sha256, outcome, operator_reason,
selected_candidate_json, evidence_asset_ids_json,
error_code, error_message, error_step, retryable,
order_submitted, received_at, received_after_execution_expiry
FROM execution_outcomes_v11;
DROP TABLE execution_outcomes_v11;
ALTER TABLE task_events RENAME TO task_events_v11;
CREATE TABLE task_events (
id TEXT PRIMARY KEY NOT NULL CHECK (length(id) = 36),
task_id TEXT NOT NULL
REFERENCES purchase_tasks(id) ON UPDATE RESTRICT ON DELETE CASCADE,
event_type TEXT NOT NULL
CHECK (
event_type IN (
'TASK_CREATED',
'TASK_CLAIMED',
'TASK_RECLAIMED',
'TASK_RELEASED',
'TASK_STARTED',
'TASK_CANCEL_REQUESTED',
'TASK_CANCELED',
'CANDIDATES_READY',
'ORDER_AUTHORIZATION_CREATED',
'ORDER_AUTHORIZATION_DELIVERED',
'ORDER_AUTHORIZATION_ACKNOWLEDGED',
'ORDER_DRY_RUN_STARTED',
'ORDER_DRY_RUN_READY'
)
),
message TEXT NOT NULL,
occurred_at TEXT NOT NULL,
actor_user_id TEXT
REFERENCES users(id) ON UPDATE RESTRICT ON DELETE RESTRICT,
actor_device_id TEXT
REFERENCES devices(id) ON UPDATE RESTRICT ON DELETE RESTRICT
);
INSERT INTO task_events (
id, task_id, event_type, message, occurred_at,
actor_user_id, actor_device_id
)
SELECT
id, task_id, event_type, message, occurred_at,
actor_user_id, actor_device_id
FROM task_events_v11;
DROP TABLE task_events_v11;
CREATE INDEX task_events_task_occurred_idx
ON task_events (task_id, occurred_at ASC, id ASC);
CREATE INDEX task_events_actor_user_idx
ON task_events (actor_user_id, occurred_at DESC, id DESC);
CREATE INDEX task_events_actor_device_idx
ON task_events (actor_device_id, occurred_at DESC, id DESC);