feat(t218): reconcile single order submissions
This commit is contained in:
@@ -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);
|
||||
Reference in New Issue
Block a user