@absolutejs/billing 0.6.0 → 0.7.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.
@@ -0,0 +1,101 @@
1
+ /** Service-credit accounting. Host authorization and payment verification happen
2
+ * before these commands. Every command and its receipt commit in one transaction. */
3
+ export type CreditAccount = {
4
+ periodId: string;
5
+ periodEnd: string | null;
6
+ periodAllowance: number;
7
+ periodRemaining: number;
8
+ purchasedRemaining: number;
9
+ promotionalRemaining: number;
10
+ debt: number;
11
+ reserved: number;
12
+ consumed: number;
13
+ };
14
+ export type CreditAllocation = {
15
+ period: number;
16
+ purchased: number;
17
+ promotional: number;
18
+ };
19
+ export type CreditReservation = {
20
+ id: string;
21
+ periodId: string;
22
+ allocation: CreditAllocation;
23
+ status: "reserved" | "settled" | "released";
24
+ charged: number | null;
25
+ };
26
+ export type CreditCommand = {
27
+ kind: "grant";
28
+ bucket: "purchased" | "promotional";
29
+ credits: number;
30
+ } | {
31
+ kind: "reverse";
32
+ bucket: "purchased" | "promotional";
33
+ credits: number;
34
+ } | {
35
+ kind: "period";
36
+ periodId: string;
37
+ periodEnd: string | null;
38
+ allowance: number;
39
+ } | {
40
+ kind: "reserve";
41
+ reservationId: string;
42
+ credits: number;
43
+ } | {
44
+ kind: "settle";
45
+ reservationId: string;
46
+ credits: number;
47
+ } | {
48
+ kind: "release";
49
+ reservationId: string;
50
+ } | {
51
+ kind: "debit";
52
+ credits: number;
53
+ allowDebt?: boolean;
54
+ reference?: string;
55
+ };
56
+ export type CreditReceipt = {
57
+ command: CreditCommand;
58
+ account: CreditAccount;
59
+ reservation?: CreditReservation;
60
+ };
61
+ export type CreditTransaction = {
62
+ account: CreditAccount;
63
+ receipt: (operationId: string) => Promise<CreditReceipt | null>;
64
+ reservation: (id: string) => Promise<CreditReservation | null>;
65
+ save: (operationId: string, receipt: CreditReceipt) => Promise<void>;
66
+ };
67
+ export type CreditAccountStore = {
68
+ /** Persist seed once. Never overwrite an existing account. */
69
+ initialize: (accountId: string, seed: CreditAccount) => Promise<void>;
70
+ read: (accountId: string) => Promise<CreditAccount | null>;
71
+ /** Lock one account, serialize its commands across processes, and roll back
72
+ * account, reservation and receipt together if the callback fails. */
73
+ transaction: <T>(accountId: string, run: (tx: CreditTransaction) => Promise<T>) => Promise<T>;
74
+ };
75
+ export declare const validateCreditAccount: (account: CreditAccount) => void;
76
+ export declare const availableCredits: (account: CreditAccount) => number;
77
+ /** Preserve the currently observable legacy balance without inventing purchase
78
+ * provenance. Historical mixed bonus grants remain promotional carryover. */
79
+ export declare const migrateLegacyCreditAccount: (input: {
80
+ periodId: string;
81
+ periodEnd?: string | null;
82
+ periodAllowance: number;
83
+ bonusCredits: number;
84
+ consumed: number;
85
+ }) => CreditAccount;
86
+ export declare const createCreditAccountLedger: (store: CreditAccountStore) => {
87
+ initialize: (accountId: string, seed: CreditAccount) => Promise<void>;
88
+ receipt: (accountId: string, operationId: string) => Promise<CreditReceipt | null>;
89
+ balance: (accountId: string) => Promise<CreditAccount | null>;
90
+ execute: (accountId: string, operationId: string, input: CreditCommand) => Promise<CreditReceipt>;
91
+ };
92
+ /** Portal access and funded MCP access are independent. Period-only free
93
+ * allowances do not silently unlock prepaid work; carryover grants may. */
94
+ export declare const creditEntitlements: (input: {
95
+ subscribed: boolean;
96
+ prepaidEnabled: boolean;
97
+ account: CreditAccount | null;
98
+ }) => {
99
+ portal: boolean;
100
+ mcp: "member" | "prepaid" | "free";
101
+ };
@@ -0,0 +1,271 @@
1
+ // @bun
2
+ var __defProp = Object.defineProperty;
3
+ var __returnValue = (v) => v;
4
+ function __exportSetter(name, newValue) {
5
+ this[name] = __returnValue.bind(null, newValue);
6
+ }
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, {
10
+ get: all[name],
11
+ enumerable: true,
12
+ configurable: true,
13
+ set: __exportSetter.bind(all, name)
14
+ });
15
+ };
16
+
17
+ // src/prepaid.ts
18
+ var integer = (value) => {
19
+ if (!Number.isSafeInteger(value) || value < 0)
20
+ throw new Error("Credits must be nonnegative safe integers");
21
+ return value;
22
+ };
23
+ var periodIdentity = (value) => {
24
+ if (!Number.isFinite(Date.parse(value)) || new Date(value).toISOString() !== value)
25
+ throw new Error("Credit period ID must be an ISO UTC start timestamp");
26
+ return value;
27
+ };
28
+ var identity = (value) => {
29
+ if (!value || value.length > 256)
30
+ throw new Error("Credit identity is invalid");
31
+ return value;
32
+ };
33
+ var validateCreditAccount = (account) => {
34
+ periodIdentity(account.periodId);
35
+ if (account.periodEnd !== null && periodIdentity(account.periodEnd) <= account.periodId)
36
+ throw new Error("Credit period end must follow its start");
37
+ for (const key of [
38
+ "periodAllowance",
39
+ "periodRemaining",
40
+ "purchasedRemaining",
41
+ "promotionalRemaining",
42
+ "debt",
43
+ "reserved",
44
+ "consumed"
45
+ ])
46
+ integer(account[key]);
47
+ integer(account.periodRemaining + account.purchasedRemaining + account.promotionalRemaining);
48
+ if (account.periodRemaining > account.periodAllowance)
49
+ throw new Error("Period balance exceeds its allowance");
50
+ };
51
+ var availableCredits = (account) => Math.max(0, account.periodRemaining + account.purchasedRemaining + account.promotionalRemaining - account.debt);
52
+ var migrateLegacyCreditAccount = (input) => {
53
+ integer(input.periodAllowance);
54
+ integer(input.consumed);
55
+ if (!Number.isSafeInteger(input.bonusCredits))
56
+ throw new Error("Invalid legacy bonus credits");
57
+ const account = {
58
+ periodId: input.periodId,
59
+ periodEnd: input.periodEnd ?? null,
60
+ periodAllowance: input.periodAllowance,
61
+ periodRemaining: Math.max(0, input.periodAllowance - input.consumed),
62
+ purchasedRemaining: 0,
63
+ promotionalRemaining: Math.max(0, input.bonusCredits - Math.max(0, input.consumed - input.periodAllowance)),
64
+ debt: Math.max(0, -input.bonusCredits) + Math.max(0, input.consumed - input.periodAllowance - Math.max(0, input.bonusCredits)),
65
+ reserved: 0,
66
+ consumed: input.consumed
67
+ };
68
+ validateCreditAccount(account);
69
+ return account;
70
+ };
71
+ var allocationTotal = (value) => value.period + value.promotional + value.purchased;
72
+ var take = (account, credits) => {
73
+ let owed = account.debt;
74
+ for (const key of [
75
+ "periodRemaining",
76
+ "promotionalRemaining",
77
+ "purchasedRemaining"
78
+ ]) {
79
+ const covered = Math.min(account[key], owed);
80
+ account[key] -= covered;
81
+ owed -= covered;
82
+ }
83
+ account.debt = owed;
84
+ const period = Math.min(account.periodRemaining, credits);
85
+ const promotional = Math.min(account.promotionalRemaining, credits - period);
86
+ const purchased = Math.min(account.purchasedRemaining, credits - period - promotional);
87
+ account.periodRemaining -= period;
88
+ account.promotionalRemaining -= promotional;
89
+ account.purchasedRemaining -= purchased;
90
+ return { period, promotional, purchased };
91
+ };
92
+ var grant = (account, key, credits) => {
93
+ const covered = Math.min(account.debt, credits);
94
+ account.debt -= covered;
95
+ account[key] = integer(account[key] + credits - covered);
96
+ };
97
+ var normalize = (command) => {
98
+ switch (command.kind) {
99
+ case "grant":
100
+ case "reverse":
101
+ if (command.bucket !== "purchased" && command.bucket !== "promotional")
102
+ throw new Error("Invalid credit bucket");
103
+ return {
104
+ kind: command.kind,
105
+ bucket: command.bucket,
106
+ credits: integer(command.credits)
107
+ };
108
+ case "period":
109
+ return {
110
+ kind: command.kind,
111
+ periodId: periodIdentity(command.periodId),
112
+ periodEnd: command.periodEnd === null ? null : periodIdentity(command.periodEnd),
113
+ allowance: integer(command.allowance)
114
+ };
115
+ case "reserve":
116
+ case "settle":
117
+ return {
118
+ kind: command.kind,
119
+ reservationId: identity(command.reservationId),
120
+ credits: integer(command.credits)
121
+ };
122
+ case "release":
123
+ return {
124
+ kind: command.kind,
125
+ reservationId: identity(command.reservationId)
126
+ };
127
+ case "debit":
128
+ return {
129
+ kind: command.kind,
130
+ credits: integer(command.credits),
131
+ allowDebt: command.allowDebt === true,
132
+ ...command.reference === undefined ? {} : { reference: identity(command.reference) }
133
+ };
134
+ default:
135
+ throw new Error("Invalid credit command");
136
+ }
137
+ };
138
+ var createCreditAccountLedger = (store) => ({
139
+ initialize: async (accountId, seed) => {
140
+ identity(accountId);
141
+ validateCreditAccount(seed);
142
+ await store.initialize(accountId, seed);
143
+ },
144
+ receipt: (accountId, operationId) => store.transaction(identity(accountId), (tx) => tx.receipt(identity(operationId))),
145
+ balance: (accountId) => store.read(identity(accountId)),
146
+ execute: async (accountId, operationId, input) => {
147
+ identity(accountId);
148
+ identity(operationId);
149
+ const command = normalize(input);
150
+ return store.transaction(accountId, async (tx) => {
151
+ const previous = await tx.receipt(operationId);
152
+ if (previous) {
153
+ if (JSON.stringify(normalize(previous.command)) !== JSON.stringify(command))
154
+ throw new Error("Credit operation ID reused with different input");
155
+ return previous;
156
+ }
157
+ const account = { ...tx.account };
158
+ validateCreditAccount(account);
159
+ let reservation;
160
+ switch (command.kind) {
161
+ case "grant":
162
+ grant(account, command.bucket === "purchased" ? "purchasedRemaining" : "promotionalRemaining", command.credits);
163
+ break;
164
+ case "reverse": {
165
+ const key = command.bucket === "purchased" ? "purchasedRemaining" : "promotionalRemaining";
166
+ const covered = Math.min(account[key], command.credits);
167
+ account[key] -= covered;
168
+ account.debt += command.credits - covered;
169
+ break;
170
+ }
171
+ case "period":
172
+ if (command.periodId < account.periodId)
173
+ throw new Error("Credit period cannot move backwards");
174
+ if (command.periodId === account.periodId) {
175
+ const increase = Math.max(0, command.allowance - account.periodAllowance);
176
+ if (account.periodEnd === null)
177
+ account.periodEnd = command.periodEnd;
178
+ account.periodAllowance += increase;
179
+ grant(account, "periodRemaining", increase);
180
+ } else {
181
+ account.periodId = command.periodId;
182
+ account.periodEnd = command.periodEnd;
183
+ account.periodAllowance = command.allowance;
184
+ account.periodRemaining = 0;
185
+ account.consumed = 0;
186
+ grant(account, "periodRemaining", command.allowance);
187
+ }
188
+ break;
189
+ case "debit": {
190
+ if (!command.allowDebt && availableCredits(account) < command.credits)
191
+ throw new Error("Insufficient credits");
192
+ const used = allocationTotal(take(account, command.credits));
193
+ account.debt += command.credits - used;
194
+ account.consumed += command.credits;
195
+ break;
196
+ }
197
+ case "reserve":
198
+ if (command.credits === 0)
199
+ throw new Error("Reservation must be positive");
200
+ if (await tx.reservation(command.reservationId))
201
+ throw new Error("Credit reservation already exists");
202
+ if (availableCredits(account) < command.credits)
203
+ throw new Error("Insufficient credits");
204
+ reservation = {
205
+ id: command.reservationId,
206
+ periodId: account.periodId,
207
+ allocation: take(account, command.credits),
208
+ status: "reserved",
209
+ charged: null
210
+ };
211
+ account.reserved += command.credits;
212
+ break;
213
+ case "settle":
214
+ case "release": {
215
+ const held = await tx.reservation(command.reservationId);
216
+ if (!held || held.status !== "reserved")
217
+ throw new Error("Credit reservation is not active");
218
+ const total = allocationTotal(held.allocation);
219
+ const charged = command.kind === "settle" ? command.credits : 0;
220
+ if (charged > total)
221
+ throw new Error("Charge exceeds reserved credits");
222
+ let refund = total - charged;
223
+ for (const [bucket, key] of [
224
+ ["purchased", "purchasedRemaining"],
225
+ ["promotional", "promotionalRemaining"],
226
+ ["period", "periodRemaining"]
227
+ ]) {
228
+ const restored = Math.min(held.allocation[bucket], refund);
229
+ refund -= restored;
230
+ if (bucket !== "period" || held.periodId === account.periodId)
231
+ grant(account, key, restored);
232
+ }
233
+ account.reserved -= total;
234
+ if (held.periodId === account.periodId)
235
+ account.consumed += charged;
236
+ reservation = {
237
+ ...held,
238
+ status: command.kind === "settle" ? "settled" : "released",
239
+ charged
240
+ };
241
+ break;
242
+ }
243
+ }
244
+ validateCreditAccount(account);
245
+ const receipt = {
246
+ command,
247
+ account,
248
+ ...reservation ? { reservation } : {}
249
+ };
250
+ await tx.save(operationId, receipt);
251
+ return receipt;
252
+ });
253
+ }
254
+ });
255
+ var creditEntitlements = (input) => {
256
+ const funded = input.prepaidEnabled && input.account !== null && input.account.purchasedRemaining + input.account.promotionalRemaining > 0 && availableCredits(input.account) > 0;
257
+ return {
258
+ portal: input.subscribed,
259
+ mcp: input.subscribed ? "member" : funded ? "prepaid" : "free"
260
+ };
261
+ };
262
+ export {
263
+ availableCredits,
264
+ createCreditAccountLedger,
265
+ creditEntitlements,
266
+ migrateLegacyCreditAccount,
267
+ validateCreditAccount
268
+ };
269
+
270
+ //# debugId=79D819ADA24F0F9A64756E2164756E21
271
+ //# sourceMappingURL=prepaid.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/prepaid.ts"],
4
+ "sourcesContent": [
5
+ "/** Service-credit accounting. Host authorization and payment verification happen\n * before these commands. Every command and its receipt commit in one transaction. */\nexport type CreditAccount = {\n periodId: string;\n periodEnd: string | null;\n periodAllowance: number;\n periodRemaining: number;\n purchasedRemaining: number;\n promotionalRemaining: number;\n debt: number;\n reserved: number;\n consumed: number;\n};\nexport type CreditAllocation = {\n period: number;\n purchased: number;\n promotional: number;\n};\nexport type CreditReservation = {\n id: string;\n periodId: string;\n allocation: CreditAllocation;\n status: \"reserved\" | \"settled\" | \"released\";\n charged: number | null;\n};\nexport type CreditCommand =\n | { kind: \"grant\"; bucket: \"purchased\" | \"promotional\"; credits: number }\n | { kind: \"reverse\"; bucket: \"purchased\" | \"promotional\"; credits: number }\n | {\n kind: \"period\";\n periodId: string;\n periodEnd: string | null;\n allowance: number;\n }\n | { kind: \"reserve\"; reservationId: string; credits: number }\n | { kind: \"settle\"; reservationId: string; credits: number }\n | { kind: \"release\"; reservationId: string }\n | { kind: \"debit\"; credits: number; allowDebt?: boolean; reference?: string };\nexport type CreditReceipt = {\n command: CreditCommand;\n account: CreditAccount;\n reservation?: CreditReservation;\n};\nexport type CreditTransaction = {\n account: CreditAccount;\n receipt: (operationId: string) => Promise<CreditReceipt | null>;\n reservation: (id: string) => Promise<CreditReservation | null>;\n save: (operationId: string, receipt: CreditReceipt) => Promise<void>;\n};\nexport type CreditAccountStore = {\n /** Persist seed once. Never overwrite an existing account. */\n initialize: (accountId: string, seed: CreditAccount) => Promise<void>;\n read: (accountId: string) => Promise<CreditAccount | null>;\n /** Lock one account, serialize its commands across processes, and roll back\n * account, reservation and receipt together if the callback fails. */\n transaction: <T>(\n accountId: string,\n run: (tx: CreditTransaction) => Promise<T>,\n ) => Promise<T>;\n};\nconst integer = (value: number) => {\n if (!Number.isSafeInteger(value) || value < 0)\n throw new Error(\"Credits must be nonnegative safe integers\");\n return value;\n};\nconst periodIdentity = (value: string) => {\n if (\n !Number.isFinite(Date.parse(value)) ||\n new Date(value).toISOString() !== value\n )\n throw new Error(\"Credit period ID must be an ISO UTC start timestamp\");\n return value;\n};\nconst identity = (value: string) => {\n if (!value || value.length > 256)\n throw new Error(\"Credit identity is invalid\");\n return value;\n};\nexport const validateCreditAccount = (account: CreditAccount) => {\n periodIdentity(account.periodId);\n if (\n account.periodEnd !== null &&\n periodIdentity(account.periodEnd) <= account.periodId\n )\n throw new Error(\"Credit period end must follow its start\");\n for (const key of [\n \"periodAllowance\",\n \"periodRemaining\",\n \"purchasedRemaining\",\n \"promotionalRemaining\",\n \"debt\",\n \"reserved\",\n \"consumed\",\n ] as const)\n integer(account[key]);\n integer(\n account.periodRemaining +\n account.purchasedRemaining +\n account.promotionalRemaining,\n );\n if (account.periodRemaining > account.periodAllowance)\n throw new Error(\"Period balance exceeds its allowance\");\n};\nexport const availableCredits = (account: CreditAccount) =>\n Math.max(\n 0,\n account.periodRemaining +\n account.purchasedRemaining +\n account.promotionalRemaining -\n account.debt,\n );\n\n/** Preserve the currently observable legacy balance without inventing purchase\n * provenance. Historical mixed bonus grants remain promotional carryover. */\nexport const migrateLegacyCreditAccount = (input: {\n periodId: string;\n periodEnd?: string | null;\n periodAllowance: number;\n bonusCredits: number;\n consumed: number;\n}): CreditAccount => {\n integer(input.periodAllowance);\n integer(input.consumed);\n if (!Number.isSafeInteger(input.bonusCredits))\n throw new Error(\"Invalid legacy bonus credits\");\n const account: CreditAccount = {\n periodId: input.periodId,\n periodEnd: input.periodEnd ?? null,\n periodAllowance: input.periodAllowance,\n periodRemaining: Math.max(0, input.periodAllowance - input.consumed),\n purchasedRemaining: 0,\n promotionalRemaining: Math.max(\n 0,\n input.bonusCredits - Math.max(0, input.consumed - input.periodAllowance),\n ),\n debt:\n Math.max(0, -input.bonusCredits) +\n Math.max(\n 0,\n input.consumed -\n input.periodAllowance -\n Math.max(0, input.bonusCredits),\n ),\n reserved: 0,\n consumed: input.consumed,\n };\n validateCreditAccount(account);\n return account;\n};\nconst allocationTotal = (value: CreditAllocation) =>\n value.period + value.promotional + value.purchased;\nconst take = (account: CreditAccount, credits: number): CreditAllocation => {\n // A reversal liability consumes existing available credits exactly once.\n let owed = account.debt;\n for (const key of [\n \"periodRemaining\",\n \"promotionalRemaining\",\n \"purchasedRemaining\",\n ] as const) {\n const covered = Math.min(account[key], owed);\n account[key] -= covered;\n owed -= covered;\n }\n account.debt = owed;\n const period = Math.min(account.periodRemaining, credits);\n const promotional = Math.min(account.promotionalRemaining, credits - period);\n const purchased = Math.min(\n account.purchasedRemaining,\n credits - period - promotional,\n );\n account.periodRemaining -= period;\n account.promotionalRemaining -= promotional;\n account.purchasedRemaining -= purchased;\n return { period, promotional, purchased };\n};\nconst grant = (\n account: CreditAccount,\n key: \"periodRemaining\" | \"purchasedRemaining\" | \"promotionalRemaining\",\n credits: number,\n) => {\n const covered = Math.min(account.debt, credits);\n account.debt -= covered;\n account[key] = integer(account[key] + credits - covered);\n};\nconst normalize = (command: CreditCommand): CreditCommand => {\n switch (command.kind) {\n case \"grant\":\n case \"reverse\":\n if (command.bucket !== \"purchased\" && command.bucket !== \"promotional\")\n throw new Error(\"Invalid credit bucket\");\n return {\n kind: command.kind,\n bucket: command.bucket,\n credits: integer(command.credits),\n };\n case \"period\":\n return {\n kind: command.kind,\n periodId: periodIdentity(command.periodId),\n periodEnd:\n command.periodEnd === null ? null : periodIdentity(command.periodEnd),\n allowance: integer(command.allowance),\n };\n case \"reserve\":\n case \"settle\":\n return {\n kind: command.kind,\n reservationId: identity(command.reservationId),\n credits: integer(command.credits),\n };\n case \"release\":\n return {\n kind: command.kind,\n reservationId: identity(command.reservationId),\n };\n case \"debit\":\n return {\n kind: command.kind,\n credits: integer(command.credits),\n allowDebt: command.allowDebt === true,\n ...(command.reference === undefined\n ? {}\n : { reference: identity(command.reference) }),\n };\n default:\n throw new Error(\"Invalid credit command\");\n }\n};\nexport const createCreditAccountLedger = (store: CreditAccountStore) => ({\n initialize: async (accountId: string, seed: CreditAccount) => {\n identity(accountId);\n validateCreditAccount(seed);\n await store.initialize(accountId, seed);\n },\n receipt: (accountId: string, operationId: string) =>\n store.transaction(identity(accountId), (tx) =>\n tx.receipt(identity(operationId)),\n ),\n balance: (accountId: string) => store.read(identity(accountId)),\n execute: async (\n accountId: string,\n operationId: string,\n input: CreditCommand,\n ): Promise<CreditReceipt> => {\n identity(accountId);\n identity(operationId);\n const command = normalize(input);\n return store.transaction(accountId, async (tx) => {\n const previous = await tx.receipt(operationId);\n if (previous) {\n if (\n JSON.stringify(normalize(previous.command)) !==\n JSON.stringify(command)\n )\n throw new Error(\"Credit operation ID reused with different input\");\n return previous;\n }\n const account = { ...tx.account };\n validateCreditAccount(account);\n let reservation: CreditReservation | undefined;\n switch (command.kind) {\n case \"grant\":\n grant(\n account,\n command.bucket === \"purchased\"\n ? \"purchasedRemaining\"\n : \"promotionalRemaining\",\n command.credits,\n );\n break;\n case \"reverse\": {\n const key =\n command.bucket === \"purchased\"\n ? \"purchasedRemaining\"\n : \"promotionalRemaining\";\n const covered = Math.min(account[key], command.credits);\n account[key] -= covered;\n account.debt += command.credits - covered;\n break;\n }\n case \"period\":\n if (command.periodId < account.periodId)\n throw new Error(\"Credit period cannot move backwards\");\n if (command.periodId === account.periodId) {\n // Upgrades add only the increase. Downgrades take effect next period.\n const increase = Math.max(\n 0,\n command.allowance - account.periodAllowance,\n );\n if (account.periodEnd === null)\n account.periodEnd = command.periodEnd;\n account.periodAllowance += increase;\n grant(account, \"periodRemaining\", increase);\n } else {\n account.periodId = command.periodId;\n account.periodEnd = command.periodEnd;\n account.periodAllowance = command.allowance;\n account.periodRemaining = 0;\n account.consumed = 0;\n grant(account, \"periodRemaining\", command.allowance);\n }\n break;\n case \"debit\": {\n if (!command.allowDebt && availableCredits(account) < command.credits)\n throw new Error(\"Insufficient credits\");\n const used = allocationTotal(take(account, command.credits));\n account.debt += command.credits - used;\n account.consumed += command.credits;\n break;\n }\n case \"reserve\":\n if (command.credits === 0)\n throw new Error(\"Reservation must be positive\");\n if (await tx.reservation(command.reservationId))\n throw new Error(\"Credit reservation already exists\");\n if (availableCredits(account) < command.credits)\n throw new Error(\"Insufficient credits\");\n reservation = {\n id: command.reservationId,\n periodId: account.periodId,\n allocation: take(account, command.credits),\n status: \"reserved\",\n charged: null,\n };\n account.reserved += command.credits;\n break;\n case \"settle\":\n case \"release\": {\n const held = await tx.reservation(command.reservationId);\n if (!held || held.status !== \"reserved\")\n throw new Error(\"Credit reservation is not active\");\n const total = allocationTotal(held.allocation);\n const charged = command.kind === \"settle\" ? command.credits : 0;\n if (charged > total)\n throw new Error(\"Charge exceeds reserved credits\");\n let refund = total - charged;\n for (const [bucket, key] of [\n [\"purchased\", \"purchasedRemaining\"],\n [\"promotional\", \"promotionalRemaining\"],\n [\"period\", \"periodRemaining\"],\n ] as const) {\n const restored = Math.min(held.allocation[bucket], refund);\n refund -= restored;\n if (bucket !== \"period\" || held.periodId === account.periodId)\n grant(account, key, restored);\n }\n account.reserved -= total;\n if (held.periodId === account.periodId) account.consumed += charged;\n reservation = {\n ...held,\n status: command.kind === \"settle\" ? \"settled\" : \"released\",\n charged,\n };\n break;\n }\n }\n validateCreditAccount(account);\n const receipt: CreditReceipt = {\n command,\n account,\n ...(reservation ? { reservation } : {}),\n };\n await tx.save(operationId, receipt);\n return receipt;\n });\n },\n});\n\n/** Portal access and funded MCP access are independent. Period-only free\n * allowances do not silently unlock prepaid work; carryover grants may. */\nexport const creditEntitlements = (input: {\n subscribed: boolean;\n prepaidEnabled: boolean;\n account: CreditAccount | null;\n}) => {\n const funded =\n input.prepaidEnabled &&\n input.account !== null &&\n input.account.purchasedRemaining + input.account.promotionalRemaining > 0 &&\n availableCredits(input.account) > 0;\n return {\n portal: input.subscribed,\n mcp: input.subscribed\n ? (\"member\" as const)\n : funded\n ? (\"prepaid\" as const)\n : (\"free\" as const),\n };\n};\n"
6
+ ],
7
+ "mappings": ";;;;;;;;;;;;;;;;;AA4DA,IAAM,UAAU,CAAC,UAAkB;AAAA,EACjC,IAAI,CAAC,OAAO,cAAc,KAAK,KAAK,QAAQ;AAAA,IAC1C,MAAM,IAAI,MAAM,2CAA2C;AAAA,EAC7D,OAAO;AAAA;AAET,IAAM,iBAAiB,CAAC,UAAkB;AAAA,EACxC,IACE,CAAC,OAAO,SAAS,KAAK,MAAM,KAAK,CAAC,KAClC,IAAI,KAAK,KAAK,EAAE,YAAY,MAAM;AAAA,IAElC,MAAM,IAAI,MAAM,qDAAqD;AAAA,EACvE,OAAO;AAAA;AAET,IAAM,WAAW,CAAC,UAAkB;AAAA,EAClC,IAAI,CAAC,SAAS,MAAM,SAAS;AAAA,IAC3B,MAAM,IAAI,MAAM,4BAA4B;AAAA,EAC9C,OAAO;AAAA;AAEF,IAAM,wBAAwB,CAAC,YAA2B;AAAA,EAC/D,eAAe,QAAQ,QAAQ;AAAA,EAC/B,IACE,QAAQ,cAAc,QACtB,eAAe,QAAQ,SAAS,KAAK,QAAQ;AAAA,IAE7C,MAAM,IAAI,MAAM,yCAAyC;AAAA,EAC3D,WAAW,OAAO;AAAA,IAChB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAAA,IACE,QAAQ,QAAQ,IAAI;AAAA,EACtB,QACE,QAAQ,kBACN,QAAQ,qBACR,QAAQ,oBACZ;AAAA,EACA,IAAI,QAAQ,kBAAkB,QAAQ;AAAA,IACpC,MAAM,IAAI,MAAM,sCAAsC;AAAA;AAEnD,IAAM,mBAAmB,CAAC,YAC/B,KAAK,IACH,GACA,QAAQ,kBACN,QAAQ,qBACR,QAAQ,uBACR,QAAQ,IACZ;AAIK,IAAM,6BAA6B,CAAC,UAMtB;AAAA,EACnB,QAAQ,MAAM,eAAe;AAAA,EAC7B,QAAQ,MAAM,QAAQ;AAAA,EACtB,IAAI,CAAC,OAAO,cAAc,MAAM,YAAY;AAAA,IAC1C,MAAM,IAAI,MAAM,8BAA8B;AAAA,EAChD,MAAM,UAAyB;AAAA,IAC7B,UAAU,MAAM;AAAA,IAChB,WAAW,MAAM,aAAa;AAAA,IAC9B,iBAAiB,MAAM;AAAA,IACvB,iBAAiB,KAAK,IAAI,GAAG,MAAM,kBAAkB,MAAM,QAAQ;AAAA,IACnE,oBAAoB;AAAA,IACpB,sBAAsB,KAAK,IACzB,GACA,MAAM,eAAe,KAAK,IAAI,GAAG,MAAM,WAAW,MAAM,eAAe,CACzE;AAAA,IACA,MACE,KAAK,IAAI,GAAG,CAAC,MAAM,YAAY,IAC/B,KAAK,IACH,GACA,MAAM,WACJ,MAAM,kBACN,KAAK,IAAI,GAAG,MAAM,YAAY,CAClC;AAAA,IACF,UAAU;AAAA,IACV,UAAU,MAAM;AAAA,EAClB;AAAA,EACA,sBAAsB,OAAO;AAAA,EAC7B,OAAO;AAAA;AAET,IAAM,kBAAkB,CAAC,UACvB,MAAM,SAAS,MAAM,cAAc,MAAM;AAC3C,IAAM,OAAO,CAAC,SAAwB,YAAsC;AAAA,EAE1E,IAAI,OAAO,QAAQ;AAAA,EACnB,WAAW,OAAO;AAAA,IAChB;AAAA,IACA;AAAA,IACA;AAAA,EACF,GAAY;AAAA,IACV,MAAM,UAAU,KAAK,IAAI,QAAQ,MAAM,IAAI;AAAA,IAC3C,QAAQ,QAAQ;AAAA,IAChB,QAAQ;AAAA,EACV;AAAA,EACA,QAAQ,OAAO;AAAA,EACf,MAAM,SAAS,KAAK,IAAI,QAAQ,iBAAiB,OAAO;AAAA,EACxD,MAAM,cAAc,KAAK,IAAI,QAAQ,sBAAsB,UAAU,MAAM;AAAA,EAC3E,MAAM,YAAY,KAAK,IACrB,QAAQ,oBACR,UAAU,SAAS,WACrB;AAAA,EACA,QAAQ,mBAAmB;AAAA,EAC3B,QAAQ,wBAAwB;AAAA,EAChC,QAAQ,sBAAsB;AAAA,EAC9B,OAAO,EAAE,QAAQ,aAAa,UAAU;AAAA;AAE1C,IAAM,QAAQ,CACZ,SACA,KACA,YACG;AAAA,EACH,MAAM,UAAU,KAAK,IAAI,QAAQ,MAAM,OAAO;AAAA,EAC9C,QAAQ,QAAQ;AAAA,EAChB,QAAQ,OAAO,QAAQ,QAAQ,OAAO,UAAU,OAAO;AAAA;AAEzD,IAAM,YAAY,CAAC,YAA0C;AAAA,EAC3D,QAAQ,QAAQ;AAAA,SACT;AAAA,SACA;AAAA,MACH,IAAI,QAAQ,WAAW,eAAe,QAAQ,WAAW;AAAA,QACvD,MAAM,IAAI,MAAM,uBAAuB;AAAA,MACzC,OAAO;AAAA,QACL,MAAM,QAAQ;AAAA,QACd,QAAQ,QAAQ;AAAA,QAChB,SAAS,QAAQ,QAAQ,OAAO;AAAA,MAClC;AAAA,SACG;AAAA,MACH,OAAO;AAAA,QACL,MAAM,QAAQ;AAAA,QACd,UAAU,eAAe,QAAQ,QAAQ;AAAA,QACzC,WACE,QAAQ,cAAc,OAAO,OAAO,eAAe,QAAQ,SAAS;AAAA,QACtE,WAAW,QAAQ,QAAQ,SAAS;AAAA,MACtC;AAAA,SACG;AAAA,SACA;AAAA,MACH,OAAO;AAAA,QACL,MAAM,QAAQ;AAAA,QACd,eAAe,SAAS,QAAQ,aAAa;AAAA,QAC7C,SAAS,QAAQ,QAAQ,OAAO;AAAA,MAClC;AAAA,SACG;AAAA,MACH,OAAO;AAAA,QACL,MAAM,QAAQ;AAAA,QACd,eAAe,SAAS,QAAQ,aAAa;AAAA,MAC/C;AAAA,SACG;AAAA,MACH,OAAO;AAAA,QACL,MAAM,QAAQ;AAAA,QACd,SAAS,QAAQ,QAAQ,OAAO;AAAA,QAChC,WAAW,QAAQ,cAAc;AAAA,WAC7B,QAAQ,cAAc,YACtB,CAAC,IACD,EAAE,WAAW,SAAS,QAAQ,SAAS,EAAE;AAAA,MAC/C;AAAA;AAAA,MAEA,MAAM,IAAI,MAAM,wBAAwB;AAAA;AAAA;AAGvC,IAAM,4BAA4B,CAAC,WAA+B;AAAA,EACvE,YAAY,OAAO,WAAmB,SAAwB;AAAA,IAC5D,SAAS,SAAS;AAAA,IAClB,sBAAsB,IAAI;AAAA,IAC1B,MAAM,MAAM,WAAW,WAAW,IAAI;AAAA;AAAA,EAExC,SAAS,CAAC,WAAmB,gBAC3B,MAAM,YAAY,SAAS,SAAS,GAAG,CAAC,OACtC,GAAG,QAAQ,SAAS,WAAW,CAAC,CAClC;AAAA,EACF,SAAS,CAAC,cAAsB,MAAM,KAAK,SAAS,SAAS,CAAC;AAAA,EAC9D,SAAS,OACP,WACA,aACA,UAC2B;AAAA,IAC3B,SAAS,SAAS;AAAA,IAClB,SAAS,WAAW;AAAA,IACpB,MAAM,UAAU,UAAU,KAAK;AAAA,IAC/B,OAAO,MAAM,YAAY,WAAW,OAAO,OAAO;AAAA,MAChD,MAAM,WAAW,MAAM,GAAG,QAAQ,WAAW;AAAA,MAC7C,IAAI,UAAU;AAAA,QACZ,IACE,KAAK,UAAU,UAAU,SAAS,OAAO,CAAC,MAC1C,KAAK,UAAU,OAAO;AAAA,UAEtB,MAAM,IAAI,MAAM,iDAAiD;AAAA,QACnE,OAAO;AAAA,MACT;AAAA,MACA,MAAM,UAAU,KAAK,GAAG,QAAQ;AAAA,MAChC,sBAAsB,OAAO;AAAA,MAC7B,IAAI;AAAA,MACJ,QAAQ,QAAQ;AAAA,aACT;AAAA,UACH,MACE,SACA,QAAQ,WAAW,cACf,uBACA,wBACJ,QAAQ,OACV;AAAA,UACA;AAAA,aACG,WAAW;AAAA,UACd,MAAM,MACJ,QAAQ,WAAW,cACf,uBACA;AAAA,UACN,MAAM,UAAU,KAAK,IAAI,QAAQ,MAAM,QAAQ,OAAO;AAAA,UACtD,QAAQ,QAAQ;AAAA,UAChB,QAAQ,QAAQ,QAAQ,UAAU;AAAA,UAClC;AAAA,QACF;AAAA,aACK;AAAA,UACH,IAAI,QAAQ,WAAW,QAAQ;AAAA,YAC7B,MAAM,IAAI,MAAM,qCAAqC;AAAA,UACvD,IAAI,QAAQ,aAAa,QAAQ,UAAU;AAAA,YAEzC,MAAM,WAAW,KAAK,IACpB,GACA,QAAQ,YAAY,QAAQ,eAC9B;AAAA,YACA,IAAI,QAAQ,cAAc;AAAA,cACxB,QAAQ,YAAY,QAAQ;AAAA,YAC9B,QAAQ,mBAAmB;AAAA,YAC3B,MAAM,SAAS,mBAAmB,QAAQ;AAAA,UAC5C,EAAO;AAAA,YACL,QAAQ,WAAW,QAAQ;AAAA,YAC3B,QAAQ,YAAY,QAAQ;AAAA,YAC5B,QAAQ,kBAAkB,QAAQ;AAAA,YAClC,QAAQ,kBAAkB;AAAA,YAC1B,QAAQ,WAAW;AAAA,YACnB,MAAM,SAAS,mBAAmB,QAAQ,SAAS;AAAA;AAAA,UAErD;AAAA,aACG,SAAS;AAAA,UACZ,IAAI,CAAC,QAAQ,aAAa,iBAAiB,OAAO,IAAI,QAAQ;AAAA,YAC5D,MAAM,IAAI,MAAM,sBAAsB;AAAA,UACxC,MAAM,OAAO,gBAAgB,KAAK,SAAS,QAAQ,OAAO,CAAC;AAAA,UAC3D,QAAQ,QAAQ,QAAQ,UAAU;AAAA,UAClC,QAAQ,YAAY,QAAQ;AAAA,UAC5B;AAAA,QACF;AAAA,aACK;AAAA,UACH,IAAI,QAAQ,YAAY;AAAA,YACtB,MAAM,IAAI,MAAM,8BAA8B;AAAA,UAChD,IAAI,MAAM,GAAG,YAAY,QAAQ,aAAa;AAAA,YAC5C,MAAM,IAAI,MAAM,mCAAmC;AAAA,UACrD,IAAI,iBAAiB,OAAO,IAAI,QAAQ;AAAA,YACtC,MAAM,IAAI,MAAM,sBAAsB;AAAA,UACxC,cAAc;AAAA,YACZ,IAAI,QAAQ;AAAA,YACZ,UAAU,QAAQ;AAAA,YAClB,YAAY,KAAK,SAAS,QAAQ,OAAO;AAAA,YACzC,QAAQ;AAAA,YACR,SAAS;AAAA,UACX;AAAA,UACA,QAAQ,YAAY,QAAQ;AAAA,UAC5B;AAAA,aACG;AAAA,aACA,WAAW;AAAA,UACd,MAAM,OAAO,MAAM,GAAG,YAAY,QAAQ,aAAa;AAAA,UACvD,IAAI,CAAC,QAAQ,KAAK,WAAW;AAAA,YAC3B,MAAM,IAAI,MAAM,kCAAkC;AAAA,UACpD,MAAM,QAAQ,gBAAgB,KAAK,UAAU;AAAA,UAC7C,MAAM,UAAU,QAAQ,SAAS,WAAW,QAAQ,UAAU;AAAA,UAC9D,IAAI,UAAU;AAAA,YACZ,MAAM,IAAI,MAAM,iCAAiC;AAAA,UACnD,IAAI,SAAS,QAAQ;AAAA,UACrB,YAAY,QAAQ,QAAQ;AAAA,YAC1B,CAAC,aAAa,oBAAoB;AAAA,YAClC,CAAC,eAAe,sBAAsB;AAAA,YACtC,CAAC,UAAU,iBAAiB;AAAA,UAC9B,GAAY;AAAA,YACV,MAAM,WAAW,KAAK,IAAI,KAAK,WAAW,SAAS,MAAM;AAAA,YACzD,UAAU;AAAA,YACV,IAAI,WAAW,YAAY,KAAK,aAAa,QAAQ;AAAA,cACnD,MAAM,SAAS,KAAK,QAAQ;AAAA,UAChC;AAAA,UACA,QAAQ,YAAY;AAAA,UACpB,IAAI,KAAK,aAAa,QAAQ;AAAA,YAAU,QAAQ,YAAY;AAAA,UAC5D,cAAc;AAAA,eACT;AAAA,YACH,QAAQ,QAAQ,SAAS,WAAW,YAAY;AAAA,YAChD;AAAA,UACF;AAAA,UACA;AAAA,QACF;AAAA;AAAA,MAEF,sBAAsB,OAAO;AAAA,MAC7B,MAAM,UAAyB;AAAA,QAC7B;AAAA,QACA;AAAA,WACI,cAAc,EAAE,YAAY,IAAI,CAAC;AAAA,MACvC;AAAA,MACA,MAAM,GAAG,KAAK,aAAa,OAAO;AAAA,MAClC,OAAO;AAAA,KACR;AAAA;AAEL;AAIO,IAAM,qBAAqB,CAAC,UAI7B;AAAA,EACJ,MAAM,SACJ,MAAM,kBACN,MAAM,YAAY,QAClB,MAAM,QAAQ,qBAAqB,MAAM,QAAQ,uBAAuB,KACxE,iBAAiB,MAAM,OAAO,IAAI;AAAA,EACpC,OAAO;AAAA,IACL,QAAQ,MAAM;AAAA,IACd,KAAK,MAAM,aACN,WACD,SACG,YACA;AAAA,EACT;AAAA;",
8
+ "debugId": "79D819ADA24F0F9A64756E2164756E21",
9
+ "names": []
10
+ }
@@ -0,0 +1,11 @@
1
+ import { type CreditAccountStore } from "./prepaid";
2
+ export type CreditSql = {
3
+ query: (query: string, parameters: readonly unknown[]) => Promise<{
4
+ rows: Record<string, unknown>[];
5
+ }>;
6
+ };
7
+ export type CreditSqlClient = CreditSql & {
8
+ transaction: <T>(run: (sql: CreditSql) => Promise<T>) => Promise<T>;
9
+ };
10
+ export declare const creditAccountPostgresSchemaSql: (schema?: string) => string;
11
+ export declare const createPostgresCreditAccountStore: (client: CreditSqlClient, schema?: string) => CreditAccountStore;