payment-kit 1.29.0 → 1.29.2

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.
Files changed (312) hide show
  1. package/api/dev.ts +41 -2
  2. package/api/hono.d.ts +42 -0
  3. package/api/node-sqlite.d.ts +12 -0
  4. package/api/src/bootstrap.ts +36 -0
  5. package/api/src/crons/base.ts +3 -3
  6. package/api/src/crons/currency.ts +1 -1
  7. package/api/src/crons/index.ts +27 -24
  8. package/api/src/crons/metering-subscription-detection.ts +1 -1
  9. package/api/src/crons/overdue-detection.ts +2 -2
  10. package/api/src/crons/retry-pending-events.ts +6 -0
  11. package/api/src/index.ts +22 -161
  12. package/api/src/integrations/app-store/client.ts +3 -4
  13. package/api/src/integrations/app-store/handlers/subscription.ts +7 -7
  14. package/api/src/integrations/app-store/signed-data-verifier.ts +3 -2
  15. package/api/src/integrations/arcblock/token.ts +21 -7
  16. package/api/src/integrations/google-play/handlers/subscription.ts +6 -6
  17. package/api/src/integrations/google-play/handlers/voided.ts +2 -2
  18. package/api/src/integrations/google-play/verify.ts +3 -2
  19. package/api/src/integrations/iap-reconcile.ts +3 -5
  20. package/api/src/integrations/stripe/handlers/invoice.ts +2 -2
  21. package/api/src/integrations/stripe/handlers/subscription.ts +3 -3
  22. package/api/src/libs/archive/query.ts +19 -0
  23. package/api/src/libs/audit.ts +61 -4
  24. package/api/src/libs/auth.ts +99 -38
  25. package/api/src/libs/context.ts +78 -1
  26. package/api/src/libs/currency.ts +2 -2
  27. package/api/src/libs/dayjs.ts +8 -2
  28. package/api/src/libs/drivers/auth-storage.ts +118 -0
  29. package/api/src/libs/drivers/cron.ts +264 -0
  30. package/api/src/libs/drivers/db.ts +170 -0
  31. package/api/src/libs/drivers/identity.ts +81 -0
  32. package/api/src/libs/drivers/index.ts +40 -0
  33. package/api/src/libs/drivers/locks.ts +226 -0
  34. package/api/src/libs/drivers/migrate-runner.ts +70 -0
  35. package/api/src/libs/drivers/queue.ts +104 -0
  36. package/api/src/libs/drivers/secrets.ts +194 -0
  37. package/api/src/libs/env.ts +170 -54
  38. package/api/src/libs/exchange-rate/service.ts +7 -6
  39. package/api/src/libs/http-fetch-adapter.ts +50 -0
  40. package/api/src/libs/invoice.ts +1 -1
  41. package/api/src/libs/lock.ts +51 -47
  42. package/api/src/libs/logger.ts +48 -8
  43. package/api/src/libs/notification/index.ts +1 -1
  44. package/api/src/libs/notification/template/customer-credit-low-balance.ts +2 -1
  45. package/api/src/libs/notification/template/customer-revenue-succeeded.ts +1 -1
  46. package/api/src/libs/notification/template/customer-reward-succeeded.ts +1 -1
  47. package/api/src/libs/overdraft-protection.ts +1 -1
  48. package/api/src/libs/payout.ts +1 -1
  49. package/api/src/libs/queue/index.ts +259 -52
  50. package/api/src/libs/queue/runtime.ts +175 -0
  51. package/api/src/libs/resource.ts +3 -3
  52. package/api/src/libs/secrets.ts +38 -0
  53. package/api/src/libs/session.ts +3 -2
  54. package/api/src/libs/subscription.ts +5 -5
  55. package/api/src/libs/tenant.ts +92 -0
  56. package/api/src/libs/url.ts +3 -3
  57. package/api/src/libs/util.ts +21 -13
  58. package/api/src/middlewares/hono/cdn.ts +63 -0
  59. package/api/src/middlewares/hono/context.ts +73 -0
  60. package/api/src/middlewares/hono/csrf.ts +72 -0
  61. package/api/src/middlewares/hono/fallback.ts +194 -0
  62. package/api/src/middlewares/hono/pipeline.ts +73 -0
  63. package/api/src/middlewares/hono/resource-mount.ts +42 -0
  64. package/api/src/middlewares/hono/resource.ts +63 -0
  65. package/api/src/middlewares/hono/security.ts +214 -0
  66. package/api/src/middlewares/hono/session.ts +114 -0
  67. package/api/src/middlewares/hono/xss.ts +61 -0
  68. package/api/src/queues/auto-recharge.ts +12 -10
  69. package/api/src/queues/checkout-session.ts +17 -12
  70. package/api/src/queues/credit-consume.ts +40 -36
  71. package/api/src/queues/credit-grant.ts +25 -18
  72. package/api/src/queues/credit-reconciliation.ts +7 -5
  73. package/api/src/queues/discount-status.ts +9 -6
  74. package/api/src/queues/event.ts +12 -4
  75. package/api/src/queues/exchange-rate-health.ts +49 -30
  76. package/api/src/queues/invoice.ts +18 -15
  77. package/api/src/queues/notification.ts +14 -7
  78. package/api/src/queues/payment.ts +41 -28
  79. package/api/src/queues/payout.ts +9 -5
  80. package/api/src/queues/refund.ts +18 -12
  81. package/api/src/queues/subscription.ts +83 -53
  82. package/api/src/queues/token-transfer.ts +15 -10
  83. package/api/src/queues/usage-record.ts +8 -5
  84. package/api/src/queues/vendors/commission.ts +7 -5
  85. package/api/src/queues/vendors/fulfillment-coordinator.ts +17 -13
  86. package/api/src/queues/vendors/fulfillment.ts +4 -2
  87. package/api/src/queues/vendors/return-processor.ts +5 -3
  88. package/api/src/queues/vendors/return-scanner.ts +5 -4
  89. package/api/src/queues/vendors/status-check.ts +10 -7
  90. package/api/src/queues/webhook.ts +60 -32
  91. package/api/src/routes/connect/shared.ts +1 -2
  92. package/api/src/routes/connect/subscribe.ts +3 -3
  93. package/api/src/routes/{archive.ts → hono/archive.ts} +69 -64
  94. package/api/src/routes/{auto-recharge-configs.ts → hono/auto-recharge-configs.ts} +39 -28
  95. package/api/src/routes/{checkout-sessions.ts → hono/checkout-sessions.ts} +790 -923
  96. package/api/src/routes/{coupons.ts → hono/coupons.ts} +93 -76
  97. package/api/src/routes/{credit-grants.ts → hono/credit-grants.ts} +140 -126
  98. package/api/src/routes/hono/credit-tokens.ts +43 -0
  99. package/api/src/routes/{credit-transactions.ts → hono/credit-transactions.ts} +37 -29
  100. package/api/src/routes/{customers.ts → hono/customers.ts} +193 -223
  101. package/api/src/routes/{donations.ts → hono/donations.ts} +41 -32
  102. package/api/src/routes/{entitlements.ts → hono/entitlements.ts} +28 -25
  103. package/api/src/routes/{events.ts → hono/events.ts} +107 -71
  104. package/api/src/routes/{exchange-rate-providers.ts → hono/exchange-rate-providers.ts} +138 -126
  105. package/api/src/routes/hono/exchange-rates.ts +77 -0
  106. package/api/src/routes/hono/index.ts +115 -0
  107. package/api/src/routes/{integrations → hono/integrations}/app-store.ts +68 -48
  108. package/api/src/routes/{integrations → hono/integrations}/google-play.ts +78 -58
  109. package/api/src/routes/hono/integrations/stripe.ts +74 -0
  110. package/api/src/routes/{invoices.ts → hono/invoices.ts} +253 -244
  111. package/api/src/routes/{meter-events.ts → hono/meter-events.ts} +120 -110
  112. package/api/src/routes/hono/meters.ts +288 -0
  113. package/api/src/routes/hono/passports.ts +73 -0
  114. package/api/src/routes/{payment-currencies.ts → hono/payment-currencies.ts} +219 -197
  115. package/api/src/routes/{payment-intents.ts → hono/payment-intents.ts} +136 -132
  116. package/api/src/routes/{payment-links.ts → hono/payment-links.ts} +145 -128
  117. package/api/src/routes/{payment-methods.ts → hono/payment-methods.ts} +125 -93
  118. package/api/src/routes/{payment-stats.ts → hono/payment-stats.ts} +30 -25
  119. package/api/src/routes/{payouts.ts → hono/payouts.ts} +55 -47
  120. package/api/src/routes/{prices.ts → hono/prices.ts} +265 -242
  121. package/api/src/routes/{pricing-table.ts → hono/pricing-table.ts} +94 -87
  122. package/api/src/routes/{products.ts → hono/products.ts} +172 -159
  123. package/api/src/routes/{promotion-codes.ts → hono/promotion-codes.ts} +207 -185
  124. package/api/src/routes/hono/redirect.ts +24 -0
  125. package/api/src/routes/{refunds.ts → hono/refunds.ts} +96 -80
  126. package/api/src/routes/{settings.ts → hono/settings.ts} +64 -55
  127. package/api/src/routes/{subscription-items.ts → hono/subscription-items.ts} +64 -57
  128. package/api/src/routes/{subscriptions.ts → hono/subscriptions.ts} +475 -528
  129. package/api/src/routes/{tax-rates.ts → hono/tax-rates.ts} +71 -70
  130. package/api/src/routes/hono/tool.ts +69 -0
  131. package/api/src/routes/{usage-records.ts → hono/usage-records.ts} +47 -42
  132. package/api/src/routes/{vendor.ts → hono/vendor.ts} +315 -167
  133. package/api/src/routes/{webhook-attempts.ts → hono/webhook-attempts.ts} +17 -13
  134. package/api/src/routes/hono/webhook-endpoints.ts +126 -0
  135. package/api/src/service.ts +667 -0
  136. package/api/src/store/migrations/20230911-seeding.ts +2 -1
  137. package/api/src/store/migrations/20260609-remove-did-space-jobs.ts +23 -0
  138. package/api/src/store/migrations/20260610-tenant-columns.ts +40 -0
  139. package/api/src/store/migrations/20260611-tenant-backfill.ts +33 -0
  140. package/api/src/store/models/auto-recharge-config.ts +22 -10
  141. package/api/src/store/models/checkout-session.ts +15 -14
  142. package/api/src/store/models/coupon.ts +29 -20
  143. package/api/src/store/models/credit-grant.ts +38 -29
  144. package/api/src/store/models/credit-transaction.ts +32 -21
  145. package/api/src/store/models/customer.ts +19 -17
  146. package/api/src/store/models/discount.ts +11 -2
  147. package/api/src/store/models/entitlement-grant.ts +21 -9
  148. package/api/src/store/models/entitlement-product.ts +21 -9
  149. package/api/src/store/models/entitlement.ts +19 -10
  150. package/api/src/store/models/event.ts +18 -9
  151. package/api/src/store/models/exchange-rate-provider.ts +17 -4
  152. package/api/src/store/models/invoice-item.ts +18 -9
  153. package/api/src/store/models/invoice.ts +16 -8
  154. package/api/src/store/models/meter-event.ts +27 -9
  155. package/api/src/store/models/meter.ts +31 -22
  156. package/api/src/store/models/payment-currency.ts +25 -8
  157. package/api/src/store/models/payment-intent.ts +15 -6
  158. package/api/src/store/models/payment-link.ts +15 -6
  159. package/api/src/store/models/payment-method.ts +38 -22
  160. package/api/src/store/models/payment-stat.ts +18 -9
  161. package/api/src/store/models/payout.ts +15 -6
  162. package/api/src/store/models/price-quote.ts +17 -8
  163. package/api/src/store/models/price.ts +24 -12
  164. package/api/src/store/models/pricing-table.ts +29 -20
  165. package/api/src/store/models/product-vendor.ts +20 -10
  166. package/api/src/store/models/product.ts +15 -6
  167. package/api/src/store/models/promotion-code.ts +14 -6
  168. package/api/src/store/models/refund.ts +15 -6
  169. package/api/src/store/models/revenue-snapshot.ts +21 -9
  170. package/api/src/store/models/setting.ts +18 -9
  171. package/api/src/store/models/setup-intent.ts +36 -27
  172. package/api/src/store/models/subscription-item.ts +21 -9
  173. package/api/src/store/models/subscription-schedule.ts +21 -9
  174. package/api/src/store/models/subscription.ts +21 -10
  175. package/api/src/store/models/tax-rate.ts +29 -21
  176. package/api/src/store/models/usage-record.ts +11 -2
  177. package/api/src/store/models/webhook-attempt.ts +18 -9
  178. package/api/src/store/models/webhook-endpoint.ts +18 -9
  179. package/api/src/store/scoped-core.ts +55 -0
  180. package/api/src/store/scoped.ts +247 -0
  181. package/api/src/store/sequelize.ts +66 -22
  182. package/api/src/store/sql-migrations.ts +20 -0
  183. package/api/src/store/tenant-backfill.ts +260 -0
  184. package/api/src/store/tenant-model.ts +124 -0
  185. package/api/src/store/tenant-tables.ts +50 -0
  186. package/api/tests/embedded/embedded-multi-mode-d3.spec.ts +257 -0
  187. package/api/tests/fixtures/bare-query-violation.ts +13 -0
  188. package/api/tests/fixtures/core-env-violation.ts +10 -0
  189. package/api/tests/fixtures/host-read-violation.ts +19 -0
  190. package/api/tests/fixtures/tenants.ts +4 -0
  191. package/api/tests/integrations/iap-tenant.spec.ts +284 -0
  192. package/api/tests/libs/archive-query.spec.ts +26 -0
  193. package/api/tests/libs/audit-tenant.spec.ts +153 -0
  194. package/api/tests/libs/context.spec.ts +204 -0
  195. package/api/tests/libs/core-config.spec.ts +115 -0
  196. package/api/tests/libs/cron-driver-d2.spec.ts +237 -0
  197. package/api/tests/libs/crons-conservation-d2.spec.ts +52 -0
  198. package/api/tests/libs/lock-tenant.spec.ts +66 -0
  199. package/api/tests/libs/scoped.spec.ts +222 -0
  200. package/api/tests/libs/secrets-facade.spec.ts +52 -0
  201. package/api/tests/libs/tenancy-slot-authority.spec.ts +209 -0
  202. package/api/tests/libs/tenant-middleware.spec.ts +42 -0
  203. package/api/tests/libs/tenant-scanner.spec.ts +120 -0
  204. package/api/tests/middlewares/hono/cdn.spec.ts +70 -0
  205. package/api/tests/middlewares/hono/context.spec.ts +113 -0
  206. package/api/tests/middlewares/hono/csrf.spec.ts +136 -0
  207. package/api/tests/middlewares/hono/fallback.spec.ts +67 -0
  208. package/api/tests/middlewares/hono/pipeline.spec.ts +47 -0
  209. package/api/tests/middlewares/hono/security.spec.ts +181 -0
  210. package/api/tests/middlewares/hono/session.spec.ts +42 -0
  211. package/api/tests/middlewares/hono/xss.spec.ts +81 -0
  212. package/api/tests/models/tenant-backfill.spec.ts +287 -0
  213. package/api/tests/models/tenant-columns-model.spec.ts +46 -0
  214. package/api/tests/models/tenant-columns.spec.ts +161 -0
  215. package/api/tests/queues/credit-consume-batch.spec.ts +8 -1
  216. package/api/tests/queues/credit-consume.spec.ts +8 -1
  217. package/api/tests/queues/event-tenant.spec.ts +236 -0
  218. package/api/tests/queues/exchange-rate-health-tenant-d6.spec.ts +62 -0
  219. package/api/tests/queues/queue-parity.spec.ts +249 -0
  220. package/api/tests/queues/queue-runtime-surface.spec.ts +277 -0
  221. package/api/tests/queues/queue-teardown-d2.spec.ts +127 -0
  222. package/api/tests/queues/tenant-matrix-a.spec.ts +245 -0
  223. package/api/tests/queues/tenant-matrix-b.spec.ts +168 -0
  224. package/api/tests/routes/connect/hono-attach.spec.ts +107 -0
  225. package/api/tests/service/collapse.spec.ts +96 -0
  226. package/api/tests/store/tenant-crosscut.spec.ts +202 -0
  227. package/api/tests/store/tenant-model-spike.spec.ts +177 -0
  228. package/api/tests/store/tenant-model.spec.ts +162 -0
  229. package/api/tests/store/tenant-residual.spec.ts +196 -0
  230. package/api/third.d.ts +4 -0
  231. package/blocklet.yml +1 -1
  232. package/cloudflare/README.md +26 -6
  233. package/cloudflare/build.ts +28 -13
  234. package/cloudflare/did-connect-auth.ts +0 -217
  235. package/cloudflare/docs/2026-06-10-bundle-size-analysis.md +288 -0
  236. package/cloudflare/migrations/0006_tenant_columns.sql +46 -0
  237. package/cloudflare/migrations/0007_tenant_backfill_indexes.sql +65 -0
  238. package/cloudflare/migrations/0008_schema_parity.sql +16 -0
  239. package/cloudflare/migrations/0009_remove_did_space_jobs.sql +5 -0
  240. package/cloudflare/queue-runtime-mode.ts +13 -0
  241. package/cloudflare/run-build.js +31 -56
  242. package/cloudflare/shims/blocklet-sdk/asset-host-transformer.ts +20 -0
  243. package/cloudflare/shims/blocklet-sdk/config.ts +8 -1
  244. package/cloudflare/shims/blocklet-sdk/login.ts +12 -0
  245. package/cloudflare/shims/blocklet-sdk/service-api.ts +14 -0
  246. package/cloudflare/shims/blocklet-sdk/session.ts +4 -2
  247. package/cloudflare/shims/blocklet-sdk/util-constants.ts +8 -0
  248. package/cloudflare/shims/blocklet-sdk/util-csrf.ts +13 -0
  249. package/cloudflare/shims/blocklet-sdk/util-wallet.ts +8 -0
  250. package/cloudflare/shims/cron.ts +38 -158
  251. package/cloudflare/shims/events.ts +124 -0
  252. package/cloudflare/shims/fastq.ts +15 -1
  253. package/cloudflare/shims/nedb-storage.ts +16 -8
  254. package/cloudflare/shims/node-fetch.ts +35 -0
  255. package/cloudflare/shims/xss.ts +8 -0
  256. package/cloudflare/tenant-middleware.ts +36 -0
  257. package/cloudflare/tests/tenant-middleware.spec.ts +160 -0
  258. package/cloudflare/tests/worker-handler-gate.spec.ts +44 -0
  259. package/cloudflare/worker.ts +204 -433
  260. package/cloudflare/wrangler.local-e2e.jsonc +26 -0
  261. package/jest.config.js +3 -1
  262. package/package.json +33 -38
  263. package/scripts/core-env-whitelist.json +1 -0
  264. package/scripts/e2e-12b-runtime.ts +149 -0
  265. package/scripts/e2e-core-config.ts +125 -0
  266. package/scripts/e2e-d1-tenancy.ts +116 -0
  267. package/scripts/e2e-d2-cron-queue.ts +139 -0
  268. package/scripts/e2e-d3-embedded-multi.ts +171 -0
  269. package/scripts/e2e-hono-s2.ts +125 -0
  270. package/scripts/e2e-hono-s3e.ts +135 -0
  271. package/scripts/e2e-hono-s4.ts +114 -0
  272. package/scripts/e2e-migration-contract.ts +100 -0
  273. package/scripts/e2e-s0.ts +61 -0
  274. package/scripts/e2e-s1.ts +107 -0
  275. package/scripts/e2e-s2.ts +178 -0
  276. package/scripts/e2e-s3.ts +110 -0
  277. package/scripts/e2e-s4.ts +191 -0
  278. package/scripts/e2e-s5.ts +139 -0
  279. package/scripts/e2e-s6.ts +127 -0
  280. package/scripts/e2e-tenant-model.ts +119 -0
  281. package/scripts/e2e-tenant-worker.ts +199 -0
  282. package/scripts/gen-sql-migrations.js +46 -0
  283. package/scripts/phase8-codemod.js +219 -0
  284. package/scripts/phase9a-env-getters-codemod.js +82 -0
  285. package/scripts/scan-core-env.js +109 -0
  286. package/scripts/scan-tenant-queries.js +235 -0
  287. package/scripts/schema-drift-guard.ts +210 -0
  288. package/scripts/tenant-scan-whitelist.json +1 -0
  289. package/src/env.d.ts +13 -1
  290. package/tsconfig.json +1 -1
  291. package/api/src/libs/did-space.ts +0 -235
  292. package/api/src/libs/middleware.ts +0 -50
  293. package/api/src/libs/security.ts +0 -192
  294. package/api/src/queues/space.ts +0 -662
  295. package/api/src/routes/credit-tokens.ts +0 -38
  296. package/api/src/routes/exchange-rates.ts +0 -87
  297. package/api/src/routes/index.ts +0 -142
  298. package/api/src/routes/integrations/stripe.ts +0 -61
  299. package/api/src/routes/meters.ts +0 -274
  300. package/api/src/routes/passports.ts +0 -68
  301. package/api/src/routes/redirect.ts +0 -20
  302. package/api/src/routes/tool.ts +0 -65
  303. package/api/src/routes/webhook-endpoints.ts +0 -126
  304. package/api/tests/routes/credit-grants.spec.ts +0 -1261
  305. package/cloudflare/shims/did-space-js.ts +0 -17
  306. package/cloudflare/shims/did-space.ts +0 -11
  307. package/cloudflare/shims/express-compat/index.ts +0 -80
  308. package/cloudflare/shims/express-compat/types.ts +0 -41
  309. package/cloudflare/shims/lock.ts +0 -115
  310. package/cloudflare/shims/queue.ts +0 -611
  311. package/cloudflare/tests/shims/queue-delayed-persist.spec.ts +0 -87
  312. package/cloudflare/tests/shims/queue-scheduled.spec.ts +0 -186
@@ -1,1261 +0,0 @@
1
- import { Request, Response } from 'express';
2
- import { AutoRechargeConfig, Customer, MeterEvent, PaymentCurrency } from '../../src/store/models';
3
- import { checkTokenBalance } from '../../src/libs/payment';
4
- import { getPriceUintAmountByCurrency } from '../../src/libs/price';
5
- import logger from '../../src/libs/logger';
6
- import creditGrantsRoute from '../../src/routes/credit-grants';
7
-
8
- // Mock dependencies
9
- jest.mock('../../src/libs/payment');
10
- jest.mock('../../src/libs/price');
11
- jest.mock('../../src/libs/logger', () => ({
12
- error: jest.fn(),
13
- info: jest.fn(),
14
- warn: jest.fn(),
15
- debug: jest.fn(),
16
- }));
17
-
18
- // Note: Models are not fully mocked to allow spying on their methods
19
-
20
- describe('GET /api/credit-grants/verify-availability', () => {
21
- let mockReq: Partial<Request>;
22
- let mockRes: Partial<Response>;
23
- let routeHandler: any;
24
-
25
- beforeEach(() => {
26
- // Setup mocks
27
- jest.clearAllMocks();
28
-
29
- mockReq = {
30
- query: {
31
- customer_id: 'customer_123',
32
- currency_id: 'currency_123',
33
- },
34
- livemode: true,
35
- } as any;
36
-
37
- mockRes = {
38
- status: jest.fn().mockReturnThis(),
39
- json: jest.fn().mockReturnThis(),
40
- } as any;
41
-
42
- // Extract route handler from router
43
- // The route structure is: router.get('/verify-availability', authMine, handler)
44
- // We'll get the handler by finding the route in the router stack
45
- try {
46
- const router = creditGrantsRoute as any;
47
- const routeLayer = router.stack.find((layer: any) => {
48
- return layer.route && layer.route.path === '/verify-availability';
49
- });
50
- if (routeLayer && routeLayer.route) {
51
- // The handler is the last middleware in the stack (after authMine)
52
- const { stack } = routeLayer.route;
53
- routeHandler = stack[stack.length - 1]?.handle;
54
- }
55
- } catch (error) {
56
- // If we can't extract the handler, we'll skip these tests
57
- console.warn('Could not extract route handler:', error);
58
- }
59
- });
60
-
61
- describe('Input validation', () => {
62
- it('should return 400 if customer_id is missing', async () => {
63
- if (!routeHandler) return;
64
- mockReq.query = { currency_id: 'currency_123' } as any;
65
-
66
- await routeHandler(mockReq as Request, mockRes as Response);
67
-
68
- expect(mockRes.status).toHaveBeenCalledWith(400);
69
- expect(mockRes.json).toHaveBeenCalledWith(
70
- expect.objectContaining({
71
- error: expect.stringContaining('customer_id'),
72
- })
73
- );
74
- });
75
-
76
- it('should return 400 if currency_id is missing', async () => {
77
- if (!routeHandler) return;
78
- mockReq.query = { customer_id: 'customer_123' } as any;
79
-
80
- await routeHandler(mockReq as Request, mockRes as Response);
81
-
82
- expect(mockRes.status).toHaveBeenCalledWith(400);
83
- expect(mockRes.json).toHaveBeenCalledWith(
84
- expect.objectContaining({
85
- error: expect.stringContaining('currency_id'),
86
- })
87
- );
88
- });
89
- });
90
-
91
- describe('Customer and currency checks', () => {
92
- it('should return 404 if customer not found', async () => {
93
- if (!routeHandler) return;
94
- jest.spyOn(Customer, 'findByPkOrDid').mockResolvedValue(null as any);
95
- jest.spyOn(PaymentCurrency, 'findByPk').mockResolvedValue({ id: 'currency_123' } as any);
96
-
97
- await routeHandler(mockReq as Request, mockRes as Response);
98
-
99
- expect(mockRes.status).toHaveBeenCalledWith(404);
100
- expect(mockRes.json).toHaveBeenCalledWith({
101
- error: 'Customer customer_123 not found',
102
- });
103
- });
104
-
105
- it('should return 404 if currency not found', async () => {
106
- if (!routeHandler) return;
107
- jest.spyOn(Customer, 'findByPkOrDid').mockResolvedValue({ id: 'customer_123' } as any);
108
- jest.spyOn(PaymentCurrency, 'findByPk').mockResolvedValue(null as any);
109
-
110
- await routeHandler(mockReq as Request, mockRes as Response);
111
-
112
- expect(mockRes.status).toHaveBeenCalledWith(404);
113
- expect(mockRes.json).toHaveBeenCalledWith({
114
- error: 'PaymentCurrency currency_123 not found',
115
- });
116
- });
117
- });
118
-
119
- describe('Auto recharge config checks', () => {
120
- beforeEach(() => {
121
- jest.spyOn(Customer, 'findByPkOrDid').mockResolvedValue({ id: 'customer_123' } as any);
122
- jest.spyOn(PaymentCurrency, 'findByPk').mockResolvedValue({
123
- id: 'currency_123',
124
- decimal: 2,
125
- } as any);
126
- });
127
-
128
- it('should return can_continue: false if auto recharge config not found', async () => {
129
- if (!routeHandler) return;
130
- jest.spyOn(AutoRechargeConfig, 'findOne').mockResolvedValue(null as any);
131
-
132
- await routeHandler(mockReq as Request, mockRes as Response);
133
-
134
- expect(mockRes.json).toHaveBeenCalledWith({
135
- can_continue: false,
136
- has_auto_recharge: false,
137
- reason: 'auto_recharge_config_not_found',
138
- });
139
- });
140
-
141
- it('should return can_continue: false if recharge currency not found', async () => {
142
- jest.spyOn(AutoRechargeConfig, 'findOne').mockResolvedValue({
143
- rechargeCurrency: null,
144
- price: { id: 'price_123' },
145
- paymentMethod: { id: 'pm_123' },
146
- } as any);
147
-
148
- await routeHandler(mockReq as Request, mockRes as Response);
149
-
150
- expect(mockRes.json).toHaveBeenCalledWith({
151
- can_continue: false,
152
- reason: 'recharge_currency_not_found',
153
- });
154
- });
155
-
156
- it('should return can_continue: false if price not found', async () => {
157
- jest.spyOn(AutoRechargeConfig, 'findOne').mockResolvedValue({
158
- rechargeCurrency: { id: 'recharge_currency_123' },
159
- price: null,
160
- paymentMethod: { id: 'pm_123' },
161
- } as any);
162
-
163
- await routeHandler(mockReq as Request, mockRes as Response);
164
-
165
- expect(mockRes.json).toHaveBeenCalledWith({
166
- can_continue: false,
167
- reason: 'price_not_found',
168
- });
169
- });
170
-
171
- it('should return can_continue: false if payment method not found', async () => {
172
- jest.spyOn(AutoRechargeConfig, 'findOne').mockResolvedValue({
173
- rechargeCurrency: { id: 'recharge_currency_123' },
174
- price: { id: 'price_123' },
175
- paymentMethod: null,
176
- } as any);
177
-
178
- await routeHandler(mockReq as Request, mockRes as Response);
179
-
180
- expect(mockRes.json).toHaveBeenCalledWith({
181
- can_continue: false,
182
- reason: 'payment_method_not_found',
183
- });
184
- });
185
-
186
- it('should return can_continue: false if payment method is stripe', async () => {
187
- jest.spyOn(AutoRechargeConfig, 'findOne').mockResolvedValue({
188
- rechargeCurrency: { id: 'recharge_currency_123' },
189
- price: { id: 'price_123' },
190
- paymentMethod: { type: 'stripe' },
191
- } as any);
192
-
193
- await routeHandler(mockReq as Request, mockRes as Response);
194
-
195
- expect(mockRes.json).toHaveBeenCalledWith({
196
- can_continue: false,
197
- reason: 'balance_check_not_supported',
198
- });
199
- });
200
-
201
- it('should return can_continue: false if price amount is invalid', async () => {
202
- jest.spyOn(AutoRechargeConfig, 'findOne').mockResolvedValue({
203
- rechargeCurrency: { id: 'recharge_currency_123' },
204
- price: { id: 'price_123' },
205
- paymentMethod: { type: 'arcblock' },
206
- quantity: 1,
207
- } as any);
208
- (getPriceUintAmountByCurrency as jest.Mock).mockResolvedValue(null);
209
-
210
- await routeHandler(mockReq as Request, mockRes as Response);
211
-
212
- expect(mockRes.json).toHaveBeenCalledWith({
213
- can_continue: false,
214
- reason: 'invalid_price_amount',
215
- });
216
- });
217
- });
218
-
219
- describe('No pending amount scenarios', () => {
220
- let mockConfig: any;
221
-
222
- beforeEach(() => {
223
- jest.spyOn(Customer, 'findByPkOrDid').mockResolvedValue({ id: 'customer_123', did: 'did:customer:123' } as any);
224
- jest.spyOn(PaymentCurrency, 'findByPk').mockResolvedValue({
225
- id: 'currency_123',
226
- decimal: 2,
227
- } as any);
228
-
229
- mockConfig = {
230
- rechargeCurrency: { id: 'recharge_currency_123' },
231
- price: {
232
- id: 'price_123',
233
- metadata: {
234
- credit_config: {
235
- credit_amount: '1',
236
- },
237
- },
238
- },
239
- paymentMethod: {
240
- type: 'arcblock',
241
- getOcapClient: jest.fn(),
242
- },
243
- quantity: 1,
244
- payment_settings: {
245
- payment_method_options: {
246
- arcblock: {
247
- payer: 'did:test:123',
248
- },
249
- },
250
- },
251
- };
252
-
253
- jest.spyOn(AutoRechargeConfig, 'findOne').mockResolvedValue(mockConfig as any);
254
- (getPriceUintAmountByCurrency as jest.Mock).mockResolvedValue('1000000'); // 1 credit with 6 decimals for ABT
255
- jest.spyOn(MeterEvent, 'getPendingAmounts').mockResolvedValue([{ currency_123: '0' }, [], []] as any);
256
- });
257
-
258
- it('should return can_continue: true if balance is sufficient for one recharge', async () => {
259
- (checkTokenBalance as jest.Mock).mockResolvedValue({
260
- sufficient: true,
261
- token: { balance: '6000000' }, // 60 with 2 decimals
262
- });
263
-
264
- await routeHandler(mockReq as Request, mockRes as Response);
265
-
266
- expect(checkTokenBalance).toHaveBeenCalledWith(
267
- expect.objectContaining({
268
- amount: '1000000', // totalAmount for one recharge (1 credit)
269
- })
270
- );
271
- expect(mockRes.json).toHaveBeenCalledWith(
272
- expect.objectContaining({
273
- can_continue: true,
274
- payment_account_sufficient: true,
275
- })
276
- );
277
- });
278
-
279
- it('should return can_continue: true if balance exactly equals one recharge amount (boundary)', async () => {
280
- // Balance: exactly 0.1 ABT (one recharge cost for 1 credit)
281
- (checkTokenBalance as jest.Mock)
282
- .mockResolvedValueOnce({
283
- sufficient: true,
284
- token: { balance: '1000000' }, // Exactly one recharge
285
- })
286
- .mockResolvedValueOnce({
287
- sufficient: true,
288
- token: { balance: '1000000' },
289
- });
290
-
291
- await routeHandler(mockReq as Request, mockRes as Response);
292
-
293
- expect(checkTokenBalance).toHaveBeenCalledWith(
294
- expect.objectContaining({
295
- amount: '1000000',
296
- })
297
- );
298
- expect(mockRes.json).toHaveBeenCalledWith(
299
- expect.objectContaining({
300
- can_continue: true,
301
- payment_account_sufficient: true,
302
- })
303
- );
304
- });
305
-
306
- it('should return can_continue: false if balance is insufficient for one recharge', async () => {
307
- (checkTokenBalance as jest.Mock).mockReset();
308
- (checkTokenBalance as jest.Mock).mockImplementation((params: any) => {
309
- if (params.amount === '1000000') {
310
- return {
311
- sufficient: false,
312
- token: { balance: '500000' }, // 0.05 ABT, insufficient for 0.1 ABT
313
- reason: 'NO_ENOUGH_TOKEN',
314
- };
315
- }
316
- return {
317
- sufficient: true,
318
- token: { balance: '500000' },
319
- };
320
- });
321
-
322
- await routeHandler(mockReq as Request, mockRes as Response);
323
-
324
- expect(mockRes.json).toHaveBeenCalledWith({
325
- can_continue: false,
326
- reason: 'insufficient_balance',
327
- payment_account_balance: '500000',
328
- pending_amount: '0',
329
- required_amount: '1000000',
330
- });
331
- });
332
-
333
- it('should return can_continue: false if balance is exactly one unit less than required (boundary)', async () => {
334
- // Balance: 0.0999999 ABT (one unit less than 0.1 ABT)
335
- (checkTokenBalance as jest.Mock).mockImplementation((params: any) => {
336
- if (params.amount === '1000000') {
337
- return {
338
- sufficient: false,
339
- token: { balance: '999999' }, // 1 unit less than required
340
- reason: 'NO_ENOUGH_TOKEN',
341
- };
342
- }
343
- return {
344
- sufficient: true,
345
- token: { balance: '999999' },
346
- };
347
- });
348
-
349
- await routeHandler(mockReq as Request, mockRes as Response);
350
-
351
- expect(mockRes.json).toHaveBeenCalledWith({
352
- can_continue: false,
353
- reason: 'insufficient_balance',
354
- payment_account_balance: '999999',
355
- pending_amount: '0',
356
- required_amount: '1000000',
357
- });
358
- });
359
-
360
- it('should return can_continue: false if balance is zero', async () => {
361
- // Balance: 0 ABT
362
- (checkTokenBalance as jest.Mock).mockImplementation((params: any) => {
363
- if (params.amount === '1000000') {
364
- return {
365
- sufficient: false,
366
- token: { balance: '0' },
367
- reason: 'NO_ENOUGH_TOKEN',
368
- };
369
- }
370
- return {
371
- sufficient: true,
372
- token: { balance: '0' },
373
- };
374
- });
375
-
376
- await routeHandler(mockReq as Request, mockRes as Response);
377
-
378
- expect(mockRes.json).toHaveBeenCalledWith({
379
- can_continue: false,
380
- reason: 'insufficient_balance',
381
- payment_account_balance: '0',
382
- pending_amount: '0',
383
- required_amount: '1000000',
384
- });
385
- });
386
- });
387
-
388
- describe('Pending amount scenarios', () => {
389
- let mockConfig: any;
390
-
391
- beforeEach(() => {
392
- jest.spyOn(Customer, 'findByPkOrDid').mockResolvedValue({ id: 'customer_123', did: 'did:customer:123' } as any);
393
- jest.spyOn(PaymentCurrency, 'findByPk').mockResolvedValue({
394
- id: 'currency_123',
395
- decimal: 2,
396
- } as any);
397
-
398
- mockConfig = {
399
- rechargeCurrency: { id: 'recharge_currency_123' },
400
- price: {
401
- id: 'price_123',
402
- metadata: {
403
- credit_config: {
404
- credit_amount: '1', // 1 credit per recharge
405
- },
406
- },
407
- },
408
- paymentMethod: {
409
- type: 'arcblock',
410
- getOcapClient: jest.fn(),
411
- },
412
- quantity: 1,
413
- payment_settings: {
414
- payment_method_options: {
415
- arcblock: {
416
- payer: 'did:test:123',
417
- },
418
- },
419
- },
420
- last_recharge_date: '2024-01-01',
421
- };
422
-
423
- jest.spyOn(AutoRechargeConfig, 'findOne').mockResolvedValue(mockConfig as any);
424
- (getPriceUintAmountByCurrency as jest.Mock).mockResolvedValue('1000000'); // 1 credit with 2 decimals = 0.1 ABT (1 * 10^6 for ABT decimal=6)
425
- });
426
-
427
- describe('Recharge times limit (3 times)', () => {
428
- it('should return can_continue: false if required recharge times > 3', async () => {
429
- // Pending: 4 credits, each recharge: 1 credit, need 4 times
430
- // 4 credits with decimal=2: 4 * 10^2 = 400 (unit format, within system limit of 500)
431
- jest.spyOn(MeterEvent, 'getPendingAmounts').mockResolvedValue([{ currency_123: '400' }, [], []] as any);
432
-
433
- await routeHandler(mockReq as Request, mockRes as Response);
434
-
435
- expect(mockRes.json).toHaveBeenCalledWith({
436
- can_continue: false,
437
- reason: 'too_many_recharges_required',
438
- pending_amount: '400',
439
- required_recharge_times: '4',
440
- max_allowed_times: 3,
441
- });
442
- });
443
-
444
- it('should return can_continue: false if required recharge times exactly equals 4 (boundary)', async () => {
445
- // Pending: 4 credits, each recharge: 1 credit, need ceil(4/1) = 4 times
446
- // 4 credits with decimal=2: 4 * 10^2 = 400 (unit format)
447
- jest.spyOn(MeterEvent, 'getPendingAmounts').mockResolvedValue([{ currency_123: '400' }, [], []] as any);
448
-
449
- await routeHandler(mockReq as Request, mockRes as Response);
450
-
451
- expect(mockRes.json).toHaveBeenCalledWith({
452
- can_continue: false,
453
- reason: 'too_many_recharges_required',
454
- pending_amount: '400',
455
- required_recharge_times: '4',
456
- max_allowed_times: 3,
457
- });
458
- });
459
-
460
- it('should continue if required recharge times exactly equals 3 (boundary)', async () => {
461
- // Pending: 3 credits, each recharge: 1 credit, need exactly 3 times
462
- // 3 credits with decimal=2: 3 * 10^2 = 300 (unit format, within system limit of 500)
463
- jest.spyOn(MeterEvent, 'getPendingAmounts').mockResolvedValue([{ currency_123: '300' }, [], []] as any);
464
- (checkTokenBalance as jest.Mock)
465
- .mockResolvedValueOnce({
466
- sufficient: true,
467
- token: { balance: '10000000' },
468
- })
469
- .mockResolvedValueOnce({
470
- sufficient: true,
471
- token: { balance: '10000000' },
472
- });
473
-
474
- await routeHandler(mockReq as Request, mockRes as Response);
475
-
476
- expect(mockRes.json).not.toHaveBeenCalledWith(
477
- expect.objectContaining({
478
- reason: 'too_many_recharges_required',
479
- })
480
- );
481
- expect(mockRes.json).toHaveBeenCalledWith(
482
- expect.objectContaining({
483
- can_continue: true,
484
- })
485
- );
486
- });
487
-
488
- it('should continue if required recharge times is 1 (minimal pending)', async () => {
489
- // Pending: 1 credit, each recharge: 1 credit, need 1 time
490
- // 1 credit with decimal=2: 1 * 10^2 = 100 (unit format)
491
- jest.spyOn(MeterEvent, 'getPendingAmounts').mockResolvedValue([{ currency_123: '100' }, [], []] as any);
492
- (checkTokenBalance as jest.Mock)
493
- .mockResolvedValueOnce({
494
- sufficient: true,
495
- token: { balance: '10000000' },
496
- })
497
- .mockResolvedValueOnce({
498
- sufficient: true,
499
- token: { balance: '10000000' },
500
- });
501
-
502
- await routeHandler(mockReq as Request, mockRes as Response);
503
-
504
- expect(mockRes.json).not.toHaveBeenCalledWith(
505
- expect.objectContaining({
506
- reason: 'too_many_recharges_required',
507
- })
508
- );
509
- expect(checkTokenBalance).toHaveBeenCalled();
510
- });
511
-
512
- it('should continue if required recharge times <= 3', async () => {
513
- // Pending: 2 credits, each recharge: 1 credit, need 2 times
514
- // 2 credits with decimal=2: 2 * 10^2 = 200 (unit format, within system limit of 500)
515
- jest.spyOn(MeterEvent, 'getPendingAmounts').mockResolvedValue([{ currency_123: '200' }, [], []] as any);
516
- (checkTokenBalance as jest.Mock).mockResolvedValue({
517
- sufficient: true,
518
- token: { balance: '20000000' },
519
- });
520
-
521
- await routeHandler(mockReq as Request, mockRes as Response);
522
-
523
- // Should not return too_many_recharges_required error
524
- expect(mockRes.json).not.toHaveBeenCalledWith(
525
- expect.objectContaining({
526
- reason: 'too_many_recharges_required',
527
- })
528
- );
529
- });
530
- });
531
-
532
- describe('Custom max_recharge_times parameter', () => {
533
- it('should use custom max_recharge_times when provided', async () => {
534
- mockReq.query = {
535
- customer_id: 'customer_123',
536
- currency_id: 'currency_123',
537
- max_recharge_times: '5', // Custom: allow up to 5 recharges
538
- } as any;
539
-
540
- // Pending: 4 credits, each recharge: 1 credit, need 4 times
541
- // 4 credits with decimal=2: 4 * 10^2 = 400 (unit format, within system limit of 500)
542
- jest.spyOn(MeterEvent, 'getPendingAmounts').mockResolvedValue([{ currency_123: '400' }, [], []] as any);
543
- (checkTokenBalance as jest.Mock).mockResolvedValue({
544
- sufficient: true,
545
- token: { balance: '30000000' },
546
- });
547
-
548
- await routeHandler(mockReq as Request, mockRes as Response);
549
-
550
- // Should pass because 4 < 5
551
- expect(mockRes.json).not.toHaveBeenCalledWith(
552
- expect.objectContaining({
553
- reason: 'too_many_recharges_required',
554
- })
555
- );
556
- expect(mockRes.json).toHaveBeenCalledWith(
557
- expect.objectContaining({
558
- can_continue: true,
559
- })
560
- );
561
- });
562
-
563
- it('should reject if required recharge times exceeds custom max_recharge_times', async () => {
564
- mockReq.query = {
565
- customer_id: 'customer_123',
566
- currency_id: 'currency_123',
567
- max_recharge_times: '2', // Custom: only allow 2 recharges
568
- } as any;
569
-
570
- // Pending: 3 credits, each recharge: 1 credit, need 3 times
571
- // 3 credits with decimal=2: 3 * 10^2 = 300 (unit format, within system limit of 500)
572
- jest.spyOn(MeterEvent, 'getPendingAmounts').mockResolvedValue([{ currency_123: '300' }, [], []] as any);
573
-
574
- await routeHandler(mockReq as Request, mockRes as Response);
575
-
576
- // Should fail because 3 > 2
577
- expect(mockRes.json).toHaveBeenCalledWith({
578
- can_continue: false,
579
- reason: 'too_many_recharges_required',
580
- pending_amount: '300',
581
- required_recharge_times: '3',
582
- max_allowed_times: 2,
583
- });
584
- });
585
-
586
- it('should use default max_recharge_times (3) when not provided', async () => {
587
- mockReq.query = {
588
- customer_id: 'customer_123',
589
- currency_id: 'currency_123',
590
- // max_recharge_times not provided, should default to 3
591
- };
592
-
593
- // Pending: 4 credits, each recharge: 1 credit, need 4 times
594
- // 4 credits with decimal=2: 4 * 10^2 = 400 (unit format, within system limit of 500)
595
- jest.spyOn(MeterEvent, 'getPendingAmounts').mockResolvedValue([{ currency_123: '400' }, [], []] as any);
596
-
597
- await routeHandler(mockReq as Request, mockRes as Response);
598
-
599
- // Should fail because 4 > 3 (default)
600
- expect(mockRes.json).toHaveBeenCalledWith({
601
- can_continue: false,
602
- reason: 'too_many_recharges_required',
603
- pending_amount: '400',
604
- required_recharge_times: '4',
605
- max_allowed_times: 3, // Default value
606
- });
607
- });
608
- });
609
-
610
- describe('Max pending amount limit', () => {
611
- it('should reject if pending amount exceeds max_pending_amount', async () => {
612
- mockReq.query = {
613
- customer_id: 'customer_123',
614
- currency_id: 'currency_123',
615
- max_pending_amount: '3', // Token format: limit to 3 credits
616
- };
617
-
618
- // Pending: 4 credits (in unit format with decimal=2: 4 * 10^2 = 400, within system limit of 500)
619
- jest.spyOn(MeterEvent, 'getPendingAmounts').mockResolvedValue([{ currency_123: '400' }, [], []] as any);
620
-
621
- await routeHandler(mockReq as Request, mockRes as Response);
622
-
623
- expect(mockRes.json).toHaveBeenCalledWith({
624
- can_continue: false,
625
- reason: 'pending_amount_exceeds_limit',
626
- pending_amount: '400',
627
- max_pending_amount: '300', // Converted to unit format: 3 * 10^2 = 300
628
- detail: 'Current pending amount exceeds the maximum allowed limit',
629
- });
630
- });
631
-
632
- it('should allow if pending amount equals max_pending_amount (boundary)', async () => {
633
- mockReq.query = {
634
- customer_id: 'customer_123',
635
- currency_id: 'currency_123',
636
- max_pending_amount: '4', // Token format: limit to 4 credits
637
- };
638
-
639
- // Pending: exactly 4 credits (in unit format: 4 * 10^2 = 400, within system limit of 500)
640
- jest.spyOn(MeterEvent, 'getPendingAmounts').mockResolvedValue([{ currency_123: '400' }, [], []] as any);
641
- (checkTokenBalance as jest.Mock).mockResolvedValue({
642
- sufficient: true,
643
- token: { balance: '20000000' },
644
- });
645
-
646
- await routeHandler(mockReq as Request, mockRes as Response);
647
-
648
- // Should not be rejected by max_pending_amount check
649
- expect(mockRes.json).not.toHaveBeenCalledWith(
650
- expect.objectContaining({
651
- reason: 'pending_amount_exceeds_limit',
652
- })
653
- );
654
- });
655
-
656
- it('should allow if pending amount is below max_pending_amount', async () => {
657
- mockReq.query = {
658
- customer_id: 'customer_123',
659
- currency_id: 'currency_123',
660
- max_pending_amount: '4', // Token format: limit to 4 credits
661
- };
662
-
663
- // Pending: 2 credits (in unit format: 2 * 10^2 = 200, within system limit of 500)
664
- jest.spyOn(MeterEvent, 'getPendingAmounts').mockResolvedValue([{ currency_123: '200' }, [], []] as any);
665
- (checkTokenBalance as jest.Mock).mockResolvedValue({
666
- sufficient: true,
667
- token: { balance: '20000000' },
668
- });
669
-
670
- await routeHandler(mockReq as Request, mockRes as Response);
671
-
672
- expect(mockRes.json).not.toHaveBeenCalledWith(
673
- expect.objectContaining({
674
- reason: 'pending_amount_exceeds_limit',
675
- })
676
- );
677
- expect(mockRes.json).toHaveBeenCalledWith(
678
- expect.objectContaining({
679
- can_continue: true,
680
- })
681
- );
682
- });
683
-
684
- it('should handle decimal max_pending_amount values', async () => {
685
- mockReq.query = {
686
- customer_id: 'customer_123',
687
- currency_id: 'currency_123',
688
- max_pending_amount: '3.5', // Token format with decimal: 3.5 credits
689
- };
690
-
691
- // Pending: 4 credits (in unit format: 4 * 10^2 = 400, within system limit of 500)
692
- jest.spyOn(MeterEvent, 'getPendingAmounts').mockResolvedValue([{ currency_123: '400' }, [], []] as any);
693
-
694
- await routeHandler(mockReq as Request, mockRes as Response);
695
-
696
- expect(mockRes.json).toHaveBeenCalledWith({
697
- can_continue: false,
698
- reason: 'pending_amount_exceeds_limit',
699
- pending_amount: '400',
700
- max_pending_amount: '350', // Converted: 3.5 * 10^2 = 350
701
- detail: 'Current pending amount exceeds the maximum allowed limit',
702
- });
703
- });
704
-
705
- it('should not check max_pending_amount if pending is zero', async () => {
706
- mockReq.query = {
707
- customer_id: 'customer_123',
708
- currency_id: 'currency_123',
709
- max_pending_amount: '10', // Very low limit
710
- };
711
-
712
- // No pending amount
713
- jest.spyOn(MeterEvent, 'getPendingAmounts').mockResolvedValue([{ currency_123: '0' }, [], []] as any);
714
- (checkTokenBalance as jest.Mock).mockResolvedValue({
715
- sufficient: true,
716
- token: { balance: '20000000' },
717
- });
718
-
719
- await routeHandler(mockReq as Request, mockRes as Response);
720
-
721
- // Should not be rejected even though limit is low, because there's no pending
722
- expect(mockRes.json).not.toHaveBeenCalledWith(
723
- expect.objectContaining({
724
- reason: 'pending_amount_exceeds_limit',
725
- })
726
- );
727
- expect(mockRes.json).toHaveBeenCalledWith(
728
- expect.objectContaining({
729
- can_continue: true,
730
- })
731
- );
732
- });
733
- });
734
-
735
- describe('Balance checks with pending amount', () => {
736
- beforeEach(() => {
737
- // Pending: 2 credits, each recharge: 1 credit, need 2 times
738
- // 2 credits with decimal=2: 2 * 10^2 = 200 (unit format, within system limit of 500)
739
- jest.spyOn(MeterEvent, 'getPendingAmounts').mockResolvedValue([{ currency_123: '200' }, [], []] as any);
740
- });
741
-
742
- it('should return can_continue: false if balance is insufficient (less than required)', async () => {
743
- // Pending: 2 credits, each recharge: 1 credit
744
- // Required: 2 * 0.1 ABT = 0.2 ABT (pay off pending) + 0.1 ABT (one more recharge) = 0.3 ABT
745
- // Balance: 0.25 ABT (insufficient)
746
- (checkTokenBalance as jest.Mock).mockReset();
747
- (checkTokenBalance as jest.Mock).mockImplementation((params: any) => {
748
- // Required amount: 200 (pending) + 100 (one more recharge) = 300 (unit format for credits)
749
- // Convert to ABT: need to check actual amount calculation
750
- const requiredAmount = '3000000'; // 0.3 ABT with 6 decimals
751
- if (params.amount === requiredAmount) {
752
- return {
753
- sufficient: false,
754
- token: { balance: '2500000' }, // 0.25 ABT
755
- reason: 'NO_ENOUGH_TOKEN',
756
- };
757
- }
758
- return {
759
- sufficient: true,
760
- token: { balance: '2500000' },
761
- };
762
- });
763
-
764
- await routeHandler(mockReq as Request, mockRes as Response);
765
-
766
- expect(mockRes.json).toHaveBeenCalledWith(
767
- expect.objectContaining({
768
- can_continue: false,
769
- reason: 'insufficient_balance',
770
- pending_amount: '200',
771
- })
772
- );
773
- });
774
-
775
- it('should return can_continue: false if balance exactly equals required amount minus 1 (boundary)', async () => {
776
- // Required: 0.3 ABT, Balance: 0.299999 ABT (insufficient by 1 unit)
777
- (checkTokenBalance as jest.Mock).mockImplementation((params: any) => {
778
- const requiredAmount = '3000000';
779
- if (params.amount === requiredAmount) {
780
- return {
781
- sufficient: false,
782
- token: { balance: '2999999' }, // 1 unit less than required
783
- reason: 'NO_ENOUGH_TOKEN',
784
- };
785
- }
786
- return {
787
- sufficient: true,
788
- token: { balance: '2999999' },
789
- };
790
- });
791
-
792
- await routeHandler(mockReq as Request, mockRes as Response);
793
-
794
- expect(mockRes.json).toHaveBeenCalledWith(
795
- expect.objectContaining({
796
- can_continue: false,
797
- reason: 'insufficient_balance',
798
- pending_amount: '200',
799
- })
800
- );
801
- });
802
-
803
- it('should return can_continue: true if balance exactly equals required amount (boundary)', async () => {
804
- // Required: 0.3 ABT, Balance: exactly 0.3 ABT (sufficient)
805
- (checkTokenBalance as jest.Mock).mockImplementation((params: any) => {
806
- const requiredAmount = '3000000';
807
- if (params.amount === requiredAmount) {
808
- return {
809
- sufficient: true,
810
- token: { balance: '3000000' }, // Exactly required amount
811
- };
812
- }
813
- return {
814
- sufficient: true,
815
- token: { balance: '3000000' },
816
- };
817
- });
818
-
819
- await routeHandler(mockReq as Request, mockRes as Response);
820
-
821
- expect(mockRes.json).toHaveBeenCalledWith(
822
- expect.objectContaining({
823
- can_continue: true,
824
- payment_account_sufficient: true,
825
- })
826
- );
827
- });
828
-
829
- it('should return can_continue: false if balance can only pay off pending but not cover one more recharge', async () => {
830
- // Pending: 2 credits (need 2 recharges = 0.2 ABT)
831
- // Balance: 0.2 ABT (can pay off pending, but cannot cover one more recharge = 0.1 ABT)
832
- // Required: 0.2 + 0.1 = 0.3 ABT
833
- (checkTokenBalance as jest.Mock).mockImplementation((params: any) => {
834
- const requiredAmount = '3000000';
835
- if (params.amount === requiredAmount) {
836
- return {
837
- sufficient: false,
838
- token: { balance: '2000000' }, // Exactly enough to pay off pending, but not enough for one more
839
- reason: 'NO_ENOUGH_TOKEN',
840
- };
841
- }
842
- return {
843
- sufficient: true,
844
- token: { balance: '2000000' },
845
- };
846
- });
847
-
848
- await routeHandler(mockReq as Request, mockRes as Response);
849
-
850
- expect(mockRes.json).toHaveBeenCalledWith(
851
- expect.objectContaining({
852
- can_continue: false,
853
- reason: 'insufficient_balance',
854
- pending_amount: '200',
855
- })
856
- );
857
- });
858
-
859
- it('should return can_continue: true if balance is sufficient (greater than required)', async () => {
860
- // Required: 0.3 ABT, Balance: 0.5 ABT (sufficient)
861
- (checkTokenBalance as jest.Mock)
862
- .mockResolvedValueOnce({
863
- sufficient: true,
864
- token: { balance: '5000000' },
865
- })
866
- .mockResolvedValueOnce({
867
- sufficient: true,
868
- token: { balance: '5000000' },
869
- });
870
-
871
- await routeHandler(mockReq as Request, mockRes as Response);
872
-
873
- expect(mockRes.json).toHaveBeenCalledWith(
874
- expect.objectContaining({
875
- can_continue: true,
876
- payment_account_sufficient: true,
877
- })
878
- );
879
- });
880
-
881
- it('should return can_continue: false if balance is zero', async () => {
882
- // Balance: 0 ABT
883
- (checkTokenBalance as jest.Mock).mockReset();
884
- (checkTokenBalance as jest.Mock).mockImplementation((params: any) => {
885
- const requiredAmount = '3000000';
886
- if (params.amount === requiredAmount) {
887
- return {
888
- sufficient: false,
889
- token: { balance: '0' },
890
- reason: 'NO_ENOUGH_TOKEN',
891
- };
892
- }
893
- return {
894
- sufficient: true,
895
- token: { balance: '0' },
896
- };
897
- });
898
-
899
- await routeHandler(mockReq as Request, mockRes as Response);
900
-
901
- expect(mockRes.json).toHaveBeenCalledWith(
902
- expect.objectContaining({
903
- can_continue: false,
904
- reason: 'insufficient_balance',
905
- pending_amount: '200',
906
- })
907
- );
908
- });
909
- });
910
-
911
- describe('Daily limit checks', () => {
912
- beforeEach(() => {
913
- // Pending: 2 credits, each recharge: 1 credit, need 2 times
914
- // 2 credits with decimal=2: 2 * 10^2 = 200 (unit format, within system limit of 500)
915
- jest.spyOn(MeterEvent, 'getPendingAmounts').mockResolvedValue([{ currency_123: '200' }, [], []] as any);
916
- });
917
-
918
- it('should skip daily limit check if it is a new day', async () => {
919
- mockConfig.last_recharge_date = '2024-01-01';
920
- jest.spyOn(AutoRechargeConfig, 'findOne').mockResolvedValue(mockConfig as any);
921
- // Mock current date to be different
922
- jest.useFakeTimers();
923
- jest.setSystemTime(new Date('2024-01-02'));
924
-
925
- (checkTokenBalance as jest.Mock).mockResolvedValue({
926
- sufficient: true,
927
- token: { balance: '20000000' },
928
- });
929
-
930
- await routeHandler(mockReq as Request, mockRes as Response);
931
-
932
- // Should not check daily limits
933
- expect(mockRes.json).not.toHaveBeenCalledWith(
934
- expect.objectContaining({
935
- reason: 'daily_limit_reached',
936
- })
937
- );
938
-
939
- jest.useRealTimers();
940
- });
941
-
942
- it('should return can_continue: false if attempt limit exceeded', async () => {
943
- // eslint-disable-next-line prefer-destructuring
944
- mockConfig.last_recharge_date = new Date().toISOString().split('T')[0];
945
- mockConfig.daily_stats = {
946
- attempt_count: 2,
947
- total_amount: '0',
948
- };
949
- mockConfig.daily_limits = {
950
- max_attempts: 3,
951
- max_amount: '0',
952
- };
953
- jest.spyOn(AutoRechargeConfig, 'findOne').mockResolvedValue(mockConfig as any);
954
-
955
- // Need 2 recharges, but only 1 attempt remaining (3 - 2 = 1)
956
- await routeHandler(mockReq as Request, mockRes as Response);
957
-
958
- expect(mockRes.json).toHaveBeenCalledWith({
959
- can_continue: false,
960
- reason: 'daily_limit_reached',
961
- detail: 'attempt_limit_exceeded',
962
- });
963
- });
964
-
965
- it('should return can_continue: false if amount limit exceeded', async () => {
966
- // eslint-disable-next-line prefer-destructuring
967
- mockConfig.last_recharge_date = new Date().toISOString().split('T')[0];
968
- mockConfig.daily_stats = {
969
- attempt_count: 0,
970
- total_amount: '1800000', // Already spent 1.8 ABT today
971
- };
972
- mockConfig.daily_limits = {
973
- max_attempts: 0,
974
- max_amount: '2000000', // Max 2 ABT per day
975
- };
976
- jest.spyOn(AutoRechargeConfig, 'findOne').mockResolvedValue(mockConfig as any);
977
-
978
- // Need 2 recharges = 0.2 ABT, but only 0.2 ABT remaining (2 - 1.8 = 0.2)
979
- // However, we need to check if the remaining amount is sufficient
980
- // Actually, if we need 2 recharges of 0.1 ABT each = 0.2 ABT, and we have 0.2 ABT remaining, it should pass
981
- // Let's set it so that we need more than what's remaining
982
- // Pending: 2 credits, need 2 recharges = 0.2 ABT, but only 0.15 ABT remaining
983
- mockConfig.daily_stats.total_amount = '1850000'; // Already spent 1.85 ABT, only 0.15 ABT remaining
984
- jest.spyOn(AutoRechargeConfig, 'findOne').mockResolvedValue(mockConfig as any);
985
-
986
- await routeHandler(mockReq as Request, mockRes as Response);
987
-
988
- expect(mockRes.json).toHaveBeenCalledWith({
989
- can_continue: false,
990
- reason: 'daily_limit_reached',
991
- detail: 'amount_limit_exceeded',
992
- });
993
- });
994
-
995
- it('should pass daily limit check if limits are sufficient', async () => {
996
- // eslint-disable-next-line prefer-destructuring
997
- mockConfig.last_recharge_date = new Date().toISOString().split('T')[0];
998
- mockConfig.daily_stats = {
999
- attempt_count: 0,
1000
- total_amount: '0',
1001
- };
1002
- mockConfig.daily_limits = {
1003
- max_attempts: 5,
1004
- max_amount: '50000000', // Max 50 ABT per day
1005
- };
1006
- jest.spyOn(AutoRechargeConfig, 'findOne').mockResolvedValue(mockConfig as any);
1007
-
1008
- (checkTokenBalance as jest.Mock)
1009
- .mockResolvedValueOnce({
1010
- sufficient: true,
1011
- token: { balance: '20000000' },
1012
- })
1013
- .mockResolvedValueOnce({
1014
- sufficient: true,
1015
- token: { balance: '20000000' },
1016
- });
1017
-
1018
- await routeHandler(mockReq as Request, mockRes as Response);
1019
-
1020
- expect(mockRes.json).not.toHaveBeenCalledWith(
1021
- expect.objectContaining({
1022
- reason: 'daily_limit_reached',
1023
- })
1024
- );
1025
- });
1026
- });
1027
-
1028
- describe('Credit config checks', () => {
1029
- it('should return can_continue: false if credit_config not found', async () => {
1030
- mockConfig.price.metadata = {};
1031
- jest.spyOn(AutoRechargeConfig, 'findOne').mockResolvedValue(mockConfig as any);
1032
- jest.spyOn(MeterEvent, 'getPendingAmounts').mockResolvedValue([{ currency_123: '200' }, [], []] as any);
1033
-
1034
- await routeHandler(mockReq as Request, mockRes as Response);
1035
-
1036
- expect(mockRes.json).toHaveBeenCalledWith({
1037
- can_continue: false,
1038
- reason: 'credit_config_not_found',
1039
- });
1040
- });
1041
-
1042
- it('should return can_continue: false if credit_amount not found', async () => {
1043
- mockConfig.price.metadata = {
1044
- credit_config: {},
1045
- };
1046
- jest.spyOn(AutoRechargeConfig, 'findOne').mockResolvedValue(mockConfig as any);
1047
- jest.spyOn(MeterEvent, 'getPendingAmounts').mockResolvedValue([{ currency_123: '200' }, [], []] as any);
1048
-
1049
- await routeHandler(mockReq as Request, mockRes as Response);
1050
-
1051
- expect(mockRes.json).toHaveBeenCalledWith({
1052
- can_continue: false,
1053
- reason: 'credit_config_not_found',
1054
- });
1055
- });
1056
- });
1057
-
1058
- describe('Payer checks', () => {
1059
- it('should return can_continue: false if payer not found', async () => {
1060
- jest.spyOn(Customer, 'findByPkOrDid').mockResolvedValue({ id: 'customer_123', did: null } as any);
1061
- mockConfig.payment_settings = {
1062
- payment_method_options: {},
1063
- };
1064
- jest.spyOn(AutoRechargeConfig, 'findOne').mockResolvedValue(mockConfig as any);
1065
- jest.spyOn(MeterEvent, 'getPendingAmounts').mockResolvedValue([{ currency_123: '200' }, [], []] as any);
1066
-
1067
- await routeHandler(mockReq as Request, mockRes as Response);
1068
-
1069
- expect(mockRes.json).toHaveBeenCalledWith({
1070
- can_continue: false,
1071
- reason: 'payer_not_found',
1072
- });
1073
- });
1074
-
1075
- it('should use customer.did as payer if not specified in payment_settings', async () => {
1076
- jest.spyOn(Customer, 'findByPkOrDid').mockResolvedValue({
1077
- id: 'customer_123',
1078
- did: 'did:customer:123',
1079
- } as any);
1080
- mockConfig.payment_settings = {
1081
- payment_method_options: {},
1082
- };
1083
- jest.spyOn(AutoRechargeConfig, 'findOne').mockResolvedValue(mockConfig as any);
1084
- jest.spyOn(MeterEvent, 'getPendingAmounts').mockResolvedValue([{ currency_123: '0' }, [], []] as any);
1085
- (checkTokenBalance as jest.Mock).mockResolvedValue({
1086
- sufficient: true,
1087
- token: { balance: '6000000' },
1088
- });
1089
-
1090
- await routeHandler(mockReq as Request, mockRes as Response);
1091
-
1092
- expect(checkTokenBalance).toHaveBeenCalledWith(
1093
- expect.objectContaining({
1094
- userDid: 'did:customer:123',
1095
- })
1096
- );
1097
- });
1098
- });
1099
-
1100
- describe('Pending amount edge cases', () => {
1101
- it('should handle pending amount exactly equals one recharge credit amount', async () => {
1102
- // Pending: exactly 1 credit (one recharge), need 1 time
1103
- // 1 credit with decimal=2: 1 * 10^2 = 100 (unit format, within system limit of 500)
1104
- jest.spyOn(MeterEvent, 'getPendingAmounts').mockResolvedValue([{ currency_123: '100' }, [], []] as any);
1105
- (checkTokenBalance as jest.Mock)
1106
- .mockResolvedValueOnce({
1107
- sufficient: true,
1108
- token: { balance: '10000000' },
1109
- })
1110
- .mockResolvedValueOnce({
1111
- sufficient: true,
1112
- token: { balance: '10000000' },
1113
- });
1114
-
1115
- await routeHandler(mockReq as Request, mockRes as Response);
1116
-
1117
- expect(mockRes.json).toHaveBeenCalledWith(
1118
- expect.objectContaining({
1119
- can_continue: true,
1120
- })
1121
- );
1122
- });
1123
-
1124
- it('should handle pending amount exactly equals three recharge credit amount (boundary)', async () => {
1125
- // Pending: exactly 3 credits (three recharges), need exactly 3 times
1126
- // 3 credits with decimal=2: 3 * 10^2 = 300 (unit format, within system limit of 500)
1127
- jest.spyOn(MeterEvent, 'getPendingAmounts').mockResolvedValue([{ currency_123: '300' }, [], []] as any);
1128
- (checkTokenBalance as jest.Mock)
1129
- .mockResolvedValueOnce({
1130
- sufficient: true,
1131
- token: { balance: '10000000' },
1132
- })
1133
- .mockResolvedValueOnce({
1134
- sufficient: true,
1135
- token: { balance: '10000000' },
1136
- });
1137
-
1138
- await routeHandler(mockReq as Request, mockRes as Response);
1139
-
1140
- expect(mockRes.json).not.toHaveBeenCalledWith(
1141
- expect.objectContaining({
1142
- reason: 'too_many_recharges_required',
1143
- })
1144
- );
1145
- expect(mockRes.json).toHaveBeenCalledWith(
1146
- expect.objectContaining({
1147
- can_continue: true,
1148
- })
1149
- );
1150
- });
1151
-
1152
- it('should handle pending amount one unit more than three recharge credit amount', async () => {
1153
- // Pending: 4 credits (one unit more than 3), need 4 times
1154
- // 4 credits with decimal=2: 4 * 10^2 = 400 (unit format, within system limit of 500)
1155
- jest.spyOn(MeterEvent, 'getPendingAmounts').mockResolvedValue([{ currency_123: '400' }, [], []] as any);
1156
-
1157
- await routeHandler(mockReq as Request, mockRes as Response);
1158
-
1159
- expect(mockRes.json).toHaveBeenCalledWith({
1160
- can_continue: false,
1161
- reason: 'too_many_recharges_required',
1162
- pending_amount: '400',
1163
- required_recharge_times: '4',
1164
- max_allowed_times: 3,
1165
- });
1166
- });
1167
-
1168
- it('should handle very small pending amount (1 credit)', async () => {
1169
- // Pending: 1 credit, need 1 time
1170
- // 1 credit with decimal=2: 1 * 10^2 = 100 (unit format)
1171
- jest.spyOn(MeterEvent, 'getPendingAmounts').mockResolvedValue([{ currency_123: '100' }, [], []] as any);
1172
- (checkTokenBalance as jest.Mock)
1173
- .mockResolvedValueOnce({
1174
- sufficient: true,
1175
- token: { balance: '10000000' }, // 10 ABT (1 * 5 + 5)
1176
- })
1177
- .mockResolvedValueOnce({
1178
- sufficient: true,
1179
- token: { balance: '10000000' },
1180
- });
1181
-
1182
- await routeHandler(mockReq as Request, mockRes as Response);
1183
-
1184
- expect(checkTokenBalance).toHaveBeenCalledWith(
1185
- expect.objectContaining({
1186
- amount: '2000000', // 0.1 ABT (pay off 1 credit) + 0.1 ABT (one more recharge)
1187
- })
1188
- );
1189
- expect(mockRes.json).toHaveBeenCalledWith(
1190
- expect.objectContaining({
1191
- can_continue: true,
1192
- })
1193
- );
1194
- });
1195
- });
1196
-
1197
- describe('Pending amount from query parameter', () => {
1198
- it('should use pending_amount from query if provided', async () => {
1199
- mockReq.query = {
1200
- customer_id: 'customer_123',
1201
- currency_id: 'currency_123',
1202
- pending_amount: '300', // 3 credits with decimal=2: 3 * 10^2 = 300 (unit format, within system limit of 500)
1203
- };
1204
-
1205
- jest.spyOn(MeterEvent, 'getPendingAmounts').mockResolvedValue([{ currency_123: '200' }, [], []] as any);
1206
- (checkTokenBalance as jest.Mock).mockResolvedValue({
1207
- sufficient: true,
1208
- token: { balance: '20000000' },
1209
- });
1210
-
1211
- await routeHandler(mockReq as Request, mockRes as Response);
1212
-
1213
- // Should use 300 from query, not 200 from getPendingAmounts
1214
- // 3 credits / 1 credit per recharge = 3 times
1215
- expect(checkTokenBalance).toHaveBeenCalled();
1216
- expect(mockRes.json).toHaveBeenCalledWith(
1217
- expect.objectContaining({
1218
- can_continue: true,
1219
- })
1220
- );
1221
- });
1222
- });
1223
-
1224
- describe('Quantity handling', () => {
1225
- it('should handle quantity > 1 correctly', async () => {
1226
- mockConfig.quantity = 2; // Buy 2 units per recharge
1227
- jest.spyOn(AutoRechargeConfig, 'findOne').mockResolvedValue(mockConfig as any);
1228
- (getPriceUintAmountByCurrency as jest.Mock).mockResolvedValue('1000000'); // 1 credit (0.1 ABT) per unit
1229
- jest.spyOn(MeterEvent, 'getPendingAmounts').mockResolvedValue([{ currency_123: '0' }, [], []] as any);
1230
-
1231
- (checkTokenBalance as jest.Mock).mockResolvedValue({
1232
- sufficient: true,
1233
- token: { balance: '20000000' },
1234
- });
1235
-
1236
- await routeHandler(mockReq as Request, mockRes as Response);
1237
-
1238
- // totalAmount should be 0.1 ABT * 2 = 0.2 ABT
1239
- expect(checkTokenBalance).toHaveBeenCalledWith(
1240
- expect.objectContaining({
1241
- amount: '2000000', // 0.2 ABT (1 credit * 2 quantity)
1242
- })
1243
- );
1244
- });
1245
- });
1246
- });
1247
-
1248
- describe('Error handling', () => {
1249
- it('should handle errors and return 400', async () => {
1250
- jest.spyOn(Customer, 'findByPkOrDid').mockRejectedValue(new Error('Database error'));
1251
-
1252
- await routeHandler(mockReq as Request, mockRes as Response);
1253
-
1254
- expect(mockRes.status).toHaveBeenCalledWith(400);
1255
- expect(mockRes.json).toHaveBeenCalledWith({
1256
- error: 'Database error',
1257
- });
1258
- expect(logger.error).toHaveBeenCalled();
1259
- });
1260
- });
1261
- });