@lenso/payments 0.0.0-stage → 0.1.2

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,461 @@
1
+ import {
2
+ PaymentsError2,
3
+ RefundNotCreatedError2
4
+ } from "./index-qtpqn1xv.js";
5
+
6
+ // src/index.ts
7
+ function identifier(value) {
8
+ if (typeof value !== "string" || !value.length || value.length > 256 || [...value].some((character) => character.charCodeAt(0) < 32))
9
+ throw new PaymentsError2("invalid-input");
10
+ }
11
+ async function digest(parts) {
12
+ const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify(parts)));
13
+ return Array.from(new Uint8Array(bytes), (byte) => byte.toString(16).padStart(2, "0")).join("");
14
+ }
15
+ function resource(record) {
16
+ return Object.freeze({
17
+ paymentId: record.paymentId,
18
+ tenantId: record.tenantId,
19
+ orderId: record.orderId,
20
+ accountId: record.accountId,
21
+ live: record.live,
22
+ amount: record.amount,
23
+ currency: record.currency
24
+ });
25
+ }
26
+ function view(record) {
27
+ return {
28
+ ...resource(record),
29
+ status: record.status,
30
+ providerId: record.providerId,
31
+ refunds: record.refunds.map(({ refundId, amount, status, providerId }) => ({
32
+ refundId,
33
+ amount,
34
+ status,
35
+ providerId
36
+ }))
37
+ };
38
+ }
39
+ function verifyPayment(record, payment) {
40
+ if (payment.paymentId !== record.paymentId || payment.tenantId !== record.tenantId || payment.orderId !== record.orderId || payment.accountId !== record.accountId || payment.live !== record.live || payment.amount !== record.amount || payment.currency !== record.currency || record.providerId !== null && payment.id !== record.providerId || payment.status === "succeeded" && payment.received !== record.amount)
41
+ throw new PaymentsError2("provider-mismatch");
42
+ }
43
+ function verifyRefund(record, refund, observed) {
44
+ if (observed.paymentId !== record.paymentId || observed.refundId !== refund.refundId || observed.paymentProviderId !== record.providerId || observed.accountId !== record.accountId || observed.live !== record.live || observed.amount !== refund.amount || observed.currency !== record.currency || refund.providerId !== null && observed.id !== refund.providerId)
45
+ throw new PaymentsError2("provider-mismatch");
46
+ }
47
+ function paymentTerminal(status) {
48
+ return status === "succeeded" || status === "canceled";
49
+ }
50
+ function refundTerminal(status) {
51
+ return status === "succeeded" || status === "failed" || status === "canceled";
52
+ }
53
+ function appendResult(record, result) {
54
+ if (!record.results.some((existing) => existing.resultId === result.resultId))
55
+ record.results.push(result);
56
+ }
57
+ function createPayments2(options) {
58
+ const { store, provider, authorize } = options;
59
+ const clock = options.clock ?? Date.now;
60
+ const leaseMs = options.leaseMs ?? 60000;
61
+ const delay = options.reconcileDelayMs ?? 60000;
62
+ const maxRefunds = options.maxRefunds ?? 100;
63
+ if (typeof authorize !== "function" || !Number.isSafeInteger(leaseMs) || leaseMs < 1000 || leaseMs > 3600000 || !Number.isSafeInteger(delay) || delay < 1000 || delay > 86400000 || !Number.isSafeInteger(maxRefunds) || maxRefunds < 1 || maxRefunds > 1000 || !Number.isSafeInteger(provider.replayWindowMs) || provider.replayWindowMs <= 0)
64
+ throw new PaymentsError2("invalid-input");
65
+ identifier(provider.accountId);
66
+ function now() {
67
+ const value = clock();
68
+ if (!Number.isSafeInteger(value) || value < 0)
69
+ throw new PaymentsError2("unavailable");
70
+ return value;
71
+ }
72
+ async function load(paymentId) {
73
+ identifier(paymentId);
74
+ const record = await store.get(paymentId);
75
+ if (!record)
76
+ throw new PaymentsError2("not-found");
77
+ if (record.accountId !== provider.accountId || record.live !== provider.live)
78
+ throw new PaymentsError2("forbidden");
79
+ return record;
80
+ }
81
+ async function allowed(actor, action, record) {
82
+ await authorize(actor, action, resource(record));
83
+ }
84
+ function renewLease(record, token) {
85
+ const time = now();
86
+ if (record.lease?.token !== token || record.lease.until <= time)
87
+ throw new PaymentsError2("conflict");
88
+ record.lease.until = time + leaseMs;
89
+ }
90
+ async function mutate(paymentId, change) {
91
+ for (let attempt = 0;attempt < 12; attempt++) {
92
+ const current = await load(paymentId);
93
+ const next = structuredClone(current);
94
+ if (!change(next))
95
+ return current;
96
+ next.revision = current.revision + 1;
97
+ next.updatedAt = now();
98
+ if (await store.compareAndSet(next, current.revision))
99
+ return next;
100
+ }
101
+ throw new PaymentsError2("conflict");
102
+ }
103
+ async function observePayment(paymentId, token, observed) {
104
+ return mutate(paymentId, (record) => {
105
+ renewLease(record, token);
106
+ verifyPayment(record, observed);
107
+ if (paymentTerminal(record.status) && observed.status !== record.status)
108
+ throw new PaymentsError2("provider-mismatch");
109
+ record.providerId = observed.id;
110
+ record.status = observed.status;
111
+ if (paymentTerminal(record.status)) {
112
+ appendResult(record, {
113
+ ...resource(record),
114
+ kind: record.status === "succeeded" ? "payment.succeeded" : "payment.canceled",
115
+ resultId: `${record.paymentId}:${record.status}`
116
+ });
117
+ }
118
+ return true;
119
+ });
120
+ }
121
+ async function observeRefund(paymentId, token, refundId, observed) {
122
+ return mutate(paymentId, (record) => {
123
+ renewLease(record, token);
124
+ const refund = record.refunds.find((item) => item.refundId === refundId);
125
+ verifyRefund(record, refund, observed);
126
+ if (refundTerminal(refund.status) && observed.status !== refund.status)
127
+ throw new PaymentsError2("provider-mismatch");
128
+ refund.providerId = observed.id;
129
+ refund.status = observed.status;
130
+ if (refundTerminal(refund.status)) {
131
+ appendResult(record, {
132
+ ...resource(record),
133
+ kind: refund.status === "succeeded" ? "refund.succeeded" : refund.status === "failed" ? "refund.failed" : "refund.canceled",
134
+ resultId: `${refund.refundId}:${refund.status}`,
135
+ refundId,
136
+ refundAmount: refund.amount
137
+ });
138
+ }
139
+ return true;
140
+ });
141
+ }
142
+ async function run(paymentId) {
143
+ const token = crypto.randomUUID();
144
+ let record = await mutate(paymentId, (current) => {
145
+ if (current.lease && current.lease.until > now())
146
+ return false;
147
+ current.lease = { token, until: now() + leaseMs };
148
+ return true;
149
+ });
150
+ if (record.lease?.token !== token)
151
+ return false;
152
+ let resolved = true;
153
+ try {
154
+ const previousAttempt = record.attemptedAt;
155
+ if (previousAttempt === null) {
156
+ record = await mutate(paymentId, (current) => {
157
+ if (current.lease?.token !== token)
158
+ throw new PaymentsError2("conflict");
159
+ current.attemptedAt = now();
160
+ return true;
161
+ });
162
+ }
163
+ let observed = record.providerId ? await provider.getPayment(record.providerId) : previousAttempt === null ? null : await provider.findPayment(record);
164
+ if (!observed) {
165
+ record = await load(paymentId);
166
+ if (record.providerId) {
167
+ observed = await provider.getPayment(record.providerId);
168
+ } else if (mayWrite(record, token, record.attemptedAt)) {
169
+ observed = await provider.createPayment(record, `payment:${record.paymentId}`, async () => {
170
+ const current = await load(paymentId);
171
+ if (!mayWrite(current, token, current.attemptedAt) || current.providerId !== null)
172
+ throw new PaymentsError2("conflict");
173
+ });
174
+ }
175
+ }
176
+ if (!observed)
177
+ return false;
178
+ record = await observePayment(paymentId, token, observed);
179
+ for (const pending of record.refunds) {
180
+ if (refundTerminal(pending.status) && pending.providerId === null)
181
+ continue;
182
+ let refund = pending;
183
+ const previousRefundAttempt = refund.attemptedAt;
184
+ record = await mutate(paymentId, (current) => {
185
+ renewLease(current, token);
186
+ const item = current.refunds.find((candidate) => candidate.refundId === refund.refundId);
187
+ item.attemptedAt ??= now();
188
+ return true;
189
+ });
190
+ refund = record.refunds.find((item) => item.refundId === refund.refundId);
191
+ let observedRefund = refund.providerId ? await provider.getRefund(refund.providerId) : previousRefundAttempt === null ? null : await provider.findRefund(record, refund);
192
+ if (!observedRefund) {
193
+ record = await load(paymentId);
194
+ refund = record.refunds.find((item) => item.refundId === refund.refundId);
195
+ if (refund.providerId) {
196
+ observedRefund = await provider.getRefund(refund.providerId);
197
+ } else if (mayWrite(record, token, refund.attemptedAt)) {
198
+ try {
199
+ observedRefund = await provider.createRefund(record, refund, `refund:${refund.refundId}`, async () => {
200
+ const current = await load(paymentId);
201
+ const item = current.refunds.find((candidate) => candidate.refundId === refund.refundId);
202
+ if (!item || !mayWrite(current, token, item.attemptedAt) || item.providerId !== null || item.status !== "unknown" || current.providerId !== record.providerId)
203
+ throw new PaymentsError2("conflict");
204
+ });
205
+ } catch (error) {
206
+ if (!(error instanceof RefundNotCreatedError2))
207
+ throw error;
208
+ record = await mutate(paymentId, (current) => {
209
+ renewLease(current, token);
210
+ const item = current.refunds.find((candidate) => candidate.refundId === refund.refundId);
211
+ if (item.providerId !== null)
212
+ throw new PaymentsError2("provider-mismatch");
213
+ item.status = "failed";
214
+ appendResult(current, {
215
+ ...resource(current),
216
+ kind: "refund.failed",
217
+ resultId: `${item.refundId}:failed`,
218
+ refundId: item.refundId,
219
+ refundAmount: item.amount
220
+ });
221
+ return true;
222
+ });
223
+ continue;
224
+ }
225
+ }
226
+ }
227
+ if (observedRefund)
228
+ record = await observeRefund(paymentId, token, refund.refundId, observedRefund);
229
+ else
230
+ resolved = false;
231
+ }
232
+ return resolved;
233
+ } catch (error) {
234
+ if (error instanceof PaymentsError2 && error.code === "provider-mismatch")
235
+ throw error;
236
+ return false;
237
+ } finally {
238
+ await mutate(paymentId, (current) => {
239
+ if (current.lease?.token !== token)
240
+ return false;
241
+ current.lease = null;
242
+ const finished = paymentTerminal(current.status) && current.refunds.every((item) => refundTerminal(item.status));
243
+ current.reconcileAt = finished ? Number.MAX_SAFE_INTEGER : now() + delay;
244
+ return true;
245
+ });
246
+ }
247
+ }
248
+ function mayWrite(record, token, attemptedAt) {
249
+ const time = now();
250
+ return record.lease?.token === token && record.lease.until > time && attemptedAt !== null && time >= attemptedAt && time - attemptedAt < provider.replayWindowMs;
251
+ }
252
+ const payments = {
253
+ async create(input, actor) {
254
+ identifier(input.tenantId);
255
+ identifier(input.orderId);
256
+ identifier(input.key);
257
+ provider.validateAmount(input.amount, input.currency);
258
+ const paymentId = await digest([
259
+ provider.accountId,
260
+ provider.live,
261
+ input.tenantId,
262
+ input.key
263
+ ]);
264
+ const orderKey = await digest([
265
+ provider.accountId,
266
+ provider.live,
267
+ input.tenantId,
268
+ input.orderId
269
+ ]);
270
+ const time = now();
271
+ const draft = {
272
+ tenantId: input.tenantId,
273
+ orderId: input.orderId,
274
+ key: input.key,
275
+ amount: input.amount,
276
+ currency: input.currency,
277
+ paymentId,
278
+ orderKey,
279
+ accountId: provider.accountId,
280
+ live: provider.live,
281
+ providerId: null,
282
+ status: "unknown",
283
+ revision: 0,
284
+ lease: null,
285
+ attemptedAt: null,
286
+ refunds: [],
287
+ results: [],
288
+ createdAt: time,
289
+ updatedAt: time,
290
+ reconcileAt: time
291
+ };
292
+ await allowed(actor, "create", draft);
293
+ if (!await store.insert(draft)) {
294
+ const existing = await store.get(paymentId) ?? await store.getByOrder(orderKey);
295
+ if (!existing)
296
+ throw new PaymentsError2("conflict");
297
+ await allowed(actor, "create", existing);
298
+ if (existing.paymentId !== paymentId || existing.orderKey !== orderKey || existing.amount !== input.amount || existing.currency !== input.currency || existing.key !== input.key)
299
+ throw new PaymentsError2("conflict");
300
+ }
301
+ const current = await load(paymentId);
302
+ if (current.attemptedAt === null)
303
+ await run(paymentId);
304
+ return view(await load(paymentId));
305
+ },
306
+ async get(input, actor) {
307
+ const record = await load(input.paymentId);
308
+ await allowed(actor, "read", record);
309
+ return view(record);
310
+ },
311
+ async refund(input, actor) {
312
+ identifier(input.key);
313
+ let record = await load(input.paymentId);
314
+ await allowed(actor, "refund", record);
315
+ provider.validateRefundAmount(input.amount, record.currency);
316
+ const refundId = await digest([record.paymentId, input.key]);
317
+ record = await mutate(record.paymentId, (current) => {
318
+ const existing = current.refunds.find((refund) => refund.key === input.key);
319
+ if (existing) {
320
+ if (existing.amount !== input.amount)
321
+ throw new PaymentsError2("conflict");
322
+ return false;
323
+ }
324
+ if (current.status !== "succeeded" || !current.providerId)
325
+ throw new PaymentsError2("conflict");
326
+ const reserved = current.refunds.reduce((sum, refund) => sum + (refund.status === "failed" || refund.status === "canceled" ? 0 : refund.amount), 0);
327
+ if (input.amount > current.amount - reserved || current.refunds.length >= maxRefunds)
328
+ throw new PaymentsError2("conflict");
329
+ current.refunds.push({
330
+ refundId,
331
+ key: input.key,
332
+ amount: input.amount,
333
+ providerId: null,
334
+ status: "unknown",
335
+ attemptedAt: null
336
+ });
337
+ current.reconcileAt = now();
338
+ return true;
339
+ });
340
+ if (record.refunds.find((refund) => refund.refundId === refundId)?.attemptedAt === null)
341
+ await run(record.paymentId);
342
+ return view(await load(record.paymentId));
343
+ },
344
+ async results(input, actor) {
345
+ const record = await load(input.paymentId);
346
+ await allowed(actor, "results", record);
347
+ return structuredClone(record.results);
348
+ },
349
+ async clientSecret(input, actor) {
350
+ const record = await load(input.paymentId);
351
+ await allowed(actor, "client-secret", record);
352
+ if (!record.providerId || paymentTerminal(record.status))
353
+ return null;
354
+ const observed = await provider.getPayment(record.providerId);
355
+ verifyPayment(record, observed);
356
+ try {
357
+ return await provider.clientSecret(record.providerId);
358
+ } catch {
359
+ throw new PaymentsError2("unavailable");
360
+ }
361
+ }
362
+ };
363
+ return {
364
+ payments,
365
+ webhook: {
366
+ async receive(raw, signature) {
367
+ const event = await provider.verifyWebhook(raw, signature);
368
+ if (!event)
369
+ return { accepted: false, duplicate: false };
370
+ if (event.accountId !== provider.accountId || event.live !== provider.live)
371
+ throw new PaymentsError2("provider-mismatch");
372
+ const record = await load(event.object.paymentId);
373
+ if ("refundId" in event.object) {
374
+ const object = event.object;
375
+ const refund = record.refunds.find((item) => item.refundId === object.refundId);
376
+ if (!refund)
377
+ throw new PaymentsError2("provider-mismatch");
378
+ verifyRefund(record, refund, object);
379
+ } else
380
+ verifyPayment(record, event.object);
381
+ const time = now();
382
+ const eventKey = await digest([provider.accountId, provider.live, event.eventId]);
383
+ const inserted = await store.receive({
384
+ eventKey,
385
+ eventId: event.eventId,
386
+ paymentId: record.paymentId,
387
+ objectId: event.object.id,
388
+ refundId: "refundId" in event.object ? event.object.refundId : null,
389
+ accountId: record.accountId,
390
+ live: record.live,
391
+ createdAt: time,
392
+ reconcileAt: time,
393
+ done: false
394
+ });
395
+ return { accepted: true, duplicate: !inserted };
396
+ }
397
+ },
398
+ reconciliation: {
399
+ run,
400
+ async drain(limit = 50) {
401
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200)
402
+ throw new PaymentsError2("invalid-input");
403
+ let events = 0;
404
+ let paymentCount = 0;
405
+ let unresolved = 0;
406
+ async function processEvent(event) {
407
+ try {
408
+ await mutate(event.paymentId, (record) => {
409
+ if (event.refundId !== null) {
410
+ const refund = record.refunds.find((item) => item.refundId === event.refundId);
411
+ if (!refund || refund.providerId !== null && refund.providerId !== event.objectId)
412
+ throw new PaymentsError2("provider-mismatch");
413
+ if (refund.providerId !== null)
414
+ return false;
415
+ refund.providerId = event.objectId;
416
+ } else {
417
+ if (record.providerId !== null && record.providerId !== event.objectId)
418
+ throw new PaymentsError2("provider-mismatch");
419
+ if (record.providerId !== null)
420
+ return false;
421
+ record.providerId = event.objectId;
422
+ }
423
+ return true;
424
+ });
425
+ if (await run(event.paymentId)) {
426
+ await store.finishEvent(event.eventKey);
427
+ events++;
428
+ return;
429
+ }
430
+ } catch {}
431
+ unresolved++;
432
+ await store.deferEvent(event.eventKey, now() + delay);
433
+ }
434
+ const inbox = await store.inbox(provider.accountId, provider.live, now(), limit);
435
+ for (const event of inbox)
436
+ await processEvent(event);
437
+ const due = await store.due(provider.accountId, provider.live, now(), limit);
438
+ for (const record of due) {
439
+ try {
440
+ if (!await run(record.paymentId))
441
+ unresolved++;
442
+ } catch {
443
+ unresolved++;
444
+ }
445
+ paymentCount++;
446
+ }
447
+ const [nextPayment] = await store.due(provider.accountId, provider.live, Number.MAX_SAFE_INTEGER - 1, 1);
448
+ const [nextEvent] = await store.inbox(provider.accountId, provider.live, Number.MAX_SAFE_INTEGER, 1);
449
+ const next = [nextPayment?.reconcileAt, nextEvent?.reconcileAt].filter((time) => time !== undefined);
450
+ return {
451
+ payments: paymentCount,
452
+ events,
453
+ unresolved,
454
+ nextRunAt: next.length ? Math.min(...next) : null
455
+ };
456
+ }
457
+ }
458
+ };
459
+ }
460
+
461
+ export { createPayments2 };
@@ -0,0 +1,18 @@
1
+ // src/contracts.ts
2
+ class PaymentsError2 extends Error {
3
+ code;
4
+ constructor(code) {
5
+ super(`Payments: ${code}`);
6
+ this.code = code;
7
+ this.name = "PaymentsError";
8
+ }
9
+ }
10
+
11
+ class RefundNotCreatedError2 extends Error {
12
+ constructor() {
13
+ super("Payments: refund-not-created");
14
+ this.name = "RefundNotCreatedError";
15
+ }
16
+ }
17
+
18
+ export { PaymentsError2, RefundNotCreatedError2 };
@@ -0,0 +1,76 @@
1
+ // src/drizzle/sqlite.ts
2
+ import { and, eq, lte } from "drizzle-orm";
3
+ import { index, integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
4
+ var payments = sqliteTable("lenso_payments", {
5
+ paymentId: text("payment_id").primaryKey(),
6
+ orderKey: text("order_key").notNull().unique(),
7
+ accountId: text("account_id").notNull(),
8
+ live: integer("live").notNull(),
9
+ revision: integer("revision").notNull(),
10
+ reconcileAt: integer("reconcile_at").notNull(),
11
+ data: text("data", { mode: "json" }).$type().notNull()
12
+ }, (t) => [index("lenso_payments_due_idx").on(t.accountId, t.live, t.reconcileAt, t.paymentId)]);
13
+ var inboxTable = sqliteTable("lenso_payment_events", {
14
+ eventKey: text("event_key").primaryKey(),
15
+ eventId: text("event_id").notNull(),
16
+ paymentId: text("payment_id").notNull(),
17
+ objectId: text("object_id").notNull(),
18
+ refundId: text("refund_id"),
19
+ accountId: text("account_id").notNull(),
20
+ live: integer("live").notNull(),
21
+ createdAt: integer("created_at").notNull(),
22
+ reconcileAt: integer("reconcile_at").notNull(),
23
+ done: integer("done").notNull()
24
+ }, (t) => [
25
+ index("lenso_payment_events_due_idx").on(t.accountId, t.live, t.done, t.reconcileAt, t.eventKey)
26
+ ]);
27
+ var copy = (value) => structuredClone(value);
28
+ function sqlitePaymentsStore2(db) {
29
+ return {
30
+ async insert(r) {
31
+ return (await db.insert(payments).values({
32
+ paymentId: r.paymentId,
33
+ orderKey: r.orderKey,
34
+ accountId: r.accountId,
35
+ live: +r.live,
36
+ revision: r.revision,
37
+ reconcileAt: r.reconcileAt,
38
+ data: copy(r)
39
+ }).onConflictDoNothing().returning().all()).length > 0;
40
+ },
41
+ async get(id) {
42
+ const [r] = await db.select().from(payments).where(eq(payments.paymentId, id)).limit(1).all();
43
+ return r ? copy(r.data) : null;
44
+ },
45
+ async getByOrder(key) {
46
+ const [r] = await db.select().from(payments).where(eq(payments.orderKey, key)).limit(1).all();
47
+ return r ? copy(r.data) : null;
48
+ },
49
+ async compareAndSet(r, revision) {
50
+ if (r.revision !== revision + 1)
51
+ return false;
52
+ return (await db.update(payments).set({
53
+ revision: r.revision,
54
+ reconcileAt: r.reconcileAt,
55
+ data: copy(r)
56
+ }).where(and(eq(payments.paymentId, r.paymentId), eq(payments.revision, revision), eq(payments.orderKey, r.orderKey), eq(payments.accountId, r.accountId), eq(payments.live, +r.live))).returning().all()).length > 0;
57
+ },
58
+ async due(accountId, live, now, limit) {
59
+ return (await db.select().from(payments).where(and(eq(payments.accountId, accountId), eq(payments.live, +live), lte(payments.reconcileAt, now))).orderBy(payments.reconcileAt, payments.paymentId).limit(Math.max(0, limit)).all()).map((r) => copy(r.data));
60
+ },
61
+ async receive(e) {
62
+ return (await db.insert(inboxTable).values({ ...copy(e), live: +e.live, done: +e.done }).onConflictDoNothing().returning().all()).length > 0;
63
+ },
64
+ async inbox(accountId, live, now, limit) {
65
+ return (await db.select().from(inboxTable).where(and(eq(inboxTable.accountId, accountId), eq(inboxTable.live, +live), eq(inboxTable.done, 0), lte(inboxTable.reconcileAt, now))).orderBy(inboxTable.reconcileAt, inboxTable.eventKey).limit(Math.max(0, limit)).all()).map((r) => ({ ...r, live: !!r.live, done: !!r.done }));
66
+ },
67
+ async finishEvent(eventKey) {
68
+ await db.update(inboxTable).set({ done: 1 }).where(eq(inboxTable.eventKey, eventKey));
69
+ },
70
+ async deferEvent(eventKey, reconcileAt) {
71
+ await db.update(inboxTable).set({ reconcileAt }).where(eq(inboxTable.eventKey, eventKey));
72
+ }
73
+ };
74
+ }
75
+
76
+ export { sqlitePaymentsStore2 };
@@ -0,0 +1,44 @@
1
+ import { type CreatePayment, type CreateRefund, type PaymentResult, type PaymentView, type PaymentsAuthorization, type PaymentsProvider, type PaymentsStore } from "./contracts";
2
+ export * from "./contracts";
3
+ export interface Payments<A> {
4
+ create(input: CreatePayment, actor: A): Promise<PaymentView>;
5
+ get(input: {
6
+ paymentId: string;
7
+ }, actor: A): Promise<PaymentView>;
8
+ refund(input: CreateRefund, actor: A): Promise<PaymentView>;
9
+ results(input: {
10
+ paymentId: string;
11
+ }, actor: A): Promise<readonly PaymentResult[]>;
12
+ /** Sensitive capability for the authenticated payer, excluded from ordinary status results. */
13
+ clientSecret(input: {
14
+ paymentId: string;
15
+ }, actor: A): Promise<string | null>;
16
+ }
17
+ export interface PaymentsRuntime<A> {
18
+ readonly payments: Payments<A>;
19
+ readonly webhook: {
20
+ receive(raw: Uint8Array, signature: string): Promise<{
21
+ accepted: boolean;
22
+ duplicate: boolean;
23
+ }>;
24
+ };
25
+ /** Trusted worker capability, not a user-facing or Manage operation. */
26
+ readonly reconciliation: {
27
+ run(paymentId: string): Promise<boolean>;
28
+ drain(limit?: number): Promise<{
29
+ payments: number;
30
+ events: number;
31
+ unresolved: number;
32
+ nextRunAt: number | null;
33
+ }>;
34
+ };
35
+ }
36
+ export declare function createPayments<A>(options: {
37
+ store: PaymentsStore;
38
+ provider: PaymentsProvider;
39
+ authorize: PaymentsAuthorization<A>;
40
+ clock?: () => number;
41
+ leaseMs?: number;
42
+ reconcileDelayMs?: number;
43
+ maxRefunds?: number;
44
+ }): PaymentsRuntime<A>;
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ import {
2
+ createPayments2
3
+ } from "./index-bzm562dq.js";
4
+ import {
5
+ PaymentsError2,
6
+ RefundNotCreatedError2
7
+ } from "./index-qtpqn1xv.js";
8
+ export {
9
+ PaymentsError2 as PaymentsError,
10
+ RefundNotCreatedError2 as RefundNotCreatedError,
11
+ createPayments2 as createPayments
12
+ };
@@ -0,0 +1,54 @@
1
+ import { type Plugin } from "@lenso/core/plugin";
2
+ import { z } from "zod";
3
+ import type { PaymentsRuntime } from "./index";
4
+ /** Explicit opt-in, read-only. The entry binding supplies an Auth-produced actor, not input JSON. */
5
+ export declare function createPaymentsManage<A>(options: {
6
+ id: string;
7
+ payments: Plugin<PaymentsRuntime<A>>;
8
+ }): {
9
+ plugin: Plugin<{
10
+ status(input: {
11
+ paymentId: string;
12
+ }, invocation: {
13
+ actor: A;
14
+ }): Promise<import("./contracts").PaymentView>;
15
+ }>;
16
+ operations: (import("@lenso/engine/operations").Operation<{
17
+ actor: A;
18
+ }> & {
19
+ readonly plugin: Plugin<{
20
+ status(input: {
21
+ paymentId: string;
22
+ }, invocation: {
23
+ actor: A;
24
+ }): Promise<import("./contracts").PaymentView>;
25
+ }>;
26
+ readonly method: "status";
27
+ readonly input: z.ZodObject<{
28
+ paymentId: z.ZodString;
29
+ }, z.core.$strict>;
30
+ readonly context: true;
31
+ })[];
32
+ manage: import("@lenso/manage").Manage<Plugin<{
33
+ status(input: {
34
+ paymentId: string;
35
+ }, invocation: {
36
+ actor: A;
37
+ }): Promise<import("./contracts").PaymentView>;
38
+ }>, (import("@lenso/engine/operations").Operation<{
39
+ actor: A;
40
+ }> & {
41
+ readonly plugin: Plugin<{
42
+ status(input: {
43
+ paymentId: string;
44
+ }, invocation: {
45
+ actor: A;
46
+ }): Promise<import("./contracts").PaymentView>;
47
+ }>;
48
+ readonly method: "status";
49
+ readonly input: z.ZodObject<{
50
+ paymentId: z.ZodString;
51
+ }, z.core.$strict>;
52
+ readonly context: true;
53
+ })[]>;
54
+ };