@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.
@@ -0,0 +1,107 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import time
5
+ from typing import Any, Protocol
6
+
7
+ from fastapi import APIRouter, Request
8
+ from fastapi.responses import JSONResponse, Response
9
+ from siglume_direct_request_payment import (
10
+ DirectRequestPaymentMerchantClient,
11
+ HostedCheckoutNotAvailableError,
12
+ classify_direct_payment_confirmation,
13
+ verify_direct_request_payment_webhook,
14
+ )
15
+
16
+
17
+ class SiglumeSdrpOrderStore(Protocol):
18
+ async def get_order_for_checkout(self, order_id: str, request: Request) -> dict[str, Any] | None: ...
19
+ async def mark_checkout_pending(self, *, order_id: str, challenge_hash: str, checkout_session_id: str) -> None: ...
20
+ async def record_webhook_event_once(self, event_id: str) -> bool: ...
21
+ async def find_order_by_challenge_hash(self, challenge_hash: str) -> dict[str, Any] | None: ...
22
+ async def mark_order_paid_once(self, *, order_id: str, requirement_id: str, chain_receipt_id: str) -> None: ...
23
+ async def mark_order_fulfilled_unsettled_once(self, *, order_id: str, requirement_id: str, pricing_band: str) -> None: ...
24
+ async def flag_payment_review(self, data: dict[str, Any]) -> None: ...
25
+
26
+
27
+ def create_siglume_sdrp_router(order_store: SiglumeSdrpOrderStore) -> APIRouter:
28
+ router = APIRouter()
29
+ merchant_key = os.environ["SIGLUME_DIRECT_PAYMENT_MERCHANT"]
30
+ shop_origin = os.environ["SHOP_PUBLIC_ORIGIN"]
31
+ merchant = DirectRequestPaymentMerchantClient(
32
+ auth_token=os.environ["SIGLUME_MERCHANT_AUTH_TOKEN"],
33
+ )
34
+
35
+ @router.post("/checkout/siglume/start")
36
+ async def start_checkout(request: Request) -> JSONResponse:
37
+ body = await request.json()
38
+ order_id = str(body.get("order_id") or "")
39
+ order = await order_store.get_order_for_checkout(order_id, request)
40
+ if not order:
41
+ return JSONResponse({"error": "order_not_found"}, status_code=404)
42
+
43
+ try:
44
+ session = merchant.create_checkout_session(
45
+ merchant=merchant_key,
46
+ amount_minor=int(order["amount_minor"]),
47
+ currency=str(order["currency"]),
48
+ nonce=f"{order['id']}-attempt_{int(time.time() * 1000)}",
49
+ success_url=f"{shop_origin}/checkout/siglume/success",
50
+ cancel_url=f"{shop_origin}/checkout/siglume/cancel",
51
+ metadata={"order_id": order["id"]},
52
+ )
53
+ except HostedCheckoutNotAvailableError:
54
+ return JSONResponse({"error": "hosted_checkout_not_enabled"}, status_code=409)
55
+
56
+ await order_store.mark_checkout_pending(
57
+ order_id=str(order["id"]),
58
+ challenge_hash=session["challenge_hash"],
59
+ checkout_session_id=session["session_id"],
60
+ )
61
+ return JSONResponse({"checkout_url": session["checkout_url"], "session_id": session["session_id"]})
62
+
63
+ @router.post("/webhooks/siglume")
64
+ async def siglume_webhook(request: Request) -> Response:
65
+ event = verify_direct_request_payment_webhook(
66
+ os.environ["SIGLUME_WEBHOOK_SECRET"],
67
+ await request.body(),
68
+ request.headers.get("Siglume-Signature", ""),
69
+ )["event"]
70
+
71
+ if not await order_store.record_webhook_event_once(str(event["id"])):
72
+ return Response(status_code=204)
73
+
74
+ if event["type"] == "direct_payment.confirmed":
75
+ confirmation = classify_direct_payment_confirmation(event)
76
+ if confirmation["kind"] == "standard_settled":
77
+ order = await order_store.find_order_by_challenge_hash(confirmation["challenge_hash"])
78
+ if order:
79
+ await order_store.mark_order_paid_once(
80
+ order_id=str(order["id"]),
81
+ requirement_id=confirmation["requirement_id"],
82
+ chain_receipt_id=confirmation["chain_receipt_id"],
83
+ )
84
+ else:
85
+ await order_store.flag_payment_review({
86
+ "reason": "unknown_challenge_hash",
87
+ "requirement_id": confirmation["requirement_id"],
88
+ })
89
+ elif confirmation["kind"] == "metered_usage_accepted":
90
+ order = await order_store.find_order_by_challenge_hash(confirmation["challenge_hash"])
91
+ if order:
92
+ await order_store.mark_order_fulfilled_unsettled_once(
93
+ order_id=str(order["id"]),
94
+ requirement_id=confirmation["requirement_id"],
95
+ pricing_band=confirmation["pricing_band"],
96
+ )
97
+ else:
98
+ await order_store.flag_payment_review({
99
+ "reason": "unknown_metered_challenge_hash",
100
+ "requirement_id": confirmation["requirement_id"],
101
+ })
102
+ else:
103
+ await order_store.flag_payment_review(dict(confirmation))
104
+
105
+ return Response(status_code=204)
106
+
107
+ return router