payweave 0.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/LICENSE +21 -0
- package/README.md +272 -0
- package/dist/_tsup-dts-rollup.d.ts +19470 -0
- package/dist/chunk-4R5OGOCC.js +94 -0
- package/dist/chunk-4R5OGOCC.js.map +1 -0
- package/dist/chunk-535Q3GUN.js +175 -0
- package/dist/chunk-535Q3GUN.js.map +1 -0
- package/dist/chunk-67NRYGRC.js +192 -0
- package/dist/chunk-67NRYGRC.js.map +1 -0
- package/dist/chunk-CGQ73I4I.js +568 -0
- package/dist/chunk-CGQ73I4I.js.map +1 -0
- package/dist/chunk-CJEYDHY6.js +18 -0
- package/dist/chunk-CJEYDHY6.js.map +1 -0
- package/dist/chunk-CR63JAAS.js +3 -0
- package/dist/chunk-CR63JAAS.js.map +1 -0
- package/dist/chunk-E633MKVX.js +1507 -0
- package/dist/chunk-E633MKVX.js.map +1 -0
- package/dist/chunk-FBPOCXRZ.js +1135 -0
- package/dist/chunk-FBPOCXRZ.js.map +1 -0
- package/dist/chunk-HTRGMZFH.js +267 -0
- package/dist/chunk-HTRGMZFH.js.map +1 -0
- package/dist/chunk-IUVGM3K7.js +765 -0
- package/dist/chunk-IUVGM3K7.js.map +1 -0
- package/dist/chunk-JD4OZHRI.js +326 -0
- package/dist/chunk-JD4OZHRI.js.map +1 -0
- package/dist/chunk-NV3MZF7L.js +139 -0
- package/dist/chunk-NV3MZF7L.js.map +1 -0
- package/dist/chunk-NXQBP35U.js +13 -0
- package/dist/chunk-NXQBP35U.js.map +1 -0
- package/dist/chunk-QJHXDZYC.js +412 -0
- package/dist/chunk-QJHXDZYC.js.map +1 -0
- package/dist/chunk-TWZK62TZ.js +83 -0
- package/dist/chunk-TWZK62TZ.js.map +1 -0
- package/dist/cli/index.js +35381 -0
- package/dist/core/index.d.ts +85 -0
- package/dist/core/index.js +6 -0
- package/dist/core/index.js.map +1 -0
- package/dist/db/drizzle/index.d.ts +6 -0
- package/dist/db/drizzle/index.js +1532 -0
- package/dist/db/drizzle/index.js.map +1 -0
- package/dist/db/index.d.ts +43 -0
- package/dist/db/index.js +3 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/mongodb/index.d.ts +3 -0
- package/dist/db/mongodb/index.js +934 -0
- package/dist/db/mongodb/index.js.map +1 -0
- package/dist/db/mysql/index.d.ts +1 -0
- package/dist/db/mysql/index.js +12 -0
- package/dist/db/mysql/index.js.map +1 -0
- package/dist/db/postgres/index.d.ts +5 -0
- package/dist/db/postgres/index.js +791 -0
- package/dist/db/postgres/index.js.map +1 -0
- package/dist/db/prisma/index.d.ts +1 -0
- package/dist/db/prisma/index.js +12 -0
- package/dist/db/prisma/index.js.map +1 -0
- package/dist/db/sqlite/index.d.ts +5 -0
- package/dist/db/sqlite/index.js +750 -0
- package/dist/db/sqlite/index.js.map +1 -0
- package/dist/express/index.d.ts +1 -0
- package/dist/express/index.js +3 -0
- package/dist/express/index.js.map +1 -0
- package/dist/fastify/index.d.ts +1 -0
- package/dist/fastify/index.js +3 -0
- package/dist/fastify/index.js.map +1 -0
- package/dist/flutterwave/index.d.ts +27 -0
- package/dist/flutterwave/index.js +9 -0
- package/dist/flutterwave/index.js.map +1 -0
- package/dist/index.d.ts +85 -0
- package/dist/index.js +3677 -0
- package/dist/index.js.map +1 -0
- package/dist/next/index.d.ts +1 -0
- package/dist/next/index.js +3 -0
- package/dist/next/index.js.map +1 -0
- package/dist/paystack/index.d.ts +33 -0
- package/dist/paystack/index.js +9 -0
- package/dist/paystack/index.js.map +1 -0
- package/dist/products/index.d.ts +22 -0
- package/dist/products/index.js +7 -0
- package/dist/products/index.js.map +1 -0
- package/dist/testing/index.d.ts +11 -0
- package/dist/testing/index.js +69 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/unified/index.d.ts +41 -0
- package/dist/unified/index.js +6 -0
- package/dist/unified/index.js.map +1 -0
- package/dist/webhooks/index.d.ts +8 -0
- package/dist/webhooks/index.js +8 -0
- package/dist/webhooks/index.js.map +1 -0
- package/package.json +164 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { PayweaveValidationError } from './chunk-HTRGMZFH.js';
|
|
2
|
+
|
|
3
|
+
// src/products/period.ts
|
|
4
|
+
var DAY_MS = 864e5;
|
|
5
|
+
var WEEK_MS = 6048e5;
|
|
6
|
+
function assertEpochMs(value, name) {
|
|
7
|
+
if (!Number.isSafeInteger(value)) {
|
|
8
|
+
throw new PayweaveValidationError(
|
|
9
|
+
`${name} must be an integer epoch-milliseconds timestamp \u2014 got ${value}`
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function assertCount(value, name, min) {
|
|
14
|
+
if (!Number.isSafeInteger(value) || value < min) {
|
|
15
|
+
throw new PayweaveValidationError(`${name} must be a safe integer >= ${min} \u2014 got ${value}`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function daysInUtcMonth(year, monthIndex) {
|
|
19
|
+
return new Date(Date.UTC(year, monthIndex + 1, 0)).getUTCDate();
|
|
20
|
+
}
|
|
21
|
+
function addUtcMonthsClamped(anchorMs, months) {
|
|
22
|
+
const anchor = new Date(anchorMs);
|
|
23
|
+
const totalMonths = anchor.getUTCMonth() + months;
|
|
24
|
+
const year = anchor.getUTCFullYear() + Math.floor(totalMonths / 12);
|
|
25
|
+
const monthIndex = (totalMonths % 12 + 12) % 12;
|
|
26
|
+
const day = Math.min(anchor.getUTCDate(), daysInUtcMonth(year, monthIndex));
|
|
27
|
+
return Date.UTC(
|
|
28
|
+
year,
|
|
29
|
+
monthIndex,
|
|
30
|
+
day,
|
|
31
|
+
anchor.getUTCHours(),
|
|
32
|
+
anchor.getUTCMinutes(),
|
|
33
|
+
anchor.getUTCSeconds(),
|
|
34
|
+
anchor.getUTCMilliseconds()
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
function advance(anchorMs, reset, count) {
|
|
38
|
+
assertEpochMs(anchorMs, "anchorMs");
|
|
39
|
+
assertCount(count, "count", 0);
|
|
40
|
+
let result;
|
|
41
|
+
switch (reset) {
|
|
42
|
+
case "day":
|
|
43
|
+
result = anchorMs + count * DAY_MS;
|
|
44
|
+
break;
|
|
45
|
+
case "week":
|
|
46
|
+
result = anchorMs + count * WEEK_MS;
|
|
47
|
+
break;
|
|
48
|
+
case "month":
|
|
49
|
+
result = addUtcMonthsClamped(anchorMs, count);
|
|
50
|
+
break;
|
|
51
|
+
case "year":
|
|
52
|
+
result = addUtcMonthsClamped(anchorMs, count * 12);
|
|
53
|
+
break;
|
|
54
|
+
default: {
|
|
55
|
+
const invalid = reset;
|
|
56
|
+
throw new PayweaveValidationError(`unknown reset interval: ${String(invalid)}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (!Number.isSafeInteger(result)) {
|
|
60
|
+
throw new PayweaveValidationError(
|
|
61
|
+
`advance(${anchorMs}, '${reset}', ${count}) is outside the representable date range`
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
function currentPeriod(anchorMs, reset, nowMs, every = 1) {
|
|
67
|
+
assertEpochMs(anchorMs, "anchorMs");
|
|
68
|
+
assertEpochMs(nowMs, "nowMs");
|
|
69
|
+
assertCount(every, "every", 1);
|
|
70
|
+
let estimate;
|
|
71
|
+
if (reset === "day" || reset === "week") {
|
|
72
|
+
const periodMs = (reset === "day" ? DAY_MS : WEEK_MS) * every;
|
|
73
|
+
estimate = Math.floor((nowMs - anchorMs) / periodMs);
|
|
74
|
+
} else {
|
|
75
|
+
const monthsPerPeriod = (reset === "month" ? 1 : 12) * every;
|
|
76
|
+
const anchor = new Date(anchorMs);
|
|
77
|
+
const now = new Date(nowMs);
|
|
78
|
+
const monthDiff = (now.getUTCFullYear() - anchor.getUTCFullYear()) * 12 + (now.getUTCMonth() - anchor.getUTCMonth());
|
|
79
|
+
estimate = Math.floor(monthDiff / monthsPerPeriod);
|
|
80
|
+
}
|
|
81
|
+
let index = Math.max(0, estimate - 1);
|
|
82
|
+
while (nowMs >= advance(anchorMs, reset, (index + 1) * every)) {
|
|
83
|
+
index += 1;
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
index,
|
|
87
|
+
start: advance(anchorMs, reset, index * every),
|
|
88
|
+
end: advance(anchorMs, reset, (index + 1) * every)
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export { DAY_MS, WEEK_MS, advance, currentPeriod };
|
|
93
|
+
//# sourceMappingURL=chunk-4R5OGOCC.js.map
|
|
94
|
+
//# sourceMappingURL=chunk-4R5OGOCC.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/products/period.ts"],"names":[],"mappings":";;;AAwBO,IAAM,MAAA,GAAS;AAGf,IAAM,OAAA,GAAU;AAUvB,SAAS,aAAA,CAAc,OAAe,IAAA,EAAoB;AACxD,EAAA,IAAI,CAAC,MAAA,CAAO,aAAA,CAAc,KAAK,CAAA,EAAG;AAChC,IAAA,MAAM,IAAI,uBAAA;AAAA,MACR,CAAA,EAAG,IAAI,CAAA,4DAAA,EAA0D,KAAK,CAAA;AAAA,KACxE;AAAA,EACF;AACF;AAGA,SAAS,WAAA,CAAY,KAAA,EAAe,IAAA,EAAc,GAAA,EAAmB;AACnE,EAAA,IAAI,CAAC,MAAA,CAAO,aAAA,CAAc,KAAK,CAAA,IAAK,QAAQ,GAAA,EAAK;AAC/C,IAAA,MAAM,IAAI,wBAAwB,CAAA,EAAG,IAAI,8BAA8B,GAAG,CAAA,YAAA,EAAU,KAAK,CAAA,CAAE,CAAA;AAAA,EAC7F;AACF;AAGA,SAAS,cAAA,CAAe,MAAc,UAAA,EAA4B;AAEhE,EAAA,OAAO,IAAI,IAAA,CAAK,IAAA,CAAK,GAAA,CAAI,IAAA,EAAM,aAAa,CAAA,EAAG,CAAC,CAAC,CAAA,CAAE,UAAA,EAAW;AAChE;AAOA,SAAS,mBAAA,CAAoB,UAAkB,MAAA,EAAwB;AACrE,EAAA,MAAM,MAAA,GAAS,IAAI,IAAA,CAAK,QAAQ,CAAA;AAChC,EAAA,MAAM,WAAA,GAAc,MAAA,CAAO,WAAA,EAAY,GAAI,MAAA;AAC3C,EAAA,MAAM,OAAO,MAAA,CAAO,cAAA,KAAmB,IAAA,CAAK,KAAA,CAAM,cAAc,EAAE,CAAA;AAClE,EAAA,MAAM,UAAA,GAAA,CAAe,WAAA,GAAc,EAAA,GAAM,EAAA,IAAM,EAAA;AAC/C,EAAA,MAAM,GAAA,GAAM,KAAK,GAAA,CAAI,MAAA,CAAO,YAAW,EAAG,cAAA,CAAe,IAAA,EAAM,UAAU,CAAC,CAAA;AAC1E,EAAA,OAAO,IAAA,CAAK,GAAA;AAAA,IACV,IAAA;AAAA,IACA,UAAA;AAAA,IACA,GAAA;AAAA,IACA,OAAO,WAAA,EAAY;AAAA,IACnB,OAAO,aAAA,EAAc;AAAA,IACrB,OAAO,aAAA,EAAc;AAAA,IACrB,OAAO,kBAAA;AAAmB,GAC5B;AACF;AAqBO,SAAS,OAAA,CAAQ,QAAA,EAAkB,KAAA,EAAsB,KAAA,EAAuB;AACrF,EAAA,aAAA,CAAc,UAAU,UAAU,CAAA;AAClC,EAAA,WAAA,CAAY,KAAA,EAAO,SAAS,CAAC,CAAA;AAC7B,EAAA,IAAI,MAAA;AACJ,EAAA,QAAQ,KAAA;AAAO,IACb,KAAK,KAAA;AACH,MAAA,MAAA,GAAS,WAAW,KAAA,GAAQ,MAAA;AAC5B,MAAA;AAAA,IACF,KAAK,MAAA;AACH,MAAA,MAAA,GAAS,WAAW,KAAA,GAAQ,OAAA;AAC5B,MAAA;AAAA,IACF,KAAK,OAAA;AACH,MAAA,MAAA,GAAS,mBAAA,CAAoB,UAAU,KAAK,CAAA;AAC5C,MAAA;AAAA,IACF,KAAK,MAAA;AACH,MAAA,MAAA,GAAS,mBAAA,CAAoB,QAAA,EAAU,KAAA,GAAQ,EAAE,CAAA;AACjD,MAAA;AAAA,IACF,SAAS;AACP,MAAA,MAAM,OAAA,GAAiB,KAAA;AACvB,MAAA,MAAM,IAAI,uBAAA,CAAwB,CAAA,wBAAA,EAA2B,MAAA,CAAO,OAAO,CAAC,CAAA,CAAE,CAAA;AAAA,IAChF;AAAA;AAEF,EAAA,IAAI,CAAC,MAAA,CAAO,aAAA,CAAc,MAAM,CAAA,EAAG;AACjC,IAAA,MAAM,IAAI,uBAAA;AAAA,MACR,CAAA,QAAA,EAAW,QAAQ,CAAA,GAAA,EAAM,KAAK,MAAM,KAAK,CAAA,yCAAA;AAAA,KAC3C;AAAA,EACF;AACA,EAAA,OAAO,MAAA;AACT;AA2BO,SAAS,aAAA,CACd,QAAA,EACA,KAAA,EACA,KAAA,EACA,QAAgB,CAAA,EACD;AACf,EAAA,aAAA,CAAc,UAAU,UAAU,CAAA;AAClC,EAAA,aAAA,CAAc,OAAO,OAAO,CAAA;AAC5B,EAAA,WAAA,CAAY,KAAA,EAAO,SAAS,CAAC,CAAA;AAM7B,EAAA,IAAI,QAAA;AACJ,EAAA,IAAI,KAAA,KAAU,KAAA,IAAS,KAAA,KAAU,MAAA,EAAQ;AACvC,IAAA,MAAM,QAAA,GAAA,CAAY,KAAA,KAAU,KAAA,GAAQ,MAAA,GAAS,OAAA,IAAW,KAAA;AACxD,IAAA,QAAA,GAAW,IAAA,CAAK,KAAA,CAAA,CAAO,KAAA,GAAQ,QAAA,IAAY,QAAQ,CAAA;AAAA,EACrD,CAAA,MAAO;AACL,IAAA,MAAM,eAAA,GAAA,CAAmB,KAAA,KAAU,OAAA,GAAU,CAAA,GAAI,EAAA,IAAM,KAAA;AACvD,IAAA,MAAM,MAAA,GAAS,IAAI,IAAA,CAAK,QAAQ,CAAA;AAChC,IAAA,MAAM,GAAA,GAAM,IAAI,IAAA,CAAK,KAAK,CAAA;AAC1B,IAAA,MAAM,SAAA,GAAA,CACH,GAAA,CAAI,cAAA,EAAe,GAAI,MAAA,CAAO,cAAA,EAAe,IAAK,EAAA,IAClD,GAAA,CAAI,WAAA,EAAY,GAAI,MAAA,CAAO,WAAA,EAAY,CAAA;AAC1C,IAAA,QAAA,GAAW,IAAA,CAAK,KAAA,CAAM,SAAA,GAAY,eAAe,CAAA;AAAA,EACnD;AAMA,EAAA,IAAI,KAAA,GAAQ,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,WAAW,CAAC,CAAA;AACpC,EAAA,OAAO,SAAS,OAAA,CAAQ,QAAA,EAAU,QAAQ,KAAA,GAAQ,CAAA,IAAK,KAAK,CAAA,EAAG;AAC7D,IAAA,KAAA,IAAS,CAAA;AAAA,EACX;AAEA,EAAA,OAAO;AAAA,IACL,KAAA;AAAA,IACA,KAAA,EAAO,OAAA,CAAQ,QAAA,EAAU,KAAA,EAAO,QAAQ,KAAK,CAAA;AAAA,IAC7C,KAAK,OAAA,CAAQ,QAAA,EAAU,KAAA,EAAA,CAAQ,KAAA,GAAQ,KAAK,KAAK;AAAA,GACnD;AACF","file":"chunk-4R5OGOCC.js","sourcesContent":["/**\n * Billing-period math for metered features.\n *\n * All arithmetic is UTC on epoch milliseconds — no timezone or DST logic in v1\n * `day`/`week` are fixed 86_400_000 / 604_800_000 ms\n * multiples of the anchor; UTC has no DST, so a \"day\" is always exactly 24h.\n *\n * THE invariant: period math is ANCHOR-RELATIVE. Period `n` is\n * `[advance(anchor, reset, n), advance(anchor, reset, n + 1))` — every\n * boundary is derived by adding `n` whole intervals to the ORIGINAL anchor,\n * with end-of-month clamping applied once per result. Iterating `advance` on\n * clamped outputs is forbidden: a Jan 31 anchor yields Feb 28/29, **Mar 31**,\n * Apr 30, May 31, … — never the drifted Jan 31 → Feb 28 → Mar 28 → … sequence.\n *\n * These functions are internal until PW-505 wires `src/products/` into the\n * exports map; PW-902 consumes them for the lazy reset inside\n * `balances.consume`.\n */\nimport { PayweaveValidationError } from \"../core/errors\";\n\n/** Reset interval for a metered feature. */\nexport type ResetInterval = \"day\" | \"week\" | \"month\" | \"year\";\n\n/** Exactly 24 UTC hours in milliseconds. */\nexport const DAY_MS = 86_400_000;\n\n/** Exactly 7 UTC days in milliseconds. */\nexport const WEEK_MS = 604_800_000;\n\n/**\n * One billing period, half-open: `start` inclusive, `end` exclusive. All\n * values are UTC epoch milliseconds; `index` is the zero-based period number\n * relative to the anchor (period 0 starts AT the anchor).\n */\nexport type BillingPeriod = { index: number; start: number; end: number };\n\n/** Throws unless `value` is an integer epoch-milliseconds timestamp. */\nfunction assertEpochMs(value: number, name: string): void {\n if (!Number.isSafeInteger(value)) {\n throw new PayweaveValidationError(\n `${name} must be an integer epoch-milliseconds timestamp — got ${value}`,\n );\n }\n}\n\n/** Throws unless `value` is a safe integer >= `min`. */\nfunction assertCount(value: number, name: string, min: number): void {\n if (!Number.isSafeInteger(value) || value < min) {\n throw new PayweaveValidationError(`${name} must be a safe integer >= ${min} — got ${value}`);\n }\n}\n\n/** Number of days in a UTC month (`monthIndex` 0-based, any overflow-free year). */\nfunction daysInUtcMonth(year: number, monthIndex: number): number {\n // Day 0 of the NEXT month is the last day of this one.\n return new Date(Date.UTC(year, monthIndex + 1, 0)).getUTCDate();\n}\n\n/**\n * Add whole calendar months to an anchor instant, clamping the day-of-month\n * to the target month's length ONCE for this result. Time of day is preserved\n * verbatim (UTC components).\n */\nfunction addUtcMonthsClamped(anchorMs: number, months: number): number {\n const anchor = new Date(anchorMs);\n const totalMonths = anchor.getUTCMonth() + months;\n const year = anchor.getUTCFullYear() + Math.floor(totalMonths / 12);\n const monthIndex = ((totalMonths % 12) + 12) % 12;\n const day = Math.min(anchor.getUTCDate(), daysInUtcMonth(year, monthIndex));\n return Date.UTC(\n year,\n monthIndex,\n day,\n anchor.getUTCHours(),\n anchor.getUTCMinutes(),\n anchor.getUTCSeconds(),\n anchor.getUTCMilliseconds(),\n );\n}\n\n/**\n * Advance an anchor instant by `count` whole reset intervals\n * (metered-usage.md §5's `advance(anchor, reset, n)`).\n *\n * - `day` / `week`: fixed {@link DAY_MS} / {@link WEEK_MS} multiples — pure\n * integer ms arithmetic, DST-free by construction (UTC).\n * - `month` / `year`: calendar addition on the ANCHOR's UTC components with\n * end-of-month clamping applied once to the result. A Jan 31 anchor gives\n * Feb 28 (Feb 29 in leap years) at `count` 1 and Mar 31 at `count` 2. A\n * Feb 29 anchor advanced whole years (or multiples of 12 months) lands on\n * Feb 29 in leap years and clamps to Feb 28 otherwise.\n *\n * `count` must be a non-negative safe integer — period boundaries never look\n * backwards from the anchor. Throws {@link PayweaveValidationError} on invalid\n * input or when the result falls outside the representable Date range.\n *\n * @example advance(Date.UTC(2025, 0, 31), \"month\", 1) // Date.UTC(2025, 1, 28)\n * @example advance(Date.UTC(2025, 0, 31), \"month\", 2) // Date.UTC(2025, 2, 31) — no drift\n */\nexport function advance(anchorMs: number, reset: ResetInterval, count: number): number {\n assertEpochMs(anchorMs, \"anchorMs\");\n assertCount(count, \"count\", 0);\n let result: number;\n switch (reset) {\n case \"day\":\n result = anchorMs + count * DAY_MS;\n break;\n case \"week\":\n result = anchorMs + count * WEEK_MS;\n break;\n case \"month\":\n result = addUtcMonthsClamped(anchorMs, count);\n break;\n case \"year\":\n result = addUtcMonthsClamped(anchorMs, count * 12);\n break;\n default: {\n const invalid: never = reset;\n throw new PayweaveValidationError(`unknown reset interval: ${String(invalid)}`);\n }\n }\n if (!Number.isSafeInteger(result)) {\n throw new PayweaveValidationError(\n `advance(${anchorMs}, '${reset}', ${count}) is outside the representable date range`,\n );\n }\n return result;\n}\n\n/**\n * Multi-period roll-forward (metered-usage.md §5's lazy reset): the CURRENT\n * billing period for `nowMs` — the smallest `index >= 0` with\n * `nowMs < advance(anchorMs, reset, (index + 1) * every)`. A balance row idle\n * for many periods lands directly in the window containing `nowMs`, never an\n * intermediate one. Runs in O(1): a calendar estimate corrected by at most a\n * couple of exact `advance` probes.\n *\n * Boundaries are half-open — `nowMs` exactly at a period end belongs to the\n * NEXT period. Every boundary is derived anchor-relatively via {@link advance},\n * so end-of-month clamping never drifts the cycle day (Jan 31 anchor, now in\n * mid-March → period `[Feb 28, Mar 31)`, not `[…, Mar 28)`).\n *\n * `nowMs` before the anchor clamps to period 0 (`[anchor, advance(anchor, …, every))`).\n * The spec is silent here: anchors are seeded at or before first use, so an\n * earlier `nowMs` means clock skew, and the conservative behavior for the\n * consume path is the first period rather than a throw.\n *\n * `every` (default 1) is the number of reset intervals per period, for\n * multi-interval cycles such as quarterly (`reset: \"month\", every: 3`). The\n * v1 plan schema always uses 1.\n *\n * @example currentPeriod(Date.UTC(2025, 0, 31), \"month\", Date.UTC(2025, 2, 15))\n * // { index: 1, start: Date.UTC(2025, 1, 28), end: Date.UTC(2025, 2, 31) }\n */\nexport function currentPeriod(\n anchorMs: number,\n reset: ResetInterval,\n nowMs: number,\n every: number = 1,\n): BillingPeriod {\n assertEpochMs(anchorMs, \"anchorMs\");\n assertEpochMs(nowMs, \"nowMs\");\n assertCount(every, \"every\", 1);\n\n // O(1) estimate of the period index. `estimate` is at most `true index + 1`\n // (float rounding for day/week; calendar-month distance ignores day-of-month\n // and time-of-day for month/year), so `estimate - 1` NEVER overshoots the\n // true index and undershoots it by at most 2.\n let estimate: number;\n if (reset === \"day\" || reset === \"week\") {\n const periodMs = (reset === \"day\" ? DAY_MS : WEEK_MS) * every;\n estimate = Math.floor((nowMs - anchorMs) / periodMs);\n } else {\n const monthsPerPeriod = (reset === \"month\" ? 1 : 12) * every;\n const anchor = new Date(anchorMs);\n const now = new Date(nowMs);\n const monthDiff =\n (now.getUTCFullYear() - anchor.getUTCFullYear()) * 12 +\n (now.getUTCMonth() - anchor.getUTCMonth());\n estimate = Math.floor(monthDiff / monthsPerPeriod);\n }\n\n // Walk up to the spec's answer — the smallest `index` whose exclusive end is\n // beyond `nowMs`. The estimate never overshoots, so at\n // most two exact anchor-relative probes run; `advance` is strictly\n // increasing in `count`, so the loop terminates.\n let index = Math.max(0, estimate - 1);\n while (nowMs >= advance(anchorMs, reset, (index + 1) * every)) {\n index += 1;\n }\n\n return {\n index,\n start: advance(anchorMs, reset, index * every),\n end: advance(anchorMs, reset, (index + 1) * every),\n };\n}\n"]}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { toMinor, exponentFor } from './chunk-TWZK62TZ.js';
|
|
2
|
+
import { PayweaveValidationError } from './chunk-HTRGMZFH.js';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
|
|
5
|
+
var PRODUCT_ID_REGEX = /^[a-z0-9][a-z0-9_-]{0,63}$/;
|
|
6
|
+
var PRODUCT_ID_MESSAGE = "must be lowercase alphanumeric with dashes or underscores, max 64 characters";
|
|
7
|
+
var RESET_INTERVALS = ["day", "week", "month", "year"];
|
|
8
|
+
var FEATURE_DEF = /* @__PURE__ */ Symbol("payweave.products.featureDef");
|
|
9
|
+
var INCLUSION = /* @__PURE__ */ Symbol("payweave.products.inclusion");
|
|
10
|
+
var featureDefSchema = z.strictObject({
|
|
11
|
+
id: z.string().regex(PRODUCT_ID_REGEX, PRODUCT_ID_MESSAGE),
|
|
12
|
+
type: z.enum(["boolean", "metered"])
|
|
13
|
+
});
|
|
14
|
+
function zodDetail(err) {
|
|
15
|
+
return err.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
|
|
16
|
+
}
|
|
17
|
+
var meteredArgSchema = z.strictObject({
|
|
18
|
+
limit: z.number({ error: "limit must be a positive safe integer" }).int("limit must be a positive safe integer").positive("limit must be a positive safe integer"),
|
|
19
|
+
reset: z.enum(RESET_INTERVALS, 'reset must be one of "day", "week", "month", "year"')
|
|
20
|
+
});
|
|
21
|
+
function createInclusion(data, invalidArg) {
|
|
22
|
+
const meta = { invalidArg };
|
|
23
|
+
const inclusion = { ...data };
|
|
24
|
+
Object.defineProperty(inclusion, INCLUSION, { value: meta, enumerable: false });
|
|
25
|
+
return Object.freeze(inclusion);
|
|
26
|
+
}
|
|
27
|
+
function inclusionMeta(value) {
|
|
28
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
29
|
+
return value[INCLUSION];
|
|
30
|
+
}
|
|
31
|
+
function featureFnDef(value) {
|
|
32
|
+
if (typeof value !== "function") return void 0;
|
|
33
|
+
return value[FEATURE_DEF];
|
|
34
|
+
}
|
|
35
|
+
function feature(def) {
|
|
36
|
+
let parsed;
|
|
37
|
+
try {
|
|
38
|
+
parsed = featureDefSchema.parse(def);
|
|
39
|
+
} catch (err) {
|
|
40
|
+
if (err instanceof z.ZodError) {
|
|
41
|
+
throw new PayweaveValidationError(`invalid feature definition \u2014 ${zodDetail(err)}`);
|
|
42
|
+
}
|
|
43
|
+
throw err;
|
|
44
|
+
}
|
|
45
|
+
const callable = (...args) => {
|
|
46
|
+
if (parsed.type === "boolean") {
|
|
47
|
+
return createInclusion({ featureId: parsed.id, type: "boolean" }, args.length > 0);
|
|
48
|
+
}
|
|
49
|
+
const argResult = meteredArgSchema.safeParse(args[0]);
|
|
50
|
+
if (!argResult.success) {
|
|
51
|
+
throw new PayweaveValidationError(
|
|
52
|
+
`metered feature "${parsed.id}" requires { limit: positive safe integer, reset: "day" | "week" | "month" | "year" } \u2014 ${zodDetail(argResult.error)}`
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
return createInclusion(
|
|
56
|
+
{
|
|
57
|
+
featureId: parsed.id,
|
|
58
|
+
type: "metered",
|
|
59
|
+
limit: argResult.data.limit,
|
|
60
|
+
reset: argResult.data.reset
|
|
61
|
+
},
|
|
62
|
+
false
|
|
63
|
+
);
|
|
64
|
+
};
|
|
65
|
+
Object.defineProperty(callable, FEATURE_DEF, { value: parsed, enumerable: false });
|
|
66
|
+
return Object.freeze(callable);
|
|
67
|
+
}
|
|
68
|
+
var planPriceSchema = z.strictObject({
|
|
69
|
+
/** Major units at definition time ONLY — e.g. `19.99` for $19.99. */
|
|
70
|
+
amount: z.number({ error: "price.amount must be a finite number" }).positive("price.amount must be positive"),
|
|
71
|
+
/** ISO 4217; omit to use `defaultCurrency` from `createPayweave`. */
|
|
72
|
+
currency: z.string().regex(/^[A-Za-z]{3}$/, 'price.currency must be a 3-letter ISO 4217 code, e.g. "USD"').transform((value) => value.toUpperCase()).optional(),
|
|
73
|
+
interval: z.enum(["month", "year"])
|
|
74
|
+
});
|
|
75
|
+
var planBaseSchema = z.strictObject({
|
|
76
|
+
id: z.string().regex(PRODUCT_ID_REGEX, PRODUCT_ID_MESSAGE),
|
|
77
|
+
name: z.string().min(1, "name must not be empty").optional(),
|
|
78
|
+
group: z.string().min(1, "group must not be empty").optional(),
|
|
79
|
+
default: z.boolean().optional(),
|
|
80
|
+
price: planPriceSchema.optional()
|
|
81
|
+
});
|
|
82
|
+
function zodDetail2(err) {
|
|
83
|
+
return err.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
|
|
84
|
+
}
|
|
85
|
+
function validateIncludes(planId, raw) {
|
|
86
|
+
const seen = /* @__PURE__ */ new Set();
|
|
87
|
+
const out = [];
|
|
88
|
+
for (const entry of raw) {
|
|
89
|
+
const rawDef = featureFnDef(entry);
|
|
90
|
+
if (rawDef !== void 0) {
|
|
91
|
+
const hint = rawDef.type === "metered" ? "{ limit, reset }" : "";
|
|
92
|
+
throw new PayweaveValidationError(`plan "${planId}": did you mean \`${rawDef.id}(${hint})\`?`);
|
|
93
|
+
}
|
|
94
|
+
const meta = inclusionMeta(entry);
|
|
95
|
+
if (meta === void 0) {
|
|
96
|
+
throw new PayweaveValidationError(
|
|
97
|
+
`plan "${planId}" includes an invalid entry \u2014 expected a called feature, e.g. messages({ limit, reset }) or proModels()`
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
const inclusion = entry;
|
|
101
|
+
if (meta.invalidArg) {
|
|
102
|
+
throw new PayweaveValidationError(
|
|
103
|
+
`plan "${planId}": boolean feature "${inclusion.featureId}" does not take arguments`
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
if (seen.has(inclusion.featureId)) {
|
|
107
|
+
throw new PayweaveValidationError(
|
|
108
|
+
`plan "${planId}" includes feature "${inclusion.featureId}" more than once`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
seen.add(inclusion.featureId);
|
|
112
|
+
out.push(inclusion);
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
function plan(def) {
|
|
117
|
+
const { includes, ...rest } = def;
|
|
118
|
+
let base;
|
|
119
|
+
try {
|
|
120
|
+
base = planBaseSchema.parse(rest);
|
|
121
|
+
} catch (err) {
|
|
122
|
+
if (err instanceof z.ZodError) {
|
|
123
|
+
throw new PayweaveValidationError(`invalid plan definition \u2014 ${zodDetail2(err)}`);
|
|
124
|
+
}
|
|
125
|
+
throw err;
|
|
126
|
+
}
|
|
127
|
+
if (base.default === true && base.group === void 0) {
|
|
128
|
+
throw new PayweaveValidationError(
|
|
129
|
+
`plan "${base.id}": default: true requires a group`
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
const validatedIncludes = validateIncludes(base.id, includes ?? []);
|
|
133
|
+
const result = Object.freeze({
|
|
134
|
+
id: base.id,
|
|
135
|
+
name: base.name,
|
|
136
|
+
group: base.group,
|
|
137
|
+
default: base.default ?? false,
|
|
138
|
+
price: base.price,
|
|
139
|
+
includes: Object.freeze(validatedIncludes)
|
|
140
|
+
});
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
function maxMajorAmount(exponent) {
|
|
144
|
+
const bound = 10 ** (6 + exponent) - 1;
|
|
145
|
+
return (bound / 10 ** exponent).toFixed(exponent);
|
|
146
|
+
}
|
|
147
|
+
function resolvePlanPricing(planValue, defaultCurrency) {
|
|
148
|
+
if (planValue.price === void 0) return void 0;
|
|
149
|
+
const currency = planValue.price.currency ?? defaultCurrency;
|
|
150
|
+
let converted;
|
|
151
|
+
try {
|
|
152
|
+
converted = toMinor(planValue.price.amount, currency);
|
|
153
|
+
} catch (err) {
|
|
154
|
+
if (err instanceof PayweaveValidationError) {
|
|
155
|
+
throw new PayweaveValidationError(`plan "${planValue.id}": ${err.message}`, { cause: err });
|
|
156
|
+
}
|
|
157
|
+
throw err;
|
|
158
|
+
}
|
|
159
|
+
const exp = exponentFor(currency);
|
|
160
|
+
const bound = 10 ** (6 + exp) - 1;
|
|
161
|
+
if (converted.value > bound) {
|
|
162
|
+
throw new PayweaveValidationError(
|
|
163
|
+
`plan "${planValue.id}": amount ${planValue.price.amount} ${converted.currency} exceeds the maximum of ${maxMajorAmount(exp)} ${converted.currency}`
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
amount: converted.value,
|
|
168
|
+
currency: converted.currency,
|
|
169
|
+
interval: planValue.price.interval
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export { feature, plan, resolvePlanPricing };
|
|
174
|
+
//# sourceMappingURL=chunk-535Q3GUN.js.map
|
|
175
|
+
//# sourceMappingURL=chunk-535Q3GUN.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/products/feature.ts","../src/products/plan.ts"],"names":["z","zodDetail"],"mappings":"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","file":"chunk-535Q3GUN.js","sourcesContent":["/**\n * `feature()` primitive.\n *\n * A feature is either a boolean access gate or a metered usage limit,\n * identified by a lowercase id. `feature()` validates the definition with Zod\n * and returns a CALLABLE: invoking it produces a {@link FeatureInclusion} for\n * a `plan()`'s `includes` array:\n *\n * ```ts\n * const proModels = feature({ id: \"pro_models\", type: \"boolean\" });\n * const messages = feature({ id: \"messages\", type: \"metered\" });\n * // ...\n * proModels(); // FeatureInclusion<\"pro_models\">\n * messages({ limit: 100, reset: \"month\" }); // FeatureInclusion<\"messages\">\n * ```\n *\n * The parsed def rides along on a module-private symbol so `plan()` (a\n * SEPARATE file) can recognize a raw, uncalled feature passed by mistake and\n * give the \"did you mean\" hint. The symbol itself is never exported —\n * {@link featureFnDef} is the only way to read it, and neither it nor\n * {@link inclusionMeta} are re-exported from `src/products/index.ts`, so they\n * never become part of the public `payweave/products` surface.\n *\n * Argument-shape validation is split by timing on purpose (agent-playbook\n * contract notes — a spec-silent decision recorded here since only ONE half\n * of it is spelled out verbatim):\n * - a METERED feature validates its `{ limit, reset }` argument IMMEDIATELY\n * when called — there is no reason to defer an unambiguous shape check.\n * - a BOOLEAN feature called with an argument is the one deviation the spec\n * calls out explicitly: the callable never throws for this — it just\n * records the violation on the returned inclusion, and `plan()` is the\n * one that rejects it, at plan-parse time (so every \"you gave `plan()`\n * something wrong\" error surfaces from one place: `plan()` itself).\n */\nimport { z } from \"zod\";\nimport { PayweaveValidationError } from \"../core/errors\";\nimport type { ResetInterval } from \"./period\";\n\n/** Feature/plan id: lowercase alphanumeric, dashes/underscores, max 64 chars. */\nexport const PRODUCT_ID_REGEX = /^[a-z0-9][a-z0-9_-]{0,63}$/;\n\n/** Shared with `plan.ts` — plans use the exact same id rule. */\nexport const PRODUCT_ID_MESSAGE =\n \"must be lowercase alphanumeric with dashes or underscores, max 64 characters\";\n\nconst RESET_INTERVALS = [\"day\", \"week\", \"month\", \"year\"] as const;\n\n/** Module-private — carries the parsed {@link FeatureDef} on a feature callable. */\nconst FEATURE_DEF: unique symbol = Symbol(\"payweave.products.featureDef\");\n/** Module-private — marks a genuine {@link FeatureInclusion} and its deferred-violation flag. */\nconst INCLUSION: unique symbol = Symbol(\"payweave.products.inclusion\");\n\nconst featureDefSchema = z.strictObject({\n id: z.string().regex(PRODUCT_ID_REGEX, PRODUCT_ID_MESSAGE),\n type: z.enum([\"boolean\", \"metered\"]),\n});\n\nfunction zodDetail(err: z.ZodError): string {\n return err.issues.map((issue) => `${issue.path.join(\".\") || \"(root)\"}: ${issue.message}`).join(\"; \");\n}\n\n/** `feature()`'s accepted input — the bound for its `const` generic. */\nexport type FeatureDefInput = z.input<typeof featureDefSchema>;\n/** The two feature kinds. */\nexport type FeatureType = FeatureDefInput[\"type\"];\n\n/** A feature's parsed definition, generic over its literal id/type. */\nexport type FeatureDef<Id extends string = string, Type extends FeatureType = FeatureType> = {\n readonly id: Id;\n readonly type: Type;\n};\n\n/** The argument a metered feature's callable requires. */\nexport type MeteredFeatureArg = { readonly limit: number; readonly reset: ResetInterval };\n\nconst meteredArgSchema = z.strictObject({\n limit: z\n .number({ error: \"limit must be a positive safe integer\" })\n .int(\"limit must be a positive safe integer\")\n .positive(\"limit must be a positive safe integer\"),\n reset: z.enum(RESET_INTERVALS, 'reset must be one of \"day\", \"week\", \"month\", \"year\"'),\n});\n\n/**\n * One feature grant inside a plan's `includes` — produced by CALLING\n * a `feature()` result, never constructed directly. `featureId`/`type` are\n * always present; `limit`/`reset` only accompany a `\"metered\"` inclusion.\n */\nexport type FeatureInclusion<Id extends string = string> =\n | BooleanFeatureInclusion<Id>\n | MeteredFeatureInclusion<Id>;\n\n/** The `\"boolean\"` half of {@link FeatureInclusion} — narrowed so `FeatureFn`'s boolean overload doesn't widen back to the full union. */\nexport type BooleanFeatureInclusion<Id extends string = string> = {\n readonly featureId: Id;\n readonly type: \"boolean\";\n};\n\n/** The `\"metered\"` half of {@link FeatureInclusion} — narrowed so `FeatureFn`'s metered overload doesn't widen back to the full union. */\nexport type MeteredFeatureInclusion<Id extends string = string> = {\n readonly featureId: Id;\n readonly type: \"metered\";\n readonly limit: number;\n readonly reset: ResetInterval;\n};\n\n/** Runtime shape of the hidden marker on every genuine {@link FeatureInclusion} (module-private). */\ntype InclusionMeta = {\n /** Set when a BOOLEAN feature's callable was invoked with an argument — `plan()` rejects it. */\n readonly invalidArg: boolean;\n};\n\nfunction createInclusion<\n Data extends { featureId: string; type: \"boolean\" } | (MeteredFeatureArg & { featureId: string; type: \"metered\" }),\n>(data: Data, invalidArg: boolean): Data {\n const meta: InclusionMeta = { invalidArg };\n const inclusion: Record<PropertyKey, unknown> = { ...data };\n Object.defineProperty(inclusion, INCLUSION, { value: meta, enumerable: false });\n return Object.freeze(inclusion) as unknown as Data;\n}\n\n/**\n * Reads the hidden marker off a value produced by {@link createInclusion}, if\n * present — `undefined` for anything that isn't a genuine `FeatureInclusion`\n * (module-private; consumed by `plan.ts` only).\n */\nexport function inclusionMeta(value: unknown): InclusionMeta | undefined {\n if (typeof value !== \"object\" || value === null) return undefined;\n return (value as Record<PropertyKey, unknown>)[INCLUSION] as InclusionMeta | undefined;\n}\n\n/** The callable `feature()` returns — call it to produce a {@link FeatureInclusion}. */\nexport type FeatureFn<Id extends string = string, Type extends FeatureType = FeatureType> = Type extends \"boolean\"\n ? () => BooleanFeatureInclusion<Id>\n : (arg: MeteredFeatureArg) => MeteredFeatureInclusion<Id>;\n\n/**\n * Reads the hidden {@link FEATURE_DEF} def off a value, if it is a `feature()`\n * callable — `undefined` otherwise (module-private; consumed by `plan.ts`\n * only, to recognize a raw uncalled feature passed to `includes`).\n */\nexport function featureFnDef(value: unknown): FeatureDef | undefined {\n if (typeof value !== \"function\") return undefined;\n return (value as unknown as Record<PropertyKey, unknown>)[FEATURE_DEF] as FeatureDef | undefined;\n}\n\n/**\n * Define a feature. Validates `id` (regex) and `type` (enum) with Zod;\n * throws {@link PayweaveValidationError} on a bad definition. Returns a\n * callable — call it to produce a `FeatureInclusion` for a `plan()`'s\n * `includes` array.\n *\n * ```ts\n * const proModels = feature({ id: \"pro_models\", type: \"boolean\" });\n * const messages = feature({ id: \"messages\", type: \"metered\" });\n * ```\n */\nexport function feature<const Def extends FeatureDefInput>(\n def: Def,\n): FeatureFn<Def[\"id\"], Def[\"type\"]> {\n let parsed: FeatureDef;\n try {\n parsed = featureDefSchema.parse(def);\n } catch (err) {\n if (err instanceof z.ZodError) {\n throw new PayweaveValidationError(`invalid feature definition — ${zodDetail(err)}`);\n }\n throw err;\n }\n\n const callable = (...args: unknown[]): FeatureInclusion => {\n if (parsed.type === \"boolean\") {\n // Deliberate deviation: never throw here — record the violation, `plan()` rejects it.\n return createInclusion({ featureId: parsed.id, type: \"boolean\" }, args.length > 0);\n }\n const argResult = meteredArgSchema.safeParse(args[0]);\n if (!argResult.success) {\n throw new PayweaveValidationError(\n `metered feature \"${parsed.id}\" requires { limit: positive safe integer, reset: \"day\" | ` +\n `\"week\" | \"month\" | \"year\" } — ${zodDetail(argResult.error)}`,\n );\n }\n return createInclusion(\n {\n featureId: parsed.id,\n type: \"metered\",\n limit: argResult.data.limit,\n reset: argResult.data.reset,\n },\n false,\n );\n };\n\n Object.defineProperty(callable, FEATURE_DEF, { value: parsed, enumerable: false });\n return Object.freeze(callable) as unknown as FeatureFn<Def[\"id\"], Def[\"type\"]>;\n}\n","/**\n * `plan()` primitive.\n *\n * `plan()` validates the parts of a plan definition that don't need the rest\n * of the `products` array: `id`, the `default: true` ⇒ `group` requirement,\n * the price shape (still MAJOR units, see below), and its own\n * `includes` (duplicate feature ids, a raw uncalled feature passed by\n * mistake, and the deferred boolean-called-with-argument violation from\n * `feature()`). Rules that need the WHOLE `products` array — duplicate plan\n * ids, more than one `default: true` per group, a feature id used with\n * conflicting types across plans, currency resolution against\n * `defaultCurrency` — are `createPayweave`'s job (agent-playbook\n * contract notes: \"cross-plan rules... belong to config parse, not here. Do\n * not half-implement them.\").\n *\n * **The money deviation** (AGENTS.md golden rule 7): `price.amount`\n * stays in MAJOR units on the value `plan()` returns — that is the ONE place\n * a float is allowed to sit in a Payweave-defined shape. Conversion to\n * integer minor units happens exactly once, via {@link resolvePlanPricing},\n * at config-parse time where a currency (the plan's own, or `defaultCurrency`)\n * is known. `plan()` itself never calls `toMinor` — it cannot: at\n * definition time (this module has no access to `createPayweave`'s config),\n * a plan omitting `price.currency` has no currency to convert against yet.\n */\nimport { z } from \"zod\";\nimport { PayweaveValidationError } from \"../core/errors\";\nimport { exponentFor, toMinor, type Money } from \"../core/money\";\nimport {\n featureFnDef,\n inclusionMeta,\n PRODUCT_ID_MESSAGE,\n PRODUCT_ID_REGEX,\n type FeatureInclusion,\n} from \"./feature\";\n\nconst planPriceSchema = z.strictObject({\n /** Major units at definition time ONLY — e.g. `19.99` for $19.99. */\n amount: z\n .number({ error: \"price.amount must be a finite number\" })\n .positive(\"price.amount must be positive\"),\n /** ISO 4217; omit to use `defaultCurrency` from `createPayweave`. */\n currency: z\n .string()\n .regex(/^[A-Za-z]{3}$/, 'price.currency must be a 3-letter ISO 4217 code, e.g. \"USD\"')\n .transform((value) => value.toUpperCase())\n .optional(),\n interval: z.enum([\"month\", \"year\"]),\n});\n\n/** A plan's price at DEFINITION time — `amount` is MAJOR units (the money deviation). */\nexport type PlanPrice = z.infer<typeof planPriceSchema>;\n/** `price` as accepted by `plan()`. */\nexport type PlanPriceInput = z.input<typeof planPriceSchema>;\n\nconst planBaseSchema = z.strictObject({\n id: z.string().regex(PRODUCT_ID_REGEX, PRODUCT_ID_MESSAGE),\n name: z.string().min(1, \"name must not be empty\").optional(),\n group: z.string().min(1, \"group must not be empty\").optional(),\n default: z.boolean().optional(),\n price: planPriceSchema.optional(),\n});\n\nfunction zodDetail(err: z.ZodError): string {\n return err.issues.map((issue) => `${issue.path.join(\".\") || \"(root)\"}: ${issue.message}`).join(\"; \");\n}\n\n/** `plan()`'s accepted input shape, minus `includes` (handled separately — see module docs). */\ntype PlanBaseInput = z.input<typeof planBaseSchema>;\n\n/** `plan()`'s accepted input — the bound for its `const` generic. */\nexport type PlanDefInput = PlanBaseInput & {\n readonly includes?: readonly FeatureInclusion[];\n};\n\n/**\n * A defined plan, generic over its literal id/group/feature-ids so\n * `PayweaveClient<C>` can extract `PlanIds<C>`/`FeatureIds<C>` from a\n * `products` array. `price` stays in MAJOR units — see\n * {@link resolvePlanPricing} for the config-parse-time conversion.\n */\nexport type Plan<\n Id extends string = string,\n Group extends string | undefined = string | undefined,\n FeatureIds extends string = string,\n> = {\n readonly id: Id;\n readonly name: string | undefined;\n readonly group: Group;\n readonly default: boolean;\n readonly price: PlanPrice | undefined;\n readonly includes: readonly FeatureInclusion<FeatureIds>[];\n};\n\n/** Extracts `Def[\"group\"]` as a literal, or `undefined` when omitted. */\nexport type PlanGroupOf<Def> = Def extends { group: infer G extends string } ? G : undefined;\n\n/** Extracts the union of every `includes` entry's literal feature id. */\nexport type PlanFeatureIdsOf<Def> = Def extends { includes: infer Includes }\n ? Includes extends readonly (infer Item)[]\n ? Item extends FeatureInclusion<infer FeatureId>\n ? FeatureId\n : never\n : never\n : never;\n\n/**\n * Validates one plan's `includes` array: a raw, uncalled feature passed\n * by mistake gets the \"did you mean\" hint; a boolean feature called with an\n * argument (deferred from `feature()`, see feature.ts docs) is rejected\n * here; duplicate feature ids within this SAME plan are rejected. Anything\n * that isn't a genuine `FeatureInclusion` (nor a raw feature callable) is a\n * generic invalid-entry error.\n */\nfunction validateIncludes(planId: string, raw: readonly unknown[]): FeatureInclusion[] {\n const seen = new Set<string>();\n const out: FeatureInclusion[] = [];\n for (const entry of raw) {\n const rawDef = featureFnDef(entry);\n if (rawDef !== undefined) {\n const hint = rawDef.type === \"metered\" ? \"{ limit, reset }\" : \"\";\n throw new PayweaveValidationError(`plan \"${planId}\": did you mean \\`${rawDef.id}(${hint})\\`?`);\n }\n\n const meta = inclusionMeta(entry);\n if (meta === undefined) {\n throw new PayweaveValidationError(\n `plan \"${planId}\" includes an invalid entry — expected a called feature, e.g. ` +\n `messages({ limit, reset }) or proModels()`,\n );\n }\n\n const inclusion = entry as FeatureInclusion;\n if (meta.invalidArg) {\n throw new PayweaveValidationError(\n `plan \"${planId}\": boolean feature \"${inclusion.featureId}\" does not take arguments`,\n );\n }\n if (seen.has(inclusion.featureId)) {\n throw new PayweaveValidationError(\n `plan \"${planId}\" includes feature \"${inclusion.featureId}\" more than once`,\n );\n }\n seen.add(inclusion.featureId);\n out.push(inclusion);\n }\n return out;\n}\n\n/**\n * Define a plan. Validates `id`, the `default: true` ⇒ `group`\n * requirement, the price shape (still major units), and\n * `includes` — every rule that needs only THIS plan, not the rest of\n * the `products` array (see module docs for what's deliberately NOT here).\n *\n * ```ts\n * export const pro = plan({\n * id: \"pro\",\n * name: \"Pro\",\n * group: \"base\",\n * price: { amount: 19, currency: \"USD\", interval: \"month\" },\n * includes: [messages({ limit: 2_000, reset: \"month\" }), proModels()],\n * });\n * ```\n */\nexport function plan<const Def extends PlanDefInput>(\n def: Def,\n): Plan<Def[\"id\"], PlanGroupOf<Def>, PlanFeatureIdsOf<Def>> {\n const { includes, ...rest } = def;\n\n let base: z.infer<typeof planBaseSchema>;\n try {\n base = planBaseSchema.parse(rest);\n } catch (err) {\n if (err instanceof z.ZodError) {\n throw new PayweaveValidationError(`invalid plan definition — ${zodDetail(err)}`);\n }\n throw err;\n }\n\n if (base.default === true && base.group === undefined) {\n throw new PayweaveValidationError(\n `plan \"${base.id}\": default: true requires a group`,\n );\n }\n\n const validatedIncludes = validateIncludes(base.id, includes ?? []);\n\n const result: Plan = Object.freeze({\n id: base.id,\n name: base.name,\n group: base.group,\n default: base.default ?? false,\n price: base.price,\n includes: Object.freeze(validatedIncludes),\n });\n\n return result as unknown as Plan<Def[\"id\"], PlanGroupOf<Def>, PlanFeatureIdsOf<Def>>;\n}\n\n// ── Money deviation — price resolution (AGENTS.md golden rule 7) ───────────\n\n/** A plan's price AFTER config-parse conversion — integer minor units. */\nexport type ResolvedPlanPrice = {\n readonly amount: number;\n readonly currency: string;\n readonly interval: \"month\" | \"year\";\n};\n\n/** The `999,999.99`-equivalent bound for a currency's minor-unit exponent. */\nfunction maxMajorAmount(exponent: number): string {\n const bound = 10 ** (6 + exponent) - 1;\n return (bound / 10 ** exponent).toFixed(exponent);\n}\n\n/**\n * Convert a paid plan's `price.amount` (major units) to integer minor\n * units — the ONE place `toMinor` runs for a plan (AGENTS.md golden rule 7).\n * Returns `undefined` for a free plan (no `price`).\n *\n * The plan's OWN `price.currency` always wins when set (\"omit it to use\n * `defaultCurrency`\") — `defaultCurrency` is the FALLBACK a plan without its\n * own currency resolves against. `createPayweave` decides \"neither\n * present → config error\" (`PayweaveConfigError`, cross-plan rule) BEFORE\n * ever calling this helper — this function only wraps the conversion itself\n * and the amount bound, naming the offending plan id on failure\n * (`toMinor`'s own message doesn't — agent-playbook contract notes).\n */\nexport function resolvePlanPricing(\n planValue: Plan,\n defaultCurrency: string,\n): ResolvedPlanPrice | undefined {\n if (planValue.price === undefined) return undefined;\n const currency = planValue.price.currency ?? defaultCurrency;\n\n let converted: Money;\n try {\n converted = toMinor(planValue.price.amount, currency);\n } catch (err) {\n if (err instanceof PayweaveValidationError) {\n throw new PayweaveValidationError(`plan \"${planValue.id}\": ${err.message}`, { cause: err });\n }\n throw err;\n }\n\n const exp = exponentFor(currency);\n const bound = 10 ** (6 + exp) - 1;\n if (converted.value > bound) {\n throw new PayweaveValidationError(\n `plan \"${planValue.id}\": amount ${planValue.price.amount} ${converted.currency} exceeds the ` +\n `maximum of ${maxMajorAmount(exp)} ${converted.currency}`,\n );\n }\n\n return {\n amount: converted.value,\n currency: converted.currency,\n interval: planValue.price.interval,\n };\n}\n"]}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { PayweaveValidationError } from './chunk-HTRGMZFH.js';
|
|
2
|
+
|
|
3
|
+
// src/unified/mappings.ts
|
|
4
|
+
var PAYSTACK_EVENT_MAP = {
|
|
5
|
+
"charge.success": "payment.succeeded",
|
|
6
|
+
"transfer.success": "transfer.succeeded",
|
|
7
|
+
"transfer.failed": "transfer.failed",
|
|
8
|
+
"transfer.reversed": "transfer.reversed",
|
|
9
|
+
"refund.processed": "refund.processed",
|
|
10
|
+
"subscription.create": "subscription.created",
|
|
11
|
+
"subscription.disable": "subscription.canceled",
|
|
12
|
+
"subscription.not_renew": "subscription.updated",
|
|
13
|
+
"invoice.update": "invoice.paid",
|
|
14
|
+
"invoice.payment_failed": "invoice.payment_failed",
|
|
15
|
+
"charge.dispute.create": "dispute.created"
|
|
16
|
+
};
|
|
17
|
+
var FLUTTERWAVE_STATUS_SPLIT_MAP = {
|
|
18
|
+
"charge.completed": { succeeded: "payment.succeeded", failed: "payment.failed" },
|
|
19
|
+
"transfer.completed": { succeeded: "transfer.succeeded", failed: "transfer.failed" }
|
|
20
|
+
};
|
|
21
|
+
var FLUTTERWAVE_EVENT_MAP = {
|
|
22
|
+
"refund.completed": "refund.processed",
|
|
23
|
+
"subscription.cancelled": "subscription.canceled",
|
|
24
|
+
"chargeback.initiated": "dispute.created"
|
|
25
|
+
};
|
|
26
|
+
var STRIPE_EVENT_MAP = {
|
|
27
|
+
"payment_intent.succeeded": "payment.succeeded",
|
|
28
|
+
"checkout.session.async_payment_succeeded": "payment.succeeded",
|
|
29
|
+
"payment_intent.payment_failed": "payment.failed",
|
|
30
|
+
"checkout.session.async_payment_failed": "payment.failed",
|
|
31
|
+
"charge.refunded": "refund.processed",
|
|
32
|
+
"customer.subscription.created": "subscription.created",
|
|
33
|
+
"customer.subscription.updated": "subscription.updated",
|
|
34
|
+
"customer.subscription.deleted": "subscription.canceled",
|
|
35
|
+
"invoice.paid": "invoice.paid",
|
|
36
|
+
"invoice.payment_succeeded": "invoice.paid",
|
|
37
|
+
"invoice.payment_failed": "invoice.payment_failed",
|
|
38
|
+
"charge.dispute.created": "dispute.created"
|
|
39
|
+
};
|
|
40
|
+
var STRIPE_EVENT_STATUS_SPLIT_MAP = {
|
|
41
|
+
"checkout.session.completed": {
|
|
42
|
+
field: "payment_status",
|
|
43
|
+
matches: ["paid", "no_payment_required"],
|
|
44
|
+
unifiedType: "payment.succeeded"
|
|
45
|
+
},
|
|
46
|
+
"refund.updated": {
|
|
47
|
+
field: "status",
|
|
48
|
+
matches: ["succeeded"],
|
|
49
|
+
unifiedType: "refund.processed"
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
var PAYSTACK_STATUS_MAP = {
|
|
53
|
+
success: "success",
|
|
54
|
+
failed: "failed",
|
|
55
|
+
pending: "pending",
|
|
56
|
+
ongoing: "pending",
|
|
57
|
+
processing: "pending",
|
|
58
|
+
queued: "pending",
|
|
59
|
+
abandoned: "abandoned",
|
|
60
|
+
reversed: "reversed"
|
|
61
|
+
};
|
|
62
|
+
var FLUTTERWAVE_V3_STATUS_MAP = {
|
|
63
|
+
successful: "success",
|
|
64
|
+
failed: "failed",
|
|
65
|
+
pending: "pending",
|
|
66
|
+
processing: "pending",
|
|
67
|
+
queued: "pending"
|
|
68
|
+
};
|
|
69
|
+
var FLUTTERWAVE_V4_STATUS_MAP = {
|
|
70
|
+
succeeded: "success",
|
|
71
|
+
failed: "failed",
|
|
72
|
+
pending: "pending",
|
|
73
|
+
processing: "pending",
|
|
74
|
+
queued: "pending"
|
|
75
|
+
};
|
|
76
|
+
var STRIPE_STATUS_MAP = {
|
|
77
|
+
// Checkout Session `payment_status`
|
|
78
|
+
paid: "success",
|
|
79
|
+
no_payment_required: "success",
|
|
80
|
+
unpaid: "pending",
|
|
81
|
+
// Checkout Session `status` (payment-outcome-relevant values only — see note above)
|
|
82
|
+
open: "pending",
|
|
83
|
+
expired: "abandoned",
|
|
84
|
+
// PaymentIntent `status`
|
|
85
|
+
succeeded: "success",
|
|
86
|
+
processing: "pending",
|
|
87
|
+
requires_action: "pending",
|
|
88
|
+
requires_confirmation: "pending",
|
|
89
|
+
requires_payment_method: "pending",
|
|
90
|
+
requires_capture: "pending",
|
|
91
|
+
canceled: "abandoned"
|
|
92
|
+
};
|
|
93
|
+
function statusTableFor(provider, version) {
|
|
94
|
+
if (provider === "paystack") return PAYSTACK_STATUS_MAP;
|
|
95
|
+
if (provider === "stripe") return STRIPE_STATUS_MAP;
|
|
96
|
+
return version === "v4" ? FLUTTERWAVE_V4_STATUS_MAP : FLUTTERWAVE_V3_STATUS_MAP;
|
|
97
|
+
}
|
|
98
|
+
function toUnifiedStatus(provider, version, nativeStatus, logger) {
|
|
99
|
+
const key = typeof nativeStatus === "string" ? nativeStatus.trim().toLowerCase() : "";
|
|
100
|
+
const table = statusTableFor(provider, version);
|
|
101
|
+
const mapped = table[key];
|
|
102
|
+
if (mapped !== void 0) return mapped;
|
|
103
|
+
logger?.({
|
|
104
|
+
type: "schema_drift",
|
|
105
|
+
message: "Unknown transaction status; defaulting to 'pending'.",
|
|
106
|
+
provider,
|
|
107
|
+
version,
|
|
108
|
+
nativeStatus
|
|
109
|
+
});
|
|
110
|
+
return "pending";
|
|
111
|
+
}
|
|
112
|
+
function toUnifiedEventType(provider, version, nativeType, data) {
|
|
113
|
+
const key = typeof nativeType === "string" ? nativeType.trim() : "";
|
|
114
|
+
if (provider === "paystack") {
|
|
115
|
+
return PAYSTACK_EVENT_MAP[key] ?? "unknown";
|
|
116
|
+
}
|
|
117
|
+
if (provider === "stripe") {
|
|
118
|
+
const split2 = STRIPE_EVENT_STATUS_SPLIT_MAP[key];
|
|
119
|
+
if (split2) {
|
|
120
|
+
const value = readStripeObjectField(data, split2.field);
|
|
121
|
+
return value !== void 0 && split2.matches.includes(value) ? split2.unifiedType : "unknown";
|
|
122
|
+
}
|
|
123
|
+
return STRIPE_EVENT_MAP[key] ?? "unknown";
|
|
124
|
+
}
|
|
125
|
+
const split = FLUTTERWAVE_STATUS_SPLIT_MAP[key];
|
|
126
|
+
if (split) {
|
|
127
|
+
const status = readStatus(data);
|
|
128
|
+
if (status === void 0) return "unknown";
|
|
129
|
+
const unified = toUnifiedStatus(provider, version, status);
|
|
130
|
+
if (unified === "success") return split.succeeded;
|
|
131
|
+
if (unified === "failed") return split.failed;
|
|
132
|
+
return "unknown";
|
|
133
|
+
}
|
|
134
|
+
return FLUTTERWAVE_EVENT_MAP[key] ?? "unknown";
|
|
135
|
+
}
|
|
136
|
+
function readStatus(data) {
|
|
137
|
+
if (data !== null && typeof data === "object" && !Array.isArray(data)) {
|
|
138
|
+
const status = data.status;
|
|
139
|
+
if (typeof status === "string") return status;
|
|
140
|
+
}
|
|
141
|
+
return void 0;
|
|
142
|
+
}
|
|
143
|
+
function readStripeObjectField(data, field) {
|
|
144
|
+
if (data === null || typeof data !== "object" || Array.isArray(data)) return void 0;
|
|
145
|
+
const obj = data.object;
|
|
146
|
+
if (obj === null || typeof obj !== "object" || Array.isArray(obj)) return void 0;
|
|
147
|
+
const value = obj[field];
|
|
148
|
+
return typeof value === "string" ? value : void 0;
|
|
149
|
+
}
|
|
150
|
+
var ALL_SUPPORTED = {
|
|
151
|
+
"checkout.create": { supported: true },
|
|
152
|
+
verify: { supported: true },
|
|
153
|
+
"refunds.create": { supported: true },
|
|
154
|
+
"transfers.create": { supported: true },
|
|
155
|
+
"banks.list": { supported: true },
|
|
156
|
+
"banks.resolveAccount": { supported: true }
|
|
157
|
+
};
|
|
158
|
+
var UNIFIED_CAPABILITY_MATRIX = {
|
|
159
|
+
paystack: ALL_SUPPORTED,
|
|
160
|
+
flutterwave: ALL_SUPPORTED,
|
|
161
|
+
stripe: {
|
|
162
|
+
"checkout.create": { supported: true },
|
|
163
|
+
verify: { supported: true },
|
|
164
|
+
"refunds.create": { supported: true },
|
|
165
|
+
"transfers.create": {
|
|
166
|
+
supported: false,
|
|
167
|
+
reason: "transfers are not supported on stripe"
|
|
168
|
+
},
|
|
169
|
+
"banks.list": {
|
|
170
|
+
supported: false,
|
|
171
|
+
reason: "banks.list is not supported on stripe"
|
|
172
|
+
},
|
|
173
|
+
"banks.resolveAccount": {
|
|
174
|
+
supported: false,
|
|
175
|
+
reason: "banks.resolveAccount is not supported on stripe"
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
function isUnifiedOpSupported(provider, op) {
|
|
180
|
+
return UNIFIED_CAPABILITY_MATRIX[provider]?.[op]?.supported ?? false;
|
|
181
|
+
}
|
|
182
|
+
function assertUnifiedCapability(provider, op) {
|
|
183
|
+
const cell = UNIFIED_CAPABILITY_MATRIX[provider]?.[op];
|
|
184
|
+
if (cell?.supported) return;
|
|
185
|
+
throw new PayweaveValidationError(cell?.reason ?? `${op} is not supported on ${provider}`, {
|
|
186
|
+
provider
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export { FLUTTERWAVE_EVENT_MAP, FLUTTERWAVE_STATUS_SPLIT_MAP, FLUTTERWAVE_V3_STATUS_MAP, FLUTTERWAVE_V4_STATUS_MAP, PAYSTACK_EVENT_MAP, PAYSTACK_STATUS_MAP, STRIPE_EVENT_MAP, STRIPE_EVENT_STATUS_SPLIT_MAP, STRIPE_STATUS_MAP, UNIFIED_CAPABILITY_MATRIX, assertUnifiedCapability, isUnifiedOpSupported, toUnifiedEventType, toUnifiedStatus };
|
|
191
|
+
//# sourceMappingURL=chunk-67NRYGRC.js.map
|
|
192
|
+
//# sourceMappingURL=chunk-67NRYGRC.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/unified/mappings.ts"],"names":["split"],"mappings":"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","file":"chunk-67NRYGRC.js","sourcesContent":["/**\n * unified/mappings.ts — the single source of truth for webhook + status\n * normalization (unified event names, provider status vocabularies) AND the\n * unified-ops capability matrix.\n *\n * ⚠️ PUBLIC CONTRACT. The `unifiedType` / `UnifiedStatus`\n * vocabularies, the provider→unified mappings, and the capability matrix below\n * are a stable, public contract. Changing their SEMANTICS (renaming a unified\n * value, remapping an existing native event/status to a different unified\n * value, flipping a capability-matrix cell from `true` to `false`) is a\n * breaking change and REQUIRES a `major`/`minor` changeset discussion. ADDING a\n * new native→unified row (so a previously-`unknown` event/status now\n * normalizes) or flipping a capability cell `false` → `true` (a provider\n * gaining an op) is a `minor`. Never DROP a mapping and never throw on an\n * unmapped input — unknown events fall through to `\"unknown\"`, unknown statuses\n * fall through to `\"pending\"` plus a `schema_drift` log.\n *\n * Design: data-driven tables keyed by provider (+ version for Flutterwave, whose\n * v3/v4 status vocabularies differ — v3 `successful` vs v4 `succeeded`). The\n * exported normalization functions are pure; an optional `logger` is injected\n * by callers so drift can be surfaced without `console.*`.\n */\nimport type { Logger } from \"../core/logger\";\nimport { PayweaveValidationError, type PayweaveProvider } from \"../core/errors\";\n\n/**\n * Provider identifier accepted by the mapping functions AND the capability\n * matrix — the full v1 provider set. Identical to {@link PayweaveProvider}\n * but kept as its own alias so this file stays free of a `core/config.ts`\n * dependency.\n */\nexport type MappingProvider = PayweaveProvider;\n/** Flutterwave version selector. `undefined` for Paystack and Stripe (unversioned). */\nexport type MappingVersion = \"v3\" | \"v4\" | undefined;\n\n/**\n * Normalized, provider-agnostic webhook event type. `\"unknown\"` is a\n * first-class value: unmapped native events are delivered as `\"unknown\"`, never\n * dropped. `subscription.updated`/`subscription.canceled`/`invoice.paid`/\n * `invoice.payment_failed` are mapped for ALL providers so the vocabulary\n * stays total.\n */\nexport type UnifiedEventType =\n | \"payment.succeeded\"\n | \"payment.failed\"\n | \"transfer.succeeded\"\n | \"transfer.failed\"\n | \"transfer.reversed\"\n | \"refund.processed\"\n | \"subscription.created\"\n | \"subscription.updated\"\n | \"subscription.canceled\"\n | \"invoice.paid\"\n | \"invoice.payment_failed\"\n | \"dispute.created\"\n | \"unknown\";\n\n/** Normalized transaction status. */\nexport type UnifiedStatus = \"success\" | \"failed\" | \"pending\" | \"abandoned\" | \"reversed\";\n\n// ── Event-name tables ────────────────────────────────────────────────────────\n\n/**\n * Paystack native webhook event → unified type. Paystack event names are\n * status-independent (a `charge.success` is always a success), so this is a flat\n * lookup. Anything absent → `\"unknown\"`.\n *\n * `subscription.disable`/`subscription.not_renew`/`invoice.update`/\n * `invoice.payment_failed` verified against\n * https://paystack.com/docs/payments/webhooks/ and\n * https://paystack.com/docs/payments/subscriptions/ (verified 2026-07-12):\n * `subscription.disable` fires both on an explicit cancellation and on the\n * final billing cycle completing — either way the subscription is no longer\n * active, so it normalizes to `subscription.canceled`; `subscription.not_renew`\n * only marks the subscription to stop renewing (it stays active until the\n * period ends), so it normalizes to `subscription.updated`, not `.canceled`.\n * `invoice.update` \"will contain the final status of the invoice ... as well as\n * information on the charge if it was successful\" and fires only on a\n * successful charge attempt — `invoice.payment_failed` fires \"instead\" on\n * failure — so `invoice.update` maps unconditionally to `invoice.paid`.\n * `invoice.create` (fired when a new upcoming-charge invoice is created, not a\n * payment result) stays unmapped → `\"unknown\"` (existing, tested behavior).\n */\nexport const PAYSTACK_EVENT_MAP: Readonly<Record<string, UnifiedEventType>> = {\n \"charge.success\": \"payment.succeeded\",\n \"transfer.success\": \"transfer.succeeded\",\n \"transfer.failed\": \"transfer.failed\",\n \"transfer.reversed\": \"transfer.reversed\",\n \"refund.processed\": \"refund.processed\",\n \"subscription.create\": \"subscription.created\",\n \"subscription.disable\": \"subscription.canceled\",\n \"subscription.not_renew\": \"subscription.updated\",\n \"invoice.update\": \"invoice.paid\",\n \"invoice.payment_failed\": \"invoice.payment_failed\",\n \"charge.dispute.create\": \"dispute.created\",\n};\n\n/**\n * Flutterwave native webhook events whose unified type SPLITS on the transaction\n * status carried in `data.status` (e.g. `charge.completed` is a success OR a\n * failure depending on the status). Resolved through {@link toUnifiedStatus} so\n * the v3 (`successful`) vs v4 (`succeeded`) vocabulary difference is handled in\n * one place.\n */\nexport const FLUTTERWAVE_STATUS_SPLIT_MAP: Readonly<\n Record<string, { readonly succeeded: UnifiedEventType; readonly failed: UnifiedEventType }>\n> = {\n \"charge.completed\": { succeeded: \"payment.succeeded\", failed: \"payment.failed\" },\n \"transfer.completed\": { succeeded: \"transfer.succeeded\", failed: \"transfer.failed\" },\n};\n\n/**\n * Flutterwave native events with a status-independent unified type.\n *\n * Verified 2026-07-12, resolving the standing NOTE(verify):\n * `refund.completed` — https://developer.flutterwave.com/reference/refund_completed_webhook\n * (a dedicated \"refund completion\" webhook, unconditionally a completed\n * refund, unlike `charge.completed`/`transfer.completed` which cover both\n * outcomes and stay in {@link FLUTTERWAVE_STATUS_SPLIT_MAP}).\n * `subscription.cancelled` — https://developer.flutterwave.com/v3.0/docs/payment-plans-1\n * (sample payload: `\"event\": \"subscription.cancelled\"`, `data.status:\n * \"deactivated\"`); no `subscription.created`/`.updated` native event is\n * documented for FLW v3 (the first `charge.completed` on a payment-plan\n * subscription doubles as both the charge AND the creation signal) — left\n * unmapped rather than invented, per AGENTS.md.\n * `chargeback.initiated` — https://developer.flutterwave.com/v3.0/docs/chargebacks\n * (sample payload `\"event\": \"chargeback.initiated\"`); `chargeback.accepted` /\n * `.declined` / `.lost` also exist but have no unified equivalent to map to,\n * so they stay unmapped.\n * No native FLW `invoice.*` webhook event is documented — Flutterwave has no\n * separate invoicing concept on webhooks, so `invoice.paid` /\n * `invoice.payment_failed` have no FLW row (stays `\"unknown\"`, honestly\n * reflecting the provider gap rather than inventing a name).\n *\n * Shared as-is across v3/v4 (same convention as the rest of this flat map —\n * only STATUS vocabularies, not event NAMES, differ by version).\n */\nexport const FLUTTERWAVE_EVENT_MAP: Readonly<Record<string, UnifiedEventType>> = {\n \"refund.completed\": \"refund.processed\",\n \"subscription.cancelled\": \"subscription.canceled\",\n \"chargeback.initiated\": \"dispute.created\",\n};\n\n/**\n * Stripe native webhook event → unified type, STATUS-INDEPENDENT rows. Event\n * type names verified against https://docs.stripe.com/api/events/types\n * (verified 2026-07-12). Status-DEPENDENT rows\n * (`checkout.session.completed`, `refund.updated`) live in\n * {@link STRIPE_EVENT_STATUS_SPLIT_MAP} — Stripe nests the resource one level\n * down (`data.object`, https://docs.stripe.com/api/events/object), unlike\n * Paystack/Flutterwave's flat `data`, so they can't share this plain lookup.\n * Anything absent from both tables → `\"unknown\"`.\n *\n * `checkout.session.async_payment_succeeded`/`.async_payment_failed` are\n * ADDITIONS beyond the documented event set, added after verifying\n * https://docs.stripe.com/checkout/fulfillment (2026-07-12): a delayed/async\n * payment method (e.g. a bank debit) can complete a Checkout Session while\n * `payment_status` is still `\"unpaid\"` — the real success/failure signal\n * arrives later via these two events, so they are mapped flat here to keep\n * `payment.succeeded`/`payment.failed` honest for that flow.\n */\nexport const STRIPE_EVENT_MAP: Readonly<Record<string, UnifiedEventType>> = {\n \"payment_intent.succeeded\": \"payment.succeeded\",\n \"checkout.session.async_payment_succeeded\": \"payment.succeeded\",\n \"payment_intent.payment_failed\": \"payment.failed\",\n \"checkout.session.async_payment_failed\": \"payment.failed\",\n \"charge.refunded\": \"refund.processed\",\n \"customer.subscription.created\": \"subscription.created\",\n \"customer.subscription.updated\": \"subscription.updated\",\n \"customer.subscription.deleted\": \"subscription.canceled\",\n \"invoice.paid\": \"invoice.paid\",\n \"invoice.payment_succeeded\": \"invoice.paid\",\n \"invoice.payment_failed\": \"invoice.payment_failed\",\n \"charge.dispute.created\": \"dispute.created\",\n};\n\n/** One {@link STRIPE_EVENT_STATUS_SPLIT_MAP} row: which field on the nested `data.object` decides the outcome, and which native values count as the success case. */\nexport interface StripeEventStatusSplit {\n readonly field: \"status\" | \"payment_status\";\n readonly matches: readonly string[];\n readonly unifiedType: UnifiedEventType;\n}\n\n/**\n * Stripe events whose unified type depends on the WRAPPED RESOURCE's status\n * (`data.object.<field>`, verified 2026-07-12):\n *\n * - `checkout.session.completed` fires once the checkout FLOW finishes, which\n * is not the same as payment succeeding for a delayed/async payment method\n * (`payment_status` can still be `\"unpaid\"` at that point —\n * https://docs.stripe.com/checkout/fulfillment). Only `payment_status:\n * \"paid\"` or `\"no_payment_required\"` (a genuinely $0 checkout) normalize to\n * `payment.succeeded`; `\"unpaid\"` falls through to `\"unknown\"` (the real\n * signal arrives later via `checkout.session.async_payment_succeeded`,\n * mapped flat in {@link STRIPE_EVENT_MAP}) — session status ∈ {open,\n * complete, expired}, payment_status ∈ {paid, unpaid, no_payment_required}\n * per https://docs.stripe.com/api/checkout/sessions/object.\n * - `refund.updated` fires for EVERY refund status transition — Refund\n * `status` ∈ {pending, requires_action, succeeded, failed, canceled} per\n * https://docs.stripe.com/api/refunds/object — only `\"succeeded\"` is a\n * completed refund; every other transition falls through to `\"unknown\"`\n * (never a false `refund.processed`).\n */\nexport const STRIPE_EVENT_STATUS_SPLIT_MAP: Readonly<Record<string, StripeEventStatusSplit>> = {\n \"checkout.session.completed\": {\n field: \"payment_status\",\n matches: [\"paid\", \"no_payment_required\"],\n unifiedType: \"payment.succeeded\",\n },\n \"refund.updated\": {\n field: \"status\",\n matches: [\"succeeded\"],\n unifiedType: \"refund.processed\",\n },\n};\n\n// ── Status tables ────────────────────────────────────────────────────────────\n\n/** Paystack transaction `status` strings → unified status. */\nexport const PAYSTACK_STATUS_MAP: Readonly<Record<string, UnifiedStatus>> = {\n success: \"success\",\n failed: \"failed\",\n pending: \"pending\",\n ongoing: \"pending\",\n processing: \"pending\",\n queued: \"pending\",\n abandoned: \"abandoned\",\n reversed: \"reversed\",\n};\n\n/** Flutterwave v3 transaction `status` strings → unified status (`successful`). */\nexport const FLUTTERWAVE_V3_STATUS_MAP: Readonly<Record<string, UnifiedStatus>> = {\n successful: \"success\",\n failed: \"failed\",\n pending: \"pending\",\n processing: \"pending\",\n queued: \"pending\",\n};\n\n/** Flutterwave v4 transaction `status` strings → unified status (`succeeded`). */\nexport const FLUTTERWAVE_V4_STATUS_MAP: Readonly<Record<string, UnifiedStatus>> = {\n succeeded: \"success\",\n failed: \"failed\",\n pending: \"pending\",\n processing: \"pending\",\n queued: \"pending\",\n};\n\n/**\n * Stripe status strings → unified status — MERGED across the object types\n * `toUnifiedStatus` normalizes for Stripe (Checkout Session `payment_status` +\n * `status`, PaymentIntent `status`); none of the verified values collide.\n * Verified 2026-07-12:\n *\n * - Checkout Session `payment_status` ∈ {paid, unpaid, no_payment_required}\n * and `status` ∈ {open, complete, expired} —\n * https://docs.stripe.com/api/checkout/sessions/object. `no_payment_required`\n * (a genuinely $0 checkout) is an ADDITION beyond the documented status set —\n * treated as `success` (the checkout completed, nothing further to charge).\n * `open` is an ADDITION (in-progress, not yet an anomaly) → `pending`.\n * `status: \"complete\"` is DELIBERATELY OMITTED: it means the checkout FLOW\n * finished, not that payment succeeded — a delayed/async payment method can\n * complete a session while `payment_status` is still `\"unpaid\"`\n * (https://docs.stripe.com/checkout/fulfillment) — callers must normalize\n * `payment_status`, the payment-outcome field, not the session's own\n * `status`; an unrecognized `\"complete\"` intentionally falls through to the\n * safe `\"pending\"` default (+ drift log) rather than a table entry that could\n * silently paper over a caller reading the wrong field.\n * - PaymentIntent `status` ∈ {requires_payment_method, requires_confirmation,\n * requires_action, processing, requires_capture, succeeded, canceled} —\n * https://docs.stripe.com/api/payment_intents/object,\n * https://docs.stripe.com/payments/paymentintents/lifecycle.\n * `requires_capture` (funds authorized, not yet captured) is an ADDITION\n * beyond the documented status set, mapped to `pending` for the same reason as\n * the other `requires_*` states — not yet succeeded.\n */\nexport const STRIPE_STATUS_MAP: Readonly<Record<string, UnifiedStatus>> = {\n // Checkout Session `payment_status`\n paid: \"success\",\n no_payment_required: \"success\",\n unpaid: \"pending\",\n // Checkout Session `status` (payment-outcome-relevant values only — see note above)\n open: \"pending\",\n expired: \"abandoned\",\n // PaymentIntent `status`\n succeeded: \"success\",\n processing: \"pending\",\n requires_action: \"pending\",\n requires_confirmation: \"pending\",\n requires_payment_method: \"pending\",\n requires_capture: \"pending\",\n canceled: \"abandoned\",\n};\n\nfunction statusTableFor(\n provider: MappingProvider,\n version: MappingVersion,\n): Readonly<Record<string, UnifiedStatus>> {\n if (provider === \"paystack\") return PAYSTACK_STATUS_MAP;\n if (provider === \"stripe\") return STRIPE_STATUS_MAP;\n return version === \"v4\" ? FLUTTERWAVE_V4_STATUS_MAP : FLUTTERWAVE_V3_STATUS_MAP;\n}\n\n// ── Pure mapping functions ───────────────────────────────────────────────────\n\n/**\n * Normalize a provider transaction status to a {@link UnifiedStatus}. Matching\n * is case-insensitive. An unrecognized status NEVER throws: it returns\n * `\"pending\"` (the safe default — never grant value off a webhook) and emits a\n * `schema_drift` log if a `logger` is supplied.\n *\n * @param provider - `\"paystack\"` | `\"flutterwave\"` | `\"stripe\"`.\n * @param version - Flutterwave version (`\"v3\"` | `\"v4\"`); `undefined` for Paystack and Stripe.\n * @param nativeStatus - Provider-native status string (e.g. `\"successful\"`).\n * @param logger - Optional injected logger for drift reporting.\n */\nexport function toUnifiedStatus(\n provider: MappingProvider,\n version: MappingVersion,\n nativeStatus: string | null | undefined,\n logger?: Logger,\n): UnifiedStatus {\n const key = typeof nativeStatus === \"string\" ? nativeStatus.trim().toLowerCase() : \"\";\n const table = statusTableFor(provider, version);\n const mapped = table[key];\n if (mapped !== undefined) return mapped;\n logger?.({\n type: \"schema_drift\",\n message: \"Unknown transaction status; defaulting to 'pending'.\",\n provider,\n version,\n nativeStatus,\n });\n return \"pending\";\n}\n\n/**\n * Normalize a provider-native webhook event name to a {@link UnifiedEventType}.\n * For Flutterwave events whose meaning depends on the transaction outcome\n * (`charge.completed`, `transfer.completed`), the split is resolved from\n * `data.status` via {@link toUnifiedStatus}. For Stripe events whose meaning\n * depends on the wrapped resource's status (`checkout.session.completed`,\n * `refund.updated`), the split is resolved directly from\n * `data.object.<field>` per {@link STRIPE_EVENT_STATUS_SPLIT_MAP} (Stripe's\n * event envelope nests the resource one level deeper than Paystack/FLW's flat\n * `data`). Anything unmapped → `\"unknown\"` (delivered, never dropped).\n *\n * @param provider - `\"paystack\"` | `\"flutterwave\"` | `\"stripe\"`.\n * @param version - Flutterwave version; `undefined` for Paystack and Stripe.\n * @param nativeType - Provider-native event name (e.g. `\"charge.success\"`).\n * @param data - The event's `data` object — FLW status splits read `data.status`; Stripe status splits read `data.object.<field>`.\n */\nexport function toUnifiedEventType(\n provider: MappingProvider,\n version: MappingVersion,\n nativeType: string | null | undefined,\n data?: unknown,\n): UnifiedEventType {\n const key = typeof nativeType === \"string\" ? nativeType.trim() : \"\";\n if (provider === \"paystack\") {\n return PAYSTACK_EVENT_MAP[key] ?? \"unknown\";\n }\n if (provider === \"stripe\") {\n const split = STRIPE_EVENT_STATUS_SPLIT_MAP[key];\n if (split) {\n const value = readStripeObjectField(data, split.field);\n return value !== undefined && split.matches.includes(value) ? split.unifiedType : \"unknown\";\n }\n return STRIPE_EVENT_MAP[key] ?? \"unknown\";\n }\n const split = FLUTTERWAVE_STATUS_SPLIT_MAP[key];\n if (split) {\n const status = readStatus(data);\n if (status === undefined) return \"unknown\";\n const unified = toUnifiedStatus(provider, version, status);\n if (unified === \"success\") return split.succeeded;\n if (unified === \"failed\") return split.failed;\n return \"unknown\";\n }\n return FLUTTERWAVE_EVENT_MAP[key] ?? \"unknown\";\n}\n\nfunction readStatus(data: unknown): string | undefined {\n if (data !== null && typeof data === \"object\" && !Array.isArray(data)) {\n const status = (data as Record<string, unknown>).status;\n if (typeof status === \"string\") return status;\n }\n return undefined;\n}\n\n/**\n * Read a field off Stripe's nested `data.object` — every Stripe event wraps\n * its resource one level down (https://docs.stripe.com/api/events/object,\n * verified 2026-07-12), unlike Paystack/Flutterwave's flat `data`.\n */\nfunction readStripeObjectField(data: unknown, field: string): string | undefined {\n if (data === null || typeof data !== \"object\" || Array.isArray(data)) return undefined;\n const obj = (data as Record<string, unknown>).object;\n if (obj === null || typeof obj !== \"object\" || Array.isArray(obj)) return undefined;\n const value = (obj as Record<string, unknown>)[field];\n return typeof value === \"string\" ? value : undefined;\n}\n\n// ── Unified-ops capability matrix ────────────────\n\n/**\n * The six unified ops (Surface B, `src/unified/types.ts`'s\n * {@link UnifiedNamespace}) as flat, dot-qualified identifiers — the keys the\n * capability matrix and the typed capability guard operate on.\n */\nexport type UnifiedOpName =\n | \"checkout.create\"\n | \"verify\"\n | \"refunds.create\"\n | \"transfers.create\"\n | \"banks.list\"\n | \"banks.resolveAccount\";\n\n/**\n * One capability-matrix cell: whether `op` is supported on a given provider,\n * and — only when it is NOT — the exact message the typed guard throws.\n */\nexport interface UnifiedOpCapability {\n readonly supported: boolean;\n /** Present only when `supported` is `false`. */\n readonly reason?: string;\n}\n\nconst ALL_SUPPORTED: Readonly<Record<UnifiedOpName, UnifiedOpCapability>> = {\n \"checkout.create\": { supported: true },\n verify: { supported: true },\n \"refunds.create\": { supported: true },\n \"transfers.create\": { supported: true },\n \"banks.list\": { supported: true },\n \"banks.resolveAccount\": { supported: true },\n};\n\n/**\n * The unified-ops capability matrix: which of the six\n * unified ops each provider supports. Data, not prose, so\n * `payweave.capabilities()` (`src/index.ts`) and doc generators\n * can render it directly.\n *\n * Paystack and Flutterwave support all six. Stripe does NOT map\n * `transfers.create` or `banks.*` in v1 — Stripe payouts/Connect transfers are\n * semantically different from Paystack/FLW bank transfers, and bank-account\n * lookup is NG-specific. `checkout.create`/`verify`/\n * `refunds.create` ARE capability-supported on stripe (mapped to\n * `checkout.sessions`/`paymentIntents`/`refunds`) — the runtime\n * wiring of that mapping (`src/unified/stripe.ts`) is separate follow-up work\n * tracked outside this capability-matrix change; calling those ops today\n * rejects with a distinct \"not implemented yet\" error rather than the\n * capability-gap error below (see `stripeUnifiedNamespace` in `src/index.ts`).\n *\n * EXTEND ONLY (same rule as the event/status tables above): a\n * `false` cell may flip to `true` (additive, `minor`) once a provider's\n * mapping lands; flipping `true` → `false` is a breaking change.\n */\nexport const UNIFIED_CAPABILITY_MATRIX: Readonly<\n Record<MappingProvider, Readonly<Record<UnifiedOpName, UnifiedOpCapability>>>\n> = {\n paystack: ALL_SUPPORTED,\n flutterwave: ALL_SUPPORTED,\n stripe: {\n \"checkout.create\": { supported: true },\n verify: { supported: true },\n \"refunds.create\": { supported: true },\n \"transfers.create\": {\n supported: false,\n reason: \"transfers are not supported on stripe\",\n },\n \"banks.list\": {\n supported: false,\n reason: \"banks.list is not supported on stripe\",\n },\n \"banks.resolveAccount\": {\n supported: false,\n reason: \"banks.resolveAccount is not supported on stripe\",\n },\n },\n};\n\n/**\n * Whether `provider` supports `op` per the {@link UNIFIED_CAPABILITY_MATRIX}.\n * An unknown provider/op pair (unreachable via the typed matrix, reachable\n * only from untyped callers) is treated as unsupported — fail closed, never\n * silently allow an unshaped call through.\n */\nexport function isUnifiedOpSupported(provider: MappingProvider, op: UnifiedOpName): boolean {\n return UNIFIED_CAPABILITY_MATRIX[provider]?.[op]?.supported ?? false;\n}\n\n/**\n * Assert that `provider` supports `op`. A no-op when it\n * is; otherwise throws {@link PayweaveValidationError}, naming the provider\n * and operation (e.g.\n * `\"transfers are not supported on stripe\"` for `transfers.create` on\n * stripe). Never throws for a supported op, and never for anything OTHER than\n * a capability gap — config/auth/network failures surface through their own\n * error classes elsewhere in the request path.\n */\nexport function assertUnifiedCapability(provider: MappingProvider, op: UnifiedOpName): void {\n const cell = UNIFIED_CAPABILITY_MATRIX[provider]?.[op];\n if (cell?.supported) return;\n throw new PayweaveValidationError(cell?.reason ?? `${op} is not supported on ${provider}`, {\n provider,\n });\n}\n"]}
|