@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.
Files changed (47) hide show
  1. package/README.md +95 -0
  2. package/dist/actor-context.d.ts +127 -0
  3. package/dist/actor-context.js +139 -0
  4. package/dist/append-only-extension.d.ts +28 -0
  5. package/dist/append-only-extension.js +72 -0
  6. package/dist/audit-extension.d.ts +18 -0
  7. package/dist/audit-extension.js +123 -0
  8. package/dist/index.d.ts +46 -0
  9. package/dist/index.js +213 -0
  10. package/dist/search-normalize.d.ts +13 -0
  11. package/dist/search-normalize.js +20 -0
  12. package/package.json +96 -0
  13. package/prisma/migration-files.ts +33 -0
  14. package/prisma/migrations/20260725150000_payments_platform_core/migration.sql +98 -0
  15. package/prisma/migrations/20260725160000_payments_oauth_connections/migration.sql +25 -0
  16. package/prisma/migrations/20260725170000_add_saved_reports/migration.sql +22 -0
  17. package/prisma/migrations/20260726090000_payments_multi_provider_failover/migration.sql +67 -0
  18. package/prisma/migrations/20260726120000_payments_failover_policy/migration.sql +29 -0
  19. package/prisma/migrations/20260726130000_add_report_lifecycle/migration.sql +18 -0
  20. package/prisma/migrations/20260727120000_add_report_archived_status/migration.sql +9 -0
  21. package/prisma/migrations/20260727120000_payments_webhook_replay_budget/migration.sql +37 -0
  22. package/prisma/migrations/20260727190000_sweep_leases/migration.sql +13 -0
  23. package/prisma/migrations/20260728120000_add_product_research/migration.sql +112 -0
  24. package/prisma/migrations/20260728170000_add_manual_price_entries/migration.sql +31 -0
  25. package/prisma/migrations/20260729090000_integration_source_singleton/migration.sql +24 -0
  26. package/prisma/migrations/20260729120000_price_source_soft_delete/migration.sql +24 -0
  27. package/prisma/migrations/20260729140000_research_term_normalized/migration.sql +26 -0
  28. package/prisma/migrations/20260730120000_offer_outside_delivery_area/migration.sql +16 -0
  29. package/prisma/migrations/20260730120000_payments_charge_verified_at/migration.sql +23 -0
  30. package/prisma/migrations/20260730130000_research_runs_created_at_index/migration.sql +12 -0
  31. package/prisma/migrations/20260730210000_add_shifts/migration.sql +95 -0
  32. package/prisma/migrations/20260731000000_offer_shipping_unknown/migration.sql +34 -0
  33. package/prisma/migrations/20260731210000_shift_delete_guard/migration.sql +43 -0
  34. package/prisma/migrations/20260810120000_add_report_default_range/migration.sql +15 -0
  35. package/prisma/migrations/20260810160000_report_default_range_month/migration.sql +15 -0
  36. package/prisma/migrations/20260810180000_add_report_working_copy/migration.sql +13 -0
  37. package/prisma/plugin-migrations.json +28 -0
  38. package/prisma/schema/entity-lifecycle.prisma +126 -0
  39. package/prisma/schema/jobs.prisma +48 -0
  40. package/prisma/schema/product-research.prisma +217 -0
  41. package/prisma/schema/schema.prisma +19 -0
  42. package/prisma/schema/shift.prisma +34 -0
  43. package/src/actor-context.ts +218 -0
  44. package/src/append-only-extension.ts +75 -0
  45. package/src/audit-extension.ts +121 -0
  46. package/src/index.ts +233 -0
  47. package/src/search-normalize.ts +19 -0
package/dist/index.js ADDED
@@ -0,0 +1,213 @@
1
+ "use strict";
2
+ /**
3
+ * Prisma client singleton.
4
+ *
5
+ * Default mode builds a PostgreSQL-backed `PrismaClient` (the real / production
6
+ * database, driven by `DATABASE_URL`). When a PGlite flag is present in the
7
+ * environment the same client is built on the community `pglite-prisma-adapter`
8
+ * so PGlite (WASM Postgres) becomes the app runtime database for local dev and
9
+ * Playwright e2e — no Docker Postgres required.
10
+ *
11
+ * PGlite mode is selected by ANY of:
12
+ * - `USE_FILE_DB=1` (or `true`) → in-memory, unless a dir is also set
13
+ * - `PGLITE_DATA_DIR=<dir>` → file-backed at <dir>
14
+ * - `DATABASE_URL` starting with `pglite:` → `pglite:memory` or `pglite:<dir>`
15
+ *
16
+ * Production safety: when `NODE_ENV=production` the database is ALWAYS real
17
+ * PostgreSQL unless `USE_FILE_DB=1` is set explicitly — so a stray
18
+ * `PGLITE_DATA_DIR` / `pglite:` URL can never silently point prod at a throwaway
19
+ * WASM DB. `USE_FILE_DB=0` forces real PostgreSQL everywhere (dev/e2e opt-out).
20
+ *
21
+ * The generated `PrismaClient` type is re-exported below so consumers stay fully
22
+ * typed without any hand-written stub interfaces. Server-only: never import this
23
+ * (directly or transitively) from middleware or an Edge-runtime route — PGlite
24
+ * needs Node's filesystem/WASM and Prisma is not Edge-safe.
25
+ */
26
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
27
+ if (k2 === undefined) k2 = k;
28
+ var desc = Object.getOwnPropertyDescriptor(m, k);
29
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
30
+ desc = { enumerable: true, get: function() { return m[k]; } };
31
+ }
32
+ Object.defineProperty(o, k2, desc);
33
+ }) : (function(o, m, k, k2) {
34
+ if (k2 === undefined) k2 = k;
35
+ o[k2] = m[k];
36
+ }));
37
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
38
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
39
+ }) : function(o, v) {
40
+ o["default"] = v;
41
+ });
42
+ var __importStar = (this && this.__importStar) || (function () {
43
+ var ownKeys = function(o) {
44
+ ownKeys = Object.getOwnPropertyNames || function (o) {
45
+ var ar = [];
46
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
47
+ return ar;
48
+ };
49
+ return ownKeys(o);
50
+ };
51
+ return function (mod) {
52
+ if (mod && mod.__esModule) return mod;
53
+ var result = {};
54
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
55
+ __setModuleDefault(result, mod);
56
+ return result;
57
+ };
58
+ })();
59
+ Object.defineProperty(exports, "__esModule", { value: true });
60
+ exports.resetPrismaClient = exports.setPrismaClient = exports.getPrismaClient = exports.normalizeSearchText = exports.setActor = exports.runWithActorScope = exports.runWithActor = exports.getActorUserId = exports.getActorAttribution = exports.AppendOnlyViolationError = void 0;
61
+ const append_only_extension_1 = require("./append-only-extension");
62
+ const audit_extension_1 = require("./audit-extension");
63
+ // Append-only guard for the audit log (FUT-209): mutating the AuditLog model
64
+ // throws. Re-exported so tests can assert on the error type.
65
+ var append_only_extension_2 = require("./append-only-extension");
66
+ Object.defineProperty(exports, "AppendOnlyViolationError", { enumerable: true, get: function () { return append_only_extension_2.AppendOnlyViolationError; } });
67
+ // Change-attribution context helpers (FUT-168): the auth layer calls `setActor`
68
+ // once a request is authorized; the audit extension applied below reads it to
69
+ // stamp created_by/updated_by. Re-exported here so consumers import them from
70
+ // the same `@12-apps/prisma` entry point as `getPrismaClient`.
71
+ var actor_context_1 = require("./actor-context");
72
+ Object.defineProperty(exports, "getActorAttribution", { enumerable: true, get: function () { return actor_context_1.getActorAttribution; } });
73
+ Object.defineProperty(exports, "getActorUserId", { enumerable: true, get: function () { return actor_context_1.getActorUserId; } });
74
+ Object.defineProperty(exports, "runWithActor", { enumerable: true, get: function () { return actor_context_1.runWithActor; } });
75
+ Object.defineProperty(exports, "runWithActorScope", { enumerable: true, get: function () { return actor_context_1.runWithActorScope; } });
76
+ Object.defineProperty(exports, "setActor", { enumerable: true, get: function () { return actor_context_1.setActor; } });
77
+ var search_normalize_1 = require("./search-normalize");
78
+ Object.defineProperty(exports, "normalizeSearchText", { enumerable: true, get: function () { return search_normalize_1.normalizeSearchText; } });
79
+ // The singleton and its in-flight init promise live on `globalThis` so that
80
+ // Next dev / Turbopack hot-reload (which re-evaluates this module) never spawns
81
+ // a second PGlite instance against the same dataDir — PGlite holds a single
82
+ // exclusive connection, and a duplicate would deadlock or corrupt the store.
83
+ const globalStore = globalThis;
84
+ /**
85
+ * Prisma log levels.
86
+ *
87
+ * Full `query` logging prints every SQL statement (table/column names) to the
88
+ * server console, so it is OFF by default — even in development — to avoid
89
+ * surfacing schema details and noise. Opt in locally with `PRISMA_LOG_QUERIES=1`.
90
+ * Production logs errors only.
91
+ */
92
+ const prismaLog = () => {
93
+ if (process.env.NODE_ENV === 'production')
94
+ return ['error'];
95
+ return process.env.PRISMA_LOG_QUERIES === '1'
96
+ ? ['query', 'error', 'warn']
97
+ : ['error', 'warn'];
98
+ };
99
+ /**
100
+ * Whether the environment selects PGlite over PostgreSQL. Production is
101
+ * PostgreSQL unless a file DB is DELIBERATELY forced on; an explicit
102
+ * `USE_FILE_DB=0` always wins.
103
+ */
104
+ const isPgliteSelected = (env) => {
105
+ if (env.forcedOff)
106
+ return false;
107
+ if (process.env.NODE_ENV === 'production')
108
+ return env.forcedOn;
109
+ return env.forcedOn || env.explicitDir !== undefined || env.urlIsPglite;
110
+ };
111
+ /**
112
+ * Resolve the PGlite target once selected: an explicit dir wins, else a
113
+ * `pglite:` URL (`pglite:memory` / `pglite://memory` ⇒ in-memory; `pglite:./dir`
114
+ * ⇒ file), else `USE_FILE_DB` with no directory ⇒ in-memory.
115
+ */
116
+ const pgliteTarget = (env) => {
117
+ if (env.explicitDir)
118
+ return { dataDir: env.explicitDir };
119
+ if (env.urlIsPglite) {
120
+ const raw = env.url.replace(/^pglite:(\/\/)?/, '');
121
+ return raw === '' || raw === 'memory' ? {} : { dataDir: raw };
122
+ }
123
+ return {};
124
+ };
125
+ const resolvePglite = () => {
126
+ const url = process.env.DATABASE_URL ?? '';
127
+ const flag = (process.env.USE_FILE_DB ?? '').toLowerCase();
128
+ const env = {
129
+ url,
130
+ explicitDir: process.env.PGLITE_DATA_DIR,
131
+ forcedOn: flag === '1' || flag === 'true',
132
+ forcedOff: flag === '0' || flag === 'false',
133
+ urlIsPglite: url.startsWith('pglite:'),
134
+ };
135
+ return isPgliteSelected(env) ? pgliteTarget(env) : null;
136
+ };
137
+ /** Build a PGlite-backed `PrismaClient` via the community driver adapter. */
138
+ const createPgliteClient = async (dataDir) => {
139
+ const { PGlite } = await Promise.resolve().then(() => __importStar(require('@electric-sql/pglite')));
140
+ // `pglite-prisma-adapter` is `exports`-only; type the dynamic import locally
141
+ // so it resolves under this package's classic ("Node") module resolution
142
+ // without a static type dependency on the package.
143
+ const { PrismaPGlite } = (await Promise.resolve().then(() => __importStar(require('pglite-prisma-adapter'))));
144
+ const { PrismaClient: GeneratedPrismaClient } = await Promise.resolve().then(() => __importStar(require('@prisma/client')));
145
+ const client = dataDir ? new PGlite(dataDir) : new PGlite();
146
+ await client.waitReady;
147
+ const adapter = new PrismaPGlite(client);
148
+ return (0, append_only_extension_1.applyAppendOnlyGuard)((0, audit_extension_1.applyAuditStamps)(new GeneratedPrismaClient({ adapter, log: prismaLog() })));
149
+ };
150
+ /** Build the default PostgreSQL-backed `PrismaClient` (real / production DB). */
151
+ const createPostgresClient = async () => {
152
+ const { PrismaPg } = await Promise.resolve().then(() => __importStar(require('@prisma/adapter-pg')));
153
+ const { PrismaClient: GeneratedPrismaClient } = await Promise.resolve().then(() => __importStar(require('@prisma/client')));
154
+ const connectionString = process.env.DATABASE_URL;
155
+ if (!connectionString) {
156
+ throw new Error('DATABASE_URL is required for the PostgreSQL Prisma client. Set it, or ' +
157
+ 'enable PGlite mode (USE_FILE_DB=1 / PGLITE_DATA_DIR / a "pglite:" URL).');
158
+ }
159
+ // Prisma 7 makes a driver adapter mandatory — PrismaClient no longer reads
160
+ // DATABASE_URL itself, and `new PrismaClient()` without an adapter throws at
161
+ // construction. PrismaPg takes a pg.PoolConfig and owns the pool internally.
162
+ const adapter = new PrismaPg({
163
+ connectionString,
164
+ });
165
+ return (0, append_only_extension_1.applyAppendOnlyGuard)((0, audit_extension_1.applyAuditStamps)(new GeneratedPrismaClient({ adapter, log: prismaLog() })));
166
+ };
167
+ /**
168
+ * Get or create the Prisma client instance.
169
+ *
170
+ * Lazy and memoised: concurrent callers share a single in-flight init promise,
171
+ * so only one client (and at most one PGlite instance) is ever created. A
172
+ * missing generated client surfaces an actionable error rather than a hard
173
+ * module-load failure.
174
+ */
175
+ const getPrismaClient = async () => {
176
+ if (globalStore.__futurePayPrisma)
177
+ return globalStore.__futurePayPrisma;
178
+ if (globalStore.__futurePayPrismaInit)
179
+ return globalStore.__futurePayPrismaInit;
180
+ const pglite = resolvePglite();
181
+ const init = (pglite ? createPgliteClient(pglite.dataDir) : createPostgresClient())
182
+ .then((client) => {
183
+ globalStore.__futurePayPrisma = client;
184
+ globalStore.__futurePayPrismaInit = undefined;
185
+ return client;
186
+ })
187
+ .catch((error) => {
188
+ globalStore.__futurePayPrismaInit = undefined;
189
+ throw new Error(`Prisma client not available (${pglite ? 'PGlite' : 'PostgreSQL'} mode). ` +
190
+ 'Run "pnpm --filter @12-apps/prisma prisma generate" from the ' +
191
+ 'monorepo root, or "pnpm prisma generate" from packages/prisma. ' +
192
+ `Cause: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
193
+ });
194
+ globalStore.__futurePayPrismaInit = init;
195
+ return init;
196
+ };
197
+ exports.getPrismaClient = getPrismaClient;
198
+ /**
199
+ * Set a custom Prisma client instance (for testing).
200
+ */
201
+ const setPrismaClient = (client) => {
202
+ globalStore.__futurePayPrisma = client;
203
+ globalStore.__futurePayPrismaInit = undefined;
204
+ };
205
+ exports.setPrismaClient = setPrismaClient;
206
+ /**
207
+ * Reset the Prisma client instance (for testing).
208
+ */
209
+ const resetPrismaClient = () => {
210
+ globalStore.__futurePayPrisma = undefined;
211
+ globalStore.__futurePayPrismaInit = undefined;
212
+ };
213
+ exports.resetPrismaClient = resetPrismaClient;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Accent- and case-insensitive search normalization (FUT-168). Used to keep a
3
+ * denormalized `search_name` column on searchable models (maintained by the
4
+ * audit extension) and to normalize the query at read time, so "sabao" matches
5
+ * "Sabão". DB-agnostic: plain lowercased, diacritic-stripped text works
6
+ * identically on PostgreSQL and PGlite (no `unaccent` extension needed).
7
+ */
8
+ /**
9
+ * Strip diacritics and lowercase. NFD splits accented letters into base +
10
+ * combining mark, then the marks are removed. Using the explicit code-point
11
+ * range (not `\p{Diacritic}`) keeps it valid under the package's compile target.
12
+ */
13
+ export declare function normalizeSearchText(value: string): string;
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ /**
3
+ * Accent- and case-insensitive search normalization (FUT-168). Used to keep a
4
+ * denormalized `search_name` column on searchable models (maintained by the
5
+ * audit extension) and to normalize the query at read time, so "sabao" matches
6
+ * "Sabão". DB-agnostic: plain lowercased, diacritic-stripped text works
7
+ * identically on PostgreSQL and PGlite (no `unaccent` extension needed).
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.normalizeSearchText = normalizeSearchText;
11
+ /** Unicode combining diacritical marks (U+0300–U+036F), split out by NFD. */
12
+ const COMBINING_MARKS = /[̀-ͯ]/g;
13
+ /**
14
+ * Strip diacritics and lowercase. NFD splits accented letters into base +
15
+ * combining mark, then the marks are removed. Using the explicit code-point
16
+ * range (not `\p{Diacritic}`) keeps it valid under the package's compile target.
17
+ */
18
+ function normalizeSearchText(value) {
19
+ return value.normalize('NFD').replace(COMBINING_MARKS, '').toLowerCase();
20
+ }
package/package.json ADDED
@@ -0,0 +1,96 @@
1
+ {
2
+ "name": "@12-apps/prisma",
3
+ "version": "1.0.0",
4
+ "description": "Prisma host: the multi-file schema folder, the plugin migration seam, and the shared PrismaClient singleton with its audit / append-only extensions",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "require": "./dist/index.js",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "scripts": {
15
+ "build": "node scripts/sync-lifecycle-schema.mjs && node scripts/sync-research-schema.mjs && node scripts/sync-shift-schema.mjs --check && node scripts/sync-jobs-schema.mjs --check && node scripts/sync-prisma-plugins.mjs --check && prisma generate && tsc",
16
+ "clean": "rm -rf dist node_modules coverage",
17
+ "test": "node ../../scripts/vitest-with-teardown.mjs run",
18
+ "test:watch": "vitest watch",
19
+ "lint:files": "bash -c 'eslint \"${@:-.}\" --max-warnings 0' _",
20
+ "lint": "pnpm run lint:files .",
21
+ "lint:fix": "bash -c 'eslint \"${@:-.}\" --max-warnings 0 --fix' _",
22
+ "typecheck": "tsc --noEmit",
23
+ "prisma:generate": "node scripts/sync-lifecycle-schema.mjs && node scripts/sync-research-schema.mjs && node scripts/sync-shift-schema.mjs --check && node scripts/sync-jobs-schema.mjs --check && node scripts/sync-prisma-plugins.mjs --check && prisma generate",
24
+ "prisma:migrate": "prisma migrate dev",
25
+ "prisma:push": "prisma db push",
26
+ "prisma:studio": "prisma studio",
27
+ "prisma:sync-lifecycle": "node scripts/sync-lifecycle-schema.mjs",
28
+ "prisma:sync-research": "node scripts/sync-research-schema.mjs",
29
+ "prisma:sync-shift": "node scripts/sync-shift-schema.mjs",
30
+ "prisma:sync-shift:check": "node scripts/sync-shift-schema.mjs --check",
31
+ "prisma:sync-jobs": "node scripts/sync-jobs-schema.mjs",
32
+ "prisma:sync-jobs:check": "node scripts/sync-jobs-schema.mjs --check",
33
+ "prisma:sync-plugins": "node scripts/sync-prisma-plugins.mjs",
34
+ "prisma:sync-plugins:check": "node scripts/sync-prisma-plugins.mjs --check"
35
+ },
36
+ "dependencies": {
37
+ "@electric-sql/pglite": "0.2.17",
38
+ "@prisma/adapter-pg": "7.8.0",
39
+ "@prisma/client": "7.8.0",
40
+ "dotenv": "^16.4.7",
41
+ "pglite-prisma-adapter": "0.7.2"
42
+ },
43
+ "devDependencies": {
44
+ "@12-apps/entity-lifecycle": "^2.0.0",
45
+ "@12-apps/eslint-config": "^1.20.0",
46
+ "@12-apps/jobs": "^2.0.0",
47
+ "@12-apps/payments-backend": "^2.0.0",
48
+ "@12-apps/product-research": "^2.0.0",
49
+ "@12-apps/report-builder": "^3.0.0",
50
+ "@12-apps/shift": "^2.0.0",
51
+ "@12-apps/typescript-config": "^1.20.0",
52
+ "@types/node": "^22.10.6",
53
+ "@vitest/coverage-v8": "^3.2.4",
54
+ "eslint": "^9.39.1",
55
+ "eslint-plugin-test-flakiness": "^1.4.0",
56
+ "prisma": "7.8.0",
57
+ "typescript": "^5.8.2",
58
+ "vitest": "^3.2.4"
59
+ },
60
+ "peerDependencies": {
61
+ "typescript": "^5.0.0"
62
+ },
63
+ "engines": {
64
+ "node": ">=22.0.0"
65
+ },
66
+ "keywords": [
67
+ "prisma",
68
+ "migrations",
69
+ "postgres",
70
+ "pglite",
71
+ "typescript"
72
+ ],
73
+ "license": "MIT",
74
+ "publishConfig": {
75
+ "registry": "https://registry.npmjs.org",
76
+ "access": "public"
77
+ },
78
+ "repository": {
79
+ "type": "git",
80
+ "url": "git+https://github.com/12-apps/shared-packages.git",
81
+ "directory": "packages/prisma"
82
+ },
83
+ "files": [
84
+ "src",
85
+ "dist",
86
+ "prisma",
87
+ "*.js",
88
+ "*.mjs",
89
+ "*.md",
90
+ "!eslint.config.js",
91
+ "!**/__tests__/**",
92
+ "!**/tests/**",
93
+ "!**/*.test.*",
94
+ "!**/*.spec.*"
95
+ ]
96
+ }
@@ -0,0 +1,33 @@
1
+ import { readdirSync, statSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ /**
6
+ * Which committed migrations exist, and in what order to replay them.
7
+ *
8
+ * Its own module — with NO side effects — so both the provisioning script and
9
+ * its test can use it. (`pglite-setup.ts` provisions a database on import.)
10
+ */
11
+
12
+ const HERE = dirname(fileURLToPath(import.meta.url));
13
+
14
+ /** The folder holding every committed migration, including symlinked ones. */
15
+ const MIGRATIONS_DIR = join(HERE, 'migrations');
16
+
17
+ /**
18
+ * Every migration directory under `dir`, in timestamp order.
19
+ *
20
+ * `statSync` rather than `Dirent.isDirectory()` on purpose: a package that OWNS
21
+ * part of the schema contributes its migrations as committed SYMLINKS into this
22
+ * folder (Prisma has no cross-package import), and `Dirent.isDirectory()` is
23
+ * FALSE for a symlink. That silently dropped those migrations and left the
24
+ * package's tables missing from every PGlite-backed run — with nothing failing
25
+ * loudly, because the schema was merely incomplete until something queried
26
+ * them. `statSync` follows the link, so an owned migration replays like any
27
+ * other.
28
+ */
29
+ export function discoverMigrations(dir: string = MIGRATIONS_DIR): string[] {
30
+ return readdirSync(dir)
31
+ .filter((name) => /^\d/.test(name) && statSync(join(dir, name)).isDirectory())
32
+ .sort();
33
+ }
@@ -0,0 +1,98 @@
1
+ -- @12-apps/payments-backend owned migration: the three self-contained payment
2
+ -- tables (provider configs, charges, webhook inbox). No FK into host tables —
3
+ -- merchant scope is (merchant_kind, merchant_id). Copy this file into the
4
+ -- host's prisma/migrations folder (keep the timestamp ordering) after running
5
+ -- `prisma:sync`; CHECK constraints carry the String-over-enum closed sets.
6
+
7
+ CREATE TABLE "payment_provider_configs" (
8
+ "id" TEXT NOT NULL,
9
+ "merchant_kind" TEXT NOT NULL,
10
+ "merchant_id" TEXT NOT NULL,
11
+ "provider" TEXT NOT NULL,
12
+ "enabled" BOOLEAN NOT NULL DEFAULT false,
13
+ "environment" TEXT NOT NULL DEFAULT 'SANDBOX',
14
+ "status" TEXT NOT NULL DEFAULT 'UNVERIFIED',
15
+ "last_verified_at" TIMESTAMP(3),
16
+ "stub" BOOLEAN NOT NULL DEFAULT false,
17
+ "credentials" TEXT NOT NULL,
18
+ "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
19
+ "updated_at" TIMESTAMP(3) NOT NULL,
20
+
21
+ CONSTRAINT "payment_provider_configs_pkey" PRIMARY KEY ("id"),
22
+ CONSTRAINT "payment_provider_configs_merchant_kind_check"
23
+ CHECK ("merchant_kind" IN ('PLATFORM', 'TENANT')),
24
+ CONSTRAINT "payment_provider_configs_environment_check"
25
+ CHECK ("environment" IN ('SANDBOX', 'PRODUCTION')),
26
+ CONSTRAINT "payment_provider_configs_status_check"
27
+ CHECK ("status" IN ('UNVERIFIED', 'VERIFIED', 'FAILED'))
28
+ );
29
+
30
+ CREATE UNIQUE INDEX "payment_provider_configs_merchant_kind_merchant_id_provider_key"
31
+ ON "payment_provider_configs"("merchant_kind", "merchant_id", "provider");
32
+ CREATE INDEX "payment_provider_configs_merchant_kind_merchant_id_idx"
33
+ ON "payment_provider_configs"("merchant_kind", "merchant_id");
34
+ -- SINGLE ACTIVE PROVIDER, enforced by the database rather than by app code:
35
+ -- at most one enabled config per merchant. A racing "enable" now fails loudly
36
+ -- on this constraint instead of silently leaving two providers enabled (which
37
+ -- would make charge routing depend on row order). Partial indexes are not
38
+ -- expressible in the Prisma schema — same migration-only pattern the repo
39
+ -- already uses for its CHECK constraints.
40
+ CREATE UNIQUE INDEX "payment_provider_configs_one_enabled_per_merchant"
41
+ ON "payment_provider_configs"("merchant_kind", "merchant_id") WHERE "enabled";
42
+
43
+ CREATE TABLE "payment_charges" (
44
+ "id" TEXT NOT NULL,
45
+ "merchant_kind" TEXT NOT NULL,
46
+ "merchant_id" TEXT NOT NULL,
47
+ "provider" TEXT NOT NULL,
48
+ "provider_charge_id" TEXT NOT NULL,
49
+ "reference" TEXT NOT NULL,
50
+ "idempotency_key" TEXT,
51
+ "status" TEXT NOT NULL DEFAULT 'PENDING',
52
+ "method" TEXT NOT NULL,
53
+ "amount_cents" INTEGER NOT NULL,
54
+ "currency" TEXT NOT NULL DEFAULT 'BRL',
55
+ "snapshot" JSONB NOT NULL,
56
+ "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
57
+ "updated_at" TIMESTAMP(3) NOT NULL,
58
+
59
+ CONSTRAINT "payment_charges_pkey" PRIMARY KEY ("id"),
60
+ CONSTRAINT "payment_charges_merchant_kind_check"
61
+ CHECK ("merchant_kind" IN ('PLATFORM', 'TENANT')),
62
+ CONSTRAINT "payment_charges_status_check"
63
+ CHECK ("status" IN ('PENDING', 'AUTHORIZED', 'PAID', 'DECLINED', 'CANCELED', 'EXPIRED', 'REFUNDED', 'PARTIALLY_REFUNDED')),
64
+ CONSTRAINT "payment_charges_method_check"
65
+ CHECK ("method" IN ('PIX', 'CARD', 'BOLETO'))
66
+ );
67
+
68
+ CREATE UNIQUE INDEX "payment_charges_provider_provider_charge_id_key"
69
+ ON "payment_charges"("provider", "provider_charge_id");
70
+ CREATE UNIQUE INDEX "payment_charges_merchant_kind_merchant_id_idempotency_key_key"
71
+ ON "payment_charges"("merchant_kind", "merchant_id", "idempotency_key");
72
+ CREATE INDEX "payment_charges_merchant_kind_merchant_id_created_at_idx"
73
+ ON "payment_charges"("merchant_kind", "merchant_id", "created_at");
74
+ CREATE INDEX "payment_charges_reference_idx" ON "payment_charges"("reference");
75
+
76
+ CREATE TABLE "payment_webhook_events" (
77
+ "id" TEXT NOT NULL,
78
+ "merchant_kind" TEXT NOT NULL,
79
+ "merchant_id" TEXT NOT NULL,
80
+ "provider" TEXT NOT NULL,
81
+ "event_id" TEXT NOT NULL,
82
+ "status" TEXT NOT NULL DEFAULT 'PENDING',
83
+ "attempts" INTEGER NOT NULL DEFAULT 0,
84
+ "last_error" TEXT,
85
+ "headers" TEXT,
86
+ "payload" TEXT NOT NULL,
87
+ "received_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
88
+ "processed_at" TIMESTAMP(3),
89
+ "updated_at" TIMESTAMP(3) NOT NULL,
90
+
91
+ CONSTRAINT "payment_webhook_events_pkey" PRIMARY KEY ("id"),
92
+ CONSTRAINT "payment_webhook_events_status_check"
93
+ CHECK ("status" IN ('PENDING', 'PROCESSED', 'FAILED'))
94
+ );
95
+
96
+ CREATE UNIQUE INDEX "payment_webhook_events_merchant_provider_event_key"
97
+ ON "payment_webhook_events"("merchant_kind", "merchant_id", "provider", "event_id");
98
+ CREATE INDEX "payment_webhook_events_status_idx" ON "payment_webhook_events"("status");
@@ -0,0 +1,25 @@
1
+ -- @12-apps/payments-backend owned migration: OAuth connection support.
2
+ --
3
+ -- `expires_at` is a first-class column rather than another key inside the
4
+ -- encrypted `credentials` blob, precisely so it is QUERYABLE: a background
5
+ -- job asks "which connections expire in the next hour" and refreshes them.
6
+ -- Nothing inside the ciphertext can be indexed or compared in SQL.
7
+ --
8
+ -- The status CHECK is widened with RECONNECT_REQUIRED, which is deliberately
9
+ -- distinct from FAILED: the remedy is reauthorizing (a button), not fixing
10
+ -- credentials (a form).
11
+
12
+ ALTER TABLE "payment_provider_configs"
13
+ ADD COLUMN "expires_at" TIMESTAMP(3);
14
+
15
+ ALTER TABLE "payment_provider_configs"
16
+ DROP CONSTRAINT "payment_provider_configs_status_check";
17
+
18
+ ALTER TABLE "payment_provider_configs"
19
+ ADD CONSTRAINT "payment_provider_configs_status_check"
20
+ CHECK ("status" IN ('UNVERIFIED', 'VERIFIED', 'FAILED', 'RECONNECT_REQUIRED'));
21
+
22
+ -- Drives the refresh sweep: only connections that actually expire (OAuth)
23
+ -- have a non-null value, so this index stays small.
24
+ CREATE INDEX "payment_provider_configs_expires_at_idx"
25
+ ON "payment_provider_configs"("expires_at") WHERE "expires_at" IS NOT NULL;
@@ -0,0 +1,22 @@
1
+ -- FUT-138: tenant-authored custom reports — the report-builder JSON ReportSpec
2
+ -- persisted as data. Owned by @12-apps/report-builder (this migration reaches the
3
+ -- host's migrations folder as a committed symlink). Unique per (client, name)
4
+ -- so MCP authoring can upsert by name. Deliberately NO foreign key into host
5
+ -- tables (self-contained package schema, payments-backend doctrine): the host
6
+ -- repository layer scopes every read/write by client_id.
7
+ CREATE TABLE "saved_reports" (
8
+ "id" TEXT NOT NULL,
9
+ "client_id" TEXT NOT NULL,
10
+ "name" TEXT NOT NULL,
11
+ "description" TEXT,
12
+ "spec" JSONB NOT NULL,
13
+ "created_by" TEXT,
14
+ "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
15
+ "updated_at" TIMESTAMP(3) NOT NULL,
16
+
17
+ CONSTRAINT "saved_reports_pkey" PRIMARY KEY ("id")
18
+ );
19
+
20
+ CREATE UNIQUE INDEX "saved_reports_client_id_name_key" ON "saved_reports"("client_id", "name");
21
+
22
+ CREATE INDEX "saved_reports_client_id_idx" ON "saved_reports"("client_id");
@@ -0,0 +1,67 @@
1
+ -- @12-apps/payments-backend owned migration: priority list + failover ledger.
2
+ --
3
+ -- This retires the SINGLE ACTIVE PROVIDER invariant. Until now the database
4
+ -- itself guaranteed at most one enabled config per merchant; a merchant may
5
+ -- now enable several and RANK them, and checkout walks that ranking when a
6
+ -- provider fails. The guarantee is not dropped so much as replaced: ranks
7
+ -- among a merchant's enabled rows must stay DISTINCT, or routing order would
8
+ -- again depend on row order — exactly the ambiguity the old index existed to
9
+ -- prevent.
10
+
11
+ ALTER TABLE "payment_provider_configs"
12
+ ADD COLUMN "priority" INTEGER NOT NULL DEFAULT 0;
13
+
14
+ -- Backfill is a no-op by construction: the index being dropped below allowed
15
+ -- at most ONE enabled row per merchant, so every pre-existing chain has
16
+ -- exactly one member and rank 0 is already correct for it. Disabled rows also
17
+ -- carry 0, which the partial index below simply ignores.
18
+
19
+ DROP INDEX "payment_provider_configs_one_enabled_per_merchant";
20
+
21
+ -- The replacement invariant: two ENABLED providers can never share a rank, so
22
+ -- a merchant's chain is a total order. A racing reorder fails loudly here
23
+ -- rather than silently producing a chain whose order depends on which row the
24
+ -- planner returned first.
25
+ CREATE UNIQUE INDEX "payment_provider_configs_enabled_priority_key"
26
+ ON "payment_provider_configs"("merchant_kind", "merchant_id", "priority")
27
+ WHERE "enabled";
28
+
29
+ -- One row per ATTEMPT in a failover walk. This is the evidence trail for the
30
+ -- proof-of-failure rule: it records not just that a provider failed but which
31
+ -- BUCKET the failure fell into and, when the outcome was ambiguous, what the
32
+ -- reconciliation probe concluded. A double charge and a stranded charge look
33
+ -- identical in provider dashboards; they do not look identical here.
34
+ CREATE TABLE "payment_charge_attempts" (
35
+ "id" TEXT NOT NULL,
36
+ "merchant_kind" TEXT NOT NULL,
37
+ "merchant_id" TEXT NOT NULL,
38
+ "idempotency_key" TEXT,
39
+ "reference" TEXT NOT NULL,
40
+ "provider" TEXT NOT NULL,
41
+ "attempt_no" INTEGER NOT NULL,
42
+ "outcome" TEXT NOT NULL,
43
+ "failure_bucket" TEXT,
44
+ "probe_result" TEXT,
45
+ "provider_charge_id" TEXT,
46
+ "error" TEXT,
47
+ "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
48
+
49
+ CONSTRAINT "payment_charge_attempts_pkey" PRIMARY KEY ("id"),
50
+ CONSTRAINT "payment_charge_attempts_merchant_kind_check"
51
+ CHECK ("merchant_kind" IN ('PLATFORM', 'TENANT')),
52
+ CONSTRAINT "payment_charge_attempts_outcome_check"
53
+ CHECK ("outcome" IN ('SUCCEEDED', 'ADOPTED', 'FAILED_OVER', 'STOPPED', 'DECLINED', 'SKIPPED')),
54
+ CONSTRAINT "payment_charge_attempts_failure_bucket_check"
55
+ CHECK ("failure_bucket" IS NULL OR "failure_bucket" IN ('DEFINITELY_NOT_CHARGED', 'AMBIGUOUS', 'BUSINESS_OUTCOME')),
56
+ CONSTRAINT "payment_charge_attempts_probe_result_check"
57
+ CHECK ("probe_result" IS NULL OR "probe_result" IN ('NOT_CHARGED', 'CHARGE_FOUND', 'PROBE_FAILED', 'UNSUPPORTED'))
58
+ );
59
+
60
+ -- Resume lookup: "what has this idempotency key already tried?" — so a
61
+ -- retried charge continues the walk instead of restarting it.
62
+ CREATE INDEX "payment_charge_attempts_merchant_idempotency_key_idx"
63
+ ON "payment_charge_attempts"("merchant_kind", "merchant_id", "idempotency_key");
64
+ CREATE INDEX "payment_charge_attempts_reference_idx"
65
+ ON "payment_charge_attempts"("reference");
66
+ CREATE INDEX "payment_charge_attempts_merchant_created_at_idx"
67
+ ON "payment_charge_attempts"("merchant_kind", "merchant_id", "created_at");
@@ -0,0 +1,29 @@
1
+ -- @12-apps/payments-backend owned migration: merchant-level failover policy.
2
+ --
3
+ -- FUT-303 shipped `failoverPolicy` as a construction-time gateway option, which
4
+ -- meant a merchant could not actually choose it — the ticket asks for a policy,
5
+ -- and a constant no store owner can reach is not one. This gives it a home.
6
+ --
7
+ -- It is merchant-level rather than per-provider on purpose: "may a decline
8
+ -- cascade to the next acquirer" is a decision about the CHAIN, and hanging it
9
+ -- off each provider row would let one merchant hold two contradictory answers.
10
+
11
+ CREATE TABLE "payment_merchant_settings" (
12
+ "id" TEXT NOT NULL,
13
+ "merchant_kind" TEXT NOT NULL,
14
+ "merchant_id" TEXT NOT NULL,
15
+ -- TECHNICAL is the default everywhere, including for merchants who never
16
+ -- get a row: cascading a declined card is opt-in, never inherited.
17
+ "failover_policy" TEXT NOT NULL DEFAULT 'TECHNICAL',
18
+ "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
19
+ "updated_at" TIMESTAMP(3) NOT NULL,
20
+
21
+ CONSTRAINT "payment_merchant_settings_pkey" PRIMARY KEY ("id"),
22
+ CONSTRAINT "payment_merchant_settings_merchant_kind_check"
23
+ CHECK ("merchant_kind" IN ('PLATFORM', 'TENANT')),
24
+ CONSTRAINT "payment_merchant_settings_failover_policy_check"
25
+ CHECK ("failover_policy" IN ('TECHNICAL', 'TECHNICAL_AND_DECLINE'))
26
+ );
27
+
28
+ CREATE UNIQUE INDEX "payment_merchant_settings_merchant_key"
29
+ ON "payment_merchant_settings"("merchant_kind", "merchant_id");
@@ -0,0 +1,18 @@
1
+ -- Report lifecycle & sharing (FUT-307): status (draft | published), visibility
2
+ -- (tenant | roles | private) and the role-id allowlist for visibility='roles'.
3
+ -- String + DB CHECK, the house style (no Prisma enums).
4
+ ALTER TABLE "saved_reports"
5
+ ADD COLUMN "status" TEXT NOT NULL DEFAULT 'draft',
6
+ ADD COLUMN "visibility" TEXT NOT NULL DEFAULT 'tenant',
7
+ ADD COLUMN "visibility_roles" JSONB NOT NULL DEFAULT '[]';
8
+
9
+ ALTER TABLE "saved_reports"
10
+ ADD CONSTRAINT "saved_reports_status_check"
11
+ CHECK ("status" IN ('draft', 'published'));
12
+ ALTER TABLE "saved_reports"
13
+ ADD CONSTRAINT "saved_reports_visibility_check"
14
+ CHECK ("visibility" IN ('tenant', 'roles', 'private'));
15
+
16
+ -- Every pre-lifecycle report is live today: backfill as published so nothing
17
+ -- disappears from any tenant's Relatórios area (new rows default to draft).
18
+ UPDATE "saved_reports" SET "status" = 'published';
@@ -0,0 +1,9 @@
1
+ -- Archived reports (FUT-391): the Relatórios area retires a report instead of
2
+ -- deleting it — the picker stops offering it, the document (and every block
3
+ -- spec in it) survives for restore. Widen the lifecycle CHECK to admit it.
4
+ ALTER TABLE "saved_reports"
5
+ DROP CONSTRAINT IF EXISTS "saved_reports_status_check";
6
+
7
+ ALTER TABLE "saved_reports"
8
+ ADD CONSTRAINT "saved_reports_status_check"
9
+ CHECK ("status" IN ('draft', 'published', 'archived'));