@siglume/direct-request-payment 0.4.25 → 0.4.26
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/CHANGELOG.md +12 -0
- package/README.md +5 -3
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/docs/pricing.md +1 -1
- package/docs/quickstart-10-minutes.md +62 -5
- package/examples/hosted-checkout-python/pyproject.toml +1 -1
- package/package.json +6 -1
- package/templates/express/README.md +13 -5
- package/templates/express/siglume-order-store.dynamodb.ts +691 -0
- package/templates/express/siglume-order-store.firestore.ts +418 -0
- package/templates/express/siglume-order-store.mongodb.ts +431 -0
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import type { Collection, Db, Document, MongoServerError, WithId } from "mongodb";
|
|
3
|
+
import type { Request } from "express";
|
|
4
|
+
|
|
5
|
+
import type { SiglumeCheckoutAttempt, SiglumeSdrpOrderStore } from "./siglume-sdrp-routes.js";
|
|
6
|
+
|
|
7
|
+
export interface MongoSiglumeOrderStoreOptions {
|
|
8
|
+
db: Db;
|
|
9
|
+
orders_collection?: string;
|
|
10
|
+
order_id_field?: string;
|
|
11
|
+
amount_minor_field?: string;
|
|
12
|
+
currency_field?: string;
|
|
13
|
+
order_status_field?: string | null;
|
|
14
|
+
order_updated_at_field?: string | null;
|
|
15
|
+
checkout_attempts_collection?: string;
|
|
16
|
+
webhook_events_collection?: string;
|
|
17
|
+
payment_reviews_collection?: string;
|
|
18
|
+
authorize_order?: (order: Record<string, unknown>, req: Request) => boolean | Promise<boolean>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface NormalizedOptions {
|
|
22
|
+
db: Db;
|
|
23
|
+
orders_collection: string;
|
|
24
|
+
order_id_field: string;
|
|
25
|
+
amount_minor_field: string;
|
|
26
|
+
currency_field: string;
|
|
27
|
+
order_status_field: string | null;
|
|
28
|
+
order_updated_at_field: string | null;
|
|
29
|
+
checkout_attempts_collection: string;
|
|
30
|
+
webhook_events_collection: string;
|
|
31
|
+
payment_reviews_collection: string;
|
|
32
|
+
authorize_order?: (order: Record<string, unknown>, req: Request) => boolean | Promise<boolean>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const CHECKOUT_CREATION_LEASE_MS = 30_000;
|
|
36
|
+
const CHECKOUT_CREATION_WAIT_MS = 10_000;
|
|
37
|
+
const CHECKOUT_CREATION_POLL_MS = 100;
|
|
38
|
+
const WEBHOOK_PROCESSING_STALE_MS = 10 * 60 * 1000;
|
|
39
|
+
|
|
40
|
+
export function createMongoSiglumeOrderStore(options: MongoSiglumeOrderStoreOptions): SiglumeSdrpOrderStore {
|
|
41
|
+
return new MongoSiglumeOrderStore(normalizeOptions(options));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function createMongoSiglumeIndexes(options: MongoSiglumeOrderStoreOptions): Promise<void> {
|
|
45
|
+
const normalized = normalizeOptions(options);
|
|
46
|
+
const attempts = normalized.db.collection(normalized.checkout_attempts_collection);
|
|
47
|
+
const events = normalized.db.collection(normalized.webhook_events_collection);
|
|
48
|
+
const reviews = normalized.db.collection(normalized.payment_reviews_collection);
|
|
49
|
+
await attempts.createIndex({ active_key: 1 }, { unique: true, sparse: true });
|
|
50
|
+
await attempts.createIndex({ challenge_hash: 1 }, { unique: true, sparse: true });
|
|
51
|
+
await attempts.createIndex({ order_id: 1, attempt_number: 1 }, { unique: true });
|
|
52
|
+
await events.createIndex({ event_id: 1 }, { unique: true });
|
|
53
|
+
await reviews.createIndex({ order_id: 1 });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
class MongoSiglumeOrderStore implements SiglumeSdrpOrderStore {
|
|
57
|
+
constructor(private readonly options: NormalizedOptions) {}
|
|
58
|
+
|
|
59
|
+
async beginCheckoutAttempt(orderId: string, req: Request): Promise<SiglumeCheckoutAttempt | null> {
|
|
60
|
+
const cleanOrderId = requireText(orderId, "order_id");
|
|
61
|
+
const waitUntil = Date.now() + CHECKOUT_CREATION_WAIT_MS;
|
|
62
|
+
for (;;) {
|
|
63
|
+
const order = await this.findProductOrder(cleanOrderId);
|
|
64
|
+
if (!order) return null;
|
|
65
|
+
if (this.options.authorize_order && !(await this.options.authorize_order(order, req))) return null;
|
|
66
|
+
|
|
67
|
+
const active = await this.attempts().findOne({ active_key: cleanOrderId });
|
|
68
|
+
if (active && isReusableCheckoutAttempt(active)) return this.toCheckoutAttempt(order, active);
|
|
69
|
+
if (active && isCreatingCheckoutAttempt(active) && !timestampHasPassed(active.creation_lease_expires_at)) {
|
|
70
|
+
if (Date.now() >= waitUntil) return this.toCheckoutAttempt(order, active, true);
|
|
71
|
+
await sleep(CHECKOUT_CREATION_POLL_MS);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (active) await this.releaseInactiveAttempt(active, active.status === "pending" ? "expired" : "failed");
|
|
75
|
+
|
|
76
|
+
const attemptNumber = active ? Number(active.attempt_number || 0) + 1 : await this.nextAttemptNumber(cleanOrderId);
|
|
77
|
+
const attempt = stableAttempt(cleanOrderId, attemptNumber);
|
|
78
|
+
try {
|
|
79
|
+
await this.attempts().insertOne({
|
|
80
|
+
order_id: cleanOrderId,
|
|
81
|
+
attempt_number: attemptNumber,
|
|
82
|
+
attempt_id: attempt.attempt_id,
|
|
83
|
+
stable_nonce: attempt.stable_nonce,
|
|
84
|
+
active_key: cleanOrderId,
|
|
85
|
+
status: "creating",
|
|
86
|
+
creation_owner_id: `sdrp_create_${randomUUID()}`,
|
|
87
|
+
creation_lease_expires_at: timestamp(Date.now() + CHECKOUT_CREATION_LEASE_MS),
|
|
88
|
+
created_at: timestamp(),
|
|
89
|
+
updated_at: timestamp(),
|
|
90
|
+
});
|
|
91
|
+
return {
|
|
92
|
+
id: String(order.id),
|
|
93
|
+
order_id: String(order.id),
|
|
94
|
+
amount_minor: Number(order.amount_minor),
|
|
95
|
+
currency: String(order.currency),
|
|
96
|
+
attempt_number: attemptNumber,
|
|
97
|
+
attempt_id: attempt.attempt_id,
|
|
98
|
+
stable_nonce: attempt.stable_nonce,
|
|
99
|
+
status: "creating",
|
|
100
|
+
};
|
|
101
|
+
} catch (error) {
|
|
102
|
+
if (isDuplicateKey(error)) {
|
|
103
|
+
if (Date.now() >= waitUntil) return this.toCheckoutAttempt(order, {
|
|
104
|
+
order_id: cleanOrderId,
|
|
105
|
+
attempt_number: attemptNumber,
|
|
106
|
+
attempt_id: attempt.attempt_id,
|
|
107
|
+
stable_nonce: attempt.stable_nonce,
|
|
108
|
+
status: "creating",
|
|
109
|
+
}, true);
|
|
110
|
+
await sleep(CHECKOUT_CREATION_POLL_MS);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async markCheckoutPending(input: {
|
|
119
|
+
order_id: string;
|
|
120
|
+
attempt_id: string;
|
|
121
|
+
stable_nonce: string;
|
|
122
|
+
challenge_hash: string;
|
|
123
|
+
checkout_session_id: string;
|
|
124
|
+
checkout_url: string;
|
|
125
|
+
expires_at?: string | null;
|
|
126
|
+
}): Promise<void> {
|
|
127
|
+
await this.attempts().updateOne(
|
|
128
|
+
{ order_id: input.order_id, attempt_id: input.attempt_id, status: "creating" },
|
|
129
|
+
{
|
|
130
|
+
$set: {
|
|
131
|
+
status: "pending",
|
|
132
|
+
stable_nonce: input.stable_nonce,
|
|
133
|
+
challenge_hash: input.challenge_hash,
|
|
134
|
+
checkout_session_id: input.checkout_session_id,
|
|
135
|
+
checkout_url: input.checkout_url,
|
|
136
|
+
expires_at: timestampOrNull(input.expires_at),
|
|
137
|
+
updated_at: timestamp(),
|
|
138
|
+
},
|
|
139
|
+
$unset: {
|
|
140
|
+
creation_owner_id: "",
|
|
141
|
+
creation_lease_expires_at: "",
|
|
142
|
+
error_message: "",
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async markCheckoutFailed(input: {
|
|
149
|
+
order_id: string;
|
|
150
|
+
attempt_id: string;
|
|
151
|
+
error_message?: string;
|
|
152
|
+
}): Promise<void> {
|
|
153
|
+
await this.attempts().updateOne(
|
|
154
|
+
{ order_id: input.order_id, attempt_id: input.attempt_id, status: "creating" },
|
|
155
|
+
{
|
|
156
|
+
$set: {
|
|
157
|
+
status: "failed",
|
|
158
|
+
failed_at: timestamp(),
|
|
159
|
+
error_message: textOrNull(input.error_message),
|
|
160
|
+
updated_at: timestamp(),
|
|
161
|
+
},
|
|
162
|
+
$unset: {
|
|
163
|
+
active_key: "",
|
|
164
|
+
creation_owner_id: "",
|
|
165
|
+
creation_lease_expires_at: "",
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async processWebhookEventOnce(eventId: string, handler: () => Promise<void>): Promise<"processed" | "duplicate"> {
|
|
172
|
+
const cleanEventId = requireText(eventId, "event_id");
|
|
173
|
+
const existing = await this.events().findOne({ event_id: cleanEventId });
|
|
174
|
+
if (existing?.status === "processed") return "duplicate";
|
|
175
|
+
if (existing?.status === "processing" && !webhookProcessingIsStale(existing.created_at)) return "duplicate";
|
|
176
|
+
|
|
177
|
+
if (existing) {
|
|
178
|
+
await this.events().updateOne(
|
|
179
|
+
{ event_id: cleanEventId },
|
|
180
|
+
{ $set: { status: "processing", error_message: null, processed_at: null, created_at: timestamp() } },
|
|
181
|
+
);
|
|
182
|
+
} else {
|
|
183
|
+
try {
|
|
184
|
+
await this.events().insertOne({ event_id: cleanEventId, status: "processing", created_at: timestamp(), processed_at: null });
|
|
185
|
+
} catch (error) {
|
|
186
|
+
if (isDuplicateKey(error)) return "duplicate";
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
await handler();
|
|
193
|
+
await this.events().updateOne(
|
|
194
|
+
{ event_id: cleanEventId },
|
|
195
|
+
{ $set: { status: "processed", error_message: null, processed_at: timestamp() } },
|
|
196
|
+
);
|
|
197
|
+
return "processed";
|
|
198
|
+
} catch (error) {
|
|
199
|
+
await this.events().updateOne(
|
|
200
|
+
{ event_id: cleanEventId },
|
|
201
|
+
{ $set: { status: "failed", error_message: errorMessage(error), processed_at: null } },
|
|
202
|
+
);
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async findOrderByChallengeHash(challengeHash: string): Promise<{ id: string } | null> {
|
|
208
|
+
const row = await this.attempts().findOne({ challenge_hash: challengeHash });
|
|
209
|
+
return row?.order_id ? { id: String(row.order_id) } : null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async markOrderPaidOnce(input: {
|
|
213
|
+
order_id: string;
|
|
214
|
+
requirement_id: string;
|
|
215
|
+
chain_receipt_id: string;
|
|
216
|
+
}): Promise<void> {
|
|
217
|
+
const changed = await this.attempts().updateOne(
|
|
218
|
+
{ order_id: input.order_id, status: { $nin: ["paid", "expired", "cancelled", "failed"] } },
|
|
219
|
+
{
|
|
220
|
+
$set: {
|
|
221
|
+
status: "paid",
|
|
222
|
+
requirement_id: input.requirement_id,
|
|
223
|
+
chain_receipt_id: input.chain_receipt_id,
|
|
224
|
+
paid_at: timestamp(),
|
|
225
|
+
updated_at: timestamp(),
|
|
226
|
+
},
|
|
227
|
+
$unset: { active_key: "" },
|
|
228
|
+
},
|
|
229
|
+
);
|
|
230
|
+
if (changed.modifiedCount > 0 && this.options.order_status_field) {
|
|
231
|
+
const update: Record<string, unknown> = { [this.options.order_status_field]: "paid" };
|
|
232
|
+
if (this.options.order_updated_at_field) update[this.options.order_updated_at_field] = timestamp();
|
|
233
|
+
await this.orders().updateOne(this.orderFilter(input.order_id), { $set: update });
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async markOrderFulfilledUnsettledOnce(input: {
|
|
238
|
+
order_id: string;
|
|
239
|
+
requirement_id: string;
|
|
240
|
+
pricing_band: string;
|
|
241
|
+
}): Promise<void> {
|
|
242
|
+
const changed = await this.attempts().updateOne(
|
|
243
|
+
{ order_id: input.order_id, status: { $nin: ["fulfilled_unsettled", "paid", "expired", "cancelled", "failed"] } },
|
|
244
|
+
{
|
|
245
|
+
$set: {
|
|
246
|
+
status: "fulfilled_unsettled",
|
|
247
|
+
requirement_id: input.requirement_id,
|
|
248
|
+
pricing_band: input.pricing_band,
|
|
249
|
+
fulfilled_unsettled_at: timestamp(),
|
|
250
|
+
updated_at: timestamp(),
|
|
251
|
+
},
|
|
252
|
+
$unset: { active_key: "" },
|
|
253
|
+
},
|
|
254
|
+
);
|
|
255
|
+
if (changed.modifiedCount > 0 && this.options.order_status_field) {
|
|
256
|
+
const update: Record<string, unknown> = { [this.options.order_status_field]: "fulfilled_unsettled" };
|
|
257
|
+
if (this.options.order_updated_at_field) update[this.options.order_updated_at_field] = timestamp();
|
|
258
|
+
await this.orders().updateOne(this.orderFilter(input.order_id), { $set: update });
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async flagPaymentReview(input: Record<string, unknown>): Promise<void> {
|
|
263
|
+
await this.reviews().insertOne({
|
|
264
|
+
review_id: `sdrp_review_${hash(`${Date.now()}:${JSON.stringify(input)}`).slice(0, 24)}`,
|
|
265
|
+
order_id: textOrNull(input.order_id),
|
|
266
|
+
reason: String(input.reason || "manual_review_required"),
|
|
267
|
+
payload_json: input,
|
|
268
|
+
created_at: timestamp(),
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
private async findProductOrder(orderId: string): Promise<Record<string, unknown> | null> {
|
|
273
|
+
const row = await this.orders().findOne(this.orderFilter(orderId));
|
|
274
|
+
if (!row) return null;
|
|
275
|
+
return {
|
|
276
|
+
...row,
|
|
277
|
+
id: row[this.options.order_id_field] ?? row._id ?? orderId,
|
|
278
|
+
amount_minor: row[this.options.amount_minor_field],
|
|
279
|
+
currency: row[this.options.currency_field],
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
private async nextAttemptNumber(orderId: string): Promise<number> {
|
|
284
|
+
const latest = await this.attempts().find({ order_id: orderId }).sort({ attempt_number: -1 }).limit(1).next();
|
|
285
|
+
const current = Number(latest?.attempt_number || 0);
|
|
286
|
+
return Number.isSafeInteger(current) && current > 0 ? current + 1 : 1;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
private async releaseInactiveAttempt(attempt: WithId<Document>, status: "expired" | "failed"): Promise<void> {
|
|
290
|
+
const timestampField = status === "expired" ? "expires_at" : "failed_at";
|
|
291
|
+
await this.attempts().updateOne(
|
|
292
|
+
{ _id: attempt._id },
|
|
293
|
+
{
|
|
294
|
+
$set: { status, [timestampField]: attempt[timestampField] ?? timestamp(), updated_at: timestamp() },
|
|
295
|
+
$unset: { active_key: "", creation_owner_id: "", creation_lease_expires_at: "" },
|
|
296
|
+
},
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
private toCheckoutAttempt(order: Record<string, unknown>, state: Record<string, unknown>, pending = false): SiglumeCheckoutAttempt {
|
|
301
|
+
return {
|
|
302
|
+
id: String(order.id),
|
|
303
|
+
order_id: String(order.id),
|
|
304
|
+
amount_minor: Number(order.amount_minor),
|
|
305
|
+
currency: String(order.currency),
|
|
306
|
+
attempt_number: Number(state.attempt_number || 1),
|
|
307
|
+
attempt_id: String(state.attempt_id),
|
|
308
|
+
stable_nonce: String(state.stable_nonce),
|
|
309
|
+
status: String(state.status || ""),
|
|
310
|
+
checkout_session_id: textOrUndefined(state.checkout_session_id),
|
|
311
|
+
checkout_url: textOrUndefined(state.checkout_url),
|
|
312
|
+
expires_at: timestampTextOrNull(state.expires_at),
|
|
313
|
+
checkout_creation_pending: pending,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
private orderFilter(orderId: string): Record<string, unknown> {
|
|
318
|
+
return { [this.options.order_id_field]: orderId };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
private orders(): Collection {
|
|
322
|
+
return this.options.db.collection(this.options.orders_collection);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
private attempts(): Collection {
|
|
326
|
+
return this.options.db.collection(this.options.checkout_attempts_collection);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
private events(): Collection {
|
|
330
|
+
return this.options.db.collection(this.options.webhook_events_collection);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
private reviews(): Collection {
|
|
334
|
+
return this.options.db.collection(this.options.payment_reviews_collection);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function normalizeOptions(options: MongoSiglumeOrderStoreOptions): NormalizedOptions {
|
|
339
|
+
return {
|
|
340
|
+
db: options.db,
|
|
341
|
+
orders_collection: options.orders_collection ?? "orders",
|
|
342
|
+
order_id_field: options.order_id_field ?? "id",
|
|
343
|
+
amount_minor_field: options.amount_minor_field ?? "amount_minor",
|
|
344
|
+
currency_field: options.currency_field ?? "currency",
|
|
345
|
+
order_status_field: options.order_status_field === undefined ? "status" : options.order_status_field,
|
|
346
|
+
order_updated_at_field: options.order_updated_at_field === undefined ? "updated_at" : options.order_updated_at_field,
|
|
347
|
+
checkout_attempts_collection: options.checkout_attempts_collection ?? "siglume_checkout_attempts",
|
|
348
|
+
webhook_events_collection: options.webhook_events_collection ?? "siglume_webhook_events",
|
|
349
|
+
payment_reviews_collection: options.payment_reviews_collection ?? "siglume_payment_reviews",
|
|
350
|
+
authorize_order: options.authorize_order,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function stableAttempt(orderId: string, attemptNumber: number): { attempt_id: string; stable_nonce: string } {
|
|
355
|
+
const digest = hash(`${orderId}:${attemptNumber}`).slice(0, 32);
|
|
356
|
+
return {
|
|
357
|
+
attempt_id: `sdrp_attempt_${digest}`,
|
|
358
|
+
stable_nonce: `sdrp-${digest}`,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function hash(value: string): string {
|
|
363
|
+
return createHash("sha256").update(value).digest("hex");
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function requireText(value: string, name: string): string {
|
|
367
|
+
const text = String(value || "").trim();
|
|
368
|
+
if (!text) throw new Error(`${name} is required.`);
|
|
369
|
+
return text;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function timestamp(value: number | string | Date = Date.now()): string {
|
|
373
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
374
|
+
return date.toISOString();
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function timestampOrNull(value: unknown): string | null {
|
|
378
|
+
if (!value) return null;
|
|
379
|
+
const parsed = Date.parse(String(value));
|
|
380
|
+
return Number.isFinite(parsed) ? timestamp(parsed) : null;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function timestampTextOrNull(value: unknown): string | null {
|
|
384
|
+
if (!value) return null;
|
|
385
|
+
if (value instanceof Date) return value.toISOString();
|
|
386
|
+
return String(value);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function timestampHasPassed(value: unknown): boolean {
|
|
390
|
+
if (!value) return false;
|
|
391
|
+
const parsed = Date.parse(String(value));
|
|
392
|
+
return Number.isFinite(parsed) && parsed <= Date.now();
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function webhookProcessingIsStale(value: unknown): boolean {
|
|
396
|
+
if (!value) return false;
|
|
397
|
+
const parsed = Date.parse(String(value));
|
|
398
|
+
return Number.isFinite(parsed) && Date.now() - parsed > WEBHOOK_PROCESSING_STALE_MS;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function isReusableCheckoutAttempt(state: Record<string, unknown>): boolean {
|
|
402
|
+
return String(state.status || "") === "pending"
|
|
403
|
+
&& Boolean(textOrUndefined(state.checkout_session_id))
|
|
404
|
+
&& Boolean(textOrUndefined(state.checkout_url))
|
|
405
|
+
&& !timestampHasPassed(state.expires_at);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function isCreatingCheckoutAttempt(state: Record<string, unknown>): boolean {
|
|
409
|
+
return String(state.status || "") === "creating";
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function textOrUndefined(value: unknown): string | undefined {
|
|
413
|
+
return typeof value === "string" && value ? value : undefined;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function textOrNull(value: unknown): string | null {
|
|
417
|
+
return typeof value === "string" && value ? value : null;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function errorMessage(error: unknown): string {
|
|
421
|
+
if (error instanceof Error) return error.message.slice(0, 1000);
|
|
422
|
+
return String(error || "webhook handler failed").slice(0, 1000);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function isDuplicateKey(error: unknown): boolean {
|
|
426
|
+
return Boolean(error && typeof error === "object" && (error as MongoServerError).code === 11000);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function sleep(ms: number): Promise<void> {
|
|
430
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
431
|
+
}
|