@siglume/direct-request-payment 0.4.22 → 0.4.24
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 +36 -0
- package/README.md +23 -12
- package/bin/siglume-sdrp.mjs +302 -19
- 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/announcement-ja.md +7 -3
- package/docs/api-reference.md +5 -3
- package/docs/merchant-quickstart.md +6 -4
- package/docs/pricing.md +1 -1
- package/docs/quickstart-10-minutes.md +91 -58
- package/docs/sandbox.md +29 -4
- package/docs/troubleshooting.md +12 -8
- package/examples/hosted-checkout-python/pyproject.toml +1 -1
- package/package.json +2 -2
- package/templates/express/siglume-order-store.sql.ts +279 -52
- package/templates/express/siglume-sdrp-routes.ts +43 -9
- package/templates/fastapi/README.md +32 -3
- package/templates/fastapi/siglume_order_store_example.py +15 -0
- package/templates/fastapi/siglume_order_store_sqlalchemy.py +238 -53
- package/templates/fastapi/siglume_order_store_sqlalchemy_async.py +496 -0
- package/templates/fastapi/siglume_sdrp_routes.py +31 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.24 - 2026-06-20
|
|
4
|
+
|
|
5
|
+
- Split CLI checks into `preflight` for pre-mount setup checks and `verify` for
|
|
6
|
+
full Hosted Checkout plus signed webhook delivery verification, so the
|
|
7
|
+
10-minute guide no longer asks users to verify a webhook route before it
|
|
8
|
+
exists.
|
|
9
|
+
- Made merchant account status fail-closed in readiness checks; only `active`
|
|
10
|
+
and `ready` pass.
|
|
11
|
+
- Reworked Express and FastAPI checkout attempts to support attempt generations,
|
|
12
|
+
expiry/failure recovery, and one active checkout attempt per order enforced by
|
|
13
|
+
a database unique key.
|
|
14
|
+
- Added Express E2E coverage for 50 concurrent checkout starts creating exactly
|
|
15
|
+
one Hosted Checkout session, new attempt creation after expiry, and stale
|
|
16
|
+
non-transactional webhook `processing` recovery.
|
|
17
|
+
- Added FastAPI SQLAlchemy expiry retry coverage and made the adapter configurable
|
|
18
|
+
for existing product order table/column names.
|
|
19
|
+
- Added a FastAPI `AsyncSession` SQLAlchemy adapter and E2E coverage for async
|
|
20
|
+
checkout concurrency, webhook idempotency, and expired-session retry.
|
|
21
|
+
- Marked readiness probe webhooks so generated routes ignore them instead of
|
|
22
|
+
writing manual-review records.
|
|
23
|
+
- Updated sandbox checkout to follow the returned success redirect after
|
|
24
|
+
confirmation and expose metered summary responses with seller-borne Micro /
|
|
25
|
+
Nano accounting fields.
|
|
26
|
+
|
|
27
|
+
## 0.4.23 - 2026-06-20
|
|
28
|
+
|
|
29
|
+
- Made the local SDRP sandbox reject invalid checkout input early, including
|
|
30
|
+
non-positive `amount_minor`, unsupported currencies, and unsafe return URLs.
|
|
31
|
+
- Made sandbox checkout confirmation idempotent so repeated confirmation calls
|
|
32
|
+
return the original event and do not send duplicate webhooks.
|
|
33
|
+
- Made the Express SQL order-store adapter recoverable for custom SQL executors
|
|
34
|
+
without a transaction hook by marking failed webhook handling as retryable
|
|
35
|
+
instead of permanently treating it as a duplicate.
|
|
36
|
+
- Added E2E coverage for sandbox idempotency, invalid sandbox input, and
|
|
37
|
+
non-transactional webhook retry recovery.
|
|
38
|
+
|
|
3
39
|
## 0.4.22 - 2026-06-20
|
|
4
40
|
|
|
5
41
|
- Fixed clean-checkout TypeScript resolution for template imports so CI and npm
|
package/README.md
CHANGED
|
@@ -86,10 +86,11 @@ CLI-first:
|
|
|
86
86
|
|
|
87
87
|
```bash
|
|
88
88
|
npm install @siglume/direct-request-payment
|
|
89
|
-
npx siglume-sdrp sandbox --webhook-url http://localhost:3000/payments/webhooks/siglume
|
|
90
|
-
npx siglume-check readiness --sandbox
|
|
91
|
-
npx siglume-check readiness
|
|
92
89
|
npx siglume-sdrp init express --target src/siglume
|
|
90
|
+
# mount the routes, start your app, then:
|
|
91
|
+
npx siglume-sdrp sandbox --webhook-url http://localhost:3000/payments/webhooks/siglume
|
|
92
|
+
npx siglume-check verify --sandbox
|
|
93
|
+
npx siglume-check verify
|
|
93
94
|
```
|
|
94
95
|
|
|
95
96
|
or:
|
|
@@ -97,14 +98,22 @@ or:
|
|
|
97
98
|
```bash
|
|
98
99
|
pip install siglume-direct-request-payment
|
|
99
100
|
siglume-sdrp init fastapi --target app/siglume
|
|
101
|
+
# mount the routes, start your app, then use the npm sandbox for local checkout:
|
|
102
|
+
npx siglume-sdrp sandbox --webhook-url http://localhost:3000/payments/webhooks/siglume
|
|
103
|
+
siglume-check verify --sandbox
|
|
100
104
|
```
|
|
101
105
|
|
|
102
106
|
The sandbox command starts a local Siglume-compatible API that creates fake
|
|
103
107
|
checkout sessions and sends signed webhooks to your product. It never charges a
|
|
104
|
-
wallet; see [SDRP Sandbox](./docs/sandbox.md).
|
|
105
|
-
account, billing, origin, webhook, and Hosted
|
|
106
|
-
|
|
107
|
-
delivery
|
|
108
|
+
wallet; see [SDRP Sandbox](./docs/sandbox.md). `siglume-check preflight`
|
|
109
|
+
checks account, billing, origin, webhook subscription metadata, and Hosted
|
|
110
|
+
Checkout availability before route mounting. `siglume-check verify` additionally
|
|
111
|
+
requires a signed webhook test delivery, so run it only after your webhook route
|
|
112
|
+
is mounted and your app is running.
|
|
113
|
+
|
|
114
|
+
The FastAPI templates include both sync `Session` and async `AsyncSession`
|
|
115
|
+
SQLAlchemy adapters. The sandbox also exposes metered summary endpoints so you
|
|
116
|
+
can verify Micro / Nano seller-borne fee fields before using live credentials.
|
|
108
117
|
|
|
109
118
|
Before implementation, confirm Hosted Checkout readiness in
|
|
110
119
|
[Troubleshooting](./docs/troubleshooting.md#hosted-checkout-readiness). For
|
|
@@ -125,7 +134,7 @@ fulfilling orders.
|
|
|
125
134
|
|
|
126
135
|
| Use case | Recommended path | 10-minute integration path? | Production work still required |
|
|
127
136
|
| --- | --- | --- | --- |
|
|
128
|
-
| EC one-time Standard payment | Hosted Checkout | Yes, with `siglume-
|
|
137
|
+
| EC one-time Standard payment | Hosted Checkout | Yes, with `siglume-sdrp init`, sandbox, and `siglume-check verify` | Product DB adapter, refund/support process, monitoring |
|
|
129
138
|
| Game consumables | Hosted Checkout or agent/API | Conditional | Idempotent entitlement grants, disconnect recovery, Micro / Nano settlement reconciliation and past-due handling |
|
|
130
139
|
| Paid API / AtoA | Direct API or Siglume marketplace tool | Conditional | Request idempotency, buyer auth context, reconciliation |
|
|
131
140
|
| SaaS subscription | Recurring challenge plus raw API | No | Renewal, cancellation, failed renewal, plan-change lifecycle |
|
|
@@ -139,7 +148,7 @@ case `createCheckoutSession(...)` / `getCheckoutSession(...)` raises
|
|
|
139
148
|
`HostedCheckoutNotAvailableError` instead of exposing the raw rollout 404/409.
|
|
140
149
|
Keep the signed `direct_payment.confirmed` webhook as the durable signal, and
|
|
141
150
|
inspect its settlement machine fields before marking any order paid.
|
|
142
|
-
|
|
151
|
+
Run preflight before route mounting and verify after your webhook is live; see
|
|
143
152
|
[Hosted Checkout readiness](./docs/troubleshooting.md#hosted-checkout-readiness).
|
|
144
153
|
|
|
145
154
|
Hosted Checkout is a Siglume-hosted page that turns a "Pay with Siglume" button
|
|
@@ -265,9 +274,11 @@ transaction. Micro / Nano settlement batches are aggregated on-chain after the
|
|
|
265
274
|
weekly or monthly close, or earlier when the fixed amount threshold is reached.
|
|
266
275
|
|
|
267
276
|
Micro Payment and Nano Payment are not separate products you opt into; they are
|
|
268
|
-
amount bands Siglume applies on your behalf.
|
|
269
|
-
|
|
270
|
-
|
|
277
|
+
amount bands Siglume applies on your behalf. Payment initiation is the same
|
|
278
|
+
across amount bands. Fulfillment, revenue recognition, reconciliation, past-due
|
|
279
|
+
handling, and terminal write-off handling differ for Micro / Nano. The full fee
|
|
280
|
+
table and the exact weekly / monthly settlement schedule plus early threshold
|
|
281
|
+
settlement rule are in
|
|
271
282
|
[docs/pricing.md](./docs/pricing.md).
|
|
272
283
|
Provider revenue in the Micro and Nano bands is not settled revenue until the
|
|
273
284
|
aggregated on-chain settlement succeeds. Siglume keeps outstanding failed
|
package/bin/siglume-sdrp.mjs
CHANGED
|
@@ -27,8 +27,14 @@ async function main() {
|
|
|
27
27
|
printHelp();
|
|
28
28
|
return;
|
|
29
29
|
}
|
|
30
|
-
if (command === "readiness" || command === "doctor") {
|
|
31
|
-
await readiness(parseArgs(args));
|
|
30
|
+
if (command === "readiness" || command === "verify" || command === "doctor") {
|
|
31
|
+
await readiness(parseArgs(args), { requireProbe: true, label: command === "verify" ? "verify" : "readiness" });
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (command === "preflight") {
|
|
35
|
+
const options = parseArgs(args);
|
|
36
|
+
options.probe = false;
|
|
37
|
+
await readiness(options, { requireProbe: false, label: "preflight" });
|
|
32
38
|
return;
|
|
33
39
|
}
|
|
34
40
|
if (command === "sandbox") {
|
|
@@ -46,7 +52,9 @@ function printHelp() {
|
|
|
46
52
|
console.log(`Siglume SDRP integration CLI
|
|
47
53
|
|
|
48
54
|
Usage:
|
|
55
|
+
siglume-check preflight --merchant <key> --origin <https://shop.example> --webhook-url <https://api.example/siglume/webhook>
|
|
49
56
|
siglume-check readiness --merchant <key> --origin <https://shop.example> --webhook-url <https://api.example/siglume/webhook>
|
|
57
|
+
siglume-check verify --merchant <key> --origin <https://shop.example> --webhook-url <https://api.example/siglume/webhook>
|
|
50
58
|
siglume-sdrp sandbox --webhook-url <http://localhost:3000/payments/webhooks/siglume>
|
|
51
59
|
siglume-sdrp init express --target src/siglume
|
|
52
60
|
siglume-sdrp init fastapi --target app/siglume
|
|
@@ -60,7 +68,7 @@ Readiness options:
|
|
|
60
68
|
--base-url <url> Siglume API base URL. Defaults to SIGLUME_API_BASE or production.
|
|
61
69
|
--sandbox Use the local sandbox default API base (http://127.0.0.1:8787/v1).
|
|
62
70
|
--no-api Validate local config only; do not call Siglume.
|
|
63
|
-
--no-probe Partial API check only; readiness will not be reported as ready.
|
|
71
|
+
--no-probe Partial API check only; readiness/verify will not be reported as ready. preflight sets this automatically.
|
|
64
72
|
--json Print machine-readable JSON.
|
|
65
73
|
|
|
66
74
|
Sandbox options:
|
|
@@ -101,7 +109,7 @@ function parseArgs(args) {
|
|
|
101
109
|
return out;
|
|
102
110
|
}
|
|
103
111
|
|
|
104
|
-
async function readiness(options) {
|
|
112
|
+
async function readiness(options, mode = { requireProbe: true, label: "readiness" }) {
|
|
105
113
|
const checks = [];
|
|
106
114
|
const sandboxMode = Boolean(options.sandbox) || String(process.env.SIGLUME_ENV || "").toLowerCase() === "sandbox";
|
|
107
115
|
const merchant = options.merchant || process.env.SIGLUME_DIRECT_PAYMENT_MERCHANT || "";
|
|
@@ -133,7 +141,7 @@ async function readiness(options) {
|
|
|
133
141
|
check(checks, "merchant_exists", Boolean(account.merchant), "Run merchant setup before checkout.");
|
|
134
142
|
check(checks, "billing_mandate", Boolean(account.billing_mandate_id), "Complete the merchant billing mandate wallet approval.");
|
|
135
143
|
check(checks, "billing_status_active", activeLike(account.billing_status), `Billing status is ${account.billing_status || "unknown"}; it must be active before accepting payments.`);
|
|
136
|
-
|
|
144
|
+
check(checks, "merchant_status_active", merchantStatusAllowed(account.status), `Merchant status is ${account.status || "unknown"}; it must be active or ready before accepting payments.`);
|
|
137
145
|
} catch (error) {
|
|
138
146
|
check(checks, "merchant_api", false, apiErrorMessage(error, "Could not read the merchant account."));
|
|
139
147
|
}
|
|
@@ -162,8 +170,10 @@ async function readiness(options) {
|
|
|
162
170
|
}
|
|
163
171
|
}
|
|
164
172
|
|
|
165
|
-
if (!options.probe && !hasFailures(checks)) {
|
|
173
|
+
if (!options.probe && mode.requireProbe && !hasFailures(checks)) {
|
|
166
174
|
check(checks, "hosted_checkout_probe", false, "--no-probe skips Hosted Checkout and webhook delivery probes. Remove --no-probe for readiness.");
|
|
175
|
+
} else if (!options.probe && !mode.requireProbe && !hasFailures(checks)) {
|
|
176
|
+
check(checks, "delivery_probe_skipped", true, "preflight only; run siglume-check verify after mounting and starting the webhook route.");
|
|
167
177
|
}
|
|
168
178
|
|
|
169
179
|
if (options.probe && !hasFailures(checks)) {
|
|
@@ -205,7 +215,11 @@ async function readiness(options) {
|
|
|
205
215
|
if (ok && !options.api) {
|
|
206
216
|
console.log("Local config checks passed. API, Hosted Checkout, and webhook delivery readiness were not verified.");
|
|
207
217
|
} else {
|
|
208
|
-
|
|
218
|
+
if (ok && !options.probe && !mode.requireProbe) {
|
|
219
|
+
console.log(`Preflight passed (${sandboxMode ? "sandbox" : "live"}). Mount the routes, start your app, then run siglume-check verify.`);
|
|
220
|
+
} else {
|
|
221
|
+
console.log(ok ? `Ready for 10-minute SDRP integration (${sandboxMode ? "sandbox" : "live"}).` : `Not ready. Fix the FAIL items before ${mode.label === "preflight" ? "mounting checkout" : "opening checkout"}.`);
|
|
222
|
+
}
|
|
209
223
|
}
|
|
210
224
|
}
|
|
211
225
|
if (!ok) {
|
|
@@ -233,7 +247,7 @@ async function init(args) {
|
|
|
233
247
|
}
|
|
234
248
|
await copyDir(from, to, Boolean(parsed.force));
|
|
235
249
|
console.log(`Copied ${framework} SDRP integration files to ${to}`);
|
|
236
|
-
console.log("Wire the exported router into your app, then run siglume-check
|
|
250
|
+
console.log("Wire the exported router into your app, start it, then run siglume-check verify before opening checkout.");
|
|
237
251
|
}
|
|
238
252
|
|
|
239
253
|
async function sandbox(options) {
|
|
@@ -260,6 +274,7 @@ async function sandbox(options) {
|
|
|
260
274
|
subscriptionId: "whsub_sandbox_local",
|
|
261
275
|
sessions: new Map(),
|
|
262
276
|
deliveries: [],
|
|
277
|
+
meteredUsageEvents: [],
|
|
263
278
|
};
|
|
264
279
|
|
|
265
280
|
const server = createServer(async (req, res) => {
|
|
@@ -294,7 +309,7 @@ async function sandbox(options) {
|
|
|
294
309
|
console.log(`SHOP_PUBLIC_ORIGIN=${origin}`);
|
|
295
310
|
console.log(`SHOP_WEBHOOK_URL=${webhookUrl}`);
|
|
296
311
|
console.log("");
|
|
297
|
-
console.log(`Then run: siglume-check
|
|
312
|
+
console.log(`Then run after your app is running: siglume-check verify --sandbox`);
|
|
298
313
|
}
|
|
299
314
|
}
|
|
300
315
|
|
|
@@ -341,18 +356,36 @@ async function handleSandboxRequest(req, res, state, port) {
|
|
|
341
356
|
sendJson(res, 404, { error: { code: "EXTERNAL_402_MERCHANT_NOT_FOUND", message: "sandbox merchant not found" } });
|
|
342
357
|
return;
|
|
343
358
|
}
|
|
359
|
+
let currency;
|
|
360
|
+
let amountMinor;
|
|
361
|
+
let successUrl;
|
|
362
|
+
let cancelUrl;
|
|
363
|
+
try {
|
|
364
|
+
currency = normalizeCurrency(body.currency || "JPY");
|
|
365
|
+
amountMinor = normalizePositiveAmountMinor(body.amount_minor);
|
|
366
|
+
successUrl = normalizeSandboxReturnUrl(body.success_url, "success_url");
|
|
367
|
+
cancelUrl = normalizeSandboxReturnUrl(body.cancel_url, "cancel_url");
|
|
368
|
+
} catch (error) {
|
|
369
|
+
sendJson(res, 400, {
|
|
370
|
+
error: {
|
|
371
|
+
code: "INVALID_CHECKOUT_SESSION_REQUEST",
|
|
372
|
+
message: error instanceof Error ? error.message : "invalid checkout session request",
|
|
373
|
+
},
|
|
374
|
+
});
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
344
377
|
const sessionId = `chk_sandbox_${state.sessions.size + 1}`;
|
|
345
378
|
const challengeHash = `sha256:sandbox_${hashString(`${sessionId}:${body.nonce || ""}`).slice(0, 32)}`;
|
|
346
379
|
const session = {
|
|
347
380
|
session_id: sessionId,
|
|
348
381
|
merchant: state.merchant,
|
|
349
|
-
amount_minor:
|
|
350
|
-
currency
|
|
351
|
-
token_symbol:
|
|
382
|
+
amount_minor: amountMinor,
|
|
383
|
+
currency,
|
|
384
|
+
token_symbol: currency === "USD" ? "USDC" : "JPYC",
|
|
352
385
|
status: "open",
|
|
353
386
|
challenge_hash: challengeHash,
|
|
354
|
-
success_url:
|
|
355
|
-
cancel_url:
|
|
387
|
+
success_url: successUrl,
|
|
388
|
+
cancel_url: cancelUrl,
|
|
356
389
|
metadata_jsonb: body.metadata && typeof body.metadata === "object" ? body.metadata : {},
|
|
357
390
|
checkout_url: `http://127.0.0.1:${port}/pay/${sessionId}`,
|
|
358
391
|
expires_at: new Date(Date.now() + 30 * 60 * 1000).toISOString(),
|
|
@@ -399,6 +432,35 @@ async function handleSandboxRequest(req, res, state, port) {
|
|
|
399
432
|
return;
|
|
400
433
|
}
|
|
401
434
|
|
|
435
|
+
if (req.method === "GET" && url.pathname === "/v1/sdrp/metered/my-summary") {
|
|
436
|
+
sendEnvelope(res, 200, sandboxBuyerMeteredSummary(state, url));
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (req.method === "GET" && url.pathname === "/v1/sdrp/metered/provider/summary") {
|
|
441
|
+
sendEnvelope(res, 200, sandboxProviderMeteredSummary(state, url));
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (req.method === "GET" && (
|
|
446
|
+
url.pathname === "/v1/sdrp/metered/my-usage-events"
|
|
447
|
+
|| url.pathname === "/v1/sdrp/metered/provider/usage-events"
|
|
448
|
+
)) {
|
|
449
|
+
sendEnvelope(res, 200, {
|
|
450
|
+
items: filterSandboxMeteredUsage(state, url),
|
|
451
|
+
next_cursor: null,
|
|
452
|
+
});
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (req.method === "GET" && url.pathname === "/v1/sdrp/metered/provider/settlement-batches") {
|
|
457
|
+
sendEnvelope(res, 200, {
|
|
458
|
+
items: sandboxSettlementBatches(state, url),
|
|
459
|
+
next_cursor: null,
|
|
460
|
+
});
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
|
|
402
464
|
if (req.method === "GET" && url.pathname.startsWith("/pay/")) {
|
|
403
465
|
const sessionId = decodeURIComponent(url.pathname.split("/").pop() || "");
|
|
404
466
|
const session = state.sessions.get(sessionId);
|
|
@@ -418,15 +480,20 @@ async function handleSandboxRequest(req, res, state, port) {
|
|
|
418
480
|
sendJson(res, 404, { error: { code: "CHECKOUT_SESSION_NOT_FOUND", message: "sandbox session not found" } });
|
|
419
481
|
return;
|
|
420
482
|
}
|
|
483
|
+
if (session.status === "paid" && session.confirmation_event) {
|
|
484
|
+
sendEnvelope(res, 200, sandboxConfirmResponse(session, session.confirmation_event));
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
421
487
|
session.status = "paid";
|
|
422
488
|
session.requirement_id = `dpr_sandbox_${sessionId}`;
|
|
423
489
|
const event = sandboxPaymentConfirmedEvent(session);
|
|
490
|
+
session.confirmation_event = event;
|
|
491
|
+
const usageEvent = sandboxMeteredUsageEvent(session, event);
|
|
492
|
+
if (usageEvent) {
|
|
493
|
+
state.meteredUsageEvents.unshift(usageEvent);
|
|
494
|
+
}
|
|
424
495
|
await deliverSandboxWebhook(state, event);
|
|
425
|
-
sendEnvelope(res, 200,
|
|
426
|
-
status: "paid",
|
|
427
|
-
redirect_url: `${session.success_url}${session.success_url.includes("?") ? "&" : "?"}session_id=${encodeURIComponent(sessionId)}`,
|
|
428
|
-
event: { id: event.id, type: event.type },
|
|
429
|
-
});
|
|
496
|
+
sendEnvelope(res, 200, sandboxConfirmResponse(session, event));
|
|
430
497
|
return;
|
|
431
498
|
}
|
|
432
499
|
|
|
@@ -468,6 +535,7 @@ function sandboxEvent({ event_type, data }) {
|
|
|
468
535
|
function sandboxPaymentConfirmedEvent(session) {
|
|
469
536
|
const pricingBand = classifySandboxAmount(session.currency, Number(session.amount_minor));
|
|
470
537
|
const metered = pricingBand === "micro" || pricingBand === "nano";
|
|
538
|
+
const accounting = metered ? sandboxMeteredAccounting(session, pricingBand) : null;
|
|
471
539
|
return sandboxEvent({
|
|
472
540
|
event_type: "direct_payment.confirmed",
|
|
473
541
|
data: {
|
|
@@ -484,10 +552,196 @@ function sandboxPaymentConfirmedEvent(session) {
|
|
|
484
552
|
settlement_status: metered ? "pending_settlement" : "settled",
|
|
485
553
|
chain_receipt_id: metered ? undefined : `chain_sandbox_${session.session_id}`,
|
|
486
554
|
environment: "sandbox",
|
|
555
|
+
...(accounting ? {
|
|
556
|
+
provider_gross_amount_minor: accounting.provider_gross_amount_minor,
|
|
557
|
+
provider_usage_amount_minor: accounting.provider_usage_amount_minor,
|
|
558
|
+
protocol_fee_minor: accounting.protocol_fee_minor,
|
|
559
|
+
provider_receivable_minor: accounting.provider_receivable_minor,
|
|
560
|
+
gross_buyer_debit_minor: accounting.gross_buyer_debit_minor,
|
|
561
|
+
buyer_debit_minor: accounting.buyer_debit_minor,
|
|
562
|
+
rounding_delta_minor: accounting.rounding_delta_minor,
|
|
563
|
+
settlement_threshold_minor: accounting.settlement_threshold_minor,
|
|
564
|
+
} : {}),
|
|
487
565
|
},
|
|
488
566
|
});
|
|
489
567
|
}
|
|
490
568
|
|
|
569
|
+
function sandboxMeteredUsageEvent(session, event) {
|
|
570
|
+
const pricingBand = classifySandboxAmount(session.currency, Number(session.amount_minor));
|
|
571
|
+
if (pricingBand !== "micro" && pricingBand !== "nano") return null;
|
|
572
|
+
const accounting = sandboxMeteredAccounting(session, pricingBand);
|
|
573
|
+
return {
|
|
574
|
+
metered_usage_id: `mu_sandbox_${session.session_id}`,
|
|
575
|
+
created_at: new Date().toISOString(),
|
|
576
|
+
plan_type: pricingBand,
|
|
577
|
+
pricing_band: pricingBand,
|
|
578
|
+
settlement_cadence: pricingBand === "micro" ? "weekly" : "monthly",
|
|
579
|
+
period_start: new Date().toISOString(),
|
|
580
|
+
period_end: null,
|
|
581
|
+
listing_id: session.metadata_jsonb?.listing_id || "sandbox_listing",
|
|
582
|
+
capability_key: session.metadata_jsonb?.capability_key || "sandbox_checkout",
|
|
583
|
+
operation_key: session.metadata_jsonb?.operation_key || "checkout.confirm",
|
|
584
|
+
currency: session.currency,
|
|
585
|
+
token_symbol: session.token_symbol,
|
|
586
|
+
status: "open",
|
|
587
|
+
settlement_batch_id: null,
|
|
588
|
+
buyer_period_ref: `buyer_sandbox:${session.merchant}:${session.token_symbol}:${pricingBand}`,
|
|
589
|
+
requirement_id: session.requirement_id,
|
|
590
|
+
challenge_hash: session.challenge_hash,
|
|
591
|
+
event_id: event.id,
|
|
592
|
+
...accounting,
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function sandboxMeteredAccounting(session, pricingBand) {
|
|
597
|
+
const currency = String(session.currency || "").toUpperCase();
|
|
598
|
+
const providerGrossTenths = Number(session.amount_minor) * 10;
|
|
599
|
+
const protocolFeeTenths = sandboxProtocolFeeTenths(currency, pricingBand);
|
|
600
|
+
const providerReceivableTenths = providerGrossTenths - protocolFeeTenths;
|
|
601
|
+
return {
|
|
602
|
+
provider_gross_amount_minor: formatTenths(providerGrossTenths),
|
|
603
|
+
provider_usage_amount_minor: formatTenths(providerGrossTenths),
|
|
604
|
+
protocol_fee_minor: formatTenths(protocolFeeTenths),
|
|
605
|
+
provider_receivable_minor: formatTenths(providerReceivableTenths),
|
|
606
|
+
gross_buyer_debit_minor: formatTenths(providerGrossTenths),
|
|
607
|
+
buyer_debit_minor: formatTenths(providerGrossTenths),
|
|
608
|
+
rounding_delta_minor: "0",
|
|
609
|
+
settlement_threshold_minor: "10000",
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function sandboxProtocolFeeTenths(currency, pricingBand) {
|
|
614
|
+
if (pricingBand === "micro") return currency === "USD" ? 10 : 20;
|
|
615
|
+
return currency === "USD" ? 1 : 2;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function sandboxBuyerMeteredSummary(state, url) {
|
|
619
|
+
const events = filterSandboxMeteredUsage(state, url);
|
|
620
|
+
const batches = sandboxSettlementBatches(state, url);
|
|
621
|
+
return {
|
|
622
|
+
role: "buyer",
|
|
623
|
+
open_periods: sandboxOpenPeriods(events),
|
|
624
|
+
settlement_batches: batches,
|
|
625
|
+
past_due_blocks: [],
|
|
626
|
+
balance_sufficiency: { sufficient: true },
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function sandboxProviderMeteredSummary(state, url) {
|
|
631
|
+
const events = filterSandboxMeteredUsage(state, url);
|
|
632
|
+
const totals = sandboxProviderTotals(events);
|
|
633
|
+
return {
|
|
634
|
+
role: "provider",
|
|
635
|
+
timezone: "UTC",
|
|
636
|
+
filters: Object.fromEntries(url.searchParams.entries()),
|
|
637
|
+
open_periods: sandboxOpenPeriods(events),
|
|
638
|
+
periods: sandboxSettlementBatches(state, url),
|
|
639
|
+
totals,
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function filterSandboxMeteredUsage(state, url) {
|
|
644
|
+
const planType = url.searchParams.get("plan_type");
|
|
645
|
+
const tokenSymbol = url.searchParams.get("token_symbol");
|
|
646
|
+
const status = url.searchParams.get("status");
|
|
647
|
+
return state.meteredUsageEvents.filter((event) => {
|
|
648
|
+
if (planType && event.plan_type !== planType) return false;
|
|
649
|
+
if (tokenSymbol && event.token_symbol !== tokenSymbol) return false;
|
|
650
|
+
if (status && event.status !== status) return false;
|
|
651
|
+
return true;
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function sandboxOpenPeriods(events) {
|
|
656
|
+
return Object.values(groupSandboxMeteredEvents(events)).map((group) => {
|
|
657
|
+
const grossTenths = sumTenths(group.events, "provider_gross_amount_minor");
|
|
658
|
+
const protocolFeeTenths = sumTenths(group.events, "protocol_fee_minor");
|
|
659
|
+
const receivableTenths = sumTenths(group.events, "provider_receivable_minor");
|
|
660
|
+
const buyerDebitTenths = sumTenths(group.events, "buyer_debit_minor");
|
|
661
|
+
const thresholdTenths = 10000 * 10;
|
|
662
|
+
const thresholdReached = grossTenths >= thresholdTenths;
|
|
663
|
+
return {
|
|
664
|
+
plan_type: group.plan_type,
|
|
665
|
+
settlement_cadence: group.plan_type === "micro" ? "weekly" : "monthly",
|
|
666
|
+
currency: group.currency,
|
|
667
|
+
token_symbol: group.token_symbol,
|
|
668
|
+
period_start: group.events[group.events.length - 1]?.created_at ?? null,
|
|
669
|
+
period_end: null,
|
|
670
|
+
close_at: null,
|
|
671
|
+
settlement_trigger: thresholdReached ? "amount_threshold" : null,
|
|
672
|
+
settlement_threshold_minor: "10000",
|
|
673
|
+
threshold_reached_at: thresholdReached ? group.events[0]?.created_at ?? null : null,
|
|
674
|
+
provider_gross_amount_minor: formatTenths(grossTenths),
|
|
675
|
+
provider_usage_amount_minor: formatTenths(grossTenths),
|
|
676
|
+
protocol_fee_minor: formatTenths(protocolFeeTenths),
|
|
677
|
+
provider_receivable_minor: formatTenths(receivableTenths),
|
|
678
|
+
buyer_debit_minor: formatTenths(buyerDebitTenths),
|
|
679
|
+
total_unsettled_exposure_minor: formatTenths(grossTenths),
|
|
680
|
+
};
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function sandboxSettlementBatches(state, url) {
|
|
685
|
+
const events = filterSandboxMeteredUsage(state, url);
|
|
686
|
+
return sandboxOpenPeriods(events)
|
|
687
|
+
.filter((period) => period.settlement_trigger === "amount_threshold")
|
|
688
|
+
.map((period) => ({
|
|
689
|
+
settlement_batch_id: `batch_sandbox_${period.plan_type}_${period.currency}_${period.token_symbol}`,
|
|
690
|
+
status: "notice_pending",
|
|
691
|
+
notice_status: "pending",
|
|
692
|
+
not_before_attempt_at: new Date(Date.now() + 72 * 60 * 60 * 1000).toISOString(),
|
|
693
|
+
expected_scheduled_debit_at: new Date(Date.now() + 72 * 60 * 60 * 1000).toISOString(),
|
|
694
|
+
...period,
|
|
695
|
+
}));
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function sandboxProviderTotals(events) {
|
|
699
|
+
return {
|
|
700
|
+
settled_provider_receivable_minor: "0",
|
|
701
|
+
unsettled_provider_receivable_minor: formatTenths(sumTenths(events, "provider_receivable_minor")),
|
|
702
|
+
past_due_provider_receivable_minor: "0",
|
|
703
|
+
terminal_provider_receivable_minor: "0",
|
|
704
|
+
uncollectible_provider_receivable_minor: "0",
|
|
705
|
+
written_off_provider_receivable_minor: "0",
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function groupSandboxMeteredEvents(events) {
|
|
710
|
+
const groups = {};
|
|
711
|
+
for (const event of events) {
|
|
712
|
+
const key = `${event.plan_type}:${event.currency}:${event.token_symbol}`;
|
|
713
|
+
groups[key] ||= {
|
|
714
|
+
plan_type: event.plan_type,
|
|
715
|
+
currency: event.currency,
|
|
716
|
+
token_symbol: event.token_symbol,
|
|
717
|
+
events: [],
|
|
718
|
+
};
|
|
719
|
+
groups[key].events.push(event);
|
|
720
|
+
}
|
|
721
|
+
return groups;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
function sumTenths(items, field) {
|
|
725
|
+
return items.reduce((total, item) => total + parseTenths(item[field]), 0);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function parseTenths(value) {
|
|
729
|
+
return Math.round(Number(value || 0) * 10);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function formatTenths(value) {
|
|
733
|
+
if (value % 10 === 0) return String(value / 10);
|
|
734
|
+
return (value / 10).toFixed(1);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function sandboxConfirmResponse(session, event) {
|
|
738
|
+
return {
|
|
739
|
+
status: "paid",
|
|
740
|
+
redirect_url: `${session.success_url}${session.success_url.includes("?") ? "&" : "?"}session_id=${encodeURIComponent(session.session_id)}`,
|
|
741
|
+
event: { id: event.id, type: event.type },
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
|
|
491
745
|
async function deliverSandboxWebhook(state, event) {
|
|
492
746
|
const rawBody = JSON.stringify(event);
|
|
493
747
|
const signature = await buildWebhookSignatureHeader(state.webhookSecret, rawBody);
|
|
@@ -540,6 +794,9 @@ function sandboxCheckoutHtml(session) {
|
|
|
540
794
|
const body = await response.json();
|
|
541
795
|
document.getElementById("status").textContent = body.data?.status || "failed";
|
|
542
796
|
document.getElementById("output").textContent = JSON.stringify(body, null, 2);
|
|
797
|
+
if (body.data?.redirect_url) {
|
|
798
|
+
window.setTimeout(() => window.location.assign(body.data.redirect_url), 400);
|
|
799
|
+
}
|
|
543
800
|
});
|
|
544
801
|
</script>
|
|
545
802
|
</body>`;
|
|
@@ -725,6 +982,27 @@ function normalizeCurrency(value) {
|
|
|
725
982
|
return currency;
|
|
726
983
|
}
|
|
727
984
|
|
|
985
|
+
function normalizePositiveAmountMinor(value) {
|
|
986
|
+
const amountMinor = Number(value);
|
|
987
|
+
if (!Number.isSafeInteger(amountMinor) || amountMinor <= 0) {
|
|
988
|
+
throw new Error("amount_minor must be a positive integer.");
|
|
989
|
+
}
|
|
990
|
+
return amountMinor;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
function normalizeSandboxReturnUrl(value, name) {
|
|
994
|
+
const text = String(value || "").trim();
|
|
995
|
+
try {
|
|
996
|
+
const url = new URL(text);
|
|
997
|
+
if (url.protocol === "https:" || (url.protocol === "http:" && isLocalhost(url.hostname))) {
|
|
998
|
+
return url.href;
|
|
999
|
+
}
|
|
1000
|
+
} catch {
|
|
1001
|
+
// Fall through to a consistent validation error.
|
|
1002
|
+
}
|
|
1003
|
+
throw new Error(`${name} must be an https URL, or a local http URL in sandbox.`);
|
|
1004
|
+
}
|
|
1005
|
+
|
|
728
1006
|
function isStandardAmount(currency, amountMinor) {
|
|
729
1007
|
return Number.isSafeInteger(amountMinor) && amountMinor >= (currency === "USD" ? 301 : 501);
|
|
730
1008
|
}
|
|
@@ -733,6 +1011,10 @@ function activeLike(value) {
|
|
|
733
1011
|
return /^(active|ready|current|ok|enabled|paid|complete|completed)$/i.test(String(value || ""));
|
|
734
1012
|
}
|
|
735
1013
|
|
|
1014
|
+
function merchantStatusAllowed(value) {
|
|
1015
|
+
return /^(active|ready)$/i.test(String(value || ""));
|
|
1016
|
+
}
|
|
1017
|
+
|
|
736
1018
|
function includesEventType(eventTypes, eventType) {
|
|
737
1019
|
if (!Array.isArray(eventTypes) || eventTypes.length === 0) return true;
|
|
738
1020
|
return eventTypes.map((item) => String(item)).includes(eventType);
|
|
@@ -764,6 +1046,7 @@ async function checkWebhookDeliveryProbe(checks, merchantClient, { merchant, sub
|
|
|
764
1046
|
subscription_ids: [id],
|
|
765
1047
|
data: {
|
|
766
1048
|
mode: "readiness_probe",
|
|
1049
|
+
readiness_probe: true,
|
|
767
1050
|
merchant,
|
|
768
1051
|
direct_payment_requirement_id: `dpr_readiness_${Date.now()}`,
|
|
769
1052
|
requirement_id: `dpr_readiness_${Date.now()}`,
|
package/dist/index.cjs
CHANGED
|
@@ -85,7 +85,7 @@ var DIRECT_REQUEST_PAYMENT_RECEIPT_KIND = "sdrp_direct_payment";
|
|
|
85
85
|
var DIRECT_REQUEST_PAYMENT_ALLOWANCE_RECEIPT_KIND = "sdrp_direct_payment_allowance";
|
|
86
86
|
var DIRECT_REQUEST_PAYMENT_REFERENCE_TYPE = "sdrp_direct_payment_requirement";
|
|
87
87
|
var DEFAULT_WEBHOOK_TOLERANCE_SECONDS = 300;
|
|
88
|
-
var DIRECT_REQUEST_PAYMENT_SDK_VERSION = "0.4.
|
|
88
|
+
var DIRECT_REQUEST_PAYMENT_SDK_VERSION = "0.4.24";
|
|
89
89
|
var DIRECT_REQUEST_PAYMENT_STANDARD_SETTLED_STATUS = "settled";
|
|
90
90
|
var DIRECT_REQUEST_PAYMENT_METERED_ACCEPTED_STATUS = "pending_settlement";
|
|
91
91
|
var DIRECT_REQUEST_PAYMENT_STANDARD_FINALITY = "per_payment_onchain";
|