@12-apps/prisma 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +95 -0
- package/dist/actor-context.d.ts +127 -0
- package/dist/actor-context.js +139 -0
- package/dist/append-only-extension.d.ts +28 -0
- package/dist/append-only-extension.js +72 -0
- package/dist/audit-extension.d.ts +18 -0
- package/dist/audit-extension.js +123 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.js +213 -0
- package/dist/search-normalize.d.ts +13 -0
- package/dist/search-normalize.js +20 -0
- package/package.json +96 -0
- package/prisma/migration-files.ts +33 -0
- package/prisma/migrations/20260725150000_payments_platform_core/migration.sql +98 -0
- package/prisma/migrations/20260725160000_payments_oauth_connections/migration.sql +25 -0
- package/prisma/migrations/20260725170000_add_saved_reports/migration.sql +22 -0
- package/prisma/migrations/20260726090000_payments_multi_provider_failover/migration.sql +67 -0
- package/prisma/migrations/20260726120000_payments_failover_policy/migration.sql +29 -0
- package/prisma/migrations/20260726130000_add_report_lifecycle/migration.sql +18 -0
- package/prisma/migrations/20260727120000_add_report_archived_status/migration.sql +9 -0
- package/prisma/migrations/20260727120000_payments_webhook_replay_budget/migration.sql +37 -0
- package/prisma/migrations/20260727190000_sweep_leases/migration.sql +13 -0
- package/prisma/migrations/20260728120000_add_product_research/migration.sql +112 -0
- package/prisma/migrations/20260728170000_add_manual_price_entries/migration.sql +31 -0
- package/prisma/migrations/20260729090000_integration_source_singleton/migration.sql +24 -0
- package/prisma/migrations/20260729120000_price_source_soft_delete/migration.sql +24 -0
- package/prisma/migrations/20260729140000_research_term_normalized/migration.sql +26 -0
- package/prisma/migrations/20260730120000_offer_outside_delivery_area/migration.sql +16 -0
- package/prisma/migrations/20260730120000_payments_charge_verified_at/migration.sql +23 -0
- package/prisma/migrations/20260730130000_research_runs_created_at_index/migration.sql +12 -0
- package/prisma/migrations/20260730210000_add_shifts/migration.sql +95 -0
- package/prisma/migrations/20260731000000_offer_shipping_unknown/migration.sql +34 -0
- package/prisma/migrations/20260731210000_shift_delete_guard/migration.sql +43 -0
- package/prisma/migrations/20260810120000_add_report_default_range/migration.sql +15 -0
- package/prisma/migrations/20260810160000_report_default_range_month/migration.sql +15 -0
- package/prisma/migrations/20260810180000_add_report_working_copy/migration.sql +13 -0
- package/prisma/plugin-migrations.json +28 -0
- package/prisma/schema/entity-lifecycle.prisma +126 -0
- package/prisma/schema/jobs.prisma +48 -0
- package/prisma/schema/product-research.prisma +217 -0
- package/prisma/schema/schema.prisma +19 -0
- package/prisma/schema/shift.prisma +34 -0
- package/src/actor-context.ts +218 -0
- package/src/append-only-extension.ts +75 -0
- package/src/audit-extension.ts +121 -0
- package/src/index.ts +233 -0
- package/src/search-normalize.ts +19 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
-- @12-apps/payments-backend owned migration: give the webhook retry drain its own
|
|
2
|
+
-- retry budget (FUT-341 follow-up).
|
|
3
|
+
--
|
|
4
|
+
-- `attempts` was doing two incompatible jobs at once. It counted every
|
|
5
|
+
-- processing attempt AND it was the cap the drain used to decide a row had had
|
|
6
|
+
-- enough — but the live inbound path increments it too, and how often THAT
|
|
7
|
+
-- happens is the provider's decision, not ours.
|
|
8
|
+
--
|
|
9
|
+
-- The failure that forces this column: a store's order-confirmation handler
|
|
10
|
+
-- throws. The live delivery fails, the endpoint answers 5xx, and the provider
|
|
11
|
+
-- starts redelivering — every ten minutes or so, for hours. Each redelivery
|
|
12
|
+
-- re-enters the pipeline, fails again, and spends one of the ten attempts the
|
|
13
|
+
-- drain was supposed to have. Under two hours later the row is at the cap, drops
|
|
14
|
+
-- out of the drain's work list permanently, and has been replayed zero or one
|
|
15
|
+
-- times. The recovery mechanism disables itself precisely for the rows whose
|
|
16
|
+
-- provider retried hardest, and nothing reports it: a row that is not listed
|
|
17
|
+
-- moves no counter, so the drain looks idle and healthy.
|
|
18
|
+
--
|
|
19
|
+
-- So the two jobs get two columns. `attempts` stays what its name says — every
|
|
20
|
+
-- attempt, from either path, which is what support wants when asking "how many
|
|
21
|
+
-- times has anything tried this". `replay_attempts` is the drain's budget and
|
|
22
|
+
-- only the drain writes it.
|
|
23
|
+
--
|
|
24
|
+
-- Backfilled to 0 rather than to `attempts`. Nothing incremented `attempts`
|
|
25
|
+
-- before the drain existed, so every historical row's real replay count IS
|
|
26
|
+
-- zero; copying `attempts` across would import live-path failures as spent
|
|
27
|
+
-- budget and re-create the exact bug this column removes.
|
|
28
|
+
ALTER TABLE "payment_webhook_events"
|
|
29
|
+
ADD COLUMN IF NOT EXISTS "replay_attempts" INTEGER NOT NULL DEFAULT 0;
|
|
30
|
+
|
|
31
|
+
-- The drain's query, exactly: unsettled rows, one branch per eligible
|
|
32
|
+
-- `replay_attempts` count (each with its own `updated_at` backoff cutoff),
|
|
33
|
+
-- ordered by `updated_at`. Leading with `status` is what keeps it away from the
|
|
34
|
+
-- PROCESSED rows, which are the overwhelming majority of this table and grow
|
|
35
|
+
-- without bound while the unsettled set stays small.
|
|
36
|
+
CREATE INDEX IF NOT EXISTS "payment_webhook_events_replay_idx"
|
|
37
|
+
ON "payment_webhook_events"("status", "replay_attempts", "updated_at");
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
-- Named, time-bounded claims on the scheduled sweeps (FUT-343).
|
|
2
|
+
--
|
|
3
|
+
-- One row per sweep. The claim is a conditional UPDATE on `expires_at`, so the
|
|
4
|
+
-- database picks the winner between racing workers; `expires_at` also makes a
|
|
5
|
+
-- crashed holder recoverable with no operator action.
|
|
6
|
+
CREATE TABLE IF NOT EXISTS "sweep_leases" (
|
|
7
|
+
"name" TEXT NOT NULL,
|
|
8
|
+
"holder" TEXT NOT NULL,
|
|
9
|
+
"acquired_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
10
|
+
"expires_at" TIMESTAMP(3) NOT NULL,
|
|
11
|
+
|
|
12
|
+
CONSTRAINT "sweep_leases_pkey" PRIMARY KEY ("name")
|
|
13
|
+
);
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
-- FUT #405: research-to-buy core tables. Owned by @12-apps/product-research (this
|
|
2
|
+
-- migration reaches the host's migrations folder via sync-prisma-plugins).
|
|
3
|
+
-- Deliberately NO foreign key into host tables (self-contained package schema,
|
|
4
|
+
-- entity-lifecycle / payments-backend doctrine): the host repository layer
|
|
5
|
+
-- scopes every read/write by client_id, and the researched catalog item is
|
|
6
|
+
-- named by value via (catalog_ref_type, catalog_ref_id).
|
|
7
|
+
|
|
8
|
+
CREATE TABLE "price_sources" (
|
|
9
|
+
"id" TEXT NOT NULL,
|
|
10
|
+
"client_id" TEXT NOT NULL,
|
|
11
|
+
"type" TEXT NOT NULL,
|
|
12
|
+
"name" TEXT NOT NULL,
|
|
13
|
+
"config" JSONB NOT NULL DEFAULT '{}',
|
|
14
|
+
"enabled" BOOLEAN NOT NULL DEFAULT true,
|
|
15
|
+
"status" TEXT NOT NULL DEFAULT 'ACTIVE',
|
|
16
|
+
"last_error_at" TIMESTAMP(3),
|
|
17
|
+
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
18
|
+
"updated_at" TIMESTAMP(3) NOT NULL,
|
|
19
|
+
|
|
20
|
+
CONSTRAINT "price_sources_pkey" PRIMARY KEY ("id"),
|
|
21
|
+
CONSTRAINT "price_sources_type_check" CHECK (
|
|
22
|
+
"type" IN ('VTEX', 'MERCADO_LIVRE', 'SERP', 'AMAZON', 'MANUAL')
|
|
23
|
+
),
|
|
24
|
+
CONSTRAINT "price_sources_status_check" CHECK (
|
|
25
|
+
"status" IN ('ACTIVE', 'DEGRADED')
|
|
26
|
+
)
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
CREATE UNIQUE INDEX "price_sources_client_id_name_key" ON "price_sources"("client_id", "name");
|
|
30
|
+
|
|
31
|
+
CREATE INDEX "price_sources_client_id_enabled_idx" ON "price_sources"("client_id", "enabled");
|
|
32
|
+
|
|
33
|
+
CREATE TABLE "research_requests" (
|
|
34
|
+
"id" TEXT NOT NULL,
|
|
35
|
+
"client_id" TEXT NOT NULL,
|
|
36
|
+
"catalog_ref_type" TEXT,
|
|
37
|
+
"catalog_ref_id" TEXT,
|
|
38
|
+
"term" TEXT NOT NULL,
|
|
39
|
+
"brand" TEXT,
|
|
40
|
+
"ean" TEXT,
|
|
41
|
+
"quantity" INTEGER NOT NULL DEFAULT 1,
|
|
42
|
+
"region" TEXT,
|
|
43
|
+
"requested_by" TEXT,
|
|
44
|
+
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
45
|
+
|
|
46
|
+
CONSTRAINT "research_requests_pkey" PRIMARY KEY ("id")
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
CREATE INDEX "research_requests_client_id_created_at_idx" ON "research_requests"("client_id", "created_at");
|
|
50
|
+
|
|
51
|
+
CREATE TABLE "research_runs" (
|
|
52
|
+
"id" TEXT NOT NULL,
|
|
53
|
+
"client_id" TEXT NOT NULL,
|
|
54
|
+
"request_id" TEXT NOT NULL,
|
|
55
|
+
"status" TEXT NOT NULL DEFAULT 'PENDING',
|
|
56
|
+
"source_stats" JSONB NOT NULL DEFAULT '[]',
|
|
57
|
+
"error" TEXT,
|
|
58
|
+
"started_at" TIMESTAMP(3),
|
|
59
|
+
"finished_at" TIMESTAMP(3),
|
|
60
|
+
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
61
|
+
"updated_at" TIMESTAMP(3) NOT NULL,
|
|
62
|
+
|
|
63
|
+
CONSTRAINT "research_runs_pkey" PRIMARY KEY ("id"),
|
|
64
|
+
CONSTRAINT "research_runs_status_check" CHECK (
|
|
65
|
+
"status" IN ('PENDING', 'RUNNING', 'COMPLETED', 'FAILED')
|
|
66
|
+
),
|
|
67
|
+
CONSTRAINT "research_runs_request_id_fkey" FOREIGN KEY ("request_id")
|
|
68
|
+
REFERENCES "research_requests"("id") ON DELETE CASCADE ON UPDATE CASCADE
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
CREATE INDEX "research_runs_client_id_created_at_idx" ON "research_runs"("client_id", "created_at");
|
|
72
|
+
|
|
73
|
+
CREATE INDEX "research_runs_request_id_idx" ON "research_runs"("request_id");
|
|
74
|
+
|
|
75
|
+
CREATE TABLE "supplier_offers" (
|
|
76
|
+
"id" TEXT NOT NULL,
|
|
77
|
+
"client_id" TEXT NOT NULL,
|
|
78
|
+
"run_id" TEXT NOT NULL,
|
|
79
|
+
"source_id" TEXT,
|
|
80
|
+
"source_type" TEXT NOT NULL,
|
|
81
|
+
"supplier_name" TEXT NOT NULL,
|
|
82
|
+
"title" TEXT NOT NULL,
|
|
83
|
+
"url" TEXT,
|
|
84
|
+
"image_url" TEXT,
|
|
85
|
+
"currency" TEXT NOT NULL DEFAULT 'BRL',
|
|
86
|
+
"price_cents" INTEGER NOT NULL,
|
|
87
|
+
"shipping_cents" INTEGER NOT NULL DEFAULT 0,
|
|
88
|
+
"pack_quantity" INTEGER NOT NULL DEFAULT 1,
|
|
89
|
+
"unit_price_cents" INTEGER NOT NULL,
|
|
90
|
+
"total_cents" INTEGER NOT NULL,
|
|
91
|
+
"availability" TEXT,
|
|
92
|
+
"eta_days" INTEGER,
|
|
93
|
+
"relevance_score" DOUBLE PRECISION NOT NULL,
|
|
94
|
+
"rank" INTEGER,
|
|
95
|
+
"raw" JSONB,
|
|
96
|
+
"expires_at" TIMESTAMP(3),
|
|
97
|
+
"purchased_at" TIMESTAMP(3),
|
|
98
|
+
"hidden_at" TIMESTAMP(3),
|
|
99
|
+
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
100
|
+
|
|
101
|
+
CONSTRAINT "supplier_offers_pkey" PRIMARY KEY ("id"),
|
|
102
|
+
CONSTRAINT "supplier_offers_run_id_fkey" FOREIGN KEY ("run_id")
|
|
103
|
+
REFERENCES "research_runs"("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
|
104
|
+
CONSTRAINT "supplier_offers_source_id_fkey" FOREIGN KEY ("source_id")
|
|
105
|
+
REFERENCES "price_sources"("id") ON DELETE SET NULL ON UPDATE CASCADE
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
CREATE INDEX "supplier_offers_run_id_rank_idx" ON "supplier_offers"("run_id", "rank");
|
|
109
|
+
|
|
110
|
+
CREATE INDEX "supplier_offers_client_id_created_at_idx" ON "supplier_offers"("client_id", "created_at");
|
|
111
|
+
|
|
112
|
+
CREATE INDEX "supplier_offers_client_id_expires_at_idx" ON "supplier_offers"("client_id", "expires_at");
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
-- Manual price import (FUT-415): the standing dataset behind MANUAL-type
|
|
2
|
+
-- price sources. Rows come from price-list imports or one-off typed quotes
|
|
3
|
+
-- and are read by the manual connector during research runs; `valid_until`
|
|
4
|
+
-- is the staleness horizon and `batch_id` groups one import so a re-import
|
|
5
|
+
-- replaces the previous list. PGlite-safe (plain DDL).
|
|
6
|
+
|
|
7
|
+
CREATE TABLE "manual_price_entries" (
|
|
8
|
+
"id" TEXT NOT NULL,
|
|
9
|
+
"client_id" TEXT NOT NULL,
|
|
10
|
+
"source_id" TEXT NOT NULL,
|
|
11
|
+
"batch_id" TEXT NOT NULL,
|
|
12
|
+
"supplier_name" TEXT NOT NULL,
|
|
13
|
+
"title" TEXT NOT NULL,
|
|
14
|
+
"brand" TEXT,
|
|
15
|
+
"ean" TEXT,
|
|
16
|
+
"pack_quantity" INTEGER,
|
|
17
|
+
"price_cents" INTEGER NOT NULL,
|
|
18
|
+
"currency" TEXT NOT NULL DEFAULT 'BRL',
|
|
19
|
+
"url" TEXT,
|
|
20
|
+
"availability" TEXT,
|
|
21
|
+
"eta_days" INTEGER,
|
|
22
|
+
"valid_until" TIMESTAMP(3) NOT NULL,
|
|
23
|
+
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
24
|
+
|
|
25
|
+
CONSTRAINT "manual_price_entries_pkey" PRIMARY KEY ("id"),
|
|
26
|
+
CONSTRAINT "manual_price_entries_source_id_fkey" FOREIGN KEY ("source_id")
|
|
27
|
+
REFERENCES "price_sources"("id") ON DELETE CASCADE ON UPDATE CASCADE
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
CREATE INDEX "manual_price_entries_client_id_source_id_valid_until_idx"
|
|
31
|
+
ON "manual_price_entries"("client_id", "source_id", "valid_until");
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
-- FUT-434: paid connectors are singleton integrations — at most ONE source row
|
|
2
|
+
-- per (tenant, type) for the credentialed types, each running on the tenant's
|
|
3
|
+
-- own API key. VTEX and MANUAL stay multi-instance, created by name.
|
|
4
|
+
--
|
|
5
|
+
-- Belt-and-braces order: any duplicates that slipped in while the create
|
|
6
|
+
-- dialog still offered these types are collapsed first (the OLDEST row wins —
|
|
7
|
+
-- deterministic, and its id is the one past runs reference; supplier_offers
|
|
8
|
+
-- survive via ON DELETE SET NULL), then the partial unique index makes the
|
|
9
|
+
-- invariant a database fact. Like the CHECK constraints, the partial index is
|
|
10
|
+
-- migration-only DDL the Prisma schema cannot express. PGlite-safe (plain DDL).
|
|
11
|
+
|
|
12
|
+
DELETE FROM "price_sources" AS dup
|
|
13
|
+
USING "price_sources" AS keeper
|
|
14
|
+
WHERE dup."type" IN ('SERP', 'AMAZON', 'MERCADO_LIVRE')
|
|
15
|
+
AND keeper."client_id" = dup."client_id"
|
|
16
|
+
AND keeper."type" = dup."type"
|
|
17
|
+
AND (
|
|
18
|
+
keeper."created_at" < dup."created_at"
|
|
19
|
+
OR (keeper."created_at" = dup."created_at" AND keeper."id" < dup."id")
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
CREATE UNIQUE INDEX "price_sources_client_id_integration_type_key"
|
|
23
|
+
ON "price_sources"("client_id", "type")
|
|
24
|
+
WHERE "type" IN ('SERP', 'AMAZON', 'MERCADO_LIVRE');
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
-- Price sources become soft-deletable (FUT-433).
|
|
2
|
+
--
|
|
3
|
+
-- Deleting a source must keep past research runs readable: `supplier_offers`
|
|
4
|
+
-- rows point at the source (`source_id`), and a run's report renders the
|
|
5
|
+
-- supplier name / source type it snapshotted by value. A hard DELETE would
|
|
6
|
+
-- SET NULL the FK (the schema's backstop), but soft-delete is strictly better
|
|
7
|
+
-- here — the row survives, so `source_id` keeps resolving, and the roster,
|
|
8
|
+
-- new runs and manual imports simply filter `archived_at IS NULL`. Same
|
|
9
|
+
-- doctrine as the host's Discount model: history-bearing rows are archived,
|
|
10
|
+
-- never removed. There is no restore path.
|
|
11
|
+
ALTER TABLE "price_sources" ADD COLUMN "archived_at" TIMESTAMP(3);
|
|
12
|
+
|
|
13
|
+
-- The tenant unique on (client_id, name) becomes PARTIAL, binding LIVE rows
|
|
14
|
+
-- only. Unconditional, a deleted "Atacadão" would squat its name forever —
|
|
15
|
+
-- the exact dead-duplicate pile-up this migration exists to clean up. The
|
|
16
|
+
-- index NAME is preserved so a P2002 keeps classifying by constraint name.
|
|
17
|
+
-- Partial indexes are plain PostgreSQL and PGlite runs the real engine, so
|
|
18
|
+
-- this replays identically there; IF EXISTS / IF NOT EXISTS keep the replay
|
|
19
|
+
-- idempotent for suites that apply every migration into a fresh database.
|
|
20
|
+
DROP INDEX IF EXISTS "price_sources_client_id_name_key";
|
|
21
|
+
|
|
22
|
+
CREATE UNIQUE INDEX IF NOT EXISTS "price_sources_client_id_name_key"
|
|
23
|
+
ON "price_sources"("client_id", "name")
|
|
24
|
+
WHERE "archived_at" IS NULL;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
-- FUT-430: widened widget lookup. `term_normalized` mirrors the package's
|
|
2
|
+
-- normalizeText(term) — lowercased, accents stripped, punctuation runs
|
|
3
|
+
-- collapsed to single spaces — so the best-prices widget finds a product's
|
|
4
|
+
-- research past renames and accent variants. Kept in sync by the host on
|
|
5
|
+
-- write; this migration backfills existing rows.
|
|
6
|
+
--
|
|
7
|
+
-- The backfill folds accents in pure SQL via translate() (no `unaccent`
|
|
8
|
+
-- extension, which PGlite lacks — same doctrine as the FUT-168 search_name
|
|
9
|
+
-- migration) so it runs identically on PostgreSQL + PGlite. Characters
|
|
10
|
+
-- outside the mapped pt-BR set fall through to the punctuation collapse,
|
|
11
|
+
-- matching normalizeText for every term the pt-BR hosts store.
|
|
12
|
+
|
|
13
|
+
ALTER TABLE "research_requests" ADD COLUMN "term_normalized" TEXT;
|
|
14
|
+
|
|
15
|
+
UPDATE "research_requests"
|
|
16
|
+
SET "term_normalized" = btrim(regexp_replace(
|
|
17
|
+
translate(
|
|
18
|
+
lower("term"),
|
|
19
|
+
'áàâãäçéèêëíìîïóòôõöúùûüýÿñ',
|
|
20
|
+
'aaaaaceeeeiiiiooooouuuuyyn'
|
|
21
|
+
),
|
|
22
|
+
'[^a-z0-9,.]+', ' ', 'g'
|
|
23
|
+
));
|
|
24
|
+
|
|
25
|
+
CREATE INDEX "research_requests_client_id_term_normalized_idx"
|
|
26
|
+
ON "research_requests"("client_id", "term_normalized");
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
-- FUT-491: an offer can now carry "this price is the store's DEFAULT region's".
|
|
2
|
+
--
|
|
3
|
+
-- A VTEX store that does not deliver to the researched CEP used to answer with
|
|
4
|
+
-- nothing (FUT-416). It now answers with its default-region catalog, flagged —
|
|
5
|
+
-- information beats silence, as long as the caveat travels with the price. The
|
|
6
|
+
-- flag has to survive the re-read, the cached run and the offers API, so it is
|
|
7
|
+
-- a real column: `raw` is the VERBATIM connector payload kept for offline
|
|
8
|
+
-- replay and must not carry fields of ours.
|
|
9
|
+
--
|
|
10
|
+
-- NOT NULL DEFAULT false: every offer written before this change was priced for
|
|
11
|
+
-- the buyer's own region (the only arms that produced offers were "served" and
|
|
12
|
+
-- "no regional knowledge"), so false is the truthful backfill rather than a
|
|
13
|
+
-- placeholder. IF NOT EXISTS keeps the replay idempotent for suites that apply
|
|
14
|
+
-- every migration into a fresh database.
|
|
15
|
+
ALTER TABLE "supplier_offers"
|
|
16
|
+
ADD COLUMN IF NOT EXISTS "outside_delivery_area" BOOLEAN NOT NULL DEFAULT false;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
-- @12-apps/payments-backend owned migration: record when a REAL charge last
|
|
2
|
+
-- succeeded, so activation can be earned instead of asserted (FUT-463).
|
|
3
|
+
--
|
|
4
|
+
-- `last_verified_at` cannot answer this. It is stamped by the credential
|
|
5
|
+
-- PROBE, and the probe only asks "do these keys authenticate" — a PagBank
|
|
6
|
+
-- Connect grant answers yes while refusing every real charge with
|
|
7
|
+
-- `403 ACCESS_DENIED` until the integration is homologated. That is not a
|
|
8
|
+
-- hypothetical: a store showed `VERIFIED`, was switched on by hand, and every
|
|
9
|
+
-- shopper was declined at checkout.
|
|
10
|
+
--
|
|
11
|
+
-- Nullable with NO backfill, deliberately. Backfilling from `last_verified_at`
|
|
12
|
+
-- would grandfather exactly the rows this exists to catch — including the one
|
|
13
|
+
-- that prompted it, which is enabled today and cannot charge. Leaving it null
|
|
14
|
+
-- means "never proven", which is the truth for every pre-existing row.
|
|
15
|
+
--
|
|
16
|
+
-- No row is disabled here either: taking a working store's checkout offline
|
|
17
|
+
-- from a migration is not recoverable by the person it happens to. The
|
|
18
|
+
-- application enforces the rule from both ends instead — enabling REQUIRES a
|
|
19
|
+
-- proven charge, and a failed verification disables — so a connection that
|
|
20
|
+
-- cannot charge switches itself off the next time it is tested, in front of
|
|
21
|
+
-- the owner, rather than silently at deploy time.
|
|
22
|
+
ALTER TABLE "payment_provider_configs"
|
|
23
|
+
ADD COLUMN "charge_verified_at" TIMESTAMP(3);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
-- FUT-496: a time-only index on research_runs.
|
|
2
|
+
--
|
|
3
|
+
-- The host's superadmin diagnostics inbox reads recent runs ACROSS EVERY TENANT
|
|
4
|
+
-- ("every failed source in the last 24 h"), i.e. `created_at >= $1` with no
|
|
5
|
+
-- client_id bound. The existing `(client_id, created_at)` index cannot serve
|
|
6
|
+
-- that predicate — its leading column is unbound and PostgreSQL has no btree
|
|
7
|
+
-- skip scan — so the planner falls back to a Parallel Seq Scan over the whole
|
|
8
|
+
-- table. research_runs has no retention/pruning job, so that scan only ever
|
|
9
|
+
-- gets more expensive. A tenant-scoped read is unaffected: it keeps using the
|
|
10
|
+
-- composite index, which is strictly better for it.
|
|
11
|
+
|
|
12
|
+
CREATE INDEX "research_runs_created_at_idx" ON "research_runs"("created_at");
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
-- @12-apps/shift (FUT-446): immutable work periods and optional tenant policy.
|
|
2
|
+
CREATE TABLE "shifts" (
|
|
3
|
+
"id" TEXT NOT NULL,
|
|
4
|
+
"client_id" TEXT NOT NULL,
|
|
5
|
+
"user_id" TEXT NOT NULL,
|
|
6
|
+
"kind" TEXT NOT NULL,
|
|
7
|
+
"started_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
8
|
+
"ended_at" TIMESTAMP(3),
|
|
9
|
+
"ended_reason" TEXT,
|
|
10
|
+
"ended_by_user_id" TEXT,
|
|
11
|
+
"resource_assignment_id" TEXT,
|
|
12
|
+
"resource_type" TEXT,
|
|
13
|
+
"resource_id" TEXT,
|
|
14
|
+
|
|
15
|
+
CONSTRAINT "shifts_pkey" PRIMARY KEY ("id"),
|
|
16
|
+
CONSTRAINT "shifts_kind_check"
|
|
17
|
+
CHECK ("kind" IN ('kitchen', 'service')),
|
|
18
|
+
CONSTRAINT "shifts_ended_reason_check"
|
|
19
|
+
CHECK ("ended_reason" IS NULL OR "ended_reason" IN ('user', 'supervisor', 'auto')),
|
|
20
|
+
CONSTRAINT "shifts_end_state_check"
|
|
21
|
+
CHECK (
|
|
22
|
+
("ended_at" IS NULL AND "ended_reason" IS NULL AND "ended_by_user_id" IS NULL)
|
|
23
|
+
OR
|
|
24
|
+
("ended_at" IS NOT NULL AND "ended_reason" IS NOT NULL
|
|
25
|
+
AND ("ended_reason" = 'auto' OR "ended_by_user_id" IS NOT NULL))
|
|
26
|
+
),
|
|
27
|
+
CONSTRAINT "shifts_time_order_check"
|
|
28
|
+
CHECK ("ended_at" IS NULL OR "ended_at" >= "started_at"),
|
|
29
|
+
CONSTRAINT "shifts_resource_snapshot_check"
|
|
30
|
+
CHECK (
|
|
31
|
+
("resource_assignment_id" IS NULL AND "resource_type" IS NULL AND "resource_id" IS NULL)
|
|
32
|
+
OR
|
|
33
|
+
("resource_assignment_id" IS NOT NULL AND "resource_type" IS NOT NULL AND "resource_id" IS NOT NULL)
|
|
34
|
+
)
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
CREATE UNIQUE INDEX "shifts_resource_assignment_id_key"
|
|
38
|
+
ON "shifts"("resource_assignment_id");
|
|
39
|
+
|
|
40
|
+
-- Prisma cannot express a partial unique index. This is the concurrency
|
|
41
|
+
-- guarantee: two tabs may race the pre-check, but only one open period commits.
|
|
42
|
+
CREATE UNIQUE INDEX "shifts_open_client_user_key"
|
|
43
|
+
ON "shifts"("client_id", "user_id")
|
|
44
|
+
WHERE "ended_at" IS NULL;
|
|
45
|
+
|
|
46
|
+
CREATE INDEX "shifts_client_id_ended_at_idx"
|
|
47
|
+
ON "shifts"("client_id", "ended_at");
|
|
48
|
+
|
|
49
|
+
CREATE INDEX "shifts_client_id_user_id_started_at_idx"
|
|
50
|
+
ON "shifts"("client_id", "user_id", "started_at");
|
|
51
|
+
|
|
52
|
+
-- A shift's identity/resource snapshots never change, and a closed row is the
|
|
53
|
+
-- immutable work record. The service exposes no edit/delete API; this trigger
|
|
54
|
+
-- keeps direct SQL and future adapters from creating a back door around it.
|
|
55
|
+
CREATE FUNCTION "guard_shift_immutability"()
|
|
56
|
+
RETURNS TRIGGER
|
|
57
|
+
LANGUAGE plpgsql
|
|
58
|
+
AS $$
|
|
59
|
+
BEGIN
|
|
60
|
+
IF TG_OP = 'DELETE' THEN
|
|
61
|
+
RAISE EXCEPTION 'shift records cannot be deleted';
|
|
62
|
+
END IF;
|
|
63
|
+
|
|
64
|
+
IF OLD."ended_at" IS NOT NULL THEN
|
|
65
|
+
RAISE EXCEPTION 'closed shifts are immutable';
|
|
66
|
+
END IF;
|
|
67
|
+
|
|
68
|
+
IF NEW."client_id" IS DISTINCT FROM OLD."client_id"
|
|
69
|
+
OR NEW."user_id" IS DISTINCT FROM OLD."user_id"
|
|
70
|
+
OR NEW."kind" IS DISTINCT FROM OLD."kind"
|
|
71
|
+
OR NEW."started_at" IS DISTINCT FROM OLD."started_at"
|
|
72
|
+
OR NEW."resource_assignment_id" IS DISTINCT FROM OLD."resource_assignment_id"
|
|
73
|
+
OR NEW."resource_type" IS DISTINCT FROM OLD."resource_type"
|
|
74
|
+
OR NEW."resource_id" IS DISTINCT FROM OLD."resource_id" THEN
|
|
75
|
+
RAISE EXCEPTION 'shift identity and resource snapshots are immutable';
|
|
76
|
+
END IF;
|
|
77
|
+
|
|
78
|
+
RETURN NEW;
|
|
79
|
+
END;
|
|
80
|
+
$$;
|
|
81
|
+
|
|
82
|
+
CREATE TRIGGER "shifts_immutable_guard"
|
|
83
|
+
BEFORE UPDATE OR DELETE ON "shifts"
|
|
84
|
+
FOR EACH ROW
|
|
85
|
+
EXECUTE FUNCTION "guard_shift_immutability"();
|
|
86
|
+
|
|
87
|
+
CREATE TABLE "shift_tenant_configs" (
|
|
88
|
+
"client_id" TEXT NOT NULL,
|
|
89
|
+
"auto_close_hours" INTEGER NOT NULL DEFAULT 16,
|
|
90
|
+
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
91
|
+
|
|
92
|
+
CONSTRAINT "shift_tenant_configs_pkey" PRIMARY KEY ("client_id"),
|
|
93
|
+
CONSTRAINT "shift_tenant_configs_auto_close_hours_check"
|
|
94
|
+
CHECK ("auto_close_hours" BETWEEN 1 AND 168)
|
|
95
|
+
);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
-- FUT-518: "the source never said what shipping costs" stops being spelled 0.
|
|
2
|
+
--
|
|
3
|
+
-- `shipping_cents` was NOT NULL DEFAULT 0, so an offer whose freight nobody
|
|
4
|
+
-- knew was stored — and RANKED — as free shipping. That is not a rare corner:
|
|
5
|
+
-- the SERP connector (Google Shopping) never states a shipping cost at all, so
|
|
6
|
+
-- every one of its offers competed on an understated total against Amazon
|
|
7
|
+
-- (which writes 0 only on a real free-shipping signal) and Mercado Livre
|
|
8
|
+
-- (which parses it). Cheapest-first then hands the recommendation to whichever
|
|
9
|
+
-- offer hid its freight best — the same understated-total failure as FUT-497.
|
|
10
|
+
--
|
|
11
|
+
-- After this: NULL = the source never stated a shipping cost, so `total_cents`
|
|
12
|
+
-- is a MINIMUM and every surface caveats it; 0 = the source stated FREE.
|
|
13
|
+
-- The DEFAULT goes too, deliberately: with two writers of this table
|
|
14
|
+
-- (`appendRunOffers` and `insertOffers`, both through `toOfferRow` in
|
|
15
|
+
-- apps/web/lib/repositories/research-store.ts) and no raw SQL insert anywhere,
|
|
16
|
+
-- the column is always stated explicitly. Keeping a default would let a future
|
|
17
|
+
-- writer that forgets the column silently re-introduce the phantom zero.
|
|
18
|
+
--
|
|
19
|
+
-- NO BACKFILL, AND THAT IS THE HONEST ANSWER. Every row written before this
|
|
20
|
+
-- migration holds 0, and 0 now means "the source stated free". Those rows are
|
|
21
|
+
-- indistinguishable from genuinely free-shipping ones — the information to tell
|
|
22
|
+
-- them apart was destroyed at write time and no query can recover it. They are
|
|
23
|
+
-- left as they are: historical runs keep reading as free shipping, which is
|
|
24
|
+
-- wrong for some of them, but rewriting them to NULL would be equally wrong for
|
|
25
|
+
-- the rest and would additionally erase the real free-shipping answers Amazon
|
|
26
|
+
-- and Mercado Livre did give. The distinction starts with the runs from here on.
|
|
27
|
+
--
|
|
28
|
+
-- Idempotent (IF EXISTS / DROP DEFAULT is a no-op when absent) so the replay
|
|
29
|
+
-- suites that apply every migration into a fresh database stay green.
|
|
30
|
+
ALTER TABLE "supplier_offers"
|
|
31
|
+
ALTER COLUMN "shipping_cents" DROP NOT NULL;
|
|
32
|
+
|
|
33
|
+
ALTER TABLE "supplier_offers"
|
|
34
|
+
ALTER COLUMN "shipping_cents" DROP DEFAULT;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
-- @12-apps/shift (FUT-446): the immutability guard becomes UPDATE-only.
|
|
2
|
+
--
|
|
3
|
+
-- The original trigger fired `BEFORE UPDATE OR DELETE` and raised
|
|
4
|
+
-- unconditionally on DELETE. `shifts.client_id` is a by-value tenant reference
|
|
5
|
+
-- with no foreign key (that is what keeps the package host-agnostic), so a host
|
|
6
|
+
-- that deletes its tenant row and then sweeps the remaining by-value tables by
|
|
7
|
+
-- `client_id` could never remove a shift: the RAISE aborted the whole
|
|
8
|
+
-- transaction. Future Pay's demo-store reset does exactly that on EVERY deploy,
|
|
9
|
+
-- so a single shift row would have wedged the deploy step.
|
|
10
|
+
--
|
|
11
|
+
-- Deletability is host policy, not a package invariant. What the guard actually
|
|
12
|
+
-- protects — a closed shift is an immutable work record, and a shift's
|
|
13
|
+
-- identity/resource snapshot never changes — is entirely an UPDATE concern and
|
|
14
|
+
-- is preserved verbatim below. The package still exposes no delete API.
|
|
15
|
+
DROP TRIGGER IF EXISTS "shifts_immutable_guard" ON "shifts";
|
|
16
|
+
|
|
17
|
+
CREATE OR REPLACE FUNCTION "guard_shift_immutability"()
|
|
18
|
+
RETURNS TRIGGER
|
|
19
|
+
LANGUAGE plpgsql
|
|
20
|
+
AS $$
|
|
21
|
+
BEGIN
|
|
22
|
+
IF OLD."ended_at" IS NOT NULL THEN
|
|
23
|
+
RAISE EXCEPTION 'closed shifts are immutable';
|
|
24
|
+
END IF;
|
|
25
|
+
|
|
26
|
+
IF NEW."client_id" IS DISTINCT FROM OLD."client_id"
|
|
27
|
+
OR NEW."user_id" IS DISTINCT FROM OLD."user_id"
|
|
28
|
+
OR NEW."kind" IS DISTINCT FROM OLD."kind"
|
|
29
|
+
OR NEW."started_at" IS DISTINCT FROM OLD."started_at"
|
|
30
|
+
OR NEW."resource_assignment_id" IS DISTINCT FROM OLD."resource_assignment_id"
|
|
31
|
+
OR NEW."resource_type" IS DISTINCT FROM OLD."resource_type"
|
|
32
|
+
OR NEW."resource_id" IS DISTINCT FROM OLD."resource_id" THEN
|
|
33
|
+
RAISE EXCEPTION 'shift identity and resource snapshots are immutable';
|
|
34
|
+
END IF;
|
|
35
|
+
|
|
36
|
+
RETURN NEW;
|
|
37
|
+
END;
|
|
38
|
+
$$;
|
|
39
|
+
|
|
40
|
+
CREATE TRIGGER "shifts_immutable_guard"
|
|
41
|
+
BEFORE UPDATE ON "shifts"
|
|
42
|
+
FOR EACH ROW
|
|
43
|
+
EXECUTE FUNCTION "guard_shift_immutability"();
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
-- The period a saved report OPENS on (FUT-755). Nullable: NULL means "no
|
|
2
|
+
-- preference", which readers resolve to 30d — the behaviour every existing row
|
|
3
|
+
-- already has, so this migration changes nothing about them.
|
|
4
|
+
ALTER TABLE "saved_reports"
|
|
5
|
+
ADD COLUMN IF NOT EXISTS "default_range" TEXT;
|
|
6
|
+
|
|
7
|
+
-- The presets the range API actually supports (REPORT_RANGES). Enforced here as
|
|
8
|
+
-- well as at the wire, because the store is written by MCP authors too and a
|
|
9
|
+
-- value the reader cannot resolve would open the report on nothing.
|
|
10
|
+
ALTER TABLE "saved_reports"
|
|
11
|
+
DROP CONSTRAINT IF EXISTS "saved_reports_default_range_check";
|
|
12
|
+
|
|
13
|
+
ALTER TABLE "saved_reports"
|
|
14
|
+
ADD CONSTRAINT "saved_reports_default_range_check"
|
|
15
|
+
CHECK ("default_range" IS NULL OR "default_range" IN ('today', '7d', '30d'));
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
-- `Este mês` joins the period toggle (FUT-755), so it joins the periods a saved
|
|
2
|
+
-- report may OPEN on. The CHECK is the third place the list is written — after
|
|
3
|
+
-- `REPORT_DEFAULT_RANGES` and the wire's `z.enum` — and the one an MCP author
|
|
4
|
+
-- writing straight to the store hits, so it has to learn the new value or a
|
|
5
|
+
-- report whose default the toggle offers cannot be saved.
|
|
6
|
+
--
|
|
7
|
+
-- Re-stated rather than altered: Postgres has no `ALTER CONSTRAINT` for a
|
|
8
|
+
-- CHECK's expression, and DROP-then-ADD under `IF EXISTS` is idempotent, so
|
|
9
|
+
-- this replays cleanly onto a database that already ran it.
|
|
10
|
+
ALTER TABLE "saved_reports"
|
|
11
|
+
DROP CONSTRAINT IF EXISTS "saved_reports_default_range_check";
|
|
12
|
+
|
|
13
|
+
ALTER TABLE "saved_reports"
|
|
14
|
+
ADD CONSTRAINT "saved_reports_default_range_check"
|
|
15
|
+
CHECK ("default_range" IS NULL OR "default_range" IN ('today', '7d', '30d', 'month'));
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
-- Unpublished changes to a PUBLISHED report (FUT-755).
|
|
2
|
+
--
|
|
3
|
+
-- A report already has `status` ('draft' | 'published' | 'archived'), but that
|
|
4
|
+
-- is the report's LIFECYCLE, not a revision of it: a report with status
|
|
5
|
+
-- 'draft' has never been published. Editing a PUBLISHED report is the case
|
|
6
|
+
-- this column exists for — its readers are looking at it right now, so the
|
|
7
|
+
-- in-progress edit is stored beside the live document and `spec` is left
|
|
8
|
+
-- alone until the author publishes.
|
|
9
|
+
--
|
|
10
|
+
-- NULL means "no unpublished changes", which is every row that predates this
|
|
11
|
+
-- migration, so nothing about existing reports changes.
|
|
12
|
+
ALTER TABLE "saved_reports"
|
|
13
|
+
ADD COLUMN IF NOT EXISTS "working_copy" JSONB;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "Migrations in prisma/migrations that were COPIED from a plugin package. Generated by scripts/sync-prisma-plugins.mjs — do not edit by hand. Its only job is to let the sync recognise a copy whose owner renamed or deleted it, since a copy is otherwise indistinguishable from a host-owned migration.",
|
|
3
|
+
"migrations": [
|
|
4
|
+
"20260725150000_payments_platform_core",
|
|
5
|
+
"20260725160000_payments_oauth_connections",
|
|
6
|
+
"20260725170000_add_saved_reports",
|
|
7
|
+
"20260726090000_payments_multi_provider_failover",
|
|
8
|
+
"20260726120000_payments_failover_policy",
|
|
9
|
+
"20260726130000_add_report_lifecycle",
|
|
10
|
+
"20260727120000_add_report_archived_status",
|
|
11
|
+
"20260727120000_payments_webhook_replay_budget",
|
|
12
|
+
"20260727190000_sweep_leases",
|
|
13
|
+
"20260728120000_add_product_research",
|
|
14
|
+
"20260728170000_add_manual_price_entries",
|
|
15
|
+
"20260729090000_integration_source_singleton",
|
|
16
|
+
"20260729120000_price_source_soft_delete",
|
|
17
|
+
"20260729140000_research_term_normalized",
|
|
18
|
+
"20260730120000_offer_outside_delivery_area",
|
|
19
|
+
"20260730120000_payments_charge_verified_at",
|
|
20
|
+
"20260730130000_research_runs_created_at_index",
|
|
21
|
+
"20260730210000_add_shifts",
|
|
22
|
+
"20260731000000_offer_shipping_unknown",
|
|
23
|
+
"20260731210000_shift_delete_guard",
|
|
24
|
+
"20260810120000_add_report_default_range",
|
|
25
|
+
"20260810160000_report_default_range_month",
|
|
26
|
+
"20260810180000_add_report_working_copy"
|
|
27
|
+
]
|
|
28
|
+
}
|