@siglume/direct-request-payment 0.4.19 → 0.4.20

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.
@@ -13,14 +13,28 @@ export interface SiglumeCheckoutOrder {
13
13
  currency: DirectRequestPaymentCurrency | string;
14
14
  }
15
15
 
16
+ export interface SiglumeCheckoutAttempt extends SiglumeCheckoutOrder {
17
+ order_id: string;
18
+ attempt_id: string;
19
+ stable_nonce: string;
20
+ checkout_url?: string;
21
+ checkout_session_id?: string;
22
+ }
23
+
16
24
  export interface SiglumeSdrpOrderStore {
17
- getOrderForCheckout(orderId: string, req: express.Request): Promise<SiglumeCheckoutOrder | null>;
25
+ beginCheckoutAttempt(orderId: string, req: express.Request): Promise<SiglumeCheckoutAttempt | null>;
18
26
  markCheckoutPending(input: {
19
27
  order_id: string;
28
+ attempt_id: string;
29
+ stable_nonce: string;
20
30
  challenge_hash: string;
21
31
  checkout_session_id: string;
32
+ checkout_url: string;
22
33
  }): Promise<void>;
23
- recordWebhookEventOnce(eventId: string): Promise<boolean>;
34
+ processWebhookEventOnce(
35
+ eventId: string,
36
+ handler: () => Promise<void>,
37
+ ): Promise<"processed" | "duplicate">;
24
38
  findOrderByChallengeHash(challengeHash: string): Promise<{ id: string } | null>;
25
39
  markOrderPaidOnce(input: {
26
40
  order_id: string;
@@ -41,9 +55,10 @@ export interface SiglumeSdrpRouterOptions {
41
55
  webhook_secret: string;
42
56
  shop_public_origin: string;
43
57
  order_store: SiglumeSdrpOrderStore;
58
+ allow_metered_payments?: boolean;
44
59
  }
45
60
 
46
- export function createSiglumeSdrpRouter(options: SiglumeSdrpRouterOptions): express.Router {
61
+ export function createSiglumeSdrpCheckoutRouter(options: SiglumeSdrpRouterOptions): express.Router {
47
62
  const router = express.Router();
48
63
  const merchant = new DirectRequestPaymentMerchantClient({
49
64
  auth_token: options.merchant_auth_token,
@@ -52,26 +67,39 @@ export function createSiglumeSdrpRouter(options: SiglumeSdrpRouterOptions): expr
52
67
  router.post("/checkout/siglume/start", express.json(), async (req, res, next) => {
53
68
  try {
54
69
  const orderId = String(req.body?.order_id || "");
55
- const order = await options.order_store.getOrderForCheckout(orderId, req);
56
- if (!order) {
70
+ const attempt = await options.order_store.beginCheckoutAttempt(orderId, req);
71
+ if (!attempt) {
57
72
  res.status(404).json({ error: "order_not_found" });
58
73
  return;
59
74
  }
60
75
 
76
+ if (!options.allow_metered_payments && !isStandardCheckoutAmount(attempt.currency, attempt.amount_minor)) {
77
+ res.status(409).json({ error: "METERED_INTEGRATION_REQUIRED" });
78
+ return;
79
+ }
80
+
81
+ if (attempt.checkout_url && attempt.checkout_session_id) {
82
+ res.json({ checkout_url: attempt.checkout_url, session_id: attempt.checkout_session_id });
83
+ return;
84
+ }
85
+
61
86
  const session = await merchant.createCheckoutSession({
62
87
  merchant: options.merchant,
63
- amount_minor: order.amount_minor,
64
- currency: order.currency,
65
- nonce: `${order.id}-attempt_${Date.now()}`,
88
+ amount_minor: attempt.amount_minor,
89
+ currency: attempt.currency,
90
+ nonce: attempt.stable_nonce,
66
91
  success_url: `${options.shop_public_origin}/checkout/siglume/success`,
67
92
  cancel_url: `${options.shop_public_origin}/checkout/siglume/cancel`,
68
- metadata: { order_id: order.id },
93
+ metadata: { order_id: attempt.order_id, attempt_id: attempt.attempt_id },
69
94
  });
70
95
 
71
96
  await options.order_store.markCheckoutPending({
72
- order_id: order.id,
97
+ order_id: attempt.order_id,
98
+ attempt_id: attempt.attempt_id,
99
+ stable_nonce: attempt.stable_nonce,
73
100
  challenge_hash: session.challenge_hash,
74
101
  checkout_session_id: session.session_id,
102
+ checkout_url: session.checkout_url,
75
103
  });
76
104
 
77
105
  res.json({ checkout_url: session.checkout_url, session_id: session.session_id });
@@ -80,7 +108,19 @@ export function createSiglumeSdrpRouter(options: SiglumeSdrpRouterOptions): expr
80
108
  }
81
109
  });
82
110
 
83
- router.post("/webhooks/siglume", express.raw({ type: "application/json" }), async (req, res, next) => {
111
+ router.use((error: unknown, _req: express.Request, res: express.Response, next: express.NextFunction) => {
112
+ if (error instanceof HostedCheckoutNotAvailableError) {
113
+ res.status(409).json({ error: "hosted_checkout_not_enabled" });
114
+ return;
115
+ }
116
+ next(error);
117
+ });
118
+
119
+ return router;
120
+ }
121
+
122
+ export function createSiglumeSdrpWebhookHandler(options: SiglumeSdrpRouterOptions): express.RequestHandler {
123
+ return async (req, res, next) => {
84
124
  try {
85
125
  const { event } = await verifyDirectRequestPaymentWebhook(
86
126
  options.webhook_secret,
@@ -88,70 +128,104 @@ export function createSiglumeSdrpRouter(options: SiglumeSdrpRouterOptions): expr
88
128
  req.header("siglume-signature") || "",
89
129
  );
90
130
 
91
- if (!(await options.order_store.recordWebhookEventOnce(event.id))) {
131
+ const result = await options.order_store.processWebhookEventOnce(event.id, async () => {
132
+ await processSiglumeWebhookEvent(options, event);
133
+ });
134
+
135
+ if (result === "duplicate") {
92
136
  res.status(204).send();
93
137
  return;
94
138
  }
95
139
 
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
140
  res.status(204).send();
143
141
  } catch (error) {
144
142
  next(error);
145
143
  }
146
- });
144
+ };
145
+ }
147
146
 
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" });
147
+ export function createSiglumeSdrpRouter(options: SiglumeSdrpRouterOptions): express.Router {
148
+ const router = createSiglumeSdrpCheckoutRouter(options);
149
+ router.post(
150
+ "/webhooks/siglume",
151
+ express.raw({ type: "application/json" }),
152
+ createSiglumeSdrpWebhookHandler(options),
153
+ );
154
+ return router;
155
+ }
156
+
157
+ async function processSiglumeWebhookEvent(
158
+ options: SiglumeSdrpRouterOptions,
159
+ event: Awaited<ReturnType<typeof verifyDirectRequestPaymentWebhook>>["event"],
160
+ ): Promise<void> {
161
+ if (event.type !== "direct_payment.confirmed") {
162
+ return;
163
+ }
164
+
165
+ const confirmation = classifyDirectPaymentConfirmation(event);
166
+
167
+ if (confirmation.kind === "standard_settled") {
168
+ const order = await options.order_store.findOrderByChallengeHash(confirmation.challenge_hash);
169
+ if (order) {
170
+ await options.order_store.markOrderPaidOnce({
171
+ order_id: order.id,
172
+ requirement_id: confirmation.requirement_id,
173
+ chain_receipt_id: confirmation.chain_receipt_id,
174
+ });
175
+ } else {
176
+ await options.order_store.flagPaymentReview({
177
+ reason: "unknown_challenge_hash",
178
+ requirement_id: confirmation.requirement_id,
179
+ });
180
+ }
181
+ return;
182
+ }
183
+
184
+ if (confirmation.kind === "metered_usage_accepted") {
185
+ if (!options.allow_metered_payments) {
186
+ await options.order_store.flagPaymentReview({
187
+ reason: "metered_integration_required",
188
+ requirement_id: confirmation.requirement_id,
189
+ pricing_band: confirmation.pricing_band,
190
+ });
151
191
  return;
152
192
  }
153
- next(error);
193
+ const order = await options.order_store.findOrderByChallengeHash(confirmation.challenge_hash);
194
+ if (order) {
195
+ await options.order_store.markOrderFulfilledUnsettledOnce({
196
+ order_id: order.id,
197
+ requirement_id: confirmation.requirement_id,
198
+ pricing_band: confirmation.pricing_band,
199
+ });
200
+ } else {
201
+ await options.order_store.flagPaymentReview({
202
+ reason: "unknown_metered_challenge_hash",
203
+ requirement_id: confirmation.requirement_id,
204
+ });
205
+ }
206
+ return;
207
+ }
208
+
209
+ if (confirmation.kind === "metered_batch_settled") {
210
+ await options.order_store.flagPaymentReview({
211
+ reason: "metered_batch_settled_reconcile_statement_api",
212
+ settlement_batch_id: confirmation.settlement_batch_id,
213
+ chain_receipt_id: confirmation.chain_receipt_id,
214
+ });
215
+ return;
216
+ }
217
+
218
+ await options.order_store.flagPaymentReview({
219
+ reason: confirmation.reason,
220
+ requirement_id: confirmation.requirement_id,
221
+ settlement_batch_id: confirmation.settlement_batch_id,
154
222
  });
223
+ }
155
224
 
156
- return router;
225
+ function isStandardCheckoutAmount(currency: string, amountMinor: number): boolean {
226
+ if (!Number.isSafeInteger(amountMinor)) return false;
227
+ const normalizedCurrency = String(currency || "").toUpperCase();
228
+ if (normalizedCurrency === "JPY") return amountMinor >= 501;
229
+ if (normalizedCurrency === "USD") return amountMinor >= 301;
230
+ return false;
157
231
  }
@@ -10,12 +10,16 @@ from .siglume.siglume_sdrp_routes import create_siglume_sdrp_router
10
10
 
11
11
  app = FastAPI()
12
12
  app.include_router(
13
- create_siglume_sdrp_router(ExampleSiglumeOrderStore()),
13
+ create_siglume_sdrp_router(ExampleSiglumeOrderStore(), allow_metered_payments=False),
14
14
  prefix="/payments",
15
15
  )
16
16
  ```
17
17
 
18
18
  Replace `siglume_order_store_example.py` with your real order database adapter.
19
+ Keep `process_webhook_event_once()` transactional: record the webhook event as
20
+ processed only after the order update or review write succeeds. The generated
21
+ route defaults to Standard-only. Enable `allow_metered_payments` only after you
22
+ implement Micro / Nano settlement reconciliation and past-due handling.
19
23
  The route paths become:
20
24
 
21
25
  - `POST /payments/checkout/siglume/start`
@@ -11,22 +11,45 @@ _processed_events: set[str] = set()
11
11
 
12
12
 
13
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)
14
+ async def begin_checkout_attempt(self, order_id: str, request: Request) -> dict[str, Any] | None:
15
+ order = _orders.get(order_id)
16
+ if order is None:
17
+ return None
18
+ order.setdefault("attempt_id", f"{order['id']}_attempt_1")
19
+ order.setdefault("stable_nonce", f"{order['id']}-attempt_1")
20
+ return {
21
+ **order,
22
+ "order_id": order["id"],
23
+ "attempt_id": order["attempt_id"],
24
+ "stable_nonce": order["stable_nonce"],
25
+ }
16
26
 
17
- async def mark_checkout_pending(self, *, order_id: str, challenge_hash: str, checkout_session_id: str) -> None:
27
+ async def mark_checkout_pending(
28
+ self,
29
+ *,
30
+ order_id: str,
31
+ attempt_id: str,
32
+ stable_nonce: str,
33
+ challenge_hash: str,
34
+ checkout_session_id: str,
35
+ checkout_url: str,
36
+ ) -> None:
18
37
  order = _orders.get(order_id)
19
38
  if not order:
20
39
  return
21
40
  order["status"] = "pending"
41
+ order["attempt_id"] = attempt_id
42
+ order["stable_nonce"] = stable_nonce
22
43
  order["challenge_hash"] = challenge_hash
23
44
  order["checkout_session_id"] = checkout_session_id
45
+ order["checkout_url"] = checkout_url
24
46
 
25
- async def record_webhook_event_once(self, event_id: str) -> bool:
47
+ async def process_webhook_event_once(self, event_id: str, handler) -> str:
26
48
  if event_id in _processed_events:
27
- return False
49
+ return "duplicate"
50
+ await handler()
28
51
  _processed_events.add(event_id)
29
- return True
52
+ return "processed"
30
53
 
31
54
  async def find_order_by_challenge_hash(self, challenge_hash: str) -> dict[str, Any] | None:
32
55
  for order in _orders.values():
@@ -1,11 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import os
4
- import time
5
- from typing import Any, Protocol
4
+ from typing import Any, Awaitable, Callable, Literal, Protocol
6
5
 
7
6
  from fastapi import APIRouter, Request
8
7
  from fastapi.responses import JSONResponse, Response
8
+ from starlette.concurrency import run_in_threadpool
9
9
  from siglume_direct_request_payment import (
10
10
  DirectRequestPaymentMerchantClient,
11
11
  HostedCheckoutNotAvailableError,
@@ -15,16 +15,33 @@ from siglume_direct_request_payment import (
15
15
 
16
16
 
17
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: ...
18
+ async def begin_checkout_attempt(self, order_id: str, request: Request) -> dict[str, Any] | None: ...
19
+ async def mark_checkout_pending(
20
+ self,
21
+ *,
22
+ order_id: str,
23
+ attempt_id: str,
24
+ stable_nonce: str,
25
+ challenge_hash: str,
26
+ checkout_session_id: str,
27
+ checkout_url: str,
28
+ ) -> None: ...
29
+ async def process_webhook_event_once(
30
+ self,
31
+ event_id: str,
32
+ handler: Callable[[], Awaitable[None]],
33
+ ) -> Literal["processed", "duplicate"]: ...
21
34
  async def find_order_by_challenge_hash(self, challenge_hash: str) -> dict[str, Any] | None: ...
22
35
  async def mark_order_paid_once(self, *, order_id: str, requirement_id: str, chain_receipt_id: str) -> None: ...
23
36
  async def mark_order_fulfilled_unsettled_once(self, *, order_id: str, requirement_id: str, pricing_band: str) -> None: ...
24
37
  async def flag_payment_review(self, data: dict[str, Any]) -> None: ...
25
38
 
26
39
 
27
- def create_siglume_sdrp_router(order_store: SiglumeSdrpOrderStore) -> APIRouter:
40
+ def create_siglume_sdrp_router(
41
+ order_store: SiglumeSdrpOrderStore,
42
+ *,
43
+ allow_metered_payments: bool = False,
44
+ ) -> APIRouter:
28
45
  router = APIRouter()
29
46
  merchant_key = os.environ["SIGLUME_DIRECT_PAYMENT_MERCHANT"]
30
47
  shop_origin = os.environ["SHOP_PUBLIC_ORIGIN"]
@@ -36,27 +53,41 @@ def create_siglume_sdrp_router(order_store: SiglumeSdrpOrderStore) -> APIRouter:
36
53
  async def start_checkout(request: Request) -> JSONResponse:
37
54
  body = await request.json()
38
55
  order_id = str(body.get("order_id") or "")
39
- order = await order_store.get_order_for_checkout(order_id, request)
40
- if not order:
56
+ attempt = await order_store.begin_checkout_attempt(order_id, request)
57
+ if not attempt:
41
58
  return JSONResponse({"error": "order_not_found"}, status_code=404)
42
59
 
60
+ if not allow_metered_payments and not _is_standard_checkout_amount(str(attempt["currency"]), int(attempt["amount_minor"])):
61
+ return JSONResponse({"error": "METERED_INTEGRATION_REQUIRED"}, status_code=409)
62
+
63
+ if attempt.get("checkout_url") and attempt.get("checkout_session_id"):
64
+ return JSONResponse({
65
+ "checkout_url": attempt["checkout_url"],
66
+ "session_id": attempt["checkout_session_id"],
67
+ })
68
+
43
69
  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"]},
70
+ session = await run_in_threadpool(
71
+ lambda: merchant.create_checkout_session(
72
+ merchant=merchant_key,
73
+ amount_minor=int(attempt["amount_minor"]),
74
+ currency=str(attempt["currency"]),
75
+ nonce=str(attempt["stable_nonce"]),
76
+ success_url=f"{shop_origin}/checkout/siglume/success",
77
+ cancel_url=f"{shop_origin}/checkout/siglume/cancel",
78
+ metadata={"order_id": attempt["order_id"], "attempt_id": attempt["attempt_id"]},
79
+ )
52
80
  )
53
81
  except HostedCheckoutNotAvailableError:
54
82
  return JSONResponse({"error": "hosted_checkout_not_enabled"}, status_code=409)
55
83
 
56
84
  await order_store.mark_checkout_pending(
57
- order_id=str(order["id"]),
85
+ order_id=str(attempt["order_id"]),
86
+ attempt_id=str(attempt["attempt_id"]),
87
+ stable_nonce=str(attempt["stable_nonce"]),
58
88
  challenge_hash=session["challenge_hash"],
59
89
  checkout_session_id=session["session_id"],
90
+ checkout_url=session["checkout_url"],
60
91
  )
61
92
  return JSONResponse({"checkout_url": session["checkout_url"], "session_id": session["session_id"]})
62
93
 
@@ -68,40 +99,72 @@ def create_siglume_sdrp_router(order_store: SiglumeSdrpOrderStore) -> APIRouter:
68
99
  request.headers.get("Siglume-Signature", ""),
69
100
  )["event"]
70
101
 
71
- if not await order_store.record_webhook_event_once(str(event["id"])):
72
- return Response(status_code=204)
102
+ async def handler() -> None:
103
+ await _process_siglume_webhook_event(
104
+ order_store,
105
+ event,
106
+ allow_metered_payments=allow_metered_payments,
107
+ )
73
108
 
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))
109
+ if await order_store.process_webhook_event_once(str(event["id"]), handler) == "duplicate":
110
+ return Response(status_code=204)
104
111
 
105
112
  return Response(status_code=204)
106
113
 
107
114
  return router
115
+
116
+
117
+ async def _process_siglume_webhook_event(
118
+ order_store: SiglumeSdrpOrderStore,
119
+ event: dict[str, Any],
120
+ *,
121
+ allow_metered_payments: bool,
122
+ ) -> None:
123
+ if event["type"] != "direct_payment.confirmed":
124
+ return
125
+
126
+ confirmation = classify_direct_payment_confirmation(event)
127
+ if confirmation["kind"] == "standard_settled":
128
+ order = await order_store.find_order_by_challenge_hash(confirmation["challenge_hash"])
129
+ if order:
130
+ await order_store.mark_order_paid_once(
131
+ order_id=str(order["id"]),
132
+ requirement_id=confirmation["requirement_id"],
133
+ chain_receipt_id=confirmation["chain_receipt_id"],
134
+ )
135
+ else:
136
+ await order_store.flag_payment_review({
137
+ "reason": "unknown_challenge_hash",
138
+ "requirement_id": confirmation["requirement_id"],
139
+ })
140
+ elif confirmation["kind"] == "metered_usage_accepted":
141
+ if not allow_metered_payments:
142
+ await order_store.flag_payment_review({
143
+ "reason": "metered_integration_required",
144
+ "requirement_id": confirmation["requirement_id"],
145
+ "pricing_band": confirmation["pricing_band"],
146
+ })
147
+ return
148
+ order = await order_store.find_order_by_challenge_hash(confirmation["challenge_hash"])
149
+ if order:
150
+ await order_store.mark_order_fulfilled_unsettled_once(
151
+ order_id=str(order["id"]),
152
+ requirement_id=confirmation["requirement_id"],
153
+ pricing_band=confirmation["pricing_band"],
154
+ )
155
+ else:
156
+ await order_store.flag_payment_review({
157
+ "reason": "unknown_metered_challenge_hash",
158
+ "requirement_id": confirmation["requirement_id"],
159
+ })
160
+ else:
161
+ await order_store.flag_payment_review(dict(confirmation))
162
+
163
+
164
+ def _is_standard_checkout_amount(currency: str, amount_minor: int) -> bool:
165
+ normalized_currency = currency.upper()
166
+ if normalized_currency == "JPY":
167
+ return amount_minor >= 501
168
+ if normalized_currency == "USD":
169
+ return amount_minor >= 301
170
+ return False