-- +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);