payment-kit 1.18.49 → 1.18.51

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 (394) hide show
  1. package/api/src/crons/currency.ts +25 -4
  2. package/api/src/libs/invoice.ts +231 -0
  3. package/api/src/libs/payment.ts +92 -0
  4. package/api/src/queues/payment.ts +0 -2
  5. package/api/src/queues/subscription.ts +24 -1
  6. package/api/src/routes/connect/change-payment.ts +62 -42
  7. package/api/src/routes/connect/shared.ts +5 -3
  8. package/api/src/routes/connect/subscribe.ts +36 -5
  9. package/api/src/routes/invoices.ts +75 -2
  10. package/api/src/routes/subscriptions.ts +37 -6
  11. package/blocklet.yml +1 -1
  12. package/package.json +18 -18
  13. package/public/currencies/0xBTC.png +0 -0
  14. package/public/currencies/1INCH.svg +1 -0
  15. package/public/currencies/AAVE.svg +1 -0
  16. package/public/currencies/ABT.svg +16 -0
  17. package/public/currencies/ACX.png +0 -0
  18. package/public/currencies/ADX.svg +12 -0
  19. package/public/currencies/AIKEK.png +0 -0
  20. package/public/currencies/AJNA.svg +12 -0
  21. package/public/currencies/AKIRA.png +0 -0
  22. package/public/currencies/ALEX.png +0 -0
  23. package/public/currencies/ALI.png +0 -0
  24. package/public/currencies/ALT.svg +12 -0
  25. package/public/currencies/AMKT.svg +5 -0
  26. package/public/currencies/AMP.svg +7 -0
  27. package/public/currencies/APU.png +0 -0
  28. package/public/currencies/APW.svg +21 -0
  29. package/public/currencies/APX.png +0 -0
  30. package/public/currencies/ARC.png +0 -0
  31. package/public/currencies/ARIA20.svg +1 -0
  32. package/public/currencies/ARPA.svg +1 -0
  33. package/public/currencies/ATH.svg +1 -0
  34. package/public/currencies/AVC.png +0 -0
  35. package/public/currencies/AVRK.png +0 -0
  36. package/public/currencies/AXGT.svg +1 -0
  37. package/public/currencies/AZUR.svg +20 -0
  38. package/public/currencies/BAL.png +0 -0
  39. package/public/currencies/BANK.png +0 -0
  40. package/public/currencies/BAXA.svg +3 -0
  41. package/public/currencies/BCAT.png +0 -0
  42. package/public/currencies/BEPRO.svg +1 -0
  43. package/public/currencies/BERRY.svg +1 -0
  44. package/public/currencies/BICO.svg +11 -0
  45. package/public/currencies/BITBOT.jpg +0 -0
  46. package/public/currencies/BITCOIN.png +0 -0
  47. package/public/currencies/BKN.svg +1 -0
  48. package/public/currencies/BLUE.png +0 -0
  49. package/public/currencies/BNB.png +0 -0
  50. package/public/currencies/BOBA.svg +16 -0
  51. package/public/currencies/BOBO.png +0 -0
  52. package/public/currencies/BOND.svg +10 -0
  53. package/public/currencies/BOTTO.svg +4 -0
  54. package/public/currencies/BRIGHT.svg +83 -0
  55. package/public/currencies/BTRST.svg +1 -0
  56. package/public/currencies/BURN.png +0 -0
  57. package/public/currencies/BitANT.png +0 -0
  58. package/public/currencies/BitBTC.png +0 -0
  59. package/public/currencies/CARD.svg +1 -0
  60. package/public/currencies/CBX.svg +1 -0
  61. package/public/currencies/CHI.svg +1 -0
  62. package/public/currencies/CIG.svg +21 -0
  63. package/public/currencies/CNG.svg +9 -0
  64. package/public/currencies/COC.svg +8 -0
  65. package/public/currencies/COLLAB.svg +8 -0
  66. package/public/currencies/COMP.svg +1 -0
  67. package/public/currencies/COW.svg +1 -0
  68. package/public/currencies/CRV.png +0 -0
  69. package/public/currencies/CRYO.svg +1 -0
  70. package/public/currencies/CTRAVL.svg +1 -0
  71. package/public/currencies/CTSI.svg +1 -0
  72. package/public/currencies/CTX.svg +1 -0
  73. package/public/currencies/CUSD.png +0 -0
  74. package/public/currencies/CXT.svg +11 -0
  75. package/public/currencies/D2D.svg +6 -0
  76. package/public/currencies/DAI.svg +13 -0
  77. package/public/currencies/DCN.svg +47 -0
  78. package/public/currencies/DF.svg +15 -0
  79. package/public/currencies/DHT.svg +1 -0
  80. package/public/currencies/DIMO.svg +1 -0
  81. package/public/currencies/DMS.png +0 -0
  82. package/public/currencies/DODO.svg +6 -0
  83. package/public/currencies/DOGEGF.png +0 -0
  84. package/public/currencies/DOLA.svg +1 -0
  85. package/public/currencies/DOSE.svg +1 -0
  86. package/public/currencies/DUCK.png +0 -0
  87. package/public/currencies/DYP.png +0 -0
  88. package/public/currencies/EIGEN.svg +13 -0
  89. package/public/currencies/ENS.png +0 -0
  90. package/public/currencies/EPOCH.svg +1 -0
  91. package/public/currencies/EQB.svg +4 -0
  92. package/public/currencies/EQZ.png +0 -0
  93. package/public/currencies/ERN.png +0 -0
  94. package/public/currencies/ERNTST.png +0 -0
  95. package/public/currencies/EST.png +0 -0
  96. package/public/currencies/ETH.svg +13 -0
  97. package/public/currencies/ETHIX.svg +1 -0
  98. package/public/currencies/EVERY.svg +13 -0
  99. package/public/currencies/F.svg +1 -0
  100. package/public/currencies/FACTR.png +0 -0
  101. package/public/currencies/FANS.svg +11 -0
  102. package/public/currencies/FARM.png +0 -0
  103. package/public/currencies/FCR.png +0 -0
  104. package/public/currencies/FET.svg +21 -0
  105. package/public/currencies/FIS.svg +22 -0
  106. package/public/currencies/FLASH.svg +22 -0
  107. package/public/currencies/FLy.png +0 -0
  108. package/public/currencies/FOAM.svg +23 -0
  109. package/public/currencies/FORTH.svg +1 -0
  110. package/public/currencies/FOX.svg +8 -0
  111. package/public/currencies/FRAX.png +0 -0
  112. package/public/currencies/FU.svg +17 -0
  113. package/public/currencies/FVT.svg +1 -0
  114. package/public/currencies/FXN.svg +25 -0
  115. package/public/currencies/FXS.png +0 -0
  116. package/public/currencies/Froglic.png +0 -0
  117. package/public/currencies/GALAXIS.svg +1 -0
  118. package/public/currencies/GCH.png +0 -0
  119. package/public/currencies/GENOME.svg +23 -0
  120. package/public/currencies/GGTK.svg +1 -0
  121. package/public/currencies/GHST.svg +15 -0
  122. package/public/currencies/GIGACHAD.png +0 -0
  123. package/public/currencies/GIV.svg +6 -0
  124. package/public/currencies/GLS.svg +22 -0
  125. package/public/currencies/GOLD.png +0 -0
  126. package/public/currencies/GRG.png +0 -0
  127. package/public/currencies/GROW.svg +1 -0
  128. package/public/currencies/GRT.svg +11 -0
  129. package/public/currencies/GTC.svg +5 -0
  130. package/public/currencies/GURU.svg +1 -0
  131. package/public/currencies/GYEN.svg +1 -0
  132. package/public/currencies/GYSR.png +0 -0
  133. package/public/currencies/HAIR.svg +1 -0
  134. package/public/currencies/HAN.svg +25 -0
  135. package/public/currencies/HANeP.svg +1 -0
  136. package/public/currencies/HAUS.png +0 -0
  137. package/public/currencies/HEU.svg +5 -0
  138. package/public/currencies/HOBA.png +0 -0
  139. package/public/currencies/HUNT.svg +1 -0
  140. package/public/currencies/IBEX.png +0 -0
  141. package/public/currencies/IDRISS.svg +4 -0
  142. package/public/currencies/IOTX.png +0 -0
  143. package/public/currencies/IPOR.svg +1 -0
  144. package/public/currencies/IPT.svg +7 -0
  145. package/public/currencies/IRARA.svg +1 -0
  146. package/public/currencies/ISK.png +0 -0
  147. package/public/currencies/IYKYK.svg +14 -0
  148. package/public/currencies/JAM.svg +20 -0
  149. package/public/currencies/JRT.svg +8 -0
  150. package/public/currencies/KAGE.png +0 -0
  151. package/public/currencies/KAI.svg +23 -0
  152. package/public/currencies/KIBSHI.png +0 -0
  153. package/public/currencies/KNC.png +0 -0
  154. package/public/currencies/KOMPETE.png +0 -0
  155. package/public/currencies/KROM.png +0 -0
  156. package/public/currencies/LCX.svg +6 -0
  157. package/public/currencies/LDO.svg +5 -0
  158. package/public/currencies/LIF3.svg +27 -0
  159. package/public/currencies/LINK.png +0 -0
  160. package/public/currencies/LIZ.png +0 -0
  161. package/public/currencies/LMEOW.png +0 -0
  162. package/public/currencies/LOCG.png +0 -0
  163. package/public/currencies/LORDS.png +0 -0
  164. package/public/currencies/LPF.svg +18 -0
  165. package/public/currencies/LQTY.svg +1 -0
  166. package/public/currencies/LRC.png +0 -0
  167. package/public/currencies/LRDS.svg +44 -0
  168. package/public/currencies/LSK.png +0 -0
  169. package/public/currencies/LUSD.svg +1 -0
  170. package/public/currencies/LYRA.png +0 -0
  171. package/public/currencies/MASK.svg +4 -0
  172. package/public/currencies/MASQ.png +0 -0
  173. package/public/currencies/MBS.png +0 -0
  174. package/public/currencies/MCADE.svg +26 -0
  175. package/public/currencies/MET.svg +15 -0
  176. package/public/currencies/MINDS.svg +32 -0
  177. package/public/currencies/MKR.png +0 -0
  178. package/public/currencies/MLN.svg +1 -0
  179. package/public/currencies/MOCHI.svg +1 -0
  180. package/public/currencies/MOM.svg +5 -0
  181. package/public/currencies/MONKE.png +0 -0
  182. package/public/currencies/MOOV.svg +5 -0
  183. package/public/currencies/MORPHO.svg +14 -0
  184. package/public/currencies/MPWR.svg +5 -0
  185. package/public/currencies/MTA.svg +15 -0
  186. package/public/currencies/MTL.png +0 -0
  187. package/public/currencies/MUGLOO.png +0 -0
  188. package/public/currencies/Mog.png +0 -0
  189. package/public/currencies/NAVI.svg +1 -0
  190. package/public/currencies/NCT.svg +49 -0
  191. package/public/currencies/NET.svg +11 -0
  192. package/public/currencies/NEURON.svg +38 -0
  193. package/public/currencies/NFD.png +0 -0
  194. package/public/currencies/NOGS.png +0 -0
  195. package/public/currencies/NOUNS.png +0 -0
  196. package/public/currencies/NOVA.png +0 -0
  197. package/public/currencies/NSTR.svg +4 -0
  198. package/public/currencies/Neiro.png +0 -0
  199. package/public/currencies/OCEAN.png +0 -0
  200. package/public/currencies/OGN.svg +8 -0
  201. package/public/currencies/OL.svg +21 -0
  202. package/public/currencies/OLAS.svg +7 -0
  203. package/public/currencies/OM.png +0 -0
  204. package/public/currencies/OMETA.svg +7 -0
  205. package/public/currencies/OMI.svg +42 -0
  206. package/public/currencies/OPENLOOT.svg +21 -0
  207. package/public/currencies/OPN.svg +1 -0
  208. package/public/currencies/OS.svg +1 -0
  209. package/public/currencies/OUTb.svg +4 -0
  210. package/public/currencies/OVR.png +0 -0
  211. package/public/currencies/PAGE.svg +1 -0
  212. package/public/currencies/PAL.svg +1 -0
  213. package/public/currencies/PAPER.svg +4 -0
  214. package/public/currencies/PENDLE.png +0 -0
  215. package/public/currencies/PEPE.png +0 -0
  216. package/public/currencies/PERP.png +0 -0
  217. package/public/currencies/PHTK.svg +14 -0
  218. package/public/currencies/PIP.svg +11 -0
  219. package/public/currencies/POGS.png +0 -0
  220. package/public/currencies/POOL.svg +22 -0
  221. package/public/currencies/POP.svg +8 -0
  222. package/public/currencies/PORK.png +0 -0
  223. package/public/currencies/PRE.svg +66 -0
  224. package/public/currencies/PREMIA.svg +4 -0
  225. package/public/currencies/PRF.png +0 -0
  226. package/public/currencies/PRO.svg +1 -0
  227. package/public/currencies/PSP.svg +35 -0
  228. package/public/currencies/PSTAKE.png +0 -0
  229. package/public/currencies/PSY.svg +26 -0
  230. package/public/currencies/RAC.png +0 -0
  231. package/public/currencies/RAI.svg +1 -0
  232. package/public/currencies/RAZOR.svg +1 -0
  233. package/public/currencies/REKT.png +0 -0
  234. package/public/currencies/RFKJ.png +0 -0
  235. package/public/currencies/RFWSTETH.svg +36 -0
  236. package/public/currencies/RGT.png +0 -0
  237. package/public/currencies/RICE.svg +22 -0
  238. package/public/currencies/RIZE.svg +21 -0
  239. package/public/currencies/RPL.svg +1 -0
  240. package/public/currencies/RSC.svg +6 -0
  241. package/public/currencies/RSR.png +0 -0
  242. package/public/currencies/RSS3.png +0 -0
  243. package/public/currencies/Reach.svg +17 -0
  244. package/public/currencies/SAIL.png +0 -0
  245. package/public/currencies/SALD.png +0 -0
  246. package/public/currencies/SARCO.png +0 -0
  247. package/public/currencies/SCM.svg +1 -0
  248. package/public/currencies/SCRY.svg +13 -0
  249. package/public/currencies/SDL.svg +1 -0
  250. package/public/currencies/SEAM.svg +5 -0
  251. package/public/currencies/SEXY.svg +1 -0
  252. package/public/currencies/SHU.png +0 -0
  253. package/public/currencies/SIPHER.png +0 -0
  254. package/public/currencies/SKAI.svg +1 -0
  255. package/public/currencies/SLN.png +0 -0
  256. package/public/currencies/SMT.svg +1 -0
  257. package/public/currencies/SMURFCAT.png +0 -0
  258. package/public/currencies/SNT.svg +14 -0
  259. package/public/currencies/SNX.svg +20 -0
  260. package/public/currencies/SOFI.png +0 -0
  261. package/public/currencies/SOFT.svg +1 -0
  262. package/public/currencies/SOVRN.svg +13 -0
  263. package/public/currencies/SPANK.png +0 -0
  264. package/public/currencies/SPC.png +0 -0
  265. package/public/currencies/SPOT.svg +1 -0
  266. package/public/currencies/SQD.png +0 -0
  267. package/public/currencies/SQT.svg +14 -0
  268. package/public/currencies/STNT.svg +38 -0
  269. package/public/currencies/STPT.png +0 -0
  270. package/public/currencies/SUDO.svg +6 -0
  271. package/public/currencies/SUKU.png +0 -0
  272. package/public/currencies/SUPER.svg +6 -0
  273. package/public/currencies/SUSHI.svg +19 -0
  274. package/public/currencies/SYNTH.svg +1 -0
  275. package/public/currencies/ShibDoge.png +0 -0
  276. package/public/currencies/Silo.svg +4 -0
  277. package/public/currencies/StaFi.svg +65 -0
  278. package/public/currencies/T.svg +4 -0
  279. package/public/currencies/TEN.svg +1 -0
  280. package/public/currencies/TETU.svg +6 -0
  281. package/public/currencies/THALES.png +0 -0
  282. package/public/currencies/THING.png +0 -0
  283. package/public/currencies/THX.svg +22 -0
  284. package/public/currencies/TKAI.svg +1 -0
  285. package/public/currencies/TKN.png +0 -0
  286. package/public/currencies/TODL.png +0 -0
  287. package/public/currencies/TOWER.svg +1 -0
  288. package/public/currencies/TRAC.png +0 -0
  289. package/public/currencies/TRB.png +0 -0
  290. package/public/currencies/TRUE.svg +11 -0
  291. package/public/currencies/TRX.svg +4 -0
  292. package/public/currencies/TSC.png +0 -0
  293. package/public/currencies/TT.png +0 -0
  294. package/public/currencies/TUSD.png +0 -0
  295. package/public/currencies/TYBENG.png +0 -0
  296. package/public/currencies/TheDAO.svg +1 -0
  297. package/public/currencies/U.png +0 -0
  298. package/public/currencies/UBI.svg +5 -0
  299. package/public/currencies/UCASH.svg +1 -0
  300. package/public/currencies/UDT.svg +4 -0
  301. package/public/currencies/UMA.png +0 -0
  302. package/public/currencies/UNI.png +0 -0
  303. package/public/currencies/USA.png +0 -0
  304. package/public/currencies/USC.svg +23 -0
  305. package/public/currencies/USD3.svg +13 -0
  306. package/public/currencies/USDD.png +0 -0
  307. package/public/currencies/USDS.png +0 -0
  308. package/public/currencies/USDbC.png +0 -0
  309. package/public/currencies/UST.png +0 -0
  310. package/public/currencies/VALX.png +0 -0
  311. package/public/currencies/VIDYA.png +0 -0
  312. package/public/currencies/VITA.svg +10 -0
  313. package/public/currencies/VSP.svg +15 -0
  314. package/public/currencies/VUSD.svg +1 -0
  315. package/public/currencies/WAAC.png +0 -0
  316. package/public/currencies/WAD.png +0 -0
  317. package/public/currencies/WALLET.svg +1 -0
  318. package/public/currencies/WAMPL.svg +53 -0
  319. package/public/currencies/WBTC.png +0 -0
  320. package/public/currencies/WCT.svg +5 -0
  321. package/public/currencies/WEXO.png +0 -0
  322. package/public/currencies/WLD.jpeg +0 -0
  323. package/public/currencies/WMC.png +0 -0
  324. package/public/currencies/WOO.png +0 -0
  325. package/public/currencies/Wildfire.svg +1 -0
  326. package/public/currencies/XCHF.png +0 -0
  327. package/public/currencies/XCN.svg +1 -0
  328. package/public/currencies/XMT.svg +1 -0
  329. package/public/currencies/XYO.svg +1 -0
  330. package/public/currencies/YFI.svg +4 -0
  331. package/public/currencies/YFX.svg +11 -0
  332. package/public/currencies/YGG.svg +11 -0
  333. package/public/currencies/YOKAI.png +0 -0
  334. package/public/currencies/YYY.svg +1 -0
  335. package/public/currencies/ZENT.svg +12 -0
  336. package/public/currencies/ZRX.png +0 -0
  337. package/public/currencies/ZUN.svg +24 -0
  338. package/public/currencies/ZUSD.svg +1 -0
  339. package/public/currencies/ZYN.png +0 -0
  340. package/public/currencies/aCRV.png +0 -0
  341. package/public/currencies/btc.png +0 -0
  342. package/public/currencies/bwAJNA.svg +1 -0
  343. package/public/currencies/cSTONE.svg +23 -0
  344. package/public/currencies/cbETH.svg +9 -0
  345. package/public/currencies/crvUSD.png +0 -0
  346. package/public/currencies/cvxFXS.png +0 -0
  347. package/public/currencies/dollar.png +0 -0
  348. package/public/currencies/ePendle.png +0 -0
  349. package/public/currencies/frxETH.png +0 -0
  350. package/public/currencies/fxUSD.svg +5 -0
  351. package/public/currencies/iFARM.png +0 -0
  352. package/public/currencies/iZi.png +0 -0
  353. package/public/currencies/imgnAI.svg +1 -0
  354. package/public/currencies/mpETH.svg +4 -0
  355. package/public/currencies/nETH.svg +11 -0
  356. package/public/currencies/rETH.svg +1 -0
  357. package/public/currencies/rgUSD.svg +5 -0
  358. package/public/currencies/rnETH.svg +11 -0
  359. package/public/currencies/sDAI.svg +13 -0
  360. package/public/currencies/sFRAX.png +0 -0
  361. package/public/currencies/sUSDS.png +0 -0
  362. package/public/currencies/scWETHv2.svg +1 -0
  363. package/public/currencies/send.svg +12 -0
  364. package/public/currencies/stETH.png +1 -0
  365. package/public/currencies/stZENT.svg +12 -0
  366. package/public/currencies/stkLYRA.svg +26 -0
  367. package/public/currencies/tGS.png +0 -0
  368. package/public/currencies/tSTAR.png +0 -0
  369. package/public/currencies/usdc.png +0 -0
  370. package/public/currencies/usdt.png +0 -0
  371. package/public/currencies/veKWENTA.svg +24 -0
  372. package/public/currencies/wHOGE.svg +14 -0
  373. package/public/currencies/wOpenX.svg +140 -0
  374. package/public/currencies/wPOKT.svg +17 -0
  375. package/public/currencies/wTBT.svg +1 -0
  376. package/public/currencies/weETH.svg +23 -0
  377. package/public/currencies/wstETH.svg +11 -0
  378. package/public/currencies/yETH.svg +8 -0
  379. package/public/currencies/zunETH.svg +46 -0
  380. package/public/currencies/zunUSD.svg +33 -0
  381. package/src/components/customer/overdraft-protection.tsx +37 -9
  382. package/src/components/invoice/action.tsx +47 -9
  383. package/src/components/payment-currency/add.tsx +12 -3
  384. package/src/components/payment-currency/edit.tsx +89 -3
  385. package/src/components/payment-currency/tokenList.json +8847 -8156
  386. package/src/components/subscription/portal/actions.tsx +6 -2
  387. package/src/libs/util.ts +3 -1
  388. package/src/locales/en.tsx +10 -2
  389. package/src/locales/zh.tsx +10 -2
  390. package/src/pages/admin/settings/vault-config/edit-form.tsx +13 -0
  391. package/src/pages/customer/recharge/account.tsx +8 -1
  392. package/src/pages/customer/recharge/subscription.tsx +8 -1
  393. package/src/pages/customer/subscription/change-payment.tsx +7 -4
  394. package/src/pages/customer/subscription/detail.tsx +7 -6
@@ -6,7 +6,12 @@ import logger from '../libs/logger';
6
6
  export async function syncCurrencyLogo() {
7
7
  const where = {
8
8
  logo: {
9
- [Op.like]: '%/methods/%.png',
9
+ [Op.or]: [
10
+ { [Op.like]: '%/methods/%.png' },
11
+ { [Op.like]: '%/currencies/%.png' },
12
+ { [Op.like]: '%/methods/%.svg' },
13
+ { [Op.like]: '%/currencies/%.svg' },
14
+ ],
10
15
  },
11
16
  };
12
17
 
@@ -19,9 +24,24 @@ export async function syncCurrencyLogo() {
19
24
 
20
25
  const updateLogo = (item: PaymentMethod | PaymentCurrency) => {
21
26
  const { logo } = item;
22
- if (/\/methods\/(stripe|arcblock|ethereum|base)\.png$/.test(logo)) {
23
- const newLogo = getUrl(logo.replace(/^.*?(\/methods\/.*)$/, '$1'));
24
- promises.push((item as any).update({ logo: newLogo }));
27
+
28
+ // 匹配 /methods/ 和 /currencies/ 路径下的 logo
29
+ const methodsMatch = /\/methods\/(stripe|arcblock|ethereum|base)\.(png|svg)$/.test(logo);
30
+ const currenciesMatch = /\/currencies\/[^/]+\.(png|svg)$/.test(logo);
31
+
32
+ if (methodsMatch || currenciesMatch) {
33
+ let newLogo: string;
34
+
35
+ if (methodsMatch) {
36
+ // 处理 /methods/ 路径
37
+ newLogo = getUrl(logo.replace(/^.*?(\/methods\/.*)$/, '$1'));
38
+ } else {
39
+ newLogo = getUrl(logo.replace(/^.*?(\/currencies\/.*)$/, '$1'));
40
+ }
41
+ if (newLogo !== logo) {
42
+ promises.push((item as any).update({ logo: newLogo }));
43
+ logger.info(`Updating logo: ${logo} -> ${newLogo}`);
44
+ }
25
45
  }
26
46
  };
27
47
 
@@ -30,6 +50,7 @@ export async function syncCurrencyLogo() {
30
50
 
31
51
  try {
32
52
  await Promise.all(promises);
53
+ logger.info(`Updated ${promises.length} logo(s)`);
33
54
  } catch (error) {
34
55
  logger.error('syncCurrency error', error);
35
56
  }
@@ -1119,3 +1119,234 @@ export async function retryUncollectibleInvoices(options: {
1119
1119
  logger.info('Released retry uncollectible lock', { lockKey });
1120
1120
  }
1121
1121
  }
1122
+
1123
+ /**
1124
+ * Migrate billing when a subscription's payment method changes.
1125
+ * Steps:
1126
+ * 1. Check if all subscription items are prepaid (licensed recurring).
1127
+ * 2. If so, check if there is an unpaid invoice for the current period with the old payment method.
1128
+ * 3. If such an invoice exists, void the old invoice and cancel its payment intent if present.
1129
+ * 4. Create a new invoice for the current period with the new payment method.
1130
+ * 5. If any step fails, throw an error and log details.
1131
+ *
1132
+ * @param subscription Subscription instance
1133
+ * @param oldCurrencyId Old payment currency ID
1134
+ * @param newCurrencyId New payment currency ID
1135
+ * @returns Migration result: { migrated, oldInvoice, newInvoice }
1136
+ */
1137
+ export const migrateSubscriptionPaymentMethodInvoice = async (
1138
+ subscription: Subscription,
1139
+ oldCurrencyId: string,
1140
+ newCurrencyId: string
1141
+ ) => {
1142
+ // 1. Check if all subscription items are prepaid (licensed recurring)
1143
+ const subscriptionItems = await SubscriptionItem.findAll({
1144
+ where: { subscription_id: subscription.id },
1145
+ include: [{ model: Price, as: 'price' }],
1146
+ });
1147
+
1148
+ const subscriptionItemsExpanded = await Price.expand(subscriptionItems.map((x) => x.toJSON()));
1149
+ const isPrepaid = subscriptionItemsExpanded.every((item: TLineItemExpanded) => {
1150
+ const price = getSubscriptionItemPrice(item);
1151
+ return price.type === 'recurring' && price.recurring?.usage_type === 'licensed';
1152
+ });
1153
+
1154
+ if (!isPrepaid) {
1155
+ logger.info('Skip billing migration for non-prepaid items', {
1156
+ subscriptionId: subscription.id,
1157
+ });
1158
+ return { migrated: false, reason: 'has_non_prepaid_items' };
1159
+ }
1160
+
1161
+ // 2. Find unpaid invoice for the current period with the old payment method
1162
+ const currentPeriodInvoice = await Invoice.findOne({
1163
+ where: {
1164
+ subscription_id: subscription.id,
1165
+ billing_reason: 'subscription_cycle',
1166
+ status: ['open', 'uncollectible'],
1167
+ currency_id: oldCurrencyId,
1168
+ period_start: {
1169
+ [Op.gte]: subscription.current_period_start,
1170
+ [Op.lt]: subscription.current_period_end,
1171
+ },
1172
+ },
1173
+ order: [['created_at', 'DESC']],
1174
+ });
1175
+
1176
+ let voidedInvoice: Invoice | null = null;
1177
+
1178
+ if (!currentPeriodInvoice) {
1179
+ voidedInvoice = await Invoice.findOne({
1180
+ where: {
1181
+ subscription_id: subscription.id,
1182
+ billing_reason: 'subscription_cycle',
1183
+ status: 'void',
1184
+ currency_id: oldCurrencyId,
1185
+ period_start: {
1186
+ [Op.gte]: subscription.current_period_start,
1187
+ [Op.lt]: subscription.current_period_end,
1188
+ },
1189
+ },
1190
+ order: [['created_at', 'DESC']],
1191
+ });
1192
+ if (!voidedInvoice) {
1193
+ logger.info('Skip billing migration for no unpaid invoice', {
1194
+ subscriptionId: subscription.id,
1195
+ periodStart: subscription.current_period_start,
1196
+ periodEnd: subscription.current_period_end,
1197
+ oldCurrencyId,
1198
+ });
1199
+ return { migrated: false, reason: 'no_unpaid_invoice' };
1200
+ }
1201
+ }
1202
+
1203
+ // 3. Get old and new payment method/currency
1204
+ const oldPaymentCurrency = await PaymentCurrency.findByPk(oldCurrencyId);
1205
+ if (!oldPaymentCurrency) {
1206
+ throw new Error(`Payment currency ${oldCurrencyId} not found`);
1207
+ }
1208
+ const oldPaymentMethod = await PaymentMethod.findByPk(oldPaymentCurrency.payment_method_id);
1209
+ if (!oldPaymentMethod) {
1210
+ throw new Error(`Payment method for currency ${oldCurrencyId} not found`);
1211
+ }
1212
+
1213
+ const newPaymentCurrency = await PaymentCurrency.findByPk(newCurrencyId);
1214
+ if (!newPaymentCurrency) {
1215
+ throw new Error(`Payment currency ${newCurrencyId} not found`);
1216
+ }
1217
+ const newPaymentMethod = await PaymentMethod.findByPk(newPaymentCurrency.payment_method_id);
1218
+ if (!newPaymentMethod) {
1219
+ throw new Error(`Payment method for currency ${newCurrencyId} not found`);
1220
+ }
1221
+
1222
+ // 4. Stripe payment method is not supported for migration
1223
+ if (newPaymentMethod.type === 'stripe') {
1224
+ logger.info('Skip billing migration for stripe payment method', {
1225
+ subscriptionId: subscription.id,
1226
+ });
1227
+ return { migrated: false, reason: 'stripe_payment_method' };
1228
+ }
1229
+
1230
+ try {
1231
+ const customer = await Customer.findByPk(subscription.customer_id);
1232
+ if (!customer) {
1233
+ throw new Error(`Customer ${subscription.customer_id} not found`);
1234
+ }
1235
+
1236
+ const cancelOldInvoice = async (invoice: Invoice) => {
1237
+ try {
1238
+ if (invoice.payment_intent_id) {
1239
+ const paymentIntent = await PaymentIntent.findByPk(invoice.payment_intent_id);
1240
+ if (paymentIntent && paymentIntent.status !== 'canceled') {
1241
+ await paymentIntent.update({
1242
+ status: 'canceled',
1243
+ canceled_at: dayjs().unix(),
1244
+ cancellation_reason: 'void_invoice',
1245
+ });
1246
+ }
1247
+ }
1248
+
1249
+ if (oldPaymentMethod.type === 'stripe' && invoice.metadata?.stripe_id) {
1250
+ const client = oldPaymentMethod.getStripeClient();
1251
+ await client.invoices.voidInvoice(invoice.metadata.stripe_id);
1252
+ }
1253
+
1254
+ await invoice.update({
1255
+ status: 'void',
1256
+ status_transitions: {
1257
+ ...(invoice.status_transitions || {}),
1258
+ voided_at: dayjs().unix(),
1259
+ },
1260
+ });
1261
+
1262
+ logger.info('Successfully voided old invoice for payment method change', {
1263
+ subscriptionId: subscription.id,
1264
+ oldInvoice: invoice.id,
1265
+ oldCurrency: oldCurrencyId,
1266
+ newCurrency: newCurrencyId,
1267
+ });
1268
+ } catch (error) {
1269
+ logger.error('Failed to void old invoice', {
1270
+ subscription: subscription.id,
1271
+ invoiceId: invoice.id,
1272
+ error: error.message,
1273
+ });
1274
+ throw error;
1275
+ }
1276
+ };
1277
+
1278
+ const createNewInvoice = async () => {
1279
+ const preInvoice = currentPeriodInvoice || voidedInvoice;
1280
+ if (!preInvoice) {
1281
+ throw new Error('No unpaid invoice found');
1282
+ }
1283
+ const metadata: Record<string, any> = {
1284
+ prev_invoice_id: preInvoice.id,
1285
+ };
1286
+ const amount = getSubscriptionCycleAmount(subscriptionItemsExpanded, newCurrencyId);
1287
+
1288
+ const { invoice } = await ensureInvoiceAndItems({
1289
+ customer,
1290
+ currency: newPaymentCurrency,
1291
+ subscription,
1292
+ trialing: subscription.status === 'trialing',
1293
+ metered: false,
1294
+ lineItems: subscriptionItemsExpanded,
1295
+ applyCredit: false,
1296
+ props: {
1297
+ status: 'open',
1298
+ total: amount.total,
1299
+ livemode: subscription.livemode,
1300
+ description: 'Subscription cycle',
1301
+ statement_descriptor: preInvoice.statement_descriptor,
1302
+ period_start: preInvoice.period_start,
1303
+ period_end: preInvoice.period_end,
1304
+ auto_advance: true,
1305
+ billing_reason: 'subscription_cycle',
1306
+ currency_id: newCurrencyId,
1307
+ default_payment_method_id: newPaymentMethod.id,
1308
+ custom_fields: preInvoice.custom_fields || [],
1309
+ footer: preInvoice.footer || '',
1310
+ payment_settings: subscription.payment_settings,
1311
+ metadata,
1312
+ } as Invoice,
1313
+ });
1314
+ return invoice;
1315
+ };
1316
+
1317
+ // 5. Cancel old invoice, then create new invoice
1318
+ if (currentPeriodInvoice) {
1319
+ await cancelOldInvoice(currentPeriodInvoice);
1320
+ }
1321
+
1322
+ const invoice = await createNewInvoice();
1323
+ if (invoice) {
1324
+ await emitAsync('invoice.queued', invoice.id, { invoiceId: invoice.id, retryOnError: true }, { sync: false });
1325
+ logger.info('Successfully queued new invoice for payment method change', {
1326
+ subscriptionId: subscription.id,
1327
+ invoiceId: invoice.id,
1328
+ });
1329
+ }
1330
+
1331
+ logger.info('Successfully migrated invoice for payment method change', {
1332
+ subscriptionId: subscription.id,
1333
+ oldInvoiceId: currentPeriodInvoice?.id || voidedInvoice?.id,
1334
+ oldCurrency: oldCurrencyId,
1335
+ newCurrency: newCurrencyId,
1336
+ newInvoiceId: invoice.id,
1337
+ });
1338
+ return {
1339
+ migrated: true,
1340
+ oldInvoice: currentPeriodInvoice,
1341
+ newInvoice: invoice,
1342
+ };
1343
+ } catch (error) {
1344
+ logger.error('Failed to migrate invoice for payment method change', {
1345
+ subscriptionId: subscription.id,
1346
+ oldCurrencyId,
1347
+ newCurrencyId,
1348
+ error: error.message,
1349
+ });
1350
+ throw error;
1351
+ }
1352
+ };
@@ -47,6 +47,98 @@ export interface SufficientForPaymentResult {
47
47
  requestedAmount?: string;
48
48
  }
49
49
 
50
+ export async function checkTokenBalance(args: {
51
+ paymentMethod: PaymentMethod;
52
+ paymentCurrency: TPaymentCurrency;
53
+ userDid: string;
54
+ amount: string;
55
+ }): Promise<SufficientForPaymentResult> {
56
+ const { paymentMethod, paymentCurrency, userDid, amount } = args;
57
+ const tokenAddress = paymentCurrency.contract as string;
58
+ const totalAmount = new BN(amount);
59
+
60
+ if (paymentMethod.type === 'arcblock') {
61
+ // get user wallet did
62
+ const { user } = await blocklet.getUser(userDid, { enableConnectedAccount: true });
63
+ const delegator = getWalletDid(user);
64
+ if (!delegator) {
65
+ return {
66
+ sufficient: false,
67
+ reason: 'NO_DID_WALLET',
68
+ requestedAmount: totalAmount.toString(),
69
+ };
70
+ }
71
+
72
+ const client = paymentMethod.getOcapClient();
73
+
74
+ // check token balance
75
+ const { tokens } = await client.getAccountTokens({ address: delegator, token: tokenAddress });
76
+ const [token] = tokens;
77
+ if (!token) {
78
+ return {
79
+ sufficient: false,
80
+ reason: 'NO_TOKEN',
81
+ requestedAmount: totalAmount.toString(),
82
+ };
83
+ }
84
+
85
+ if (new BN(token.balance).lt(totalAmount)) {
86
+ return {
87
+ sufficient: false,
88
+ reason: 'NO_ENOUGH_TOKEN',
89
+ token,
90
+ requestedAmount: totalAmount.toString(),
91
+ };
92
+ }
93
+
94
+ return {
95
+ sufficient: true,
96
+ token,
97
+ requestedAmount: totalAmount.toString(),
98
+ };
99
+ }
100
+
101
+ if (EVM_CHAIN_TYPES.includes(paymentMethod.type)) {
102
+ if (!paymentCurrency.contract) {
103
+ return {
104
+ sufficient: false,
105
+ reason: 'NO_TOKEN',
106
+ requestedAmount: totalAmount.toString(),
107
+ };
108
+ }
109
+ if (isEthereumDid(userDid) === false) {
110
+ return {
111
+ sufficient: false,
112
+ reason: 'NO_TOKEN',
113
+ requestedAmount: totalAmount.toString(),
114
+ };
115
+ }
116
+
117
+ const provider = paymentMethod.getEvmClient();
118
+ const balance = await fetchErc20Balance(provider, paymentCurrency.contract, userDid);
119
+
120
+ if (new BN(balance).lt(totalAmount)) {
121
+ return {
122
+ sufficient: false,
123
+ reason: 'NO_ENOUGH_TOKEN',
124
+ token: { address: tokenAddress, balance },
125
+ requestedAmount: totalAmount.toString(),
126
+ };
127
+ }
128
+
129
+ return {
130
+ sufficient: true,
131
+ token: { address: tokenAddress, balance },
132
+ requestedAmount: totalAmount.toString(),
133
+ };
134
+ }
135
+
136
+ return {
137
+ sufficient: true,
138
+ requestedAmount: totalAmount.toString(),
139
+ };
140
+ }
141
+
50
142
  export async function isDelegationSufficientForPayment(args: {
51
143
  paymentMethod: PaymentMethod;
52
144
  paymentCurrency: TPaymentCurrency;
@@ -846,10 +846,8 @@ export const handlePayment = async (job: PaymentJob) => {
846
846
  wallet,
847
847
  delegator: result.delegator,
848
848
  });
849
- logger.info('PaymentIntent signed', { signed });
850
849
  // @ts-ignore
851
850
  const { buffer } = await client.encodeTransferV2Tx({ tx: signed });
852
- logger.info('PaymentIntent buffer', { buffer, gas: getGasPayerExtra(buffer) });
853
851
  const txHash = await client.sendTransferV2Tx(
854
852
  // @ts-ignore
855
853
  { tx: signed, wallet, delegator: result.delegator },
@@ -26,7 +26,7 @@ import {
26
26
  shouldCancelSubscription,
27
27
  slashOverdraftProtectionStake,
28
28
  } from '../libs/subscription';
29
- import { ensureInvoiceAndItems } from '../libs/invoice';
29
+ import { ensureInvoiceAndItems, migrateSubscriptionPaymentMethodInvoice } from '../libs/invoice';
30
30
  import { PaymentCurrency, PaymentIntent, PaymentMethod, Refund, SetupIntent, UsageRecord } from '../store/models';
31
31
  import { Customer } from '../store/models/customer';
32
32
  import { Invoice } from '../store/models/invoice';
@@ -1280,5 +1280,28 @@ events.on('setup_intent.succeeded', async (setupIntent: SetupIntent) => {
1280
1280
  logger.error('create return overdraft protection stake job failed', { error, subscription: subscription.id });
1281
1281
  }
1282
1282
  }
1283
+
1284
+ try {
1285
+ const migrationResult = await migrateSubscriptionPaymentMethodInvoice(
1286
+ subscription,
1287
+ setupIntent.metadata?.from_currency,
1288
+ setupIntent.metadata?.to_currency
1289
+ );
1290
+ if (migrationResult.migrated) {
1291
+ logger.info('Subscription payment method billing migration completed', {
1292
+ subscription: subscription.id,
1293
+ migrated: migrationResult.migrated,
1294
+ oldInvoice: migrationResult.oldInvoice?.id,
1295
+ newInvoice: migrationResult.newInvoice?.id,
1296
+ });
1297
+ }
1298
+ } catch (error) {
1299
+ logger.error('Failed to migrate billing for payment method change', {
1300
+ subscription: subscription.id,
1301
+ fromCurrency: setupIntent.metadata?.from_currency,
1302
+ toCurrency: setupIntent.metadata?.to_currency,
1303
+ error,
1304
+ });
1305
+ }
1283
1306
  }
1284
1307
  });
@@ -13,6 +13,8 @@ import {
13
13
  import { ensureStakeInvoice } from '../../libs/invoice';
14
14
  import { EVM_CHAIN_TYPES } from '../../libs/constants';
15
15
  import logger from '../../libs/logger';
16
+ import { getFastCheckoutAmount } from '../../libs/session';
17
+ import { isDelegationSufficientForPayment } from '../../libs/payment';
16
18
 
17
19
  export default {
18
20
  action: 'change-payment',
@@ -26,41 +28,53 @@ export default {
26
28
  },
27
29
  onConnect: async ({ userDid, userPk, extraParams }: CallbackArgs) => {
28
30
  const { subscriptionId } = extraParams;
29
- const { subscription, paymentMethod, paymentCurrency } = await ensureChangePaymentContext(subscriptionId);
31
+ const { subscription, paymentMethod, paymentCurrency, customer } = await ensureChangePaymentContext(subscriptionId);
30
32
 
31
33
  const claimsList: any[] = [];
32
34
  // @ts-ignore
33
35
  const items = subscription!.items as TLineItemExpanded[];
34
36
  const trialing = true;
35
37
  const billingThreshold = Number(subscription.billing_thresholds?.amount_gte || 0);
38
+ const fastCheckoutAmount = getFastCheckoutAmount(items, 'subscription', paymentCurrency.id, false);
36
39
 
37
40
  if (paymentMethod.type === 'arcblock') {
38
- claimsList.push({
39
- signature: await getDelegationTxClaim({
40
- mode: 'setup',
41
- userDid,
42
- userPk,
43
- nonce: `change-method-${subscription.id}`,
44
- data: getTxMetadata({ subscriptionId: subscription.id }),
45
- paymentCurrency,
46
- paymentMethod,
47
- trialing,
48
- billingThreshold,
49
- items,
50
- }),
51
- });
52
-
53
- claimsList.push({
54
- prepareTx: await getStakeTxClaim({
55
- userDid,
56
- userPk,
57
- paymentCurrency,
58
- paymentMethod,
59
- items,
60
- subscription,
61
- }),
41
+ const delegation = await isDelegationSufficientForPayment({
42
+ paymentMethod,
43
+ paymentCurrency,
44
+ userDid: customer!.did,
45
+ amount: fastCheckoutAmount,
62
46
  });
47
+ const needDelegation = delegation.sufficient === false;
48
+ const noStake = subscription.billing_thresholds?.no_stake;
49
+ if (needDelegation || noStake) {
50
+ claimsList.push({
51
+ signature: await getDelegationTxClaim({
52
+ mode: 'setup',
53
+ userDid,
54
+ userPk,
55
+ nonce: `change-method-${subscription.id}`,
56
+ data: getTxMetadata({ subscriptionId: subscription.id }),
57
+ paymentCurrency,
58
+ paymentMethod,
59
+ trialing,
60
+ billingThreshold,
61
+ items,
62
+ }),
63
+ });
64
+ }
63
65
 
66
+ if (!noStake) {
67
+ claimsList.push({
68
+ prepareTx: await getStakeTxClaim({
69
+ userDid,
70
+ userPk,
71
+ paymentCurrency,
72
+ paymentMethod,
73
+ items,
74
+ subscription,
75
+ }),
76
+ });
77
+ }
64
78
  return claimsList;
65
79
  }
66
80
 
@@ -95,6 +109,8 @@ export default {
95
109
  const { setupIntent, subscription, paymentMethod, paymentCurrency, customer } =
96
110
  await ensureChangePaymentContext(subscriptionId);
97
111
 
112
+ const noStake = subscription.billing_thresholds?.no_stake;
113
+
98
114
  const result = request?.context?.store?.result || [];
99
115
  result.push({
100
116
  step,
@@ -106,7 +122,8 @@ export default {
106
122
 
107
123
  // 判断是否为最后一步
108
124
  const staking = result.find((x: any) => x.claim?.type === 'prepareTx' && x.claim?.meta?.purpose === 'staking');
109
- const isFinalStep = (paymentMethod.type === 'arcblock' && staking) || paymentMethod.type !== 'arcblock';
125
+ const isFinalStep =
126
+ (paymentMethod.type === 'arcblock' && (staking || noStake)) || paymentMethod.type !== 'arcblock';
110
127
 
111
128
  if (!isFinalStep) {
112
129
  await updateSession({
@@ -181,25 +198,28 @@ export default {
181
198
  subscription?.id,
182
199
  paymentCurrency.contract
183
200
  );
184
- await ensureStakeInvoice(
185
- {
186
- total: stakingAmount,
187
- description: 'Stake for subscription payment change',
188
- currency_id: paymentCurrency.id,
189
- metadata: {
190
- payment_details: {
191
- arcblock: {
192
- tx_hash: paymentDetails?.staking?.tx_hash,
193
- payer: paymentDetails?.payer,
194
- address: paymentDetails?.staking?.address,
201
+ if (stakingAmount && stakingAmount !== '0') {
202
+ await ensureStakeInvoice(
203
+ {
204
+ total: stakingAmount,
205
+ description: 'Stake for subscription payment change',
206
+ currency_id: paymentCurrency.id,
207
+ metadata: {
208
+ payment_details: {
209
+ arcblock: {
210
+ tx_hash: paymentDetails?.staking?.tx_hash,
211
+ payer: paymentDetails?.payer,
212
+ address: paymentDetails?.staking?.address,
213
+ },
195
214
  },
196
215
  },
197
216
  },
198
- },
199
- subscription!,
200
- paymentMethod,
201
- customer!
202
- );
217
+ subscription!,
218
+ paymentMethod,
219
+ customer!
220
+ );
221
+ }
222
+
203
223
  await afterTxExecution(paymentDetails);
204
224
  return { hash: paymentDetails.tx_hash };
205
225
  }
@@ -400,7 +400,7 @@ export async function ensureInvoiceForCheckout({
400
400
  }
401
401
 
402
402
  // invoice currency is aligned
403
- if (invoice.currency_id === checkoutSession.currency_id) {
403
+ if (invoice.currency_id === checkoutSession.currency_id && invoice.status !== 'draft') {
404
404
  await invoice.update({ status: 'open' });
405
405
  logger.info(`Invoice status reset for checkout session ${checkoutSession.id}: ${existingInvoice}`);
406
406
 
@@ -512,7 +512,8 @@ export async function ensureInvoicesForSubscriptions({
512
512
  checkoutSession,
513
513
  customer,
514
514
  subscriptions,
515
- }: Omit<Args, 'subscription'> & { subscriptions: Subscription[] }): Promise<{
515
+ invoiceProps,
516
+ }: Omit<Args, 'subscription'> & { subscriptions: Subscription[]; invoiceProps?: Partial<TInvoice> }): Promise<{
516
517
  invoices: Invoice[];
517
518
  }> {
518
519
  if (!subscriptions?.length) {
@@ -531,7 +532,8 @@ export async function ensureInvoicesForSubscriptions({
531
532
  customer,
532
533
  subscription,
533
534
  subscriptions,
534
- lineItems: subItems
535
+ lineItems: subItems,
536
+ props: invoiceProps,
535
537
  });
536
538
  return invoice;
537
539
  })