@siglume/direct-request-payment 0.4.18 → 0.4.19

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.
@@ -1,176 +1,145 @@
1
- # 10-Minute First Test Payment
1
+ # 10-Minute Product Integration
2
2
 
3
- This guide is the shortest supported path to one **Standard Payment** test
4
- through Siglume wallet Hosted Checkout. It is not a full production launch.
3
+ This guide is the supported 10-minute path for adding SDRP Hosted Checkout to
4
+ an existing product. The goal is to
5
+ add two routes to your own server:
5
6
 
6
- ## What the 10 minutes cover
7
+ - `POST /payments/checkout/siglume/start`
8
+ - `POST /payments/webhooks/siglume`
7
9
 
8
- You can count the 10 minutes only after the prerequisites below are already
9
- ready. The target outcome is:
10
+ The SDK supplies the readiness check, route files, webhook verification, payment
11
+ classification, and the order-store adapter contract. Your app supplies the
12
+ real order lookup and fulfillment writes.
10
13
 
11
- - one Standard-band order,
12
- - one Hosted Checkout session,
13
- - one signed `direct_payment.confirmed` webhook,
14
- - one idempotent local fulfillment decision.
14
+ ## 0. Readiness first
15
15
 
16
- This guide does **not** cover production monitoring, refunds, subscriptions,
17
- scheduled autopay, game entitlement recovery, or Micro / Nano accounting.
16
+ Install the SDK in your product.
18
17
 
19
- ## Prerequisites
18
+ Node / Express:
20
19
 
21
- Before starting, confirm:
20
+ ```bash
21
+ npm install @siglume/direct-request-payment
22
+ ```
23
+
24
+ Python / FastAPI:
25
+
26
+ ```bash
27
+ pip install siglume-direct-request-payment
28
+ ```
22
29
 
23
- - You have a Siglume merchant account and merchant Siglume bearer token.
24
- - Hosted Checkout is enabled for that merchant account.
25
- - The merchant billing mandate is active, including any required wallet
26
- approval.
27
- - You have a public HTTPS webhook URL that can receive the raw request body.
28
- - Your checkout return URL origin is known and can be registered.
29
- - The buyer has a Siglume wallet funded in the settlement token for the test
30
- market: JPYC for JPY, USDC for USD.
31
- - Your order amount is in the Standard band: JPY 501+ or USD 3.01+.
30
+ Set these environment variables in your app or `.env`:
32
31
 
33
- If Hosted Checkout is not enabled, stop here. The SDK raises
34
- `HostedCheckoutNotAvailableError` for rollout 404/409 responses; contact
35
- Siglume support or your Siglume account contact to enable the account before
36
- continuing with a human web checkout.
32
+ ```bash
33
+ SIGLUME_MERCHANT_AUTH_TOKEN=<merchant Siglume bearer token>
34
+ SIGLUME_DIRECT_PAYMENT_MERCHANT=<merchant key>
35
+ SHOP_PUBLIC_ORIGIN=https://www.your-product.example
36
+ SHOP_WEBHOOK_URL=https://api.your-product.example/payments/webhooks/siglume
37
+ ```
37
38
 
38
- ## 1. Install
39
+ Then run:
39
40
 
40
- Runnable starter directories are available if you want a small server to edit:
41
+ ```bash
42
+ npx siglume-check readiness
43
+ ```
41
44
 
42
- - [TypeScript Express starter](../examples/hosted-checkout-typescript)
43
- - [Python Flask starter](../examples/hosted-checkout-python)
45
+ The readiness check fails before you write checkout code if any required item is
46
+ missing. It checks local config, reads the merchant account, and creates one
47
+ unpaid expiring Hosted Checkout probe session to prove the account is enabled
48
+ and the return origin is accepted. No buyer is charged.
44
49
 
45
- For an existing app, install the SDK directly:
50
+ For CI or a preflight script:
46
51
 
47
52
  ```bash
48
- npm install @siglume/direct-request-payment
53
+ npx siglume-check readiness --json
49
54
  ```
50
55
 
51
- or:
56
+ If this command fails, fix the reported item first. Do not build a human web
57
+ checkout path until readiness passes.
58
+
59
+ ## 1. Copy integration files into your product
60
+
61
+ For Express:
52
62
 
53
63
  ```bash
54
- pip install siglume-direct-request-payment
64
+ npx siglume-sdrp init express --target src/siglume
55
65
  ```
56
66
 
57
- ## 2. Set environment variables
67
+ For FastAPI:
58
68
 
59
69
  ```bash
60
- SIGLUME_MERCHANT_AUTH_TOKEN=<merchant Siglume bearer token>
61
- SIGLUME_DIRECT_PAYMENT_MERCHANT=example_merchant
62
- SHOP_PUBLIC_ORIGIN=https://www.example.com
63
- SHOP_WEBHOOK_URL=https://api.example.com/siglume/webhook
70
+ siglume-sdrp init fastapi --target app/siglume
64
71
  ```
65
72
 
66
- Do not use a Developer Portal `cli_` API key. Merchant setup requires the
67
- merchant's Siglume bearer token.
73
+ These commands copy framework-specific route files into your codebase. The
74
+ generated files are intentionally small and are meant to be edited.
68
75
 
69
- ## 3. Register merchant settings
76
+ ## 2. Mount the routes
70
77
 
71
- Run setup once from your server, CI, or integration machine:
78
+ Express:
72
79
 
73
80
  ```ts
74
- import { DirectRequestPaymentMerchantClient } from "@siglume/direct-request-payment";
81
+ import { createSiglumeSdrpRouter } from "./siglume/siglume-sdrp-routes.js";
82
+ import { siglumeOrderStore } from "./siglume/siglume-order-store.example.js";
75
83
 
76
- const merchant = new DirectRequestPaymentMerchantClient({
77
- auth_token: process.env.SIGLUME_MERCHANT_AUTH_TOKEN!,
78
- });
79
-
80
- const setup = await merchant.setupCheckout({
84
+ app.use("/payments", createSiglumeSdrpRouter({
81
85
  merchant: process.env.SIGLUME_DIRECT_PAYMENT_MERCHANT!,
82
- display_name: "Example Merchant",
83
- billing_plan: "launch",
84
- billing_currency: "JPY",
85
- webhook_callback_url: process.env.SHOP_WEBHOOK_URL!,
86
- checkout_allowed_origins: [process.env.SHOP_PUBLIC_ORIGIN!],
87
- });
88
-
89
- console.log(setup.env.SIGLUME_DIRECT_PAYMENT_MERCHANT);
86
+ merchant_auth_token: process.env.SIGLUME_MERCHANT_AUTH_TOKEN!,
87
+ webhook_secret: process.env.SIGLUME_WEBHOOK_SECRET!,
88
+ shop_public_origin: process.env.SHOP_PUBLIC_ORIGIN!,
89
+ order_store: siglumeOrderStore,
90
+ }));
90
91
  ```
91
92
 
92
- Store the returned `SIGLUME_DIRECT_PAYMENT_CHALLENGE_SECRET` and
93
- `SIGLUME_WEBHOOK_SECRET` in a server-side secret store. Secret values are
94
- returned only when created or rotated.
95
-
96
- ## 4. Create a Standard checkout session
93
+ FastAPI:
97
94
 
98
- For each order, create the order on your server first. Then create a Hosted
99
- Checkout session:
95
+ ```py
96
+ from .siglume.siglume_order_store_example import ExampleSiglumeOrderStore
97
+ from .siglume.siglume_sdrp_routes import create_siglume_sdrp_router
100
98
 
101
- ```ts
102
- const session = await merchant.createCheckoutSession({
103
- merchant: process.env.SIGLUME_DIRECT_PAYMENT_MERCHANT!,
104
- amount_minor: 1200,
105
- currency: "JPY",
106
- nonce: "order_123-attempt_1",
107
- success_url: `${process.env.SHOP_PUBLIC_ORIGIN}/thanks`,
108
- cancel_url: `${process.env.SHOP_PUBLIC_ORIGIN}/cart`,
109
- metadata: { order_id: "order_123" },
110
- });
111
-
112
- await orders.update("order_123", {
113
- siglume_challenge_hash: session.challenge_hash,
114
- siglume_checkout_session_id: session.session_id,
115
- siglume_payment_status: "pending",
116
- });
117
-
118
- redirect(session.checkout_url);
99
+ app.include_router(
100
+ create_siglume_sdrp_router(ExampleSiglumeOrderStore()),
101
+ prefix="/payments",
102
+ )
119
103
  ```
120
104
 
121
- The browser must never choose the amount, currency, nonce, or return URL. The
122
- session is single-use and expires.
105
+ ## 3. Replace the order-store example
123
106
 
124
- ## 5. Fulfill from the signed webhook
107
+ Replace the example store with your product's order database. The adapter must:
125
108
 
126
- The browser return path is not the source of truth. Use the signed webhook and
127
- classify the confirmation:
109
+ - load the order by your `order_id`,
110
+ - verify the current user is allowed to pay for that order,
111
+ - return the server-authored `amount_minor` and `currency`,
112
+ - persist `challenge_hash` and `checkout_session_id` before redirecting,
113
+ - record webhook event ids durably,
114
+ - mark Standard orders paid exactly once,
115
+ - mark Micro / Nano orders as fulfilled but unsettled exactly once,
116
+ - route unknown classifications to manual review.
128
117
 
129
- ```ts
130
- import {
131
- classifyDirectPaymentConfirmation,
132
- verifyDirectRequestPaymentWebhook,
133
- } from "@siglume/direct-request-payment";
134
-
135
- const { event } = await verifyDirectRequestPaymentWebhook(
136
- process.env.SIGLUME_WEBHOOK_SECRET!,
137
- rawRequestBody,
138
- siglumeSignatureHeader,
139
- );
140
-
141
- if (event.type === "direct_payment.confirmed") {
142
- const confirmation = classifyDirectPaymentConfirmation(event);
143
-
144
- if (confirmation.kind === "standard_settled") {
145
- await orders.markPaidOnceByChallengeHash(confirmation.challenge_hash, {
146
- requirement_id: confirmation.requirement_id,
147
- chain_receipt_id: confirmation.chain_receipt_id,
148
- });
149
- } else if (confirmation.kind === "metered_usage_accepted") {
150
- await orders.markFulfilledButUnsettledOnceByChallengeHash(
151
- confirmation.challenge_hash,
152
- { requirement_id: confirmation.requirement_id },
153
- );
154
- } else {
155
- await orders.flagForPaymentStateReview(confirmation);
156
- }
157
- }
118
+ Do not calculate the amount from browser input.
119
+
120
+ ## 4. Start checkout from your frontend
121
+
122
+ Call your own server route:
123
+
124
+ ```bash
125
+ curl -X POST https://api.your-product.example/payments/checkout/siglume/start \
126
+ -H "content-type: application/json" \
127
+ -d "{\"order_id\":\"order_123\"}"
158
128
  ```
159
129
 
160
- For this 10-minute guide, keep the test order in the Standard band so the
161
- expected successful branch is `standard_settled`.
130
+ Redirect the shopper to the returned `checkout_url`.
162
131
 
163
- ## Done means
132
+ ## 5. Done means
164
133
 
165
- You are done with the quickstart when:
134
+ Your product is integrated when:
166
135
 
167
- - the checkout session is created,
168
- - the buyer reaches the Siglume wallet hosted checkout page,
136
+ - `npx siglume-check readiness` passes,
137
+ - your product has mounted checkout and webhook routes,
138
+ - your order database stores `challenge_hash` for the order,
169
139
  - the signed webhook verifies against the raw body,
170
- - `classifyDirectPaymentConfirmation(event)` returns `standard_settled`,
171
- - your order is marked paid once, keyed by the stored `challenge_hash`.
140
+ - `standard_settled` marks the order paid once,
141
+ - `metered_usage_accepted` uses a separate fulfilled-but-unsettled state.
172
142
 
173
- Before production, complete the full checklist in
174
- [Merchant Quickstart](./merchant-quickstart.md#go-live-checklist), read
175
- [Payment lifecycle](./payment-lifecycle.md), and prepare the failure handling in
176
- [Troubleshooting](./troubleshooting.md).
143
+ For Micro / Nano revenue reconciliation, read
144
+ [Payment lifecycle](./payment-lifecycle.md) and
145
+ [Micro / Nano Statements and Notices](./metered-statements.md).
@@ -10,6 +10,14 @@ Siglume account contact.
10
10
  Hosted Checkout is enabled account by account during beta. Check this before
11
11
  building a human web checkout:
12
12
 
13
+ ```bash
14
+ npx siglume-check readiness
15
+ ```
16
+
17
+ The command validates local configuration, reads the merchant account, and
18
+ creates one unpaid expiring checkout session to prove Hosted Checkout is
19
+ available for this merchant account.
20
+
13
21
  - The merchant account exists.
14
22
  - The merchant billing mandate is active.
15
23
  - The webhook callback URL is HTTPS and reachable.
@@ -5,5 +5,5 @@ requires-python = ">=3.11"
5
5
  dependencies = [
6
6
  "Flask>=3.0,<4",
7
7
  "python-dotenv>=1.0,<2",
8
- "siglume-direct-request-payment>=0.4.18,<0.5",
8
+ "siglume-direct-request-payment>=0.4.19,<0.5",
9
9
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@siglume/direct-request-payment",
3
- "version": "0.4.18",
3
+ "version": "0.4.19",
4
4
  "description": "SDK for the Siglume Direct Request Payment SDRP payment protocol",
5
5
  "keywords": [
6
6
  "siglume",
@@ -33,6 +33,10 @@
33
33
  "main": "./dist/index.cjs",
34
34
  "module": "./dist/index.js",
35
35
  "types": "./dist/index.d.ts",
36
+ "bin": {
37
+ "siglume-sdrp": "./bin/siglume-sdrp.mjs",
38
+ "siglume-check": "./bin/siglume-sdrp.mjs"
39
+ },
36
40
  "exports": {
37
41
  ".": {
38
42
  "types": "./dist/index.d.ts",
@@ -52,11 +56,15 @@
52
56
  },
53
57
  "files": [
54
58
  "dist",
59
+ "bin",
55
60
  "docs",
56
61
  "examples",
62
+ "templates",
57
63
  "!examples/**/node_modules",
58
64
  "!examples/**/__pycache__",
59
65
  "!examples/**/*.pyc",
66
+ "!templates/**/__pycache__",
67
+ "!templates/**/*.pyc",
60
68
  "README.md",
61
69
  "CHANGELOG.md",
62
70
  "LICENSE"
@@ -0,0 +1,22 @@
1
+ # Express Integration Files
2
+
3
+ Mount the router in your existing app:
4
+
5
+ ```ts
6
+ import { createSiglumeSdrpRouter } from "./siglume/siglume-sdrp-routes.js";
7
+ import { siglumeOrderStore } from "./siglume/siglume-order-store.example.js";
8
+
9
+ app.use("/payments", createSiglumeSdrpRouter({
10
+ merchant: process.env.SIGLUME_DIRECT_PAYMENT_MERCHANT!,
11
+ merchant_auth_token: process.env.SIGLUME_MERCHANT_AUTH_TOKEN!,
12
+ webhook_secret: process.env.SIGLUME_WEBHOOK_SECRET!,
13
+ shop_public_origin: process.env.SHOP_PUBLIC_ORIGIN!,
14
+ order_store: siglumeOrderStore,
15
+ }));
16
+ ```
17
+
18
+ Replace `siglume-order-store.example.ts` with your real order database adapter.
19
+ The route paths become:
20
+
21
+ - `POST /payments/checkout/siglume/start`
22
+ - `POST /payments/webhooks/siglume`
@@ -0,0 +1,53 @@
1
+ import type { Request } from "express";
2
+
3
+ import type { SiglumeCheckoutOrder, SiglumeSdrpOrderStore } from "./siglume-sdrp-routes.js";
4
+
5
+ type Order = SiglumeCheckoutOrder & {
6
+ status: "created" | "pending" | "paid" | "fulfilled_unsettled" | "review_required";
7
+ challenge_hash?: string;
8
+ checkout_session_id?: string;
9
+ requirement_id?: string;
10
+ chain_receipt_id?: string;
11
+ };
12
+
13
+ const orders = new Map<string, Order>([
14
+ ["order_123", { id: "order_123", amount_minor: 1200, currency: "JPY", status: "created" }],
15
+ ]);
16
+ const processedEvents = new Set<string>();
17
+
18
+ export const siglumeOrderStore: SiglumeSdrpOrderStore = {
19
+ async getOrderForCheckout(orderId: string, _req: Request) {
20
+ return orders.get(orderId) || null;
21
+ },
22
+ async markCheckoutPending(input) {
23
+ const order = orders.get(input.order_id);
24
+ if (!order) return;
25
+ order.status = "pending";
26
+ order.challenge_hash = input.challenge_hash;
27
+ order.checkout_session_id = input.checkout_session_id;
28
+ },
29
+ async recordWebhookEventOnce(eventId) {
30
+ if (processedEvents.has(eventId)) return false;
31
+ processedEvents.add(eventId);
32
+ return true;
33
+ },
34
+ async findOrderByChallengeHash(challengeHash) {
35
+ return [...orders.values()].find((order) => order.challenge_hash === challengeHash) || null;
36
+ },
37
+ async markOrderPaidOnce(input) {
38
+ const order = orders.get(input.order_id);
39
+ if (!order || order.status === "paid") return;
40
+ order.status = "paid";
41
+ order.requirement_id = input.requirement_id;
42
+ order.chain_receipt_id = input.chain_receipt_id;
43
+ },
44
+ async markOrderFulfilledUnsettledOnce(input) {
45
+ const order = orders.get(input.order_id);
46
+ if (!order || order.status === "fulfilled_unsettled") return;
47
+ order.status = "fulfilled_unsettled";
48
+ order.requirement_id = input.requirement_id;
49
+ },
50
+ async flagPaymentReview(input) {
51
+ console.warn("payment review required", input);
52
+ },
53
+ };
@@ -0,0 +1,157 @@
1
+ import express from "express";
2
+ import {
3
+ classifyDirectPaymentConfirmation,
4
+ DirectRequestPaymentMerchantClient,
5
+ HostedCheckoutNotAvailableError,
6
+ verifyDirectRequestPaymentWebhook,
7
+ type DirectRequestPaymentCurrency,
8
+ } from "@siglume/direct-request-payment";
9
+
10
+ export interface SiglumeCheckoutOrder {
11
+ id: string;
12
+ amount_minor: number;
13
+ currency: DirectRequestPaymentCurrency | string;
14
+ }
15
+
16
+ export interface SiglumeSdrpOrderStore {
17
+ getOrderForCheckout(orderId: string, req: express.Request): Promise<SiglumeCheckoutOrder | null>;
18
+ markCheckoutPending(input: {
19
+ order_id: string;
20
+ challenge_hash: string;
21
+ checkout_session_id: string;
22
+ }): Promise<void>;
23
+ recordWebhookEventOnce(eventId: string): Promise<boolean>;
24
+ findOrderByChallengeHash(challengeHash: string): Promise<{ id: string } | null>;
25
+ markOrderPaidOnce(input: {
26
+ order_id: string;
27
+ requirement_id: string;
28
+ chain_receipt_id: string;
29
+ }): Promise<void>;
30
+ markOrderFulfilledUnsettledOnce(input: {
31
+ order_id: string;
32
+ requirement_id: string;
33
+ pricing_band: string;
34
+ }): Promise<void>;
35
+ flagPaymentReview(input: Record<string, unknown>): Promise<void>;
36
+ }
37
+
38
+ export interface SiglumeSdrpRouterOptions {
39
+ merchant: string;
40
+ merchant_auth_token: string;
41
+ webhook_secret: string;
42
+ shop_public_origin: string;
43
+ order_store: SiglumeSdrpOrderStore;
44
+ }
45
+
46
+ export function createSiglumeSdrpRouter(options: SiglumeSdrpRouterOptions): express.Router {
47
+ const router = express.Router();
48
+ const merchant = new DirectRequestPaymentMerchantClient({
49
+ auth_token: options.merchant_auth_token,
50
+ });
51
+
52
+ router.post("/checkout/siglume/start", express.json(), async (req, res, next) => {
53
+ try {
54
+ const orderId = String(req.body?.order_id || "");
55
+ const order = await options.order_store.getOrderForCheckout(orderId, req);
56
+ if (!order) {
57
+ res.status(404).json({ error: "order_not_found" });
58
+ return;
59
+ }
60
+
61
+ const session = await merchant.createCheckoutSession({
62
+ merchant: options.merchant,
63
+ amount_minor: order.amount_minor,
64
+ currency: order.currency,
65
+ nonce: `${order.id}-attempt_${Date.now()}`,
66
+ success_url: `${options.shop_public_origin}/checkout/siglume/success`,
67
+ cancel_url: `${options.shop_public_origin}/checkout/siglume/cancel`,
68
+ metadata: { order_id: order.id },
69
+ });
70
+
71
+ await options.order_store.markCheckoutPending({
72
+ order_id: order.id,
73
+ challenge_hash: session.challenge_hash,
74
+ checkout_session_id: session.session_id,
75
+ });
76
+
77
+ res.json({ checkout_url: session.checkout_url, session_id: session.session_id });
78
+ } catch (error) {
79
+ next(error);
80
+ }
81
+ });
82
+
83
+ router.post("/webhooks/siglume", express.raw({ type: "application/json" }), async (req, res, next) => {
84
+ try {
85
+ const { event } = await verifyDirectRequestPaymentWebhook(
86
+ options.webhook_secret,
87
+ req.body,
88
+ req.header("siglume-signature") || "",
89
+ );
90
+
91
+ if (!(await options.order_store.recordWebhookEventOnce(event.id))) {
92
+ res.status(204).send();
93
+ return;
94
+ }
95
+
96
+ if (event.type === "direct_payment.confirmed") {
97
+ const confirmation = classifyDirectPaymentConfirmation(event);
98
+
99
+ if (confirmation.kind === "standard_settled") {
100
+ const order = await options.order_store.findOrderByChallengeHash(confirmation.challenge_hash);
101
+ if (order) {
102
+ await options.order_store.markOrderPaidOnce({
103
+ order_id: order.id,
104
+ requirement_id: confirmation.requirement_id,
105
+ chain_receipt_id: confirmation.chain_receipt_id,
106
+ });
107
+ } else {
108
+ await options.order_store.flagPaymentReview({
109
+ reason: "unknown_challenge_hash",
110
+ requirement_id: confirmation.requirement_id,
111
+ });
112
+ }
113
+ } else if (confirmation.kind === "metered_usage_accepted") {
114
+ const order = await options.order_store.findOrderByChallengeHash(confirmation.challenge_hash);
115
+ if (order) {
116
+ await options.order_store.markOrderFulfilledUnsettledOnce({
117
+ order_id: order.id,
118
+ requirement_id: confirmation.requirement_id,
119
+ pricing_band: confirmation.pricing_band,
120
+ });
121
+ } else {
122
+ await options.order_store.flagPaymentReview({
123
+ reason: "unknown_metered_challenge_hash",
124
+ requirement_id: confirmation.requirement_id,
125
+ });
126
+ }
127
+ } else if (confirmation.kind === "metered_batch_settled") {
128
+ await options.order_store.flagPaymentReview({
129
+ reason: "metered_batch_settled_reconcile_statement_api",
130
+ settlement_batch_id: confirmation.settlement_batch_id,
131
+ chain_receipt_id: confirmation.chain_receipt_id,
132
+ });
133
+ } else {
134
+ await options.order_store.flagPaymentReview({
135
+ reason: confirmation.reason,
136
+ requirement_id: confirmation.requirement_id,
137
+ settlement_batch_id: confirmation.settlement_batch_id,
138
+ });
139
+ }
140
+ }
141
+
142
+ res.status(204).send();
143
+ } catch (error) {
144
+ next(error);
145
+ }
146
+ });
147
+
148
+ router.use((error: unknown, _req: express.Request, res: express.Response, next: express.NextFunction) => {
149
+ if (error instanceof HostedCheckoutNotAvailableError) {
150
+ res.status(409).json({ error: "hosted_checkout_not_enabled" });
151
+ return;
152
+ }
153
+ next(error);
154
+ });
155
+
156
+ return router;
157
+ }
@@ -0,0 +1,22 @@
1
+ # FastAPI Integration Files
2
+
3
+ Mount the router in your existing app:
4
+
5
+ ```py
6
+ from fastapi import FastAPI
7
+
8
+ from .siglume.siglume_order_store_example import ExampleSiglumeOrderStore
9
+ from .siglume.siglume_sdrp_routes import create_siglume_sdrp_router
10
+
11
+ app = FastAPI()
12
+ app.include_router(
13
+ create_siglume_sdrp_router(ExampleSiglumeOrderStore()),
14
+ prefix="/payments",
15
+ )
16
+ ```
17
+
18
+ Replace `siglume_order_store_example.py` with your real order database adapter.
19
+ The route paths become:
20
+
21
+ - `POST /payments/checkout/siglume/start`
22
+ - `POST /payments/webhooks/siglume`
@@ -0,0 +1,54 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from fastapi import Request
6
+
7
+ _orders: dict[str, dict[str, Any]] = {
8
+ "order_123": {"id": "order_123", "amount_minor": 1200, "currency": "JPY", "status": "created"}
9
+ }
10
+ _processed_events: set[str] = set()
11
+
12
+
13
+ class ExampleSiglumeOrderStore:
14
+ async def get_order_for_checkout(self, order_id: str, request: Request) -> dict[str, Any] | None:
15
+ return _orders.get(order_id)
16
+
17
+ async def mark_checkout_pending(self, *, order_id: str, challenge_hash: str, checkout_session_id: str) -> None:
18
+ order = _orders.get(order_id)
19
+ if not order:
20
+ return
21
+ order["status"] = "pending"
22
+ order["challenge_hash"] = challenge_hash
23
+ order["checkout_session_id"] = checkout_session_id
24
+
25
+ async def record_webhook_event_once(self, event_id: str) -> bool:
26
+ if event_id in _processed_events:
27
+ return False
28
+ _processed_events.add(event_id)
29
+ return True
30
+
31
+ async def find_order_by_challenge_hash(self, challenge_hash: str) -> dict[str, Any] | None:
32
+ for order in _orders.values():
33
+ if order.get("challenge_hash") == challenge_hash:
34
+ return order
35
+ return None
36
+
37
+ async def mark_order_paid_once(self, *, order_id: str, requirement_id: str, chain_receipt_id: str) -> None:
38
+ order = _orders.get(order_id)
39
+ if not order or order.get("status") == "paid":
40
+ return
41
+ order["status"] = "paid"
42
+ order["requirement_id"] = requirement_id
43
+ order["chain_receipt_id"] = chain_receipt_id
44
+
45
+ async def mark_order_fulfilled_unsettled_once(self, *, order_id: str, requirement_id: str, pricing_band: str) -> None:
46
+ order = _orders.get(order_id)
47
+ if not order or order.get("status") == "fulfilled_unsettled":
48
+ return
49
+ order["status"] = "fulfilled_unsettled"
50
+ order["requirement_id"] = requirement_id
51
+ order["pricing_band"] = pricing_band
52
+
53
+ async def flag_payment_review(self, data: dict[str, Any]) -> None:
54
+ print("payment review required", data)