@siglume/direct-request-payment 0.4.6 → 0.4.8
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 +20 -0
- package/README.md +55 -46
- package/dist/index.cjs +116 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +48 -2
- package/dist/index.d.ts +48 -2
- package/dist/index.js +116 -1
- package/dist/index.js.map +1 -1
- package/docs/announcement-ja.md +2 -2
- package/docs/api-reference.md +40 -15
- package/docs/merchant-quickstart.md +76 -64
- package/docs/pricing.md +14 -10
- package/examples/express-checkout.ts +32 -22
- package/package.json +1 -1
|
@@ -16,11 +16,15 @@ order challenge; the buyer-facing Siglume payment flow pays it.
|
|
|
16
16
|
|
|
17
17
|
This quickstart uses Standard-band example amounts. Micro Payment and Nano
|
|
18
18
|
Payment are applied automatically by amount through the same Hosted Checkout or
|
|
19
|
-
agent/API flow; you do not create a separate Micro/Nano object or
|
|
20
|
-
|
|
19
|
+
agent/API flow; you do not create a separate Micro/Nano object or manually
|
|
20
|
+
select the amount band. Micro/Nano are settled on account-assigned weekly /
|
|
21
21
|
monthly slots after the final notice and close-plus-3-day window (see
|
|
22
22
|
[Pricing](./pricing.md#settlement-schedule)), and provider revenue remains
|
|
23
|
-
unsettled until the later on-chain settlement succeeds.
|
|
23
|
+
unsettled until the later on-chain settlement succeeds. Completing merchant
|
|
24
|
+
setup and the billing mandate means accepting this Micro/Nano delayed aggregated
|
|
25
|
+
settlement model for low-price items. If your product requires immediate
|
|
26
|
+
on-chain settlement, keep its price above the Micro/Nano thresholds instead of
|
|
27
|
+
offering JPY 500-and-under or USD 3-and-under amounts.
|
|
24
28
|
|
|
25
29
|
## Two Buyer Systems
|
|
26
30
|
|
|
@@ -388,7 +392,10 @@ signer and verifier only.
|
|
|
388
392
|
Use the webhook as the durable signal, not just the browser return path.
|
|
389
393
|
|
|
390
394
|
```ts
|
|
391
|
-
import {
|
|
395
|
+
import {
|
|
396
|
+
classifyDirectPaymentConfirmation,
|
|
397
|
+
verifyDirectRequestPaymentWebhook,
|
|
398
|
+
} from "@siglume/direct-request-payment";
|
|
392
399
|
|
|
393
400
|
const { event } = await verifyDirectRequestPaymentWebhook(
|
|
394
401
|
process.env.SIGLUME_WEBHOOK_SECRET!,
|
|
@@ -400,57 +407,57 @@ if (event.type !== "direct_payment.confirmed") {
|
|
|
400
407
|
return new Response(null, { status: 204 });
|
|
401
408
|
}
|
|
402
409
|
|
|
403
|
-
const
|
|
410
|
+
const confirmation = classifyDirectPaymentConfirmation(event);
|
|
404
411
|
|
|
405
|
-
if (
|
|
412
|
+
if (confirmation.kind === "metered_batch_settled") {
|
|
406
413
|
// Aggregated Micro/Nano settlement events do not carry an order challenge.
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
});
|
|
414
|
-
}
|
|
414
|
+
await orders.reconcileMeteredSettlementOnce({
|
|
415
|
+
settlement_batch_id: confirmation.settlement_batch_id,
|
|
416
|
+
chain_receipt_id: confirmation.chain_receipt_id,
|
|
417
|
+
usage_event_digest: confirmation.usage_event_digest,
|
|
418
|
+
settled_at: confirmation.settled_at ?? null,
|
|
419
|
+
});
|
|
415
420
|
return new Response(null, { status: 204 });
|
|
416
421
|
}
|
|
417
422
|
|
|
418
|
-
if (
|
|
419
|
-
|
|
420
|
-
data.finality === "per_payment_onchain" &&
|
|
421
|
-
data.settlement_status === "settled"
|
|
422
|
-
) {
|
|
423
|
-
const order = await orders.findByChallengeHash(String(data.challenge_hash ?? ""));
|
|
423
|
+
if (confirmation.kind === "standard_settled") {
|
|
424
|
+
const order = await orders.findByChallengeHash(confirmation.challenge_hash);
|
|
424
425
|
if (!order) {
|
|
425
426
|
await orders.flagForPaymentStateReview({
|
|
426
427
|
reason: "unknown_challenge_hash",
|
|
427
|
-
requirement_id:
|
|
428
|
+
requirement_id: confirmation.requirement_id,
|
|
428
429
|
});
|
|
429
430
|
return new Response(null, { status: 204 });
|
|
430
431
|
}
|
|
431
432
|
await orders.markPaidOnce(order.id, {
|
|
432
|
-
siglume_requirement_id:
|
|
433
|
-
chain_receipt_id:
|
|
433
|
+
siglume_requirement_id: confirmation.requirement_id,
|
|
434
|
+
chain_receipt_id: confirmation.chain_receipt_id,
|
|
434
435
|
});
|
|
435
436
|
return new Response(null, { status: 204 });
|
|
436
437
|
}
|
|
437
438
|
|
|
438
|
-
if (
|
|
439
|
-
const order = await orders.findByChallengeHash(
|
|
440
|
-
if (order) {
|
|
441
|
-
await orders.
|
|
442
|
-
|
|
443
|
-
|
|
439
|
+
if (confirmation.kind === "metered_usage_accepted") {
|
|
440
|
+
const order = await orders.findByChallengeHash(confirmation.challenge_hash);
|
|
441
|
+
if (!order) {
|
|
442
|
+
await orders.flagForPaymentStateReview({
|
|
443
|
+
reason: "unknown_metered_challenge_hash",
|
|
444
|
+
requirement_id: confirmation.requirement_id,
|
|
444
445
|
});
|
|
446
|
+
return new Response(null, { status: 204 });
|
|
445
447
|
}
|
|
448
|
+
await orders.markFulfilledButUnsettledOnce(order.id, {
|
|
449
|
+
siglume_requirement_id: confirmation.requirement_id,
|
|
450
|
+
pricing_band: confirmation.pricing_band,
|
|
451
|
+
});
|
|
446
452
|
return new Response(null, { status: 204 });
|
|
447
453
|
}
|
|
448
454
|
|
|
449
455
|
// Missing or unknown machine fields: do not mark the order paid from the event
|
|
450
456
|
// name alone. Fetch the requirement or route it to manual review.
|
|
451
457
|
await orders.flagForPaymentStateReview({
|
|
452
|
-
reason:
|
|
453
|
-
requirement_id:
|
|
458
|
+
reason: confirmation.reason,
|
|
459
|
+
requirement_id: confirmation.requirement_id ?? "",
|
|
460
|
+
settlement_batch_id: confirmation.settlement_batch_id ?? null,
|
|
454
461
|
});
|
|
455
462
|
return new Response(null, { status: 204 });
|
|
456
463
|
```
|
|
@@ -460,7 +467,10 @@ Python:
|
|
|
460
467
|
```py
|
|
461
468
|
import os
|
|
462
469
|
|
|
463
|
-
from siglume_direct_request_payment import
|
|
470
|
+
from siglume_direct_request_payment import (
|
|
471
|
+
classify_direct_payment_confirmation,
|
|
472
|
+
verify_direct_request_payment_webhook,
|
|
473
|
+
)
|
|
464
474
|
|
|
465
475
|
verified = verify_direct_request_payment_webhook(
|
|
466
476
|
os.environ["SIGLUME_WEBHOOK_SECRET"],
|
|
@@ -471,53 +481,54 @@ verified = verify_direct_request_payment_webhook(
|
|
|
471
481
|
if verified["event"]["type"] != "direct_payment.confirmed":
|
|
472
482
|
return "", 204
|
|
473
483
|
|
|
474
|
-
|
|
484
|
+
confirmation = classify_direct_payment_confirmation(verified["event"])
|
|
475
485
|
|
|
476
|
-
if
|
|
486
|
+
if confirmation["kind"] == "metered_batch_settled":
|
|
477
487
|
# Aggregated Micro/Nano settlement events do not carry an order challenge.
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
)
|
|
488
|
+
orders.reconcile_metered_settlement_once(
|
|
489
|
+
settlement_batch_id=confirmation["settlement_batch_id"],
|
|
490
|
+
chain_receipt_id=confirmation["chain_receipt_id"],
|
|
491
|
+
usage_event_digest=confirmation["usage_event_digest"],
|
|
492
|
+
settled_at=confirmation.get("settled_at"),
|
|
493
|
+
)
|
|
485
494
|
return "", 204
|
|
486
495
|
|
|
487
|
-
if
|
|
488
|
-
|
|
489
|
-
and data.get("finality") == "per_payment_onchain"
|
|
490
|
-
and data.get("settlement_status") == "settled"
|
|
491
|
-
):
|
|
492
|
-
order = orders.find_by_challenge_hash(str(data.get("challenge_hash") or ""))
|
|
496
|
+
if confirmation["kind"] == "standard_settled":
|
|
497
|
+
order = orders.find_by_challenge_hash(confirmation["challenge_hash"])
|
|
493
498
|
if not order:
|
|
494
499
|
orders.flag_for_payment_state_review(
|
|
495
500
|
reason="unknown_challenge_hash",
|
|
496
|
-
requirement_id=
|
|
501
|
+
requirement_id=confirmation["requirement_id"],
|
|
497
502
|
)
|
|
498
503
|
return "", 204
|
|
499
504
|
orders.mark_paid_once(
|
|
500
505
|
order["id"],
|
|
501
|
-
siglume_requirement_id=
|
|
502
|
-
chain_receipt_id=
|
|
506
|
+
siglume_requirement_id=confirmation["requirement_id"],
|
|
507
|
+
chain_receipt_id=confirmation["chain_receipt_id"],
|
|
503
508
|
)
|
|
504
509
|
return "", 204
|
|
505
510
|
|
|
506
|
-
if
|
|
507
|
-
order = orders.find_by_challenge_hash(
|
|
508
|
-
if order:
|
|
509
|
-
orders.
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
pricing_band=str(data.get("pricing_band") or ""),
|
|
511
|
+
if confirmation["kind"] == "metered_usage_accepted":
|
|
512
|
+
order = orders.find_by_challenge_hash(confirmation["challenge_hash"])
|
|
513
|
+
if not order:
|
|
514
|
+
orders.flag_for_payment_state_review(
|
|
515
|
+
reason="unknown_metered_challenge_hash",
|
|
516
|
+
requirement_id=confirmation["requirement_id"],
|
|
513
517
|
)
|
|
518
|
+
return "", 204
|
|
519
|
+
orders.mark_fulfilled_but_unsettled_once(
|
|
520
|
+
order["id"],
|
|
521
|
+
siglume_requirement_id=confirmation["requirement_id"],
|
|
522
|
+
pricing_band=confirmation["pricing_band"],
|
|
523
|
+
)
|
|
514
524
|
return "", 204
|
|
515
525
|
|
|
516
526
|
# Missing or unknown machine fields: do not mark the order paid from the event
|
|
517
527
|
# name alone. Fetch the requirement or route it to manual review.
|
|
518
528
|
orders.flag_for_payment_state_review(
|
|
519
|
-
reason="
|
|
520
|
-
requirement_id=
|
|
529
|
+
reason=confirmation["reason"],
|
|
530
|
+
requirement_id=confirmation.get("requirement_id") or "",
|
|
531
|
+
settlement_batch_id=confirmation.get("settlement_batch_id"),
|
|
521
532
|
)
|
|
522
533
|
return "", 204
|
|
523
534
|
```
|
|
@@ -525,11 +536,12 @@ return "", 204
|
|
|
525
536
|
## Reconcile Micro / Nano Statements
|
|
526
537
|
|
|
527
538
|
Standard Payment can be marked paid from the verified `direct_payment.confirmed`
|
|
528
|
-
webhook only when `
|
|
529
|
-
`
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
539
|
+
webhook only when `classifyDirectPaymentConfirmation(event)` returns
|
|
540
|
+
`standard_settled`, which requires Standard pricing, per-payment on-chain
|
|
541
|
+
finality, settled status, a challenge hash, a requirement id, and a chain
|
|
542
|
+
receipt id. Micro Payment and Nano Payment are different: they are automatic
|
|
543
|
+
amount bands and are settled later in aggregated on-chain batches. Use the
|
|
544
|
+
statement APIs to answer:
|
|
533
545
|
|
|
534
546
|
- how much Micro / Nano usage is open this week or month,
|
|
535
547
|
- when the buyer's assigned period closes,
|
package/docs/pricing.md
CHANGED
|
@@ -7,7 +7,9 @@ fee data returned at runtime.
|
|
|
7
7
|
|
|
8
8
|
Pricing has one structure: a merchant selects the Standard Payment plan during
|
|
9
9
|
setup, then Siglume applies the fee for each payment by amount. Micro Payment
|
|
10
|
-
and Nano Payment are automatic amount bands, not separate choices.
|
|
10
|
+
and Nano Payment are automatic amount bands, not separate choices. Merchant
|
|
11
|
+
setup and the billing mandate terms assume the merchant accepts Micro / Nano
|
|
12
|
+
delayed aggregated settlement whenever they offer amounts in those bands.
|
|
11
13
|
|
|
12
14
|
## Settlement Currencies
|
|
13
15
|
|
|
@@ -36,15 +38,17 @@ aggregated and settled in account-assigned weekly / monthly slots - see
|
|
|
36
38
|
pre-debit notice window elapses, when revenue becomes settled, and how rejected
|
|
37
39
|
requests behave.
|
|
38
40
|
|
|
39
|
-
The current public API chooses the band from `amount_minor`;
|
|
40
|
-
|
|
41
|
-
merchant needs immediate on-chain finality, the payment amount
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
41
|
+
The current public API chooses the band from `amount_minor`; JPY 500-and-under /
|
|
42
|
+
USD 3-and-under payments are routed to Micro / Nano delayed aggregated
|
|
43
|
+
settlement. If a merchant needs immediate on-chain finality, the payment amount
|
|
44
|
+
must be in the Standard band. In practice, do not offer JPY 500-and-under or
|
|
45
|
+
USD 3-and-under items for a product that cannot accept Micro / Nano delayed
|
|
46
|
+
aggregated settlement. For public one-time Direct Payment / Hosted Checkout,
|
|
47
|
+
`amount_minor` is a positive integer in minor currency units. That means the
|
|
48
|
+
smallest public one-time checkout amount is JPY 1 or USD 0.01. Nano Payment on
|
|
49
|
+
this public path therefore means JPY 1-49 or USD 0.01-0.30. Sub-minor Nano
|
|
50
|
+
protocol fees are settlement-accounting amounts, not externally submitted
|
|
51
|
+
one-time item prices.
|
|
48
52
|
|
|
49
53
|
For the operational statement APIs, CSV export, buyer past-due blocks, and the
|
|
50
54
|
field-by-field meaning of `scheduled_debit_at`, `not_before_attempt_at`,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import express from "express";
|
|
2
2
|
import {
|
|
3
|
+
classifyDirectPaymentConfirmation,
|
|
3
4
|
DirectRequestPaymentMerchantClient,
|
|
4
5
|
verifyDirectRequestPaymentWebhook,
|
|
5
6
|
} from "@siglume/direct-request-payment";
|
|
@@ -22,45 +23,54 @@ app.use((req, res, next) => {
|
|
|
22
23
|
|
|
23
24
|
const orders = new Map<string, any>();
|
|
24
25
|
|
|
25
|
-
async function
|
|
26
|
-
|
|
26
|
+
async function flagForPaymentStateReview(payload: Record<string, any>): Promise<void> {
|
|
27
|
+
console.warn("payment state review required", payload);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function handleDirectPaymentConfirmed(event: any): Promise<void> {
|
|
31
|
+
const classification = classifyDirectPaymentConfirmation(event);
|
|
32
|
+
|
|
33
|
+
if (classification.kind === "metered_batch_settled") {
|
|
27
34
|
// Aggregated Micro/Nano settlement events do not carry an order challenge.
|
|
28
35
|
// Reconcile them against statement / settlement batch data instead.
|
|
29
|
-
|
|
30
|
-
console.log("settled metered batch", data.settlement_batch_id || data.usage_event_digest);
|
|
31
|
-
}
|
|
36
|
+
console.log("settled metered batch", classification.settlement_batch_id, classification.chain_receipt_id);
|
|
32
37
|
return;
|
|
33
38
|
}
|
|
34
39
|
|
|
35
|
-
if (
|
|
36
|
-
|
|
37
|
-
data.finality === "per_payment_onchain" &&
|
|
38
|
-
data.settlement_status === "settled"
|
|
39
|
-
) {
|
|
40
|
-
const challengeHash = String(data.challenge_hash || "");
|
|
41
|
-
const order = [...orders.values()].find((item) => item.siglume_challenge_hash === challengeHash);
|
|
40
|
+
if (classification.kind === "standard_settled") {
|
|
41
|
+
const order = [...orders.values()].find((item) => item.siglume_challenge_hash === classification.challenge_hash);
|
|
42
42
|
if (order) {
|
|
43
43
|
order.siglume_payment_status = "paid";
|
|
44
|
-
order.siglume_requirement_id =
|
|
45
|
-
order.siglume_chain_receipt_id =
|
|
44
|
+
order.siglume_requirement_id = classification.requirement_id;
|
|
45
|
+
order.siglume_chain_receipt_id = classification.chain_receipt_id;
|
|
46
|
+
} else {
|
|
47
|
+
await flagForPaymentStateReview({
|
|
48
|
+
reason: "unknown_challenge_hash",
|
|
49
|
+
requirement_id: classification.requirement_id,
|
|
50
|
+
});
|
|
46
51
|
}
|
|
47
52
|
return;
|
|
48
53
|
}
|
|
49
54
|
|
|
50
|
-
if (
|
|
51
|
-
const
|
|
52
|
-
const order = [...orders.values()].find((item) => item.siglume_challenge_hash === challengeHash);
|
|
55
|
+
if (classification.kind === "metered_usage_accepted") {
|
|
56
|
+
const order = [...orders.values()].find((item) => item.siglume_challenge_hash === classification.challenge_hash);
|
|
53
57
|
if (order) {
|
|
54
58
|
order.siglume_payment_status = "fulfilled_unsettled";
|
|
55
|
-
order.siglume_requirement_id =
|
|
59
|
+
order.siglume_requirement_id = classification.requirement_id;
|
|
60
|
+
} else {
|
|
61
|
+
await flagForPaymentStateReview({
|
|
62
|
+
reason: "unknown_metered_challenge_hash",
|
|
63
|
+
requirement_id: classification.requirement_id,
|
|
64
|
+
});
|
|
56
65
|
}
|
|
57
66
|
return;
|
|
58
67
|
}
|
|
59
68
|
|
|
60
69
|
// Unknown or older payload shape: do not mark paid from the event name alone.
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
requirement_id:
|
|
70
|
+
await flagForPaymentStateReview({
|
|
71
|
+
reason: classification.reason,
|
|
72
|
+
requirement_id: classification.requirement_id,
|
|
73
|
+
settlement_batch_id: classification.settlement_batch_id,
|
|
64
74
|
});
|
|
65
75
|
}
|
|
66
76
|
|
|
@@ -111,7 +121,7 @@ app.post("/siglume/webhook", express.raw({ type: "application/json" }), asyncRou
|
|
|
111
121
|
);
|
|
112
122
|
|
|
113
123
|
if (event.type === "direct_payment.confirmed") {
|
|
114
|
-
await handleDirectPaymentConfirmed(event
|
|
124
|
+
await handleDirectPaymentConfirmed(event);
|
|
115
125
|
}
|
|
116
126
|
|
|
117
127
|
res.status(204).send();
|