436 lines
15 KiB
SQL
436 lines
15 KiB
SQL
-- +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);
|