@lenso/payments 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +153 -2
- package/dist/auth.d.ts +4 -0
- package/dist/auth.js +9 -0
- package/dist/config.d.ts +13 -0
- package/dist/config.js +17 -0
- package/dist/contracts.d.ts +155 -0
- package/dist/drizzle/d1.d.ts +10 -0
- package/dist/drizzle/d1.js +17 -0
- package/dist/drizzle/pg.d.ts +3 -0
- package/dist/drizzle/pg.js +81 -0
- package/dist/drizzle/sqlite.d.ts +6 -0
- package/dist/drizzle/sqlite.js +6 -0
- package/dist/fetch.d.ts +8 -0
- package/dist/fetch.js +61 -0
- package/dist/index-bzm562dq.js +461 -0
- package/dist/index-qtpqn1xv.js +18 -0
- package/dist/index-w0b3sn1n.js +76 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.js +12 -0
- package/dist/manage.d.ts +54 -0
- package/dist/manage.js +38 -0
- package/dist/plugin.d.ts +11 -0
- package/dist/plugin.js +25 -0
- package/dist/scheduler.d.ts +17 -0
- package/dist/scheduler.js +24 -0
- package/dist/stripe.d.ts +17 -0
- package/dist/stripe.js +308 -0
- package/dist/tasks.d.ts +18 -0
- package/dist/tasks.js +40 -0
- package/migrations/postgres.sql +23 -0
- package/migrations/sqlite.sql +23 -0
- package/package.json +127 -4
package/dist/manage.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// src/manage.ts
|
|
2
|
+
import { definePlugin } from "@lenso/core/plugin";
|
|
3
|
+
import { defineOperation } from "@lenso/engine/operations";
|
|
4
|
+
import { defineManage } from "@lenso/manage";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
function createPaymentsManage(options) {
|
|
7
|
+
const plugin = definePlugin({
|
|
8
|
+
id: options.id,
|
|
9
|
+
requires: [options.payments],
|
|
10
|
+
setup(context) {
|
|
11
|
+
const payments = context.get(options.payments).payments;
|
|
12
|
+
return {
|
|
13
|
+
status(input, invocation) {
|
|
14
|
+
return payments.get(input, invocation.actor);
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
const operations = [
|
|
20
|
+
defineOperation({
|
|
21
|
+
plugin,
|
|
22
|
+
method: "status",
|
|
23
|
+
context: true,
|
|
24
|
+
input: z.object({ paymentId: z.string().min(1).max(256) }).strict(),
|
|
25
|
+
description: "Read an authorized payment and refund status, without payment secrets.",
|
|
26
|
+
effect: "read",
|
|
27
|
+
destructive: false,
|
|
28
|
+
retry: "safe",
|
|
29
|
+
cancellation: "none",
|
|
30
|
+
outputDescription: "Safe payment status; no client secret or provider credentials.",
|
|
31
|
+
source: { file: "packages/payments/src/manage.ts", export: "createPaymentsManage" }
|
|
32
|
+
})
|
|
33
|
+
];
|
|
34
|
+
return { plugin, operations, manage: defineManage({ plugin, operations }) };
|
|
35
|
+
}
|
|
36
|
+
export {
|
|
37
|
+
createPaymentsManage
|
|
38
|
+
};
|
package/dist/plugin.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type Plugin } from "@lenso/core/plugin";
|
|
2
|
+
import { type PaymentsRuntime } from "./index";
|
|
3
|
+
import type { PaymentsAuthorization, PaymentsProvider, PaymentsStore } from "./contracts";
|
|
4
|
+
import type { PaymentsConfigBinding } from "./config";
|
|
5
|
+
export declare function createPaymentsPlugin<A>(options: {
|
|
6
|
+
id: string;
|
|
7
|
+
store: Plugin<PaymentsStore>;
|
|
8
|
+
provider: Plugin<PaymentsProvider>;
|
|
9
|
+
authorization: Plugin<PaymentsAuthorization<A>>;
|
|
10
|
+
config?: PaymentsConfigBinding;
|
|
11
|
+
}): Plugin<PaymentsRuntime<A>>;
|
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createPayments2
|
|
3
|
+
} from "./index-bzm562dq.js";
|
|
4
|
+
|
|
5
|
+
// src/plugin.ts
|
|
6
|
+
import { definePlugin } from "@lenso/core/plugin";
|
|
7
|
+
function createPaymentsPlugin(options) {
|
|
8
|
+
return definePlugin({
|
|
9
|
+
id: options.id,
|
|
10
|
+
requires: [options.store, options.provider, options.authorization],
|
|
11
|
+
config: options.config,
|
|
12
|
+
setup(context) {
|
|
13
|
+
const config = options.config ? context.config(options.config) : undefined;
|
|
14
|
+
return createPayments2({
|
|
15
|
+
store: context.get(options.store),
|
|
16
|
+
provider: context.get(options.provider),
|
|
17
|
+
authorize: context.get(options.authorization),
|
|
18
|
+
...config
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
export {
|
|
24
|
+
createPaymentsPlugin
|
|
25
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Actor } from "@lenso/auth";
|
|
2
|
+
import type { Scheduler, ScheduleRule, ScheduleSummary } from "@lenso/scheduler";
|
|
3
|
+
import type { createPaymentsReconciliationTask } from "./tasks";
|
|
4
|
+
/**
|
|
5
|
+
* Explicit one-time provisioning, not startup work. Persist the returned schedule ID;
|
|
6
|
+
* subsequent management uses Scheduler's authorized get/update/pause/cancel methods.
|
|
7
|
+
*/
|
|
8
|
+
export declare function createPaymentsRecoverySchedule<A extends Actor>(options: {
|
|
9
|
+
scheduler: Pick<Scheduler<A>, "create">;
|
|
10
|
+
/** The exact reconciliation Task registered in both Scheduler and the existing Tasks queue. */
|
|
11
|
+
task: ReturnType<typeof createPaymentsReconciliationTask>;
|
|
12
|
+
rule: Extract<ScheduleRule, {
|
|
13
|
+
kind: "cron";
|
|
14
|
+
}>;
|
|
15
|
+
limit?: number;
|
|
16
|
+
graceMs?: number;
|
|
17
|
+
}, actor: A): Promise<ScheduleSummary>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PaymentsError2
|
|
3
|
+
} from "./index-qtpqn1xv.js";
|
|
4
|
+
|
|
5
|
+
// src/scheduler.ts
|
|
6
|
+
async function createPaymentsRecoverySchedule(options, actor) {
|
|
7
|
+
const limit = options.limit ?? 50;
|
|
8
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200 || options.rule.kind !== "cron")
|
|
9
|
+
throw new PaymentsError2("invalid-input");
|
|
10
|
+
return options.scheduler.create({
|
|
11
|
+
task: options.task.name,
|
|
12
|
+
input: { limit },
|
|
13
|
+
rule: {
|
|
14
|
+
kind: "cron",
|
|
15
|
+
expression: options.rule.expression,
|
|
16
|
+
timezone: options.rule.timezone
|
|
17
|
+
},
|
|
18
|
+
misfire: "coalesce",
|
|
19
|
+
graceMs: options.graceMs ?? 5000
|
|
20
|
+
}, actor);
|
|
21
|
+
}
|
|
22
|
+
export {
|
|
23
|
+
createPaymentsRecoverySchedule
|
|
24
|
+
};
|
package/dist/stripe.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import Stripe from "stripe";
|
|
2
|
+
import { type PaymentsProvider } from "./contracts";
|
|
3
|
+
export interface StripeProviderOptions {
|
|
4
|
+
/** Borrowed, single-account SDK client. Its owner retains its lifetime. */
|
|
5
|
+
client: Stripe;
|
|
6
|
+
accountId: string;
|
|
7
|
+
live: boolean;
|
|
8
|
+
webhookSecret: string;
|
|
9
|
+
currencies: Record<string, {
|
|
10
|
+
min: number;
|
|
11
|
+
max: number;
|
|
12
|
+
multiple?: number;
|
|
13
|
+
}>;
|
|
14
|
+
toleranceSeconds?: number;
|
|
15
|
+
requestTimeoutMs?: number;
|
|
16
|
+
}
|
|
17
|
+
export declare function createStripeProvider(options: StripeProviderOptions): PaymentsProvider;
|
package/dist/stripe.js
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PaymentsError2,
|
|
3
|
+
RefundNotCreatedError2
|
|
4
|
+
} from "./index-qtpqn1xv.js";
|
|
5
|
+
|
|
6
|
+
// src/stripe.ts
|
|
7
|
+
import Stripe from "stripe";
|
|
8
|
+
var paymentStatuses = new Set([
|
|
9
|
+
"requires_payment_method",
|
|
10
|
+
"requires_confirmation",
|
|
11
|
+
"requires_action",
|
|
12
|
+
"processing",
|
|
13
|
+
"requires_capture",
|
|
14
|
+
"succeeded",
|
|
15
|
+
"canceled"
|
|
16
|
+
]);
|
|
17
|
+
var refundStatuses = new Set(["pending", "requires_action", "succeeded", "failed", "canceled"]);
|
|
18
|
+
var positiveInteger = (value) => Number.isSafeInteger(value) && value > 0;
|
|
19
|
+
var identity = (value) => typeof value === "string" && value.length > 0 && value.length <= 256;
|
|
20
|
+
var mismatch = () => new PaymentsError2("provider-mismatch");
|
|
21
|
+
function createStripeProvider(options) {
|
|
22
|
+
const { client, accountId, live, webhookSecret } = options;
|
|
23
|
+
const tolerance = options.toleranceSeconds ?? 300;
|
|
24
|
+
const timeout = options.requestTimeoutMs ?? 1e4;
|
|
25
|
+
if (!identity(accountId) || typeof live !== "boolean" || !webhookSecret || !positiveInteger(tolerance) || !positiveInteger(timeout) || timeout > 60000) {
|
|
26
|
+
throw new PaymentsError2("invalid-input");
|
|
27
|
+
}
|
|
28
|
+
const currencies = new Map(Object.entries(options.currencies).map(([currency, rule]) => {
|
|
29
|
+
if (!/^[a-z]{3}$/.test(currency) || !positiveInteger(rule.min) || !positiveInteger(rule.max) || rule.min > rule.max || rule.multiple !== undefined && !positiveInteger(rule.multiple)) {
|
|
30
|
+
throw new PaymentsError2("invalid-input");
|
|
31
|
+
}
|
|
32
|
+
return [currency, { ...rule }];
|
|
33
|
+
}));
|
|
34
|
+
const requestOptions = { maxNetworkRetries: 0, timeout };
|
|
35
|
+
function validate(amount, currency, charge) {
|
|
36
|
+
const rule = currencies.get(currency);
|
|
37
|
+
if (!rule || !positiveInteger(amount) || amount > rule.max || charge && amount < rule.min || amount % (rule.multiple ?? 1) !== 0 || (currency === "isk" || currency === "ugx") && amount % 100 !== 0) {
|
|
38
|
+
throw new PaymentsError2("invalid-input");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async function request(operation) {
|
|
42
|
+
try {
|
|
43
|
+
return await operation();
|
|
44
|
+
} catch (error) {
|
|
45
|
+
if (error instanceof PaymentsError2)
|
|
46
|
+
throw error;
|
|
47
|
+
throw new PaymentsError2("unavailable");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function checkClient() {
|
|
51
|
+
if (client.getApiField("stripeAccount") || client.getApiField("stripeContext")) {
|
|
52
|
+
throw mismatch();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
let accountCheck;
|
|
56
|
+
async function checkAccount() {
|
|
57
|
+
checkClient();
|
|
58
|
+
accountCheck ??= request(async () => {
|
|
59
|
+
const account = await client.accounts.retrieve(null, {}, requestOptions);
|
|
60
|
+
if (account.id !== accountId)
|
|
61
|
+
throw mismatch();
|
|
62
|
+
}).catch((error) => {
|
|
63
|
+
accountCheck = undefined;
|
|
64
|
+
throw error;
|
|
65
|
+
});
|
|
66
|
+
await accountCheck;
|
|
67
|
+
}
|
|
68
|
+
function projectPayment(pi) {
|
|
69
|
+
const metadata = pi.metadata;
|
|
70
|
+
if (pi.object !== "payment_intent" || !identity(pi.id) || pi.livemode !== live || !metadata || !identity(metadata.lenso_payment_id) || !identity(metadata.lenso_tenant_id) || !identity(metadata.lenso_order_id) || !paymentStatuses.has(pi.status) || !Number.isSafeInteger(pi.amount_received) || pi.amount_received < 0 || pi.amount_received > pi.amount)
|
|
71
|
+
throw mismatch();
|
|
72
|
+
try {
|
|
73
|
+
validate(pi.amount, pi.currency, true);
|
|
74
|
+
} catch {
|
|
75
|
+
throw mismatch();
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
id: pi.id,
|
|
79
|
+
paymentId: metadata.lenso_payment_id,
|
|
80
|
+
tenantId: metadata.lenso_tenant_id,
|
|
81
|
+
orderId: metadata.lenso_order_id,
|
|
82
|
+
accountId,
|
|
83
|
+
live,
|
|
84
|
+
amount: pi.amount,
|
|
85
|
+
received: pi.amount_received,
|
|
86
|
+
currency: pi.currency,
|
|
87
|
+
status: pi.status
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function matchPayment(payment, record) {
|
|
91
|
+
if (record.accountId !== accountId || record.live !== live || payment.paymentId !== record.paymentId || payment.tenantId !== record.tenantId || payment.orderId !== record.orderId || payment.amount !== record.amount || payment.currency !== record.currency || record.providerId !== null && payment.id !== record.providerId)
|
|
92
|
+
throw mismatch();
|
|
93
|
+
}
|
|
94
|
+
async function retrievePayment(id) {
|
|
95
|
+
if (!identity(id))
|
|
96
|
+
throw new PaymentsError2("invalid-input");
|
|
97
|
+
await checkAccount();
|
|
98
|
+
const pi = await request(() => client.paymentIntents.retrieve(id, {}, requestOptions));
|
|
99
|
+
if (pi.id !== id)
|
|
100
|
+
throw mismatch();
|
|
101
|
+
projectPayment(pi);
|
|
102
|
+
return pi;
|
|
103
|
+
}
|
|
104
|
+
async function projectRefund(refund) {
|
|
105
|
+
const piId = typeof refund.payment_intent === "string" ? refund.payment_intent : refund.payment_intent?.id;
|
|
106
|
+
if (refund.object !== "refund" || !identity(refund.id) || !identity(piId) || !refund.metadata || !identity(refund.metadata.lenso_refund_id) || !identity(refund.metadata.lenso_payment_id) || !refundStatuses.has(refund.status ?? "")) {
|
|
107
|
+
throw mismatch();
|
|
108
|
+
}
|
|
109
|
+
const payment = projectPayment(await retrievePayment(piId));
|
|
110
|
+
if (refund.metadata.lenso_payment_id !== payment.paymentId || refund.currency !== payment.currency || refund.amount > payment.amount)
|
|
111
|
+
throw mismatch();
|
|
112
|
+
try {
|
|
113
|
+
validate(refund.amount, refund.currency, false);
|
|
114
|
+
} catch {
|
|
115
|
+
throw mismatch();
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
id: refund.id,
|
|
119
|
+
refundId: refund.metadata.lenso_refund_id,
|
|
120
|
+
paymentId: payment.paymentId,
|
|
121
|
+
paymentProviderId: payment.id,
|
|
122
|
+
accountId,
|
|
123
|
+
live: payment.live,
|
|
124
|
+
amount: refund.amount,
|
|
125
|
+
currency: refund.currency,
|
|
126
|
+
status: refund.status
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
function matchRefund(value, record, refund) {
|
|
130
|
+
if (value.refundId !== refund.refundId || value.paymentId !== record.paymentId || value.paymentProviderId !== record.providerId || value.amount !== refund.amount || value.currency !== record.currency || refund.providerId !== null && value.id !== refund.providerId)
|
|
131
|
+
throw mismatch();
|
|
132
|
+
}
|
|
133
|
+
function writeOptions(key) {
|
|
134
|
+
if (typeof key !== "string" || !key.length || key.length > 255) {
|
|
135
|
+
throw new PaymentsError2("invalid-input");
|
|
136
|
+
}
|
|
137
|
+
return { ...requestOptions, idempotencyKey: key };
|
|
138
|
+
}
|
|
139
|
+
async function scan(page, matches) {
|
|
140
|
+
let cursor;
|
|
141
|
+
let found = null;
|
|
142
|
+
for (let count = 0;count < 100; count++) {
|
|
143
|
+
const result = await request(() => page(cursor));
|
|
144
|
+
for (const value of result.data) {
|
|
145
|
+
if (matches(value)) {
|
|
146
|
+
if (found)
|
|
147
|
+
throw mismatch();
|
|
148
|
+
found = value;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (!result.has_more)
|
|
152
|
+
return found;
|
|
153
|
+
const next = result.data.at(-1)?.id;
|
|
154
|
+
if (!next || next === cursor)
|
|
155
|
+
throw new PaymentsError2("unavailable");
|
|
156
|
+
cursor = next;
|
|
157
|
+
}
|
|
158
|
+
throw new PaymentsError2("unavailable");
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
accountId,
|
|
162
|
+
live,
|
|
163
|
+
replayWindowMs: 23 * 60 * 60 * 1000,
|
|
164
|
+
validateAmount: (amount, currency) => validate(amount, currency, true),
|
|
165
|
+
validateRefundAmount: (amount, currency) => validate(amount, currency, false),
|
|
166
|
+
async createPayment(record, key, beforeWrite) {
|
|
167
|
+
validate(record.amount, record.currency, true);
|
|
168
|
+
if (record.accountId !== accountId || record.live !== live || !identity(record.paymentId) || !identity(record.tenantId) || !identity(record.orderId)) {
|
|
169
|
+
throw new PaymentsError2("invalid-input");
|
|
170
|
+
}
|
|
171
|
+
const opts = writeOptions(key);
|
|
172
|
+
await checkAccount();
|
|
173
|
+
await beforeWrite?.();
|
|
174
|
+
const payment = projectPayment(await request(() => client.paymentIntents.create({
|
|
175
|
+
amount: record.amount,
|
|
176
|
+
currency: record.currency,
|
|
177
|
+
capture_method: "automatic",
|
|
178
|
+
automatic_payment_methods: { enabled: true },
|
|
179
|
+
metadata: {
|
|
180
|
+
lenso_payment_id: record.paymentId,
|
|
181
|
+
lenso_tenant_id: record.tenantId,
|
|
182
|
+
lenso_order_id: record.orderId
|
|
183
|
+
}
|
|
184
|
+
}, opts)));
|
|
185
|
+
matchPayment(payment, record);
|
|
186
|
+
return payment;
|
|
187
|
+
},
|
|
188
|
+
async getPayment(id) {
|
|
189
|
+
return projectPayment(await retrievePayment(id));
|
|
190
|
+
},
|
|
191
|
+
async findPayment(record) {
|
|
192
|
+
if (record.accountId !== accountId || record.live !== live)
|
|
193
|
+
throw mismatch();
|
|
194
|
+
const since = record.attemptedAt ?? record.createdAt;
|
|
195
|
+
if (!Number.isSafeInteger(since) || since < 0)
|
|
196
|
+
throw new PaymentsError2("invalid-input");
|
|
197
|
+
await checkAccount();
|
|
198
|
+
const pi = await scan((cursor) => client.paymentIntents.list({
|
|
199
|
+
limit: 100,
|
|
200
|
+
created: { gte: Math.max(0, Math.floor(since / 1000) - tolerance) },
|
|
201
|
+
...cursor ? { starting_after: cursor } : {}
|
|
202
|
+
}, requestOptions), (value) => value.metadata?.lenso_payment_id === record.paymentId);
|
|
203
|
+
if (!pi)
|
|
204
|
+
return null;
|
|
205
|
+
const payment = projectPayment(pi);
|
|
206
|
+
matchPayment(payment, record);
|
|
207
|
+
return payment;
|
|
208
|
+
},
|
|
209
|
+
async clientSecret(id) {
|
|
210
|
+
const pi = await retrievePayment(id);
|
|
211
|
+
if (pi.client_secret !== null && typeof pi.client_secret !== "string")
|
|
212
|
+
throw mismatch();
|
|
213
|
+
return pi.client_secret;
|
|
214
|
+
},
|
|
215
|
+
async createRefund(record, refund, key, beforeWrite) {
|
|
216
|
+
validate(refund.amount, record.currency, false);
|
|
217
|
+
if (!identity(refund.refundId) || !identity(record.providerId) || refund.amount > record.amount) {
|
|
218
|
+
throw new PaymentsError2("invalid-input");
|
|
219
|
+
}
|
|
220
|
+
const opts = writeOptions(key);
|
|
221
|
+
const payment = projectPayment(await retrievePayment(record.providerId));
|
|
222
|
+
matchPayment(payment, record);
|
|
223
|
+
await beforeWrite?.();
|
|
224
|
+
let created;
|
|
225
|
+
try {
|
|
226
|
+
created = await client.refunds.create({
|
|
227
|
+
payment_intent: record.providerId,
|
|
228
|
+
amount: refund.amount,
|
|
229
|
+
metadata: { lenso_refund_id: refund.refundId, lenso_payment_id: record.paymentId }
|
|
230
|
+
}, opts);
|
|
231
|
+
} catch (error) {
|
|
232
|
+
if (error instanceof Stripe.errors.StripeInvalidRequestError && error.statusCode === 400 && ["charge_already_refunded", "charge_disputed", "refund_disputed_payment"].includes(error.code ?? "")) {
|
|
233
|
+
throw new RefundNotCreatedError2;
|
|
234
|
+
}
|
|
235
|
+
throw new PaymentsError2("unavailable");
|
|
236
|
+
}
|
|
237
|
+
const value = await projectRefund(created);
|
|
238
|
+
matchRefund(value, record, refund);
|
|
239
|
+
return value;
|
|
240
|
+
},
|
|
241
|
+
async getRefund(id) {
|
|
242
|
+
if (!identity(id))
|
|
243
|
+
throw new PaymentsError2("invalid-input");
|
|
244
|
+
await checkAccount();
|
|
245
|
+
const refund = await request(() => client.refunds.retrieve(id, {}, requestOptions));
|
|
246
|
+
if (refund.id !== id)
|
|
247
|
+
throw mismatch();
|
|
248
|
+
return projectRefund(refund);
|
|
249
|
+
},
|
|
250
|
+
async findRefund(record, refund) {
|
|
251
|
+
if (!identity(record.providerId))
|
|
252
|
+
throw new PaymentsError2("invalid-input");
|
|
253
|
+
const payment = projectPayment(await retrievePayment(record.providerId));
|
|
254
|
+
matchPayment(payment, record);
|
|
255
|
+
const value = await scan((cursor) => client.refunds.list({
|
|
256
|
+
payment_intent: record.providerId,
|
|
257
|
+
limit: 100,
|
|
258
|
+
...cursor ? { starting_after: cursor } : {}
|
|
259
|
+
}, requestOptions), (item) => item.metadata?.lenso_refund_id === refund.refundId);
|
|
260
|
+
if (!value)
|
|
261
|
+
return null;
|
|
262
|
+
const result = await projectRefund(value);
|
|
263
|
+
matchRefund(result, record, refund);
|
|
264
|
+
return result;
|
|
265
|
+
},
|
|
266
|
+
async verifyWebhook(raw, signature) {
|
|
267
|
+
let event;
|
|
268
|
+
try {
|
|
269
|
+
event = await client.webhooks.constructEventAsync(raw, signature, webhookSecret, tolerance, Stripe.createSubtleCryptoProvider());
|
|
270
|
+
let timestamp = NaN;
|
|
271
|
+
for (const part of signature.split(",")) {
|
|
272
|
+
const [name, value] = part.split("=");
|
|
273
|
+
if (name === "t")
|
|
274
|
+
timestamp = Number(value);
|
|
275
|
+
}
|
|
276
|
+
if (!Number.isSafeInteger(timestamp) || timestamp > Math.floor(Date.now() / 1000) + tolerance) {
|
|
277
|
+
throw new Error;
|
|
278
|
+
}
|
|
279
|
+
} catch {
|
|
280
|
+
throw new PaymentsError2("bad-signature");
|
|
281
|
+
}
|
|
282
|
+
if (!identity(event.id) || event.livemode !== live || event.account !== undefined || event.context !== undefined) {
|
|
283
|
+
throw mismatch();
|
|
284
|
+
}
|
|
285
|
+
if (event.type.startsWith("payment_intent.")) {
|
|
286
|
+
await checkAccount();
|
|
287
|
+
return {
|
|
288
|
+
eventId: event.id,
|
|
289
|
+
accountId,
|
|
290
|
+
live,
|
|
291
|
+
object: projectPayment(event.data.object)
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
if (event.type === "refund.created" || event.type === "refund.updated" || event.type === "refund.failed") {
|
|
295
|
+
return {
|
|
296
|
+
eventId: event.id,
|
|
297
|
+
accountId,
|
|
298
|
+
live,
|
|
299
|
+
object: await projectRefund(event.data.object)
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
export {
|
|
307
|
+
createStripeProvider
|
|
308
|
+
};
|
package/dist/tasks.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type Task, type TaskQueue } from "@lenso/tasks";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import type { PaymentsRuntime } from "./index";
|
|
4
|
+
/** Register this exact Task object in the existing queue; credentials never enter the payload. */
|
|
5
|
+
export declare function createPaymentsReconciliationTask(options: {
|
|
6
|
+
name: string;
|
|
7
|
+
runtime: () => PaymentsRuntime<unknown>["reconciliation"];
|
|
8
|
+
queue: () => Pick<TaskQueue, "enqueue">;
|
|
9
|
+
/** Disable when Scheduler owns periodic recovery; avoids accumulating parallel polling chains. */
|
|
10
|
+
continuation?: boolean;
|
|
11
|
+
}): Task<z.ZodObject<{
|
|
12
|
+
limit: z.ZodDefault<z.ZodNumber>;
|
|
13
|
+
}, z.core.$strict>, {
|
|
14
|
+
payments: number;
|
|
15
|
+
events: number;
|
|
16
|
+
unresolved: number;
|
|
17
|
+
nextRunAt: number | null;
|
|
18
|
+
}>;
|
package/dist/tasks.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PaymentsError2
|
|
3
|
+
} from "./index-qtpqn1xv.js";
|
|
4
|
+
|
|
5
|
+
// src/tasks.ts
|
|
6
|
+
import { defineTask } from "@lenso/tasks";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
function createPaymentsReconciliationTask(options) {
|
|
9
|
+
if (options.continuation !== undefined && typeof options.continuation !== "boolean")
|
|
10
|
+
throw new PaymentsError2("invalid-input");
|
|
11
|
+
const input = z.object({ limit: z.number().int().min(1).max(200).default(50) }).strict();
|
|
12
|
+
const task = defineTask({
|
|
13
|
+
name: options.name,
|
|
14
|
+
input,
|
|
15
|
+
maxAttempts: 5,
|
|
16
|
+
retry: { delaySeconds: 30, backoff: true, maxDelaySeconds: 300 },
|
|
17
|
+
async handler({ limit }, context) {
|
|
18
|
+
context.signal.throwIfAborted();
|
|
19
|
+
const result = await options.runtime().drain(limit);
|
|
20
|
+
context.signal.throwIfAborted();
|
|
21
|
+
if (options.continuation !== false && result.nextRunAt !== null) {
|
|
22
|
+
await options.queue().enqueue(task, { limit }, {
|
|
23
|
+
runAt: new Date(Math.max(result.nextRunAt, Date.now() + 1000)),
|
|
24
|
+
deduplicationKey: `payments.continue:${context.jobId}:${result.nextRunAt}`
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
return result;
|
|
28
|
+
},
|
|
29
|
+
result: (value) => ({
|
|
30
|
+
payments: value.payments,
|
|
31
|
+
events: value.events,
|
|
32
|
+
unresolved: value.unresolved,
|
|
33
|
+
nextRunAt: value.nextRunAt
|
|
34
|
+
})
|
|
35
|
+
});
|
|
36
|
+
return task;
|
|
37
|
+
}
|
|
38
|
+
export {
|
|
39
|
+
createPaymentsReconciliationTask
|
|
40
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
CREATE TABLE lenso_payments (
|
|
2
|
+
payment_id text PRIMARY KEY,
|
|
3
|
+
order_key text NOT NULL UNIQUE,
|
|
4
|
+
account_id text NOT NULL,
|
|
5
|
+
live integer NOT NULL CHECK (live IN (0, 1)),
|
|
6
|
+
revision integer NOT NULL CHECK (revision >= 0),
|
|
7
|
+
reconcile_at bigint NOT NULL,
|
|
8
|
+
data jsonb NOT NULL
|
|
9
|
+
);
|
|
10
|
+
CREATE INDEX lenso_payments_due_idx ON lenso_payments (account_id, live, reconcile_at, payment_id);
|
|
11
|
+
CREATE TABLE lenso_payment_events (
|
|
12
|
+
event_key text PRIMARY KEY,
|
|
13
|
+
event_id text NOT NULL,
|
|
14
|
+
payment_id text NOT NULL,
|
|
15
|
+
object_id text NOT NULL,
|
|
16
|
+
refund_id text,
|
|
17
|
+
account_id text NOT NULL,
|
|
18
|
+
live integer NOT NULL CHECK (live IN (0, 1)),
|
|
19
|
+
created_at bigint NOT NULL,
|
|
20
|
+
reconcile_at bigint NOT NULL,
|
|
21
|
+
done integer NOT NULL CHECK (done IN (0, 1))
|
|
22
|
+
);
|
|
23
|
+
CREATE INDEX lenso_payment_events_due_idx ON lenso_payment_events (account_id, live, done, reconcile_at, event_key);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
CREATE TABLE lenso_payments (
|
|
2
|
+
payment_id TEXT PRIMARY KEY,
|
|
3
|
+
order_key TEXT NOT NULL UNIQUE,
|
|
4
|
+
account_id TEXT NOT NULL,
|
|
5
|
+
live INTEGER NOT NULL CHECK (live IN (0, 1)),
|
|
6
|
+
revision INTEGER NOT NULL CHECK (revision >= 0),
|
|
7
|
+
reconcile_at INTEGER NOT NULL,
|
|
8
|
+
data TEXT NOT NULL CHECK (json_valid(data))
|
|
9
|
+
);
|
|
10
|
+
CREATE INDEX lenso_payments_due_idx ON lenso_payments (account_id, live, reconcile_at, payment_id);
|
|
11
|
+
CREATE TABLE lenso_payment_events (
|
|
12
|
+
event_key TEXT PRIMARY KEY,
|
|
13
|
+
event_id TEXT NOT NULL,
|
|
14
|
+
payment_id TEXT NOT NULL,
|
|
15
|
+
object_id TEXT NOT NULL,
|
|
16
|
+
refund_id TEXT,
|
|
17
|
+
account_id TEXT NOT NULL,
|
|
18
|
+
live INTEGER NOT NULL CHECK (live IN (0, 1)),
|
|
19
|
+
created_at INTEGER NOT NULL,
|
|
20
|
+
reconcile_at INTEGER NOT NULL,
|
|
21
|
+
done INTEGER NOT NULL CHECK (done IN (0, 1))
|
|
22
|
+
);
|
|
23
|
+
CREATE INDEX lenso_payment_events_due_idx ON lenso_payment_events (account_id, live, done, reconcile_at, event_key);
|