@vandenberghinc/volt 1.1.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 (451) hide show
  1. package/.vrepo +28 -0
  2. package/.vscode/tasks.json +87 -0
  3. package/README.md +67 -0
  4. package/backend/dist/cjs/blacklist.d.ts +10 -0
  5. package/backend/dist/cjs/blacklist.js +53 -0
  6. package/backend/dist/cjs/cli.d.ts +2 -0
  7. package/backend/dist/cjs/cli.js +263 -0
  8. package/backend/dist/cjs/database.d.ts +364 -0
  9. package/backend/dist/cjs/database.js +1962 -0
  10. package/backend/dist/cjs/endpoint.d.ts +57 -0
  11. package/backend/dist/cjs/endpoint.js +425 -0
  12. package/backend/dist/cjs/file_watcher.d.ts +44 -0
  13. package/backend/dist/cjs/file_watcher.js +348 -0
  14. package/backend/dist/cjs/frontend.d.ts +13 -0
  15. package/backend/dist/cjs/frontend.js +30 -0
  16. package/backend/dist/cjs/image_endpoint.d.ts +24 -0
  17. package/backend/dist/cjs/image_endpoint.js +210 -0
  18. package/backend/dist/cjs/logger.d.ts +5 -0
  19. package/backend/dist/cjs/logger.js +16 -0
  20. package/backend/dist/cjs/meta.d.ts +50 -0
  21. package/backend/dist/cjs/meta.js +153 -0
  22. package/backend/dist/cjs/mutex.d.ts +24 -0
  23. package/backend/dist/cjs/mutex.js +52 -0
  24. package/backend/dist/cjs/package.json +1 -0
  25. package/backend/dist/cjs/payments/paddle.d.ts +161 -0
  26. package/backend/dist/cjs/payments/paddle.js +2301 -0
  27. package/backend/dist/cjs/plugins/browser.d.ts +36 -0
  28. package/backend/dist/cjs/plugins/browser.js +183 -0
  29. package/backend/dist/cjs/plugins/communication.d.ts +70 -0
  30. package/backend/dist/cjs/plugins/communication.js +177 -0
  31. package/backend/dist/cjs/plugins/css.d.ts +10 -0
  32. package/backend/dist/cjs/plugins/css.js +71 -0
  33. package/backend/dist/cjs/plugins/mail.d.ts +277 -0
  34. package/backend/dist/cjs/plugins/mail.js +1419 -0
  35. package/backend/dist/cjs/plugins/pdf.d.ts +757 -0
  36. package/backend/dist/cjs/plugins/pdf.js +1694 -0
  37. package/backend/dist/cjs/plugins/thread_monitor.d.ts +18 -0
  38. package/backend/dist/cjs/plugins/thread_monitor.js +127 -0
  39. package/backend/dist/cjs/plugins/ts/compiler.d.ts +132 -0
  40. package/backend/dist/cjs/plugins/ts/compiler.js +944 -0
  41. package/backend/dist/cjs/plugins/ts/preprocessing.d.ts +14 -0
  42. package/backend/dist/cjs/plugins/ts/preprocessing.js +762 -0
  43. package/backend/dist/cjs/rate_limit.d.ts +65 -0
  44. package/backend/dist/cjs/rate_limit.js +463 -0
  45. package/backend/dist/cjs/request.deprc.d.ts +48 -0
  46. package/backend/dist/cjs/request.deprc.js +572 -0
  47. package/backend/dist/cjs/response.deprc.d.ts +55 -0
  48. package/backend/dist/cjs/response.deprc.js +275 -0
  49. package/backend/dist/cjs/server.d.ts +311 -0
  50. package/backend/dist/cjs/server.js +3475 -0
  51. package/backend/dist/cjs/splash_screen.d.ts +35 -0
  52. package/backend/dist/cjs/splash_screen.js +152 -0
  53. package/backend/dist/cjs/status.d.ts +60 -0
  54. package/backend/dist/cjs/status.js +199 -0
  55. package/backend/dist/cjs/stream.d.ts +75 -0
  56. package/backend/dist/cjs/stream.js +954 -0
  57. package/backend/dist/cjs/users.d.ts +111 -0
  58. package/backend/dist/cjs/users.js +1945 -0
  59. package/backend/dist/cjs/utils.d.ts +27 -0
  60. package/backend/dist/cjs/utils.js +329 -0
  61. package/backend/dist/cjs/view.d.ts +52 -0
  62. package/backend/dist/cjs/view.js +568 -0
  63. package/backend/dist/cjs/vinc.d.ts +2 -0
  64. package/backend/dist/cjs/vinc.dev.d.ts +2 -0
  65. package/backend/dist/cjs/vinc.dev.js +42 -0
  66. package/backend/dist/cjs/vinc.js +42 -0
  67. package/backend/dist/cjs/volt.d.ts +15 -0
  68. package/backend/dist/cjs/volt.js +64 -0
  69. package/backend/dist/css/adyen.css +92 -0
  70. package/backend/dist/css/volt.css +65 -0
  71. package/backend/dist/esm/blacklist.d.ts +10 -0
  72. package/backend/dist/esm/blacklist.js +49 -0
  73. package/backend/dist/esm/cli.d.ts +2 -0
  74. package/backend/dist/esm/cli.js +228 -0
  75. package/backend/dist/esm/database.d.ts +364 -0
  76. package/backend/dist/esm/database.js +1957 -0
  77. package/backend/dist/esm/endpoint.d.ts +57 -0
  78. package/backend/dist/esm/endpoint.js +421 -0
  79. package/backend/dist/esm/file_watcher.d.ts +44 -0
  80. package/backend/dist/esm/file_watcher.js +313 -0
  81. package/backend/dist/esm/frontend.d.ts +13 -0
  82. package/backend/dist/esm/frontend.js +27 -0
  83. package/backend/dist/esm/image_endpoint.d.ts +24 -0
  84. package/backend/dist/esm/image_endpoint.js +206 -0
  85. package/backend/dist/esm/logger.d.ts +5 -0
  86. package/backend/dist/esm/logger.js +13 -0
  87. package/backend/dist/esm/meta.d.ts +50 -0
  88. package/backend/dist/esm/meta.js +149 -0
  89. package/backend/dist/esm/mutex.d.ts +24 -0
  90. package/backend/dist/esm/mutex.js +48 -0
  91. package/backend/dist/esm/payments/paddle.d.ts +161 -0
  92. package/backend/dist/esm/payments/paddle.js +2261 -0
  93. package/backend/dist/esm/plugins/browser.d.ts +36 -0
  94. package/backend/dist/esm/plugins/browser.js +176 -0
  95. package/backend/dist/esm/plugins/communication.d.ts +70 -0
  96. package/backend/dist/esm/plugins/communication.js +169 -0
  97. package/backend/dist/esm/plugins/css.d.ts +10 -0
  98. package/backend/dist/esm/plugins/css.js +64 -0
  99. package/backend/dist/esm/plugins/mail.d.ts +277 -0
  100. package/backend/dist/esm/plugins/mail.js +1403 -0
  101. package/backend/dist/esm/plugins/pdf.d.ts +757 -0
  102. package/backend/dist/esm/plugins/pdf.js +1694 -0
  103. package/backend/dist/esm/plugins/thread_monitor.d.ts +18 -0
  104. package/backend/dist/esm/plugins/thread_monitor.js +120 -0
  105. package/backend/dist/esm/plugins/ts/compiler.d.ts +132 -0
  106. package/backend/dist/esm/plugins/ts/compiler.js +907 -0
  107. package/backend/dist/esm/plugins/ts/preprocessing.d.ts +14 -0
  108. package/backend/dist/esm/plugins/ts/preprocessing.js +724 -0
  109. package/backend/dist/esm/rate_limit.d.ts +65 -0
  110. package/backend/dist/esm/rate_limit.js +425 -0
  111. package/backend/dist/esm/request.deprc.d.ts +48 -0
  112. package/backend/dist/esm/request.deprc.js +572 -0
  113. package/backend/dist/esm/response.deprc.d.ts +55 -0
  114. package/backend/dist/esm/response.deprc.js +275 -0
  115. package/backend/dist/esm/server.d.ts +311 -0
  116. package/backend/dist/esm/server.js +3435 -0
  117. package/backend/dist/esm/splash_screen.d.ts +35 -0
  118. package/backend/dist/esm/splash_screen.js +148 -0
  119. package/backend/dist/esm/status.d.ts +60 -0
  120. package/backend/dist/esm/status.js +196 -0
  121. package/backend/dist/esm/stream.d.ts +75 -0
  122. package/backend/dist/esm/stream.js +947 -0
  123. package/backend/dist/esm/users.d.ts +111 -0
  124. package/backend/dist/esm/users.js +1908 -0
  125. package/backend/dist/esm/utils.d.ts +27 -0
  126. package/backend/dist/esm/utils.js +324 -0
  127. package/backend/dist/esm/view.d.ts +52 -0
  128. package/backend/dist/esm/view.js +561 -0
  129. package/backend/dist/esm/vinc.d.ts +2 -0
  130. package/backend/dist/esm/vinc.dev.d.ts +2 -0
  131. package/backend/dist/esm/vinc.dev.js +6 -0
  132. package/backend/dist/esm/vinc.js +6 -0
  133. package/backend/dist/esm/volt.d.ts +15 -0
  134. package/backend/dist/esm/volt.js +23 -0
  135. package/backend/dist/esm-dev/blacklist.d.ts +10 -0
  136. package/backend/dist/esm-dev/blacklist.js +49 -0
  137. package/backend/dist/esm-dev/cli.d.ts +2 -0
  138. package/backend/dist/esm-dev/cli.js +228 -0
  139. package/backend/dist/esm-dev/database.d.ts +364 -0
  140. package/backend/dist/esm-dev/database.js +1957 -0
  141. package/backend/dist/esm-dev/endpoint.d.ts +57 -0
  142. package/backend/dist/esm-dev/endpoint.js +421 -0
  143. package/backend/dist/esm-dev/file_watcher.d.ts +44 -0
  144. package/backend/dist/esm-dev/file_watcher.js +313 -0
  145. package/backend/dist/esm-dev/frontend.d.ts +13 -0
  146. package/backend/dist/esm-dev/frontend.js +27 -0
  147. package/backend/dist/esm-dev/image_endpoint.d.ts +24 -0
  148. package/backend/dist/esm-dev/image_endpoint.js +206 -0
  149. package/backend/dist/esm-dev/logger.d.ts +5 -0
  150. package/backend/dist/esm-dev/logger.js +13 -0
  151. package/backend/dist/esm-dev/meta.d.ts +50 -0
  152. package/backend/dist/esm-dev/meta.js +149 -0
  153. package/backend/dist/esm-dev/mutex.d.ts +24 -0
  154. package/backend/dist/esm-dev/mutex.js +48 -0
  155. package/backend/dist/esm-dev/payments/paddle.d.ts +161 -0
  156. package/backend/dist/esm-dev/payments/paddle.js +2261 -0
  157. package/backend/dist/esm-dev/plugins/browser.d.ts +36 -0
  158. package/backend/dist/esm-dev/plugins/browser.js +176 -0
  159. package/backend/dist/esm-dev/plugins/communication.d.ts +70 -0
  160. package/backend/dist/esm-dev/plugins/communication.js +169 -0
  161. package/backend/dist/esm-dev/plugins/css.d.ts +10 -0
  162. package/backend/dist/esm-dev/plugins/css.js +64 -0
  163. package/backend/dist/esm-dev/plugins/mail.d.ts +277 -0
  164. package/backend/dist/esm-dev/plugins/mail.js +1403 -0
  165. package/backend/dist/esm-dev/plugins/pdf.d.ts +757 -0
  166. package/backend/dist/esm-dev/plugins/pdf.js +1694 -0
  167. package/backend/dist/esm-dev/plugins/thread_monitor.d.ts +18 -0
  168. package/backend/dist/esm-dev/plugins/thread_monitor.js +120 -0
  169. package/backend/dist/esm-dev/plugins/ts/compiler.d.ts +132 -0
  170. package/backend/dist/esm-dev/plugins/ts/compiler.js +907 -0
  171. package/backend/dist/esm-dev/plugins/ts/preprocessing.d.ts +14 -0
  172. package/backend/dist/esm-dev/plugins/ts/preprocessing.js +724 -0
  173. package/backend/dist/esm-dev/rate_limit.d.ts +65 -0
  174. package/backend/dist/esm-dev/rate_limit.js +425 -0
  175. package/backend/dist/esm-dev/request.deprc.d.ts +48 -0
  176. package/backend/dist/esm-dev/request.deprc.js +572 -0
  177. package/backend/dist/esm-dev/response.deprc.d.ts +55 -0
  178. package/backend/dist/esm-dev/response.deprc.js +275 -0
  179. package/backend/dist/esm-dev/server.d.ts +311 -0
  180. package/backend/dist/esm-dev/server.js +3435 -0
  181. package/backend/dist/esm-dev/splash_screen.d.ts +35 -0
  182. package/backend/dist/esm-dev/splash_screen.js +148 -0
  183. package/backend/dist/esm-dev/status.d.ts +60 -0
  184. package/backend/dist/esm-dev/status.js +196 -0
  185. package/backend/dist/esm-dev/stream.d.ts +75 -0
  186. package/backend/dist/esm-dev/stream.js +947 -0
  187. package/backend/dist/esm-dev/users.d.ts +111 -0
  188. package/backend/dist/esm-dev/users.js +1908 -0
  189. package/backend/dist/esm-dev/utils.d.ts +27 -0
  190. package/backend/dist/esm-dev/utils.js +324 -0
  191. package/backend/dist/esm-dev/view.d.ts +52 -0
  192. package/backend/dist/esm-dev/view.js +561 -0
  193. package/backend/dist/esm-dev/vinc.d.ts +2 -0
  194. package/backend/dist/esm-dev/vinc.dev.d.ts +2 -0
  195. package/backend/dist/esm-dev/vinc.dev.js +6 -0
  196. package/backend/dist/esm-dev/vinc.js +6 -0
  197. package/backend/dist/esm-dev/volt.d.ts +15 -0
  198. package/backend/dist/esm-dev/volt.js +23 -0
  199. package/backend/src/blacklist.ts +69 -0
  200. package/backend/src/cli.js +245 -0
  201. package/backend/src/database.ts +2241 -0
  202. package/backend/src/endpoint.ts +494 -0
  203. package/backend/src/file_watcher.ts +359 -0
  204. package/backend/src/frontend.ts +35 -0
  205. package/backend/src/globals.d.ts +8 -0
  206. package/backend/src/image_endpoint.ts +258 -0
  207. package/backend/src/logger.ts +18 -0
  208. package/backend/src/meta.ts +202 -0
  209. package/backend/src/mutex.ts +51 -0
  210. package/backend/src/payments/paddle.ts +2659 -0
  211. package/backend/src/plugins/browser.ts +188 -0
  212. package/backend/src/plugins/communication.ts +204 -0
  213. package/backend/src/plugins/css.ts +84 -0
  214. package/backend/src/plugins/fonts/Menlo-Bold.ttf +0 -0
  215. package/backend/src/plugins/fonts/Menlo-Regular.ttf +0 -0
  216. package/backend/src/plugins/mail.ts +1720 -0
  217. package/backend/src/plugins/pdf.js +1932 -0
  218. package/backend/src/plugins/thread_monitor.ts +164 -0
  219. package/backend/src/plugins/ts/compiler.ts +1242 -0
  220. package/backend/src/plugins/ts/preprocessing.ts +812 -0
  221. package/backend/src/rate_limit.ts +503 -0
  222. package/backend/src/request.deprc.js +626 -0
  223. package/backend/src/response.deprc.js +354 -0
  224. package/backend/src/server.ts +4149 -0
  225. package/backend/src/splash_screen.ts +192 -0
  226. package/backend/src/status.ts +199 -0
  227. package/backend/src/stream.ts +1070 -0
  228. package/backend/src/users.ts +2077 -0
  229. package/backend/src/utils.ts +359 -0
  230. package/backend/src/view.ts +655 -0
  231. package/backend/src/vinc.dev.js +6 -0
  232. package/backend/src/vinc.ts +6 -0
  233. package/backend/src/volt.js +25 -0
  234. package/backend/tsconfig.cjs.json +29 -0
  235. package/backend/tsconfig.esm.dev.json +34 -0
  236. package/backend/tsconfig.esm.json +30 -0
  237. package/backend/tsconfig.json +2 -0
  238. package/frontend/compile.js +436 -0
  239. package/frontend/dist/elements/base.d.ts +9891 -0
  240. package/frontend/dist/elements/base.js +8818 -0
  241. package/frontend/dist/elements/module.d.ts +16 -0
  242. package/frontend/dist/elements/module.js +178 -0
  243. package/frontend/dist/modules/array.d.ts +37 -0
  244. package/frontend/dist/modules/array.js +284 -0
  245. package/frontend/dist/modules/auth.d.ts +45 -0
  246. package/frontend/dist/modules/auth.js +138 -0
  247. package/frontend/dist/modules/colors.d.ts +26 -0
  248. package/frontend/dist/modules/colors.js +340 -0
  249. package/frontend/dist/modules/compression.d.ts +6 -0
  250. package/frontend/dist/modules/compression.js +999 -0
  251. package/frontend/dist/modules/cookies.d.ts +17 -0
  252. package/frontend/dist/modules/cookies.js +166 -0
  253. package/frontend/dist/modules/date.d.ts +142 -0
  254. package/frontend/dist/modules/date.js +493 -0
  255. package/frontend/dist/modules/events.d.ts +7 -0
  256. package/frontend/dist/modules/events.js +90 -0
  257. package/frontend/dist/modules/google.d.ts +10 -0
  258. package/frontend/dist/modules/google.js +53 -0
  259. package/frontend/dist/modules/meta.d.ts +9 -0
  260. package/frontend/dist/modules/meta.js +45 -0
  261. package/frontend/dist/modules/mutex.d.ts +8 -0
  262. package/frontend/dist/modules/mutex.js +52 -0
  263. package/frontend/dist/modules/number.d.ts +12 -0
  264. package/frontend/dist/modules/number.js +8 -0
  265. package/frontend/dist/modules/object.d.ts +50 -0
  266. package/frontend/dist/modules/object.js +147 -0
  267. package/frontend/dist/modules/paddle.d.ts +1403 -0
  268. package/frontend/dist/modules/paddle.js +2641 -0
  269. package/frontend/dist/modules/scheme.d.ts +207 -0
  270. package/frontend/dist/modules/scheme.js +649 -0
  271. package/frontend/dist/modules/settings.d.ts +3 -0
  272. package/frontend/dist/modules/settings.js +4 -0
  273. package/frontend/dist/modules/statics.d.ts +4 -0
  274. package/frontend/dist/modules/statics.js +45 -0
  275. package/frontend/dist/modules/string.d.ts +163 -0
  276. package/frontend/dist/modules/string.js +291 -0
  277. package/frontend/dist/modules/support.d.ts +18 -0
  278. package/frontend/dist/modules/support.js +102 -0
  279. package/frontend/dist/modules/themes.d.ts +8 -0
  280. package/frontend/dist/modules/themes.js +17 -0
  281. package/frontend/dist/modules/user.d.ts +58 -0
  282. package/frontend/dist/modules/user.js +279 -0
  283. package/frontend/dist/modules/utils.d.ts +58 -0
  284. package/frontend/dist/modules/utils.js +1159 -0
  285. package/frontend/dist/types/gradient.d.ts +12 -0
  286. package/frontend/dist/types/gradient.js +79 -0
  287. package/frontend/dist/ui/border_button.d.ts +177 -0
  288. package/frontend/dist/ui/border_button.js +235 -0
  289. package/frontend/dist/ui/button.d.ts +42 -0
  290. package/frontend/dist/ui/button.js +114 -0
  291. package/frontend/dist/ui/canvas.d.ts +56 -0
  292. package/frontend/dist/ui/canvas.js +411 -0
  293. package/frontend/dist/ui/checkbox.d.ts +72 -0
  294. package/frontend/dist/ui/checkbox.js +277 -0
  295. package/frontend/dist/ui/code.d.ts +232 -0
  296. package/frontend/dist/ui/code.js +977 -0
  297. package/frontend/dist/ui/color.d.ts +1 -0
  298. package/frontend/dist/ui/color.js +110 -0
  299. package/frontend/dist/ui/context_menu.d.ts +30 -0
  300. package/frontend/dist/ui/context_menu.js +211 -0
  301. package/frontend/dist/ui/css.d.ts +10 -0
  302. package/frontend/dist/ui/css.js +44 -0
  303. package/frontend/dist/ui/divider.d.ts +18 -0
  304. package/frontend/dist/ui/divider.js +82 -0
  305. package/frontend/dist/ui/dropdown.d.ts +115 -0
  306. package/frontend/dist/ui/dropdown.js +446 -0
  307. package/frontend/dist/ui/for_each.d.ts +38 -0
  308. package/frontend/dist/ui/for_each.js +97 -0
  309. package/frontend/dist/ui/form.d.ts +25 -0
  310. package/frontend/dist/ui/form.js +227 -0
  311. package/frontend/dist/ui/frame_modes.d.ts +28 -0
  312. package/frontend/dist/ui/frame_modes.js +116 -0
  313. package/frontend/dist/ui/google_map.d.ts +31 -0
  314. package/frontend/dist/ui/google_map.js +111 -0
  315. package/frontend/dist/ui/gradient.d.ts +24 -0
  316. package/frontend/dist/ui/gradient.js +115 -0
  317. package/frontend/dist/ui/image.d.ts +138 -0
  318. package/frontend/dist/ui/image.js +570 -0
  319. package/frontend/dist/ui/input.d.ts +316 -0
  320. package/frontend/dist/ui/input.js +1187 -0
  321. package/frontend/dist/ui/link.d.ts +39 -0
  322. package/frontend/dist/ui/link.js +146 -0
  323. package/frontend/dist/ui/list.d.ts +33 -0
  324. package/frontend/dist/ui/list.js +161 -0
  325. package/frontend/dist/ui/loader_button.d.ts +108 -0
  326. package/frontend/dist/ui/loader_button.js +207 -0
  327. package/frontend/dist/ui/loaders.d.ts +60 -0
  328. package/frontend/dist/ui/loaders.js +150 -0
  329. package/frontend/dist/ui/popup.d.ts +84 -0
  330. package/frontend/dist/ui/popup.js +331 -0
  331. package/frontend/dist/ui/pseudo.d.ts +16 -0
  332. package/frontend/dist/ui/pseudo.js +81 -0
  333. package/frontend/dist/ui/scroller.d.ts +131 -0
  334. package/frontend/dist/ui/scroller.js +1251 -0
  335. package/frontend/dist/ui/slider.d.ts +35 -0
  336. package/frontend/dist/ui/slider.js +203 -0
  337. package/frontend/dist/ui/spacer.d.ts +20 -0
  338. package/frontend/dist/ui/spacer.js +83 -0
  339. package/frontend/dist/ui/span.d.ts +11 -0
  340. package/frontend/dist/ui/span.js +75 -0
  341. package/frontend/dist/ui/stack.d.ts +123 -0
  342. package/frontend/dist/ui/stack.js +344 -0
  343. package/frontend/dist/ui/steps.d.ts +72 -0
  344. package/frontend/dist/ui/steps.js +306 -0
  345. package/frontend/dist/ui/style.d.ts +12 -0
  346. package/frontend/dist/ui/style.js +78 -0
  347. package/frontend/dist/ui/switch.d.ts +44 -0
  348. package/frontend/dist/ui/switch.js +280 -0
  349. package/frontend/dist/ui/table.d.ts +118 -0
  350. package/frontend/dist/ui/table.js +411 -0
  351. package/frontend/dist/ui/tabs.d.ts +85 -0
  352. package/frontend/dist/ui/tabs.js +392 -0
  353. package/frontend/dist/ui/text.d.ts +19 -0
  354. package/frontend/dist/ui/text.js +88 -0
  355. package/frontend/dist/ui/theme.d.ts +25 -0
  356. package/frontend/dist/ui/theme.js +237 -0
  357. package/frontend/dist/ui/title.d.ts +36 -0
  358. package/frontend/dist/ui/title.js +127 -0
  359. package/frontend/dist/ui/ui.d.ts +38 -0
  360. package/frontend/dist/ui/ui.js +41 -0
  361. package/frontend/dist/ui/view.d.ts +25 -0
  362. package/frontend/dist/ui/view.js +93 -0
  363. package/frontend/dist/volt.d.ts +22 -0
  364. package/frontend/dist/volt.js +27 -0
  365. package/frontend/exports.json +1340 -0
  366. package/frontend/src/css/adyen.css +92 -0
  367. package/frontend/src/css/volt.css +65 -0
  368. package/frontend/src/elements/base.ts +16790 -0
  369. package/frontend/src/elements/module.ts +184 -0
  370. package/frontend/src/elements/types.d.ts +155 -0
  371. package/frontend/src/modules/array.ts +366 -0
  372. package/frontend/src/modules/auth.ts +188 -0
  373. package/frontend/src/modules/colors.ts +449 -0
  374. package/frontend/src/modules/compression.ts +67 -0
  375. package/frontend/src/modules/cookies.ts +182 -0
  376. package/frontend/src/modules/date.js +535 -0
  377. package/frontend/src/modules/date.ts +583 -0
  378. package/frontend/src/modules/events.ts +96 -0
  379. package/frontend/src/modules/google.ts +60 -0
  380. package/frontend/src/modules/meta.ts +59 -0
  381. package/frontend/src/modules/mutex.ts +59 -0
  382. package/frontend/src/modules/number.ts +20 -0
  383. package/frontend/src/modules/object.ts +212 -0
  384. package/frontend/src/modules/paddle.ts +2990 -0
  385. package/frontend/src/modules/scheme.ts +740 -0
  386. package/frontend/src/modules/settings.ts +5 -0
  387. package/frontend/src/modules/statics.ts +47 -0
  388. package/frontend/src/modules/string.ts +500 -0
  389. package/frontend/src/modules/support.ts +118 -0
  390. package/frontend/src/modules/themes.ts +24 -0
  391. package/frontend/src/modules/user.ts +321 -0
  392. package/frontend/src/modules/utils.ts +1260 -0
  393. package/frontend/src/static/admin/admin.png +0 -0
  394. package/frontend/src/static/admin/password.webp +0 -0
  395. package/frontend/src/static/icons/copy.webp +0 -0
  396. package/frontend/src/static/payments/arrow.long.webp +0 -0
  397. package/frontend/src/static/payments/arrow.long2.webp +0 -0
  398. package/frontend/src/static/payments/cancelled.webp +0 -0
  399. package/frontend/src/static/payments/check.sign.webp +0 -0
  400. package/frontend/src/static/payments/check.webp +0 -0
  401. package/frontend/src/static/payments/close.webp +0 -0
  402. package/frontend/src/static/payments/error.webp +0 -0
  403. package/frontend/src/static/payments/exclamation.webp +0 -0
  404. package/frontend/src/static/payments/minus.webp +0 -0
  405. package/frontend/src/static/payments/party.webp +0 -0
  406. package/frontend/src/static/payments/plus.webp +0 -0
  407. package/frontend/src/static/payments/shopping_cart.webp +0 -0
  408. package/frontend/src/static/payments/trash.webp +0 -0
  409. package/frontend/src/types/global.d.ts +4 -0
  410. package/frontend/src/types/gradient.ts +87 -0
  411. package/frontend/src/ui/any_element.d.ts +5 -0
  412. package/frontend/src/ui/border_button.ts +320 -0
  413. package/frontend/src/ui/button.ts +62 -0
  414. package/frontend/src/ui/canvas.ts +431 -0
  415. package/frontend/src/ui/checkbox.ts +284 -0
  416. package/frontend/src/ui/code.ts +1049 -0
  417. package/frontend/src/ui/color.ts +117 -0
  418. package/frontend/src/ui/context_menu.ts +194 -0
  419. package/frontend/src/ui/css.ts +57 -0
  420. package/frontend/src/ui/divider.ts +28 -0
  421. package/frontend/src/ui/dropdown.ts +503 -0
  422. package/frontend/src/ui/for_each.ts +71 -0
  423. package/frontend/src/ui/form.ts +208 -0
  424. package/frontend/src/ui/frame_modes.ts +140 -0
  425. package/frontend/src/ui/google_map.ts +70 -0
  426. package/frontend/src/ui/gradient.ts +73 -0
  427. package/frontend/src/ui/image.ts +587 -0
  428. package/frontend/src/ui/input.ts +1284 -0
  429. package/frontend/src/ui/link.ts +77 -0
  430. package/frontend/src/ui/list.ts +88 -0
  431. package/frontend/src/ui/loader_button.ts +192 -0
  432. package/frontend/src/ui/loaders.ts +126 -0
  433. package/frontend/src/ui/popup.ts +370 -0
  434. package/frontend/src/ui/pseudo.ts +33 -0
  435. package/frontend/src/ui/scroller.ts +1324 -0
  436. package/frontend/src/ui/slider.ts +215 -0
  437. package/frontend/src/ui/spacer.ts +29 -0
  438. package/frontend/src/ui/span.ts +23 -0
  439. package/frontend/src/ui/stack.ts +238 -0
  440. package/frontend/src/ui/steps.ts +334 -0
  441. package/frontend/src/ui/style.ts +26 -0
  442. package/frontend/src/ui/switch.ts +286 -0
  443. package/frontend/src/ui/table.ts +323 -0
  444. package/frontend/src/ui/tabs.ts +441 -0
  445. package/frontend/src/ui/text.ts +38 -0
  446. package/frontend/src/ui/theme.ts +279 -0
  447. package/frontend/src/ui/title.ts +64 -0
  448. package/frontend/src/ui/ui.ts +47 -0
  449. package/frontend/src/ui/view.ts +44 -0
  450. package/frontend/src/volt.ts +31 -0
  451. package/package.json +58 -0
@@ -0,0 +1,2659 @@
1
+ /*
2
+ * Author: Daan van den Bergh
3
+ * Copyright: © 2022 - 2024 Daan van den Bergh.
4
+ */
5
+
6
+ // Imports.
7
+ import * as https from "https";
8
+ import * as fs from "fs";
9
+ import * as PDFDocument from "pdfkit";
10
+ import * as libcrypto from "crypto";
11
+ import blobstream from 'blob-stream';
12
+
13
+
14
+ import { vlib } from "@vinc";
15
+ import { Utils, FrontendError } from "../utils.js";
16
+ import { logger } from "../logger.js";
17
+ import { Status } from "../status.js";
18
+
19
+ const log_source = logger.LogSource("Payments")
20
+
21
+
22
+ // Request error.
23
+ class RequestError extends Error {
24
+ public status_code?: number;
25
+ constructor(err: string, status_code?: number) {
26
+ super(err);
27
+ this.status_code = status_code;
28
+ }
29
+ }
30
+
31
+
32
+ // --------------------------------------------------------------------------------------------------------
33
+ // Interfaces.
34
+
35
+ export interface ProductObject {
36
+ id: string;
37
+ name: string;
38
+ price: number;
39
+ currency: string;
40
+ tax_category: string;
41
+ icon?: string;
42
+ frequency?: number;
43
+ interval?: 'day' | 'week' | 'month' | 'year';
44
+ trial?: {
45
+ frequency: number;
46
+ interval: 'day' | 'week' | 'month' | 'year';
47
+ } | null;
48
+ plans?: ProductObject[];
49
+ description: string;
50
+ paddle_prod_id?: string;
51
+ price_id?: string;
52
+ is_subscription?: boolean;
53
+ subscription_id?: string;
54
+ }
55
+
56
+ export interface PaymentBillingDetails {
57
+ name?: string;
58
+ email?: string;
59
+ business?: string;
60
+ vat_id?: string;
61
+ address?: string;
62
+ city?: string;
63
+ postal_code?: string;
64
+ province?: string;
65
+ country?: string;
66
+ tax_identifier?: string;
67
+ }
68
+
69
+ export interface LineItem {
70
+ product: string;
71
+ item_id: string;
72
+ paddle_prod_id: string;
73
+ quantity: number;
74
+ tax_rate: number;
75
+ tax: number;
76
+ discount: number;
77
+ subtotal: number;
78
+ total: number;
79
+ status: 'paid' | 'refunded' | 'refunding';
80
+ }
81
+ export interface ExpandedLineItem extends Omit<LineItem, 'product'> {
82
+ product: ProductObject;
83
+ }
84
+
85
+ export type PaymentStatus = 'open' | 'paid' | 'past_due' | 'unknown';
86
+
87
+ export interface Payment {
88
+ id: string;
89
+ uid: string;
90
+ cus_id: string;
91
+ tran_id: string;
92
+ timestamp: number;
93
+ status: PaymentStatus;
94
+ line_items: LineItem[];
95
+ billing_details: PaymentBillingDetails;
96
+ sub_id?: string;
97
+ }
98
+ export interface ExpandedPayment extends Omit<Payment, 'line_items'> {
99
+ line_items: ExpandedLineItem[];
100
+ }
101
+
102
+ export type SubscriptionStatus = 'active' | 'cancelling' | 'cancelled';
103
+
104
+ export interface Subscription {
105
+ uid: string;
106
+ id: string;
107
+ cus_id: string;
108
+ status: SubscriptionStatus;
109
+ plans: string[];
110
+ }
111
+
112
+ export interface PaddleConstructorOptions {
113
+ api_key: string;
114
+ client_key: string;
115
+ sandbox?: boolean;
116
+ products?: ProductObject[];
117
+ inclusive_tax?: boolean;
118
+ _server?: any; // Replace with actual server type if available
119
+ }
120
+
121
+
122
+ // The paddle payments class.
123
+ // @todo check if a user can subscribe twice to a sub, should not be allowed for system logic.
124
+ // @todo still need to manage the reactivation of a subscription after a chargeback has been reversed.
125
+ // @todo still check if a subscription is automatically cancelled by paddle when it is refunded.
126
+ /* @docs:
127
+ @nav: Backend
128
+ @chapter: Payments
129
+ @title: Paddle
130
+ @description:
131
+ The paddle payments class.
132
+
133
+ Sandbox env: https://sandbox-vendors.paddle.com
134
+ @param:
135
+ @name: api_key
136
+ @type: string
137
+ @description: Your paddle api key.
138
+ @required: true
139
+ @param:
140
+ @name: client_key
141
+ @type: string
142
+ @description: Your paddle client key.
143
+ @required: true
144
+ @param:
145
+ @name: sandbox
146
+ @type: boolean
147
+ @description: Enable the sandbox environment.
148
+ @param:
149
+ @name: inclusive_tax
150
+ @type: boolean
151
+ @description: Enable when prices are inclusive tax.
152
+ @param:
153
+ @name: products
154
+ @type: object
155
+ @warning: The payment product objects are accessable by anyone through the backend rest api so they should not contain any sensitive data.
156
+ @attributes_type: ProductObject
157
+ @attribute:
158
+ @name: id
159
+ @type: string
160
+ @required: true
161
+ @desc: The id of product
162
+ @warning: The id can not be changed
163
+ @warning: The id must be unique across all your products.
164
+ @attribute:
165
+ @name: name
166
+ @type: string
167
+ @required: true
168
+ @desc: The name of the product.
169
+ @attribute:
170
+ @name: price
171
+ @type: number
172
+ @required: true
173
+ @desc: The price of the product, digits after the decimal are the minor units (e.g. cents).
174
+ @attribute:
175
+ @name: currency
176
+ @type: string
177
+ @required: true
178
+ @desc: The ISO currency code of the price.
179
+ @attribute:
180
+ @name: tax_category
181
+ @type: string
182
+ @required: true
183
+ @desc: The tax category https://developer.paddle.com/api-reference/products/create-product.
184
+ @attribute:
185
+ @name: icon
186
+ @type: string
187
+ @desc: The icon url of the product, may also be an endpoint url of your website.
188
+ @attribute:
189
+ @name: frequency
190
+ @type: number
191
+ @desc: The recurring frequency, when this is defined a product will become a subscription product.
192
+ @attribute:
193
+ @name: interval
194
+ @type: string
195
+ @desc: The recurring interval, when this is defined a product will become a subscription product.
196
+ @enum:
197
+ @value: "day"
198
+ @desc: Use this value to create a subscription product that renews at a daily interval.
199
+ @enum:
200
+ @value: "week"
201
+ @desc: Use this value to create a subscription product that renews at a weekly interval.
202
+ @enum:
203
+ @value: "month"
204
+ @desc: Use this value to create a subscription product that renews at a monthly interval.
205
+ @enum:
206
+ @value: "year"
207
+ @desc: Use this value to create a subscription product that renews at a yearly interval.
208
+ @attribute:
209
+ @name: trial
210
+ @type: null, object
211
+ @desc: The trial settings for this product. Leave undefined to disable a trialing period. This attribute will be ignored for one-time payments.
212
+ @attribute:
213
+ @name: frequency
214
+ @type: number
215
+ @desc: The trial frequency.
216
+ @attribute:
217
+ @name: interval
218
+ @type: string
219
+ @desc: The trial interval.
220
+ @enum:
221
+ @value: "day"
222
+ @desc: Daily interval.
223
+ @enum:
224
+ @value: "week"
225
+ @desc: Weekly interval.
226
+ @enum:
227
+ @value: "month"
228
+ @desc: Monthly interval.
229
+ @enum:
230
+ @value: "year"
231
+ @desc: Yearly interval.
232
+ @attribute:
233
+ @name: plans
234
+ @type: array[ProductObject]
235
+ @desc: The plans for this subscription product. Every item is a product object. However, attributes `currency`, `frequency`, `interval`, `tax_category` and `icon` can either be defined in the subscription product or on each individual plan.
236
+ @parameter:
237
+ @name: _server
238
+ @ignore: true
239
+ */
240
+ export class Paddle {
241
+ private type: string;
242
+ private client_key: string;
243
+ private sandbox: boolean;
244
+ private inclusive_tax: boolean;
245
+ private products: ProductObject[];
246
+ private server: any;
247
+ private _host: string;
248
+ private _headers: Record<string, string>;
249
+ private webhook_key?: string;
250
+ private _has_create_products_permission?: boolean;
251
+ private _settings_db: any;
252
+ private _sub_db: any;
253
+ private _active_sub_db: any;
254
+ private _pay_db: any;
255
+ private _inv_db: any;
256
+ private performance: any;
257
+
258
+ constructor({
259
+ api_key,
260
+ client_key,
261
+ sandbox = false,
262
+ products = [],
263
+ inclusive_tax = false,
264
+ _server = null,
265
+ }: PaddleConstructorOptions) {
266
+ // Original constructor implementation remains the same
267
+ // Verify args.
268
+ vlib.Scheme.verify({object: arguments[0], check_unknown: true, parent: "payments", scheme: {
269
+ type: {type: "string", default: "paddle"},
270
+ api_key: "string",
271
+ client_key: "string",
272
+ sandbox: {type: "boolean", default: false},
273
+ inclusive_tax: {type: "boolean", default: false},
274
+ products: "array",
275
+ _server: "object",
276
+ }});
277
+
278
+ // Attributes.
279
+ this.type = "paddle";
280
+ this.client_key = client_key;
281
+ this.sandbox = sandbox;
282
+ this.inclusive_tax = inclusive_tax;
283
+ this.products = products;
284
+ this.server = _server;
285
+
286
+ // Request headers.
287
+ this._host = this.sandbox ? "sandbox-api.paddle.com" : "api.paddle.com";
288
+ this._headers = {
289
+ "Content-Type": "application/json",
290
+ "Accept": "application/json",
291
+ "Authorization": "Bearer " + api_key,
292
+ }
293
+
294
+ // Extend the csp.
295
+ this.server.csp["default-src"] += " https://*.paddle.com/";
296
+ this.server.csp["script-src"] += " https://*.paddle.com/ https://*.payments-amazon.com https://*.paypal.com https://*.google.com";
297
+ this.server.csp["style-src"] += " https://*.paddle.com/ https://*.media-amazon.com https://*.paypal.com https://*.google.com";
298
+ this.server.csp["img-src"] += " https://*.paddle.com/ https://*.media-amazon.com https://*.paypal.com https://*.google.com";
299
+
300
+ /* @performance */ this.performance = new vlib.Performance("Payments performance");
301
+ }
302
+
303
+ // ---------------------------------------------------------
304
+ // Products and prices (private).
305
+
306
+ // ---------------------------------------------------------
307
+ // Utils (private).
308
+
309
+ private async _req(method: string, endpoint: string, params: any = null): Promise<any> {
310
+ const promise = new Promise<any>((resolve, reject) => {
311
+
312
+ // Options.
313
+ const options = {
314
+ method: method,
315
+ hostname: this._host,
316
+ path: method === "GET" && params != null ? `${endpoint}?${new URLSearchParams(params).toString()}`: endpoint,
317
+ port: 443,
318
+ headers: this._headers,
319
+ };
320
+
321
+ // Make the HTTP request
322
+ const request = https.request(options, (response) => {
323
+ let data: any = '';
324
+
325
+ response.on('data', (chunk) => {
326
+ data += chunk;
327
+ });
328
+ response.on('end', () => {
329
+ if (response?.statusCode! >= 200 && response?.statusCode! < 300) {
330
+ try {
331
+ resolve(JSON.parse(data));
332
+ } catch (error) {
333
+ reject(new Error('Failed to parse response data'));
334
+ }
335
+ } else {
336
+ if (data == null || data === "") {
337
+ return reject(new RequestError(`${method}:${endpoint}: Request failed [${response.statusCode}].`, response.statusCode));
338
+ }
339
+ try {
340
+ data = JSON.parse(data);
341
+ } catch (e) {
342
+ return reject(new RequestError(`${method}:${endpoint}: Request failed [${response.statusCode}].`, response.statusCode));
343
+ }
344
+ if (data.error == null) {
345
+ return reject(new RequestError(`${method}:${endpoint}: Request failed [${response.statusCode}].`, response.statusCode));
346
+ }
347
+ data = data.error;
348
+ let errs = "";
349
+ if (data.errors) {
350
+ errs += ". ";
351
+ data.errors.iterate((item) => {
352
+ errs += `Field: "${item.field}" ${item.message}. `;
353
+ })
354
+ errs = errs.substr(0, errs.length - 2);
355
+ }
356
+ return reject(new RequestError(`${method}:${endpoint}: ${data.detail} [${response.statusCode}]${errs}.`, response.statusCode));
357
+ }
358
+ });
359
+ });
360
+
361
+ // Write body params.
362
+ if (params != null) {
363
+ // request.write(JSON.stringify(params));
364
+ const requestBody = JSON.stringify(params);
365
+ request.setHeader('Content-Length', Buffer.byteLength(requestBody));
366
+ request.write(requestBody);
367
+ }
368
+
369
+ // On error.
370
+ request.on('error', (error) => {
371
+ reject(error);
372
+ });
373
+
374
+ // End.
375
+ request.end();
376
+ });
377
+
378
+ // So the traceback still includes the call function of _req.
379
+ try {
380
+ return await promise;
381
+ } catch (e: any) {
382
+ if (e instanceof Error || e instanceof RequestError) { throw e }
383
+ throw new Error(e);
384
+ }
385
+ }
386
+
387
+ // ---------------------------------------------------------
388
+ // Database (private).
389
+
390
+ // Add or remove a subscription to the user's active subscriptions.
391
+ private async _add_subscription(uid: string, prod_id: string, sub_id: string): Promise<void> {
392
+ await this._active_sub_db.save(uid, prod_id, {prod_id, sub_id});
393
+ }
394
+ private async _delete_subscription(uid: string, prod_id: string): Promise<void> {
395
+ await this._active_sub_db.delete(uid, prod_id);
396
+ }
397
+ private async _check_subscription(uid: string, prod_id: string, load_data?: true): Promise<{exists: boolean, sub_id?: string}>;
398
+ private async _check_subscription(uid: string, prod_id: string, load_data?: false): Promise<boolean>;
399
+ private async _check_subscription(uid: string, prod_id: string, load_data: true | false = false): Promise<{exists: boolean, sub_id?: string} | boolean> {
400
+ const doc = await this._active_sub_db.load(uid, prod_id);
401
+ let exists = false, sub_id;
402
+ if (doc == null) {
403
+ if (load_data) {
404
+ return {exists, sub_id};
405
+ } else {
406
+ return exists;
407
+ }
408
+ }
409
+ exists = true;
410
+ sub_id = doc.sub_id;
411
+ if (load_data) {
412
+ return {exists, sub_id};
413
+ } else {
414
+ return exists;
415
+ }
416
+ }
417
+ private async _get_active_subscriptions(uid: string, detailed: boolean = false): Promise<any[]> {
418
+ const list = await this._active_sub_db.list_query({_uid: uid});
419
+ if (detailed) { return list; }
420
+ const products: string[] = [];
421
+ list.iterate((doc) => {
422
+ products.push(doc.prod_id);
423
+ })
424
+ return products;
425
+ }
426
+ private async _save_subscription(subscription: Subscription): Promise<void> {
427
+ await this._sub_db.save(subscription.uid == null ? "unauth" : subscription.uid, subscription.id, subscription);
428
+ }
429
+ private async _load_subscription(id: string): Promise<Subscription> {
430
+ const subscription = await this._sub_db.find(null, {_path: id});
431
+ if (subscription == null) {
432
+ throw Error(`Unable to find subscription "${id}".`);
433
+ }
434
+ return subscription;
435
+ }
436
+ private async _get_subscriptions(uid: string): Promise<Subscription[]> {
437
+ if (uid === "unauth" || uid == null) { return []; }
438
+ const list = await this._sub_db.list_query({_uid: uid});
439
+ return list;
440
+ }
441
+
442
+ // Save and delete payments, all failed payments should be deleted from the database.
443
+ private async _save_payment(payment: Payment): Promise<void> {
444
+ await this._pay_db.save(payment.uid == null ? "unauth" : payment.uid, payment.id, payment);
445
+ }
446
+ private async _load_payment(id: string): Promise<Payment> {
447
+ const uid = id.split("_")[1];
448
+ const payment = await this._pay_db.load(uid, id);
449
+ if (payment == null) {
450
+ throw Error(`Unable to find payment "${id}".`);
451
+ }
452
+ if (uid == null || uid == "unauth") {
453
+ delete payment.billing_details;
454
+ }
455
+ return payment;
456
+ }
457
+ private async _load_payment_by_transaction(id: string): Promise<Payment> {
458
+ const payment = await this._pay_db.find(null, {tran_id: id});
459
+ if (payment == null) {
460
+ throw Error(`Unable to find the payment by transaction id "${id}".`);
461
+ }
462
+ if (payment.uid == null || payment.uid == "unauth") {
463
+ delete payment.billing_details;
464
+ }
465
+ return payment;
466
+ }
467
+ private async _delete_payment(id: string): Promise<void> {
468
+ const uid = id.split("_")[1];
469
+ await this._pay_db.delete(uid, id);
470
+ }
471
+
472
+ // Delete all info of a user.
473
+ async _delete_user(uid: string): Promise<void> {
474
+ await this._sub_db.delete_all(uid);
475
+ await this._active_sub_db.delete_all(uid);
476
+ await this._pay_db.delete_all(uid);
477
+ await this._inv_db.delete_all(uid);
478
+ }
479
+
480
+ // List all active subscriptions.
481
+ public async _get_all_active_subscriptions(): Promise<any[]> {
482
+ return await this._active_sub_db.list_query({});
483
+ }
484
+
485
+ // ---------------------------------------------------------
486
+ // Overall (private).
487
+
488
+ // Get product by paddle product id.
489
+ private _get_product_by_paddle_prod_id(id: string, throw_err: boolean = false): ProductObject | null {
490
+ const product = this.products.iterate((p) => {
491
+ if (p.is_subscription) {
492
+ if (p.plans == null) {
493
+ throw Error(`Invalid project "${p.id}" subscription is activated yet no plans are defined.`);
494
+ }
495
+ return p.plans.iterate((plan) => {
496
+ if (plan.paddle_prod_id === id) {
497
+ return plan;
498
+ }
499
+ })
500
+ }
501
+ else if (p.paddle_prod_id === id) {
502
+ return p;
503
+ }
504
+ })
505
+ if (product == null && throw_err) {
506
+ throw Error(`Unable to find product "${id}".`);
507
+ }
508
+ return product;
509
+ }
510
+
511
+ // Get all active products.
512
+ private async _get_products(): Promise<any[]> {
513
+ let response, next = null;
514
+ let items = [];
515
+ while (true) {
516
+ if (next == null) {
517
+ response = await this._req("GET", "/products", {status: ["active"], per_page: 100});
518
+ } else {
519
+ response = await this._req("GET", next);
520
+ }
521
+ items = items.concat(response.data);
522
+ if (response.meta.has_more) {
523
+ next = response.meta.next;
524
+ } else {
525
+ break;
526
+ }
527
+ }
528
+ return items;
529
+ }
530
+
531
+ // Get all active prices.
532
+ private async _get_prices(): Promise<any[]> {
533
+ let response, next = null;
534
+ let items = [];
535
+ while (true) {
536
+ if (next == null) {
537
+ response = await this._req("GET", "/prices", {status: ["active"], per_page: 100});
538
+ } else {
539
+ response = await this._req("GET", next);
540
+ }
541
+ items = items.concat(response.data);
542
+ if (response.meta.has_more) {
543
+ next = response.meta.next;
544
+ } else {
545
+ break;
546
+ }
547
+ }
548
+ return items;
549
+ }
550
+
551
+ // Create or update a product, when existing product is undefined a new product and price will be created.
552
+ private async _check_product(
553
+ product: ProductObject,
554
+ existing_products: any[] = [],
555
+ existing_prices: any[] = []
556
+ ): Promise<void> {
557
+
558
+ // Check create product permission.
559
+ const has_create_products_permission = async () => {
560
+ if (process.argv.includes("--no-payment-edits")) {
561
+ return false;
562
+ }
563
+ if (this._has_create_products_permission) {
564
+ return true;
565
+ }
566
+ const input = await vlib.Utils.prompt("Some paddle products have to be edited, do you wish to make these changes? [y/n]: ")
567
+ if (["y", "yes", "ok"].includes(input.toLowerCase())) {
568
+ this._has_create_products_permission = true;
569
+ } else {
570
+ this._has_create_products_permission = false;
571
+ }
572
+ return this._has_create_products_permission;
573
+ }
574
+
575
+ // Find existing product.
576
+ const existing_product = existing_products.iterate((item) => {
577
+ if (item.custom_data.id === product.id) {
578
+ return item;
579
+ }
580
+ })
581
+
582
+ // No existing product so create.
583
+ if (existing_product == null) {
584
+
585
+ // Check permission.
586
+ if (!await has_create_products_permission()) {
587
+ return ;
588
+ }
589
+
590
+ // Create product.
591
+ logger.log(0, log_source, `Creating product ${product.name}.`);
592
+ const created_product = await this._req("POST", "/products", {
593
+ name: product.name,
594
+ description: product.description,
595
+ image_url: product.icon,
596
+ tax_category: product.tax_category,
597
+ custom_data: {id: product.id},
598
+ })
599
+ product.paddle_prod_id = created_product.data.id;
600
+
601
+ // Create price.
602
+ logger.log(0, log_source, `Creating a price for product ${product.name}.`);
603
+ const created_price = await this._req("POST", "/prices", {
604
+ product_id: product.paddle_prod_id,
605
+ name: product.name,
606
+ description: product.description,
607
+ unit_price: {amount: Math.floor(product.price * 100).toString(), currency_code: product.currency},
608
+ billing_cycle: product.is_subscription ? {interval: product.interval, frequency: product.frequency} : null,
609
+ trial_period: product.trial,
610
+ tax_mode: this.inclusive_tax ? "internal" : "external",
611
+ })
612
+ product.price_id = created_price.data.id;
613
+ }
614
+
615
+ // Passed an existing product so check.
616
+ else {
617
+
618
+ // Vars.
619
+ product.paddle_prod_id = existing_product.id;
620
+ const has_trial = product.trial != null;
621
+
622
+ // Check if the product should be updated.
623
+ const update_product = (
624
+ existing_product.name !== product.name ||
625
+ existing_product.description !== product.description ||
626
+ existing_product.image_url !== product.icon ||
627
+ existing_product.tax_category !== product.tax_category ||
628
+ existing_product.status !== "active"
629
+ );
630
+
631
+ // Update product.
632
+ if (update_product) {
633
+ if (!await has_create_products_permission()) {
634
+ return ;
635
+ }
636
+ logger.log(0, log_source, `Updating product ${product.name}.`);
637
+ await this._req("PATCH", `/products/${product.paddle_prod_id}`, {
638
+ name: product.name,
639
+ description: product.description,
640
+ image_url: product.icon,
641
+ tax_category: product.tax_category,
642
+ custom_data: {id: product.id},
643
+ status: "active",
644
+ })
645
+ }
646
+
647
+ // Fetch the attached price.
648
+ const existing_price = existing_prices.iterate((item) => {
649
+ if (item.product_id === product.paddle_prod_id) {
650
+ return item;
651
+ }
652
+ })
653
+
654
+ // Create price.
655
+ if (existing_price == null) {
656
+ if (!await has_create_products_permission()) {
657
+ return ;
658
+ }
659
+ logger.log(0, log_source, `Creating a price for product ${product.name}.`);
660
+ const price = await this._req("POST", "/prices", {
661
+ product_id: product.paddle_prod_id,
662
+ name: product.name,
663
+ description: product.description,
664
+ unit_price: {amount: Math.floor(product.price * 100).toString(), currency_code: product.currency},
665
+ billing_cycle: product.is_subscription ? {interval: product.interval, frequency: product.frequency} : null,
666
+ trial_period: product.trial,
667
+ tax_mode: this.inclusive_tax ? "internal" : "external",
668
+ })
669
+ product.price_id = price.data.id;
670
+ }
671
+
672
+ // Update price.
673
+ else {
674
+
675
+ // Set id.
676
+ product.price_id = existing_price.id;
677
+
678
+ // Update price.
679
+ const update_price = (
680
+ existing_price.product_id !== product.paddle_prod_id ||
681
+ existing_price.name !== product.name ||
682
+ existing_price.description !== product.description ||
683
+ existing_price.tax_mode !== (this.inclusive_tax ? "internal" : "external") ||
684
+ existing_price.unit_price == null ||
685
+ existing_price.unit_price.amount !== Math.floor(product.price * 100).toString() ||
686
+ existing_price.unit_price.currency_code !== product.currency ||
687
+ (product.is_subscription && (
688
+ existing_price.billing_cycle == null ||
689
+ existing_price.billing_cycle.interval !== product.interval ||
690
+ existing_price.billing_cycle.frequency !== product.frequency
691
+ )) ||
692
+ (has_trial && (
693
+ existing_price.trial_period == null ||
694
+ existing_price.trial_period.interval !== product.trial?.interval ||
695
+ existing_price.trial_period.frequency !== product.trial?.frequency
696
+ )) ||
697
+ existing_price.status !== "active"
698
+ )
699
+
700
+ // Update price.
701
+ if (update_price) {
702
+ if (!await has_create_products_permission()) {
703
+ return ;
704
+ }
705
+ logger.log(0, log_source, `Updating the price of product ${product.name}.`);
706
+ await this._req("PATCH", `/prices/${product.price_id}`, {
707
+ // product_id: product.id, // not allowed.
708
+ name: product.name,
709
+ description: product.description,
710
+ unit_price: {amount: Math.floor(product.price * 100).toString(), currency_code: product.currency},
711
+ billing_cycle: product.is_subscription ? {interval: product.interval, frequency: product.frequency} : null,
712
+ trial_period: product.trial,
713
+ tax_mode: this.inclusive_tax ? "internal" : "external",
714
+ status: "active",
715
+ })
716
+ }
717
+ }
718
+ }
719
+ }
720
+
721
+ // Cancel subscription by subscription id.
722
+ private async _cancel_subscription(id: string, immediate: boolean = false): Promise<void> {
723
+ if (id == null) {
724
+ throw Error(`Define parameter \"id\".`);
725
+ }
726
+
727
+ // Load subscription object.
728
+ const subscription = await this._load_subscription(id);
729
+ if (subscription == null) {
730
+ throw Error(`Unable to find subscription "${id}".`);
731
+ }
732
+
733
+ // Cancel.
734
+ if (subscription.status !== "active") {
735
+ throw new FrontendError(`This subscription does not contain any cancellable items, the subscription is likely already cancelled.`, Status.bad_request);
736
+ }
737
+ await this._req("POST", `/subscriptions/${subscription.id}/cancel`, {
738
+ effective_from: immediate ? "immediately" : null,
739
+ });
740
+
741
+ // Update subscription.
742
+ subscription.status = "cancelling" as SubscriptionStatus;
743
+ await this._save_subscription(subscription);
744
+ }
745
+
746
+ // async _cancel_subscription(payment) {
747
+ // if (typeof payment === "string") {
748
+ // payment = await this._load_payment(payment);
749
+ // }
750
+ // if (payment.cus_id == null) {
751
+ // throw Error(`Payment "${payment.id}" does not have an assigned customer id attribute.`);
752
+ // }
753
+ // if (payment.sub_id == null) {
754
+ // throw Error(`Payment "${payment.id}" does not have an assigned subscription id attribute, it may not be a subscription payment.`);
755
+ // }
756
+ // if (payment.line_items.length == 0) {
757
+ // throw Error(`Payment "${payment.id}" does not contain any line items.`);
758
+ // }
759
+
760
+ // // Cancel.
761
+ // const cancellable = [];
762
+ // let all_cancelled = null;
763
+ // payment.line_items.iterate((item) => {
764
+ // const product = this.get_product_sync(item.product);
765
+ // if (product.is_subscription) {
766
+ // if (item.status === "cancelled" || item.status === "cancelling") {
767
+ // if (all_cancelled == null) {
768
+ // all_cancelled = true;
769
+ // }
770
+ // } else if (item.status === "paid" || item.status === "refunding" || item.status === "refunded") {
771
+ // all_cancelled = false;
772
+ // cancellable.push(item);
773
+ // }
774
+
775
+ // }
776
+ // })
777
+ // if (all_cancelled) {
778
+ // throw new FrontendError(`This subscription is already cancelled and will become inactive at the end of the billing period.`, Status.bad_request);
779
+ // }
780
+ // if (cancellable.length === 0) {
781
+ // throw new FrontendError(`This subscription does not contain any cancellable items, the subscription is likely already cancelled or refunded.`, Status.bad_request);
782
+ // }
783
+ // await this._req("POST", `/subscriptions/${payment.sub_id}/cancel`, {
784
+ // // effective_from: "immediately",
785
+ // });
786
+
787
+ // // Update payment.
788
+ // cancellable.iterate((item) => {
789
+ // if (item.status === "paid") {
790
+ // item.status = "cancelling";
791
+ // }
792
+ // })
793
+ // await this._save_payment(payment);
794
+
795
+ // /* V1 cancel per product but since the webhook subscription event does not show which sub items are cancelled, this is not possible.
796
+
797
+ // // Update the subscription items.
798
+ // const sub = await this._req("GET", `/subscriptions/${payment.sub_id}`);
799
+ // const items = [];
800
+ // const cancelled_line_items = [];
801
+ // let edits = 0;
802
+ // sub.data.items.iterate((sub_item) => {
803
+
804
+ // // Only for active subscription items.
805
+ // if (sub_item.recurring && (sub_item.status === "active" || sub_item.status === "trailing")) {
806
+
807
+ // // Recurring items.
808
+ // const item = payment.line_items.iterate((item) => {
809
+ // if (item.paddle_prod_id === sub_item.price.product_id) {
810
+ // return item;
811
+ // }
812
+ // })
813
+
814
+ // // Item not found, so cancel but do not update status since it is not found.
815
+ // if (item == null) {
816
+ // console.error(`Unable to find subscription item "${sub_item.price.product_id}" while cancelling. Items: ${JSON.stringify(payment.line_items)}`)
817
+ // ++edits;
818
+ // }
819
+
820
+ // // Already cancelling.
821
+ // // else if (item.status === "cancelling") {
822
+ // // items.push({
823
+ // // price_id: sub_item.price.id,
824
+ // // quantity: sub_item.quantity,
825
+ // // })
826
+ // // }
827
+
828
+ // // Cancel item.
829
+ // else if (products == null || products.includes(item.id)) {
830
+ // item.status = "cancelling";
831
+ // ++edits;
832
+ // cancelled_line_items.push(item);
833
+ // }
834
+
835
+ // // Keep item.
836
+ // else {
837
+ // items.push({
838
+ // price_id: sub_item.price.id,
839
+ // quantity: sub_item.quantity,
840
+ // })
841
+ // }
842
+ // }
843
+
844
+ // // Keep all non recurring.
845
+ // else if (sub_item.recurring === false) {
846
+ // items.push({
847
+ // price_id: sub_item.price.id,
848
+ // quantity: sub_item.quantity,
849
+ // })
850
+ // }
851
+ // })
852
+
853
+ // // No edits.
854
+ // if (edits === 0) {
855
+ // throw Error("This payment does not contain any cancellable subscriptions.");
856
+ // }
857
+
858
+ // // Catch certain error.
859
+ // try {
860
+
861
+ // // Delete the subscription.
862
+ // if (items.length === 0) {
863
+ // await this._req("POST", `/subscriptions/${payment.sub_id}/cancel`, {});
864
+ // }
865
+
866
+ // // Update the subscription.
867
+ // else {
868
+ // await this._req("PATCH", `/subscriptions/${payment.sub_id}`, {
869
+ // items: items,
870
+ // scheduled_change: null,
871
+ // proration_billing_mode: "full_next_billing_period",
872
+ // });
873
+ // }
874
+ // } catch (error) {
875
+ // if (error.message.indexOf("cannot update subscription, pending scheduled changes") === -1) {
876
+ // throw error;
877
+ // }
878
+ // }
879
+
880
+ // // Update payment.
881
+ // cancelled_line_items.iterate((item) => {
882
+ // item.status = "cancelling";
883
+ // })
884
+ // await this._save_payment(payment);
885
+ // */
886
+ // }
887
+
888
+ // Initialize all products.
889
+ private async _initialize_products(): Promise<void> {
890
+
891
+ const file_watcher_restart = process.argv.includes("--file-watcher-restart");
892
+
893
+ /* @performance */ let now = this.performance.start();
894
+
895
+ // Extend and initialize all products.
896
+ // Check a payment product / plan product.
897
+ const product_ids: string[] = [];
898
+ let product_index = 0;
899
+ const initialize_product = (product: ProductObject) => {
900
+ ++product_index;
901
+
902
+ // Check if the product has a name.
903
+ if (product.id == null || product.id === "") {
904
+ throw Error(`Product ${product_index} does not have an assigned "id" attribute (string).`);
905
+ }
906
+ else if (product_ids.includes(product.id)) {
907
+ throw Error(`Product ${product_index} has a non unique name "${product.id}".`);
908
+ }
909
+ product_ids.push(product.id);
910
+
911
+ // Set the icon absolute url.
912
+ if (typeof product.icon === "string" && product.icon.charAt(0) === "/") {
913
+ product.icon = `${this.server.full_domain}/${product.icon}`;
914
+ }
915
+
916
+ // Check attributes.
917
+ if (typeof product.id !== "string" || product.id === "") {
918
+ throw Error(`Product "${product_index}" does not have an assigned "id" attribute (string).`);
919
+ }
920
+ if (typeof product.name !== "string" || product.name === "") {
921
+ throw Error(`Product "${product.id}" does not have an assigned "name" attribute (string).`);
922
+ }
923
+ if (typeof product.description !== "string" || product.description === "") {
924
+ throw Error(`Product "${product.id}" does not have an assigned "description" attribute (string).`);
925
+ }
926
+ if (typeof product.currency !== "string" || product.currency === "") {
927
+ throw Error(`Product "${product.id}" does not have an assigned "currency" attribute (string).`);
928
+ }
929
+ if (typeof product.price !== "number") {
930
+ throw Error(`Product "${product.id}" does not have an assigned "price" attribute (number).`);
931
+ }
932
+ if (typeof product.tax_category !== "string") {
933
+ throw Error(`Product "${product.id}" does not have an assigned "tax_category" attribute (number).`);
934
+ }
935
+ if (product.is_subscription && typeof product.frequency !== "number") {
936
+ throw Error(`Product "${product.id}" does not have an assigned "frequency" attribute (number).`);
937
+ }
938
+ if (product.is_subscription && typeof product.interval !== "string") {
939
+ throw Error(`Product "${product.id}" does not have an assigned "interval" attribute (string).`);
940
+ }
941
+ }
942
+
943
+ // Expand the payment products.
944
+ let sub_products = 0;
945
+ this.products.iterate((product) => {
946
+ if (product.plans != null) {
947
+
948
+ // Check plans.
949
+ if (product.plans != null && Array.isArray(product.plans) === false) {
950
+ throw Error(`Product "${product_index}" has an incorrect value type for attribute "plans", the valid type is "array".`);
951
+ }
952
+
953
+ // Generate sub id.
954
+ product.id = `sub_${sub_products}`
955
+ if (product_ids.includes(product.id)) {
956
+ throw Error(`Another product has a reserved name "${product.id}".`);
957
+ }
958
+ product_ids.push(product.id);
959
+ ++sub_products;
960
+
961
+ // Attributes.
962
+ product.is_subscription = true;
963
+
964
+ // Expand plan attributes.
965
+ product.plans.iterate((plan) => {
966
+ plan.is_subscription = true;
967
+ plan.subscription_id = product.id;
968
+ if (plan.description == null ) { plan.description = product.description; }
969
+ if (plan.currency == null ) { plan.currency = product.currency; }
970
+ if (plan.frequency == null ) { plan.frequency = product.frequency; }
971
+ if (plan.interval == null ) { plan.interval = product.interval; }
972
+ if (plan.tax_category == null ) { plan.tax_category = product.tax_category; }
973
+ if (plan.icon == null ) { plan.icon = product.icon; }
974
+ initialize_product(plan);
975
+ })
976
+ }
977
+ else if (product.frequency != null || product.interval != null) {
978
+ throw Error(`Subscription products should be nested as plans of a subscription "{... plans: [...]}". Not as a direct product without a subscription parent.`);
979
+ }
980
+ else {
981
+ product.is_subscription = false;
982
+ initialize_product(product);
983
+ }
984
+ })
985
+
986
+ /* @performance */ now = this.performance.end("init-products", now);
987
+
988
+ // Check registered products.
989
+ const last_products = await this._settings_db.load(`last_products${this.server.production ? "" : "_demo"}`);
990
+ if (Object.eq(last_products, this.products)) {
991
+ const product_ids = await this._settings_db.load(`product_ids${this.server.production ? "" : "_demo"}`);
992
+ product_ids.iterate((item) => {
993
+ const product = this.get_product_sync(item.id);
994
+ if (product != null) {
995
+ product.paddle_prod_id = item.paddle_prod_id;
996
+ product.price_id = item.price_id;
997
+ }
998
+ })
999
+ /* @performance */ now = this.performance.end("assign-product-ids", now);
1000
+ }
1001
+ else if (this.server.offline === false) {
1002
+
1003
+ // Get all products and prices.
1004
+ const existing_products = await this._get_products();
1005
+ const existing_prices = await this._get_prices();
1006
+ /* @performance */ now = this.performance.end("get-prices-and-products", now);
1007
+
1008
+ // Check all products.
1009
+ const product_ids: {id:string, paddle_prod_id: string, price_id: string}[] = [];
1010
+ await this.products.iterate_async_await(async (product) => {
1011
+ if (product.plans != null) {
1012
+ await product.plans.iterate_async_await(async (plan) => {
1013
+ await this._check_product(plan, existing_products, existing_prices)
1014
+ product_ids.append({
1015
+ id: plan.id,
1016
+ paddle_prod_id: plan.paddle_prod_id as string,
1017
+ price_id: plan.price_id as string,
1018
+ })
1019
+ });
1020
+ } else {
1021
+ await this._check_product(product, existing_products, existing_prices)
1022
+ product_ids.append({
1023
+ id: product.id,
1024
+ paddle_prod_id: product.paddle_prod_id as string,
1025
+ price_id: product.price_id as string,
1026
+ })
1027
+ }
1028
+ });
1029
+ /* @performance */ now = this.performance.end("check-products", now);
1030
+
1031
+ // Save last products.
1032
+ await this._settings_db.save(`last_products${this.server.production ? "" : "_demo"}`, Object.delete_recursively(Object.deep_copy(this.products), ["paddle_prod_id", "price_id"]));
1033
+
1034
+ // Save price ids.
1035
+ await this._settings_db.save(`product_ids${this.server.production ? "" : "_demo"}`, product_ids);
1036
+
1037
+ /* @performance */ now = this.performance.end("save-products-to-db", now);
1038
+ }
1039
+ }
1040
+
1041
+ // Initialize the payments.
1042
+ public async _initialize(): Promise<void> {
1043
+
1044
+ /* @performance */ this.performance.start();
1045
+
1046
+ const file_watcher_restart = process.argv.includes("--file-watcher-restart");
1047
+
1048
+ // Create database collections.
1049
+ this._settings_db = this.server.db.create_collection("_payment_settings");
1050
+ this._sub_db = this.server.db.create_uid_collection("_subscriptions");
1051
+ this._active_sub_db = this.server.db.create_uid_collection("_active_subscriptions");
1052
+ this._pay_db = this.server.db.create_uid_collection("_payments");
1053
+ this._inv_db = this.server.db.create_uid_collection("_invoices");
1054
+ /* @performance */ this.performance.end("init-db");
1055
+
1056
+
1057
+ // Initialize products.
1058
+ await this._initialize_products();
1059
+ /* @performance */ let now = this.performance.start();
1060
+
1061
+ // Add endpoints.
1062
+ this.server.endpoint(
1063
+
1064
+ // Initialize and verify an order, check if the user is authenticated when subscriptions are present and check if the user is not already subscribed to the same item.
1065
+ {
1066
+ method: "POST",
1067
+ endpoint: "/volt/payments/init",
1068
+ content_type: "application/json",
1069
+ rate_limit: "global",
1070
+ params: {
1071
+ items: "array",
1072
+ },
1073
+ callback: async (stream, params) => {
1074
+
1075
+ // Check items.
1076
+ if (params.items.length === 0) {
1077
+ return stream.error({status: Status.bad_request, data: {error: "Shopping cart is empty."}})
1078
+ }
1079
+ let sub_plan_count = {};
1080
+ const error = await params.items.iterate_async_await(async (item) => {
1081
+ if (item.product.is_subscription) {
1082
+ if (stream.uid == null) {
1083
+ return "You must be signed-in to purchase a subscription.";
1084
+ }
1085
+ if (item.quantity != null && item.quantity > 1) {
1086
+ return "Subscriptions have a max quantity of 1.";
1087
+ }
1088
+ if (sub_plan_count[item.product.subscription_id] == null) {
1089
+ sub_plan_count[item.product.subscription_id] = 1;
1090
+ } else {
1091
+ return "You can not charge two different subscription plans from the same subscription product.";
1092
+ }
1093
+ if (await this._check_subscription(stream.uid, item.product.id, false)) {
1094
+ return `You are already subscribed to product "${item.product.name}".`;
1095
+ }
1096
+ }
1097
+ })
1098
+ if (error) {
1099
+ return stream.error({status: Status.bad_request, data: {error}})
1100
+ }
1101
+
1102
+ // Success.
1103
+ return stream.success({data: {message: "Successfully initialized the order."}});
1104
+ }
1105
+ },
1106
+
1107
+ // Get products.
1108
+ {
1109
+ method: "GET",
1110
+ endpoint: "/volt/payments/products",
1111
+ content_type: "application/json",
1112
+ rate_limit: "global",
1113
+ callback: (stream) => {
1114
+ return stream.success({data: this.products});
1115
+ }
1116
+ },
1117
+
1118
+ // Get payment by id.
1119
+ {
1120
+ method: "GET",
1121
+ endpoint: "/volt/payments/payment",
1122
+ content_type: "application/json",
1123
+ rate_limit: "global",
1124
+ params: {
1125
+ id: "string",
1126
+ },
1127
+ callback: async (stream, params) => {
1128
+ return stream.success({data: (await this._load_payment(params.id))});
1129
+ }
1130
+ },
1131
+
1132
+ // Get payments.
1133
+ {
1134
+ method: "GET",
1135
+ endpoint: "/volt/payments/payments",
1136
+ content_type: "application/json",
1137
+ authenticated: true,
1138
+ rate_limit: "global",
1139
+ params: {
1140
+ days: {type: "number", default: 30},
1141
+ limit: {type: "number", default: null},
1142
+ status: {type: "string", default: null},
1143
+ },
1144
+ callback: async (stream, params) => {
1145
+ const result = await this.get_payments({
1146
+ uid: stream.uid,
1147
+ days: params.days,
1148
+ limit: params.limit,
1149
+ status: params.status,
1150
+ })
1151
+ return stream.success({data: result});
1152
+ }
1153
+ },
1154
+
1155
+ // Get refundable payments.
1156
+ {
1157
+ method: "GET",
1158
+ endpoint: "/volt/payments/payments/refundable",
1159
+ content_type: "application/json",
1160
+ authenticated: true,
1161
+ rate_limit: "global",
1162
+ params: {
1163
+ days: {type: "number", default: 30},
1164
+ limit: {type: ["null", "number"], default: null},
1165
+ status: {type: ["null", "string"], default: null},
1166
+ },
1167
+ callback: async (stream, params) => {
1168
+ const result = await this.get_refundable_payments({
1169
+ uid: stream.uid,
1170
+ days: params.days,
1171
+ limit: params.limit,
1172
+ })
1173
+ return stream.success({data: result});
1174
+ }
1175
+ },
1176
+
1177
+ // Get refunded payments.
1178
+ {
1179
+ method: "GET",
1180
+ endpoint: "/volt/payments/payments/refunded",
1181
+ content_type: "application/json",
1182
+ authenticated: true,
1183
+ rate_limit: "global",
1184
+ params: {
1185
+ days: {type: "number", default: 30},
1186
+ limit: {type: ["null", "number"], default: null},
1187
+ },
1188
+ callback: async (stream, params) => {
1189
+ const result = await this.get_refunded_payments({
1190
+ uid: stream.uid,
1191
+ days: params.days,
1192
+ limit: params.limit,
1193
+ })
1194
+ return stream.success({data: result});
1195
+ }
1196
+ },
1197
+
1198
+ // Get refunding payments.
1199
+ {
1200
+ method: "GET",
1201
+ endpoint: "/volt/payments/payments/refunding",
1202
+ content_type: "application/json",
1203
+ authenticated: true,
1204
+ rate_limit: "global",
1205
+ params: {
1206
+ days: {type: ["null", "number"], default: null},
1207
+ limit: {type: ["null", "number"], default: null},
1208
+ },
1209
+ callback: async (stream, params) => {
1210
+ const result = await this.get_refunding_payments({
1211
+ uid: stream.uid,
1212
+ days: params.days,
1213
+ limit: params.limit,
1214
+ })
1215
+ return stream.success({data: result});
1216
+ }
1217
+ },
1218
+
1219
+ // Create a refund.
1220
+ {
1221
+ method: "POST",
1222
+ endpoint: "/volt/payments/refund",
1223
+ content_type: "application/json",
1224
+ rate_limit: "global",
1225
+ params: {
1226
+ payment: {type: ["string", "object"]},
1227
+ line_items: {type: ["array", "null"], default: null},
1228
+ reason: {type: "string", default: "refund"},
1229
+ },
1230
+ callback: async (stream, params) => {
1231
+ await this.create_refund(params.payment, params.line_items, params.reason);
1232
+ return stream.success();
1233
+ }
1234
+ },
1235
+
1236
+ // Cancel a subscription.
1237
+ {
1238
+ method: "DELETE",
1239
+ endpoint: "/volt/payments/subscription",
1240
+ content_type: "application/json",
1241
+ authenticated: true,
1242
+ rate_limit: "global",
1243
+ params: {
1244
+ product: "string",
1245
+ },
1246
+ callback: async (stream, params) => {
1247
+ await this.cancel_subscription(stream.uid, params.product);
1248
+ return stream.success();
1249
+ }
1250
+ },
1251
+
1252
+ // Cancel a subscription by payment.
1253
+ // {
1254
+ // method: "DELETE",
1255
+ // endpoint: "/volt/payments/subscription_by_payment",
1256
+ // content_type: "application/json",
1257
+ // authenticated: true,
1258
+ // rate_limit: "global",
1259
+ // params: {
1260
+ // payment: {type: ["string", "object"]},
1261
+ // },
1262
+ // callback: async (stream, params) => {
1263
+ // await this.cancel_subscription_by_payment(params.payment);
1264
+ // return stream.success();
1265
+ // }
1266
+ // },
1267
+
1268
+ // Get active subscriptions.
1269
+ {
1270
+ method: "GET",
1271
+ endpoint: "/volt/payments/active_subscriptions",
1272
+ content_type: "application/json",
1273
+ authenticated: true,
1274
+ rate_limit: "global",
1275
+ callback: async (stream, params) => {
1276
+ return stream.success({
1277
+ data: {subscriptions: (await this.get_active_subscriptions(stream.uid))},
1278
+ });
1279
+ }
1280
+ },
1281
+
1282
+ // Is subscribed
1283
+ {
1284
+ method: "GET",
1285
+ endpoint: "/volt/payments/subscribed",
1286
+ content_type: "application/json",
1287
+ authenticated: true,
1288
+ rate_limit: "global",
1289
+ params: {
1290
+ product: "string",
1291
+ },
1292
+ callback: async (stream, params) => {
1293
+ return stream.success({
1294
+ data: {is_subscribed: (await this.is_subscribed(stream.uid, params.product))}
1295
+ });
1296
+ }
1297
+ },
1298
+
1299
+ // Webhook.
1300
+ this.server.offline ? null : (await this._create_webhook()),
1301
+
1302
+ );
1303
+ /* @performance */ now = this.performance.end("init-endpoints", now);
1304
+ // /* @performance */ this.performance.dump();
1305
+ }
1306
+
1307
+ // ---------------------------------------------------------
1308
+ // Webhook (private).
1309
+
1310
+ // Execute a webhook user defined callback.
1311
+ private async _exec_user_callback(callback: Function | null, args: any): Promise<void> {
1312
+ if (callback != null) {
1313
+ try {
1314
+ let res = callback(args);
1315
+ if (res instanceof Promise) { res = await res; }
1316
+ } catch (error) {
1317
+ console.error(error);
1318
+ }
1319
+ }
1320
+ }
1321
+
1322
+ // Send a payment mail.
1323
+ // async _send_payment_mail({payment, subject, attachments = [], mail}) {
1324
+ // await this.server.send_mail({
1325
+ // recipients: [payment.billing_details.name == null ? payment.billing_details.email : [payment.billing_details.name, payment.billing_details.email]],
1326
+ // subject,
1327
+ // body: mail.html(),
1328
+ // attachments,
1329
+ // })
1330
+ // }
1331
+
1332
+ // On a successfull payment webhook event.
1333
+ private async _payment_webhook(data: any): Promise<void> {
1334
+
1335
+ // ---------------------------------------------------------
1336
+ // Get the transaction.
1337
+ // Only used in the paid webhook, other parts use the saved payment object.
1338
+ // This is required to manage the statuses of payments.
1339
+
1340
+ // Make request.
1341
+ let obj = (await this._req("GET", `/transactions/${data.id}`, {include: ["address", "adjustment", "business", "customer"]})).data;
1342
+
1343
+ // Initialize.
1344
+ const id = `pay_${obj.custom_data.uid == null ? "unauth" : obj.custom_data.uid}_${String.random(4)}${Date.now()}`
1345
+ const payment: Payment = {
1346
+ id: id, // payment id.
1347
+ uid: obj.custom_data.uid, // user id,
1348
+ cus_id: obj.customer_id, // customer id.
1349
+ tran_id: obj.id, // transaction id.
1350
+ timestamp: Date.now(),
1351
+ status: "unknown", // payment status, possible values are "open" or "paid".
1352
+ line_items: [], // cart line items as {quantity: 1, product: "prod_xxx"}.
1353
+ billing_details: {
1354
+ name: undefined,
1355
+ email: undefined,
1356
+ business: undefined,
1357
+ vat_id: undefined,
1358
+ address: undefined,
1359
+ city: undefined,
1360
+ postal_code: undefined,
1361
+ province: undefined,
1362
+ country: undefined,
1363
+ tax_identifier: undefined,
1364
+ },
1365
+ }
1366
+
1367
+ // Set business details.
1368
+ if (obj.business != null) {
1369
+ const b = obj.business;
1370
+ if (b != null && b.name != null && b.name.length > 0) {
1371
+ payment.billing_details.business = b.name;
1372
+ }
1373
+ if (b.tax_identifier != null && b.tax_identifier.length > 0) {
1374
+ payment.billing_details.tax_identifier = b.tax_identifier;
1375
+ }
1376
+ if (b.contacts.length > 0) {
1377
+ const contact = b.contacts[0];
1378
+ payment.billing_details.name = contact.name;
1379
+ payment.billing_details.email = contact.email;
1380
+ }
1381
+ }
1382
+
1383
+ // Set email when not already set.
1384
+ if (payment.billing_details.email == null && obj.customer != null && obj.customer.email != null && obj.customer.email.length > 0) {
1385
+ payment.billing_details.email = obj.customer.email;
1386
+ }
1387
+
1388
+ // Set name when not already set.
1389
+ if (payment.billing_details.name == null && obj.custom_data.customer_name != null && obj.custom_data.customer_name != null && obj.custom_data.customer_name.length > 0) {
1390
+ payment.billing_details.name = obj.custom_data.customer_name;
1391
+ }
1392
+
1393
+ // Set address details.
1394
+ if (obj.address != null) {
1395
+ const a = obj.address;
1396
+ if (a.first_line != null && a.first_line.length > 0) {
1397
+ payment.billing_details.address = a.first_line;
1398
+ }
1399
+ if (a.city != null && a.city.length > 0) {
1400
+ payment.billing_details.city = a.city;
1401
+ }
1402
+ if (a.postal_code != null && a.postal_code.length > 0) {
1403
+ payment.billing_details.postal_code = a.postal_code;
1404
+ }
1405
+ if (a.region != null && a.region.length > 0) {
1406
+ payment.billing_details.province = a.region;
1407
+ }
1408
+ if (a.country_code != null && a.country_code.length > 0) {
1409
+ payment.billing_details.country = a.country_code;
1410
+ }
1411
+ }
1412
+
1413
+ // Set the status.
1414
+ switch (obj.status) {
1415
+ case "draft":
1416
+ case "ready":
1417
+ payment.status = "open"; break;
1418
+ case "billed":
1419
+ case "paid":
1420
+ case "completed":
1421
+ payment.status = "paid"; break;
1422
+ case "past_due":
1423
+ payment.status = "past_due"; break;
1424
+ default:
1425
+ logger.error(log_source, `Payment Webhook: Unknown payment status "${obj.status}".`);
1426
+ payment.status = "unknown";
1427
+ break;
1428
+ }
1429
+
1430
+ // Set line items.
1431
+ obj.details.line_items.iterate((item) => {
1432
+ payment.line_items.push({
1433
+ product: item.product.custom_data.id, // product id, keep as id since we do not want to save the product object to the database since this can change.
1434
+ item_id: item.id, // transaction item id.
1435
+ paddle_prod_id: item.product.id, // paddle product id.
1436
+ quantity: item.quantity,
1437
+ tax_rate: parseFloat(item.tax_rate as any),
1438
+ tax: parseFloat(item.totals.tax / 100 as any), // should not be changed to unit totals, since mails and invoices depend on this behaviour, just divide by quantity.
1439
+ discount: parseFloat(item.totals.discount / 100 as any), // should not be changed to unit totals, since mails and invoices depend on this behaviour, just divide by quantity.
1440
+ subtotal: parseFloat(item.totals.subtotal / 100 as any), // should not be changed to unit totals, since mails and invoices depend on this behaviour, just divide by quantity.
1441
+ total: parseFloat(item.totals.total / 100 as any), // should not be changed to unit totals, since mails and invoices depend on this behaviour, just divide by quantity.
1442
+ status: "paid", // can be "paid", "refunded", "refunding".
1443
+ })
1444
+ })
1445
+
1446
+ // Parse refunds.
1447
+ if (obj.adustments != null) {
1448
+ obj.adustments.iterate((adj) => {
1449
+ switch (adj.action) {
1450
+ case "refund":
1451
+ case "cargeback":
1452
+ case "cargeback_warning":
1453
+ adj.items.iterate((adj_item) => {
1454
+ payment.line_items.iterate((item) => {
1455
+ if (adj_item.item_id === item.item_id) {
1456
+ item.status = "refunded";
1457
+ return false;
1458
+ }
1459
+ })
1460
+ })
1461
+ break;
1462
+ default:
1463
+ break;
1464
+ }
1465
+ })
1466
+ }
1467
+
1468
+ // Save the payment object in the database.
1469
+ await this._save_payment(payment);
1470
+
1471
+ // ---------------------------------------------------------
1472
+ // Process the payment.
1473
+ const {uid, cus_id} = payment;
1474
+
1475
+ // Check the payment line items.
1476
+ await payment.line_items.iterate_async_await(async (item) => {
1477
+ const product = this.get_product_sync(item.product as string, false);
1478
+
1479
+ // @todo REFUND PAYMENT SINCE PRODUCT WAS NOT FOUND SO NO WAY OF DELIVERY.
1480
+ // Refund the payment since there is no way of delivery.
1481
+ // 1) Product not found.
1482
+ // 2) No subscription id found from the webhook data.
1483
+ if (product == null) {
1484
+ return null;
1485
+ }
1486
+
1487
+ // Subscription.
1488
+ else if (product.is_subscription) {
1489
+
1490
+ // No need to activate the sub, this is already handled by the sub activated webhook.
1491
+ //
1492
+
1493
+ // Cancel the other subscriptions plans that are part of this product.
1494
+ // The `create_payment()` function makes sure there are not multiple subscription plans of the same subscription product charged in a single request.
1495
+ const subscription = await this.get_product(product.subscription_id as string, true);
1496
+ await subscription.plans?.iterate_async_await(async (plan) => {
1497
+ if (plan.id != product.id) {
1498
+ const {exists, sub_id} = await this._check_subscription(uid, plan.id);
1499
+ if (exists) {
1500
+ logger.log(0, log_source, `Cancelling subscription "${plan.id}" due too downgrade/upgrade to "${product.id}" of user "${payment.uid}".`)
1501
+ // @todo cancel sub by sub id.
1502
+ await this._cancel_subscription(sub_id as string);
1503
+ }
1504
+ }
1505
+ })
1506
+
1507
+ // No need to execute the callback, this is already handled by the sub activated webhook.
1508
+ //
1509
+ }
1510
+
1511
+ // One time payment.
1512
+ else {
1513
+
1514
+ // Execute callback.
1515
+ await this._exec_user_callback(this.server.on_payment, {product, payment});
1516
+ }
1517
+ })
1518
+
1519
+ // Send an email to the user.
1520
+ // try {
1521
+ // await this._send_payment_mail({
1522
+ // payment,
1523
+ // subject: "Payment Successful",
1524
+ // mail: this.server.on_payment_mail({payment}),
1525
+ // attachments: [this.generate_invoice_sync(payment)],
1526
+ // });
1527
+ // } catch (error) {
1528
+ // console.error(error);
1529
+ // }
1530
+ }
1531
+
1532
+ // On subscription activated webhook event.
1533
+ // Even though the payment webhook could take care of this, still keep it seperated for customization, and possibly a new activation in certain scenerario's perhaps past due invoice, not sure just in case.
1534
+ private async _subscription_webhook(data: any): Promise<void> {
1535
+
1536
+ // Vars.
1537
+ const uid = data.custom_data.uid;
1538
+ const subscription: Subscription = {
1539
+ uid,
1540
+ id: data.id,
1541
+ cus_id: data.customer_id, // customer id.
1542
+ status: "active", // can be "active", "cancelling", "cancelled".
1543
+ plans: [],
1544
+ }
1545
+
1546
+ // Check the subscription line items.
1547
+ await data.items.iterate_async_await(async (item) => {
1548
+ const product = this._get_product_by_paddle_prod_id(item.price.product_id, false)
1549
+
1550
+ // Product not found or no sub id found, nothing to do here, the payment webhook already handles this scenario.
1551
+ if (product == null) {
1552
+ logger.error(log_source, `Subscription webhook [#sub1]: Unable to find product with id ${item.price.product_id}. This is a serious error which causes a non activated subscription for a paid transaction. You should manually cancel the subscription. Event: ${JSON.stringify(data, null, 4)}.`);
1553
+ return null;
1554
+ }
1555
+
1556
+ // Subscription.
1557
+ else if (product.is_subscription) {
1558
+
1559
+ // Add to plans.
1560
+ subscription.plans.append(product.id);
1561
+
1562
+ // Active the user's subscription in the database.
1563
+ logger.log(0, log_source, `Activating subscription "${product.id}" of user "${subscription.uid}".`)
1564
+ await this._add_subscription(uid, product.id, subscription.id);
1565
+
1566
+ // No need to cancel other subs, this is already handled by the payment webhook.
1567
+
1568
+ // Execute callback.
1569
+ await this._exec_user_callback(this.server.on_subscription, {product, subscription});
1570
+ }
1571
+ })
1572
+
1573
+ // Save subscription.
1574
+ await this._save_subscription(subscription);
1575
+
1576
+ // No need to send mail, payment webhook already handles this.
1577
+ }
1578
+
1579
+ // On a subscription cancelled webhook event.
1580
+ private async _subscription_cancelled_webhook(data: any): Promise<void> {
1581
+
1582
+ // Vars.
1583
+ const subscription = await this._load_subscription(data.id);
1584
+
1585
+ // Delete subscriptions made by this subscription.
1586
+ await subscription.plans.iterate_async_await(async (plan_id) => {
1587
+ await this._delete_subscription(subscription.uid, plan_id);
1588
+ logger.log(0, log_source, `Deactivating subscription "${plan_id}" of user "${subscription.uid}".`)
1589
+ })
1590
+
1591
+ // Update database.
1592
+ subscription.status = "cancelled";
1593
+ await this._save_subscription(subscription);
1594
+
1595
+ // Execute callback.
1596
+ await this._exec_user_callback(this.server.on_cancellation, {subscription});
1597
+
1598
+ // Send an email to the user.
1599
+ // try {
1600
+ // await this._send_payment_mail({
1601
+ // payment,
1602
+ // subject: "Cancellation Successful",
1603
+ // mail: this.server.on_cancellation_mail({payment, line_items}),
1604
+ // });
1605
+ // } catch (error) {
1606
+ // console.error(error);
1607
+ // }
1608
+ }
1609
+
1610
+ // On a adjustment (refunds) updated webhook event.
1611
+ private async _adjustment_webhook(data: any): Promise<void> {
1612
+
1613
+ // Refund or chageback.
1614
+ const is_refund = data.action === "refund";
1615
+ const is_chargeback = data.action === "chargeback";
1616
+ if (is_refund || is_chargeback) {
1617
+ if (data.status === "pending_approval") {
1618
+ return
1619
+ }
1620
+ const is_approved = data.status === "approved";
1621
+
1622
+ // Vars.
1623
+ const payment = await this._load_payment_by_transaction(data.transaction_id);
1624
+
1625
+ // Get and update line items.
1626
+ const line_items: LineItem[] = [], cancel_products: string[] = [];
1627
+ data.items.iterate((adj_item) => {
1628
+ payment.line_items.iterate((item) => {
1629
+ if (item.item_id === adj_item.item_id) {
1630
+ item.status = is_approved ? "refunded" : "paid";
1631
+ cancel_products.push(item.product as string);
1632
+ line_items.push(item);
1633
+ return false;
1634
+ }
1635
+ })
1636
+ })
1637
+
1638
+ // Manage subscriptions.
1639
+ if (payment.sub_id != null && is_approved) {
1640
+ await this._cancel_subscription(payment.sub_id, true);
1641
+ }
1642
+
1643
+ // Update database.
1644
+ if (line_items.length > 0) {
1645
+ await this._save_payment(payment);
1646
+ }
1647
+
1648
+ // Execute callback.
1649
+ if (is_approved) {
1650
+ logger.log(0, log_source, `Refunded items of payment "${payment.id}" of user "${payment.uid}".`)
1651
+ await this._exec_user_callback(
1652
+ is_refund ? this.server.on_refund : this.server.on_chargeback,
1653
+ {payment, line_items}
1654
+ );
1655
+ // try {
1656
+ // await this._send_payment_mail({
1657
+ // payment,
1658
+ // subject: "Successful " + (is_refund ? "Refund" : "Chargeback"),
1659
+ // mail: is_refund ? this.server.on_refund_mail({payment, line_items}) : this.server.on_chargeback_mail({payment, line_items}),
1660
+ // });
1661
+ // } catch (error) {
1662
+ // console.error(error);
1663
+ // }
1664
+ } else {
1665
+ logger.log(0, log_source, `Refund denied for items of payment ${payment.id} of user "${payment.uid}".`)
1666
+ await this._exec_user_callback(
1667
+ is_refund ? this.server.on_failed_refund : this.server.on_failed_chargeback,
1668
+ {payment, line_items}
1669
+ );
1670
+ // try {
1671
+ // await this._send_payment_mail({
1672
+ // payment,
1673
+ // subject: "Failed " + (is_refund ? "Refund" : "Chargeback"),
1674
+ // mail: is_refund ? this.server.on_failed_refund_mail({payment, line_items}) : this.server.on_failed_chargeback_mail({payment, line_items}),
1675
+ // });
1676
+ // } catch (error) {
1677
+ // console.error(error);
1678
+ // }
1679
+ }
1680
+ }
1681
+
1682
+ // Chargeback reversal.
1683
+ else if (data.action === "chargeback_reverse" && data.status === "reversed") {
1684
+
1685
+ // Vars.
1686
+ const payment = await this._load_payment_by_transaction(data.transaction_id);
1687
+
1688
+ // Log reactivation subscriptions on chargeback reverse.
1689
+ if (payment.sub_id != null) {
1690
+ logger.log(0, log_source, `Chargeback reversed for payment ${payment.id} from user "${payment.uid}".`)
1691
+ // @todo.
1692
+ }
1693
+
1694
+ // Get and update line items.
1695
+ let line_items: LineItem[] = [];
1696
+ data.items.iterate((adj_item) => {
1697
+ payment.line_items.iterate((item) => {
1698
+ if (item.item_id === adj_item.item_id) {
1699
+ item.status = "paid"
1700
+ line_items.push(item);
1701
+ }
1702
+ })
1703
+ })
1704
+
1705
+ // Update database.
1706
+ if (line_items.length > 0) {
1707
+ await this._save_payment(payment);
1708
+ }
1709
+ }
1710
+ }
1711
+
1712
+ // Create and register the webhook endpoint.
1713
+ private async _create_webhook(): Promise<{
1714
+ method: string;
1715
+ endpoint: string;
1716
+ rate_limit: boolean;
1717
+ callback: (stream: any) => Promise<void>;
1718
+ }> {
1719
+ const file_watcher_restart = process.argv.includes("--file-watcher-restart");
1720
+
1721
+ // Register the webhook.
1722
+ const webhook_doc = await this._settings_db.load(`webhook${this.server.production ? "" : "_demo"}`);
1723
+ const webhook_settings = {
1724
+ description: "volt webhook",
1725
+ destination: `${this.server.full_domain}/volt/payments/webhook`,
1726
+ type: "url",
1727
+ subscribed_events: [
1728
+ // "transaction.billed",
1729
+ // "transaction.canceled",
1730
+ // "transaction.completed",
1731
+ // "transaction.created",
1732
+ "transaction.paid",
1733
+ // "transaction.past_due",
1734
+ // "transaction.payment_failed",
1735
+ // "transaction.ready",
1736
+ // "transaction.updated",
1737
+ "subscription.activated",
1738
+ "subscription.canceled",
1739
+ // "subscription.created",
1740
+ // "subscription.imported",
1741
+ // "subscription.past_due",
1742
+ "subscription.paused",
1743
+ "subscription.resumed",
1744
+ "subscription.trialing",
1745
+ // "subscription.updated",
1746
+ "adjustment.updated",
1747
+ ],
1748
+ };
1749
+
1750
+ // Register webhook.
1751
+ const register_webhook = async () => {
1752
+ logger.log(0, log_source, "Registering payments webhook.");
1753
+ const response = await this._req("POST", "/notification-settings", webhook_settings)
1754
+ this.webhook_key = response.data.endpoint_secret_key;
1755
+ await this._settings_db.save(`webhook${this.server.production ? "" : "_demo"}`, {
1756
+ id: response.data.id,
1757
+ key: this.webhook_key,
1758
+ });
1759
+ }
1760
+
1761
+ // Webhook registered.
1762
+ if (webhook_doc != null) {
1763
+ this.webhook_key = webhook_doc.key;
1764
+
1765
+ // Check update required.
1766
+ const last_webhook = await this._settings_db.load(`last_webhook${this.server.production ? "" : "_demo"}`);
1767
+ if (last_webhook !== this.server.hash(webhook_settings) && file_watcher_restart === false) {
1768
+ logger.log(0, log_source, `Checking payments webhook.`);
1769
+
1770
+ // Check update required.
1771
+ const webhook_id = webhook_doc.id;
1772
+ let registered
1773
+ try {
1774
+ registered = await this._req("GET", `/notification-settings/${webhook_id}`);
1775
+ } catch (error: any) {
1776
+ if (error.status === 404 || error.status_code === 404) {
1777
+ registered = undefined;
1778
+ await register_webhook();
1779
+ } else {
1780
+ throw error;
1781
+ }
1782
+ }
1783
+ if (registered) {
1784
+ const item = registered.data;
1785
+ const patch = (() => {
1786
+ if (
1787
+ item.active !== true ||
1788
+ item.destination !== webhook_settings.destination ||
1789
+ item.type !== webhook_settings.type ||
1790
+ item.description !== webhook_settings.description ||
1791
+ item.subscribed_events.length != webhook_settings.subscribed_events.length
1792
+ ) {
1793
+ return true;
1794
+ }
1795
+ return webhook_settings.subscribed_events.iterate((x) => {
1796
+ const found = item.subscribed_events.iterate((y) => {
1797
+ if (x === y.name) {
1798
+ return true;
1799
+ }
1800
+ })
1801
+ if (found === false) {
1802
+ return true;
1803
+ }
1804
+ })
1805
+ })();
1806
+
1807
+ // Update.
1808
+ if (patch === true) {
1809
+ logger.log(0, log_source, "Updating payments webhook.");
1810
+ await this._req("PATCH", `/notification-settings/${webhook_id}`, {...webhook_settings, active: true});
1811
+ }
1812
+
1813
+ // Save.
1814
+ await this._settings_db.save(`last_webhook${this.server.production ? "" : "_demo"}`, this.server.hash(webhook_settings));
1815
+ }
1816
+ }
1817
+ }
1818
+
1819
+ // Register webhook.
1820
+ else {
1821
+ await register_webhook();
1822
+ }
1823
+
1824
+ // Ip whitelist.
1825
+ const ip_whitelist = [
1826
+ // Live.
1827
+ "34.232.58.13",
1828
+ "34.195.105.136",
1829
+ "34.237.3.244",
1830
+ "35.155.119.135",
1831
+ "52.11.166.252",
1832
+ "34.212.5.7",
1833
+ // Sandbox.
1834
+ "34.194.127.46",
1835
+ "54.234.237.108",
1836
+ "3.208.120.145",
1837
+ "44.226.236.210",
1838
+ "44.241.183.62",
1839
+ "100.20.172.113",
1840
+ ];
1841
+
1842
+ // Create the endpoint.
1843
+ return {
1844
+ method: "POST",
1845
+ endpoint: "/volt/payments/webhook",
1846
+ rate_limit: false,
1847
+ callback: async (stream) => {
1848
+
1849
+ // Ip whitelist.
1850
+ if (ip_whitelist.includes(stream.ip) === false) {
1851
+ logger.log(0, log_source, `POST:/volt/payments/webhook: Warning: Blocking non whitelisted ip "${stream.ip}".`);
1852
+ return stream.error({status: Status.unauthorized});
1853
+ }
1854
+
1855
+ // Verify.
1856
+ const full_signature = stream.headers["paddle-signature"];
1857
+ if (full_signature == null) {
1858
+ logger.log(0, log_source, "POST:/volt/payments/webhook: Error: No paddle signature found in the request headers.");
1859
+ return stream.error({status: Status.unauthorized, data: {error: "Webhook signature verification failed."}});
1860
+ }
1861
+ const ts_index = full_signature.indexOf(";");
1862
+ const ts = full_signature.substr(3, ts_index - 3);
1863
+ const signature = full_signature.substr(ts_index + 4);
1864
+ const digest = libcrypto.createHmac("sha256", this.webhook_key as string).update(`${ts}:${stream.body}`).digest("hex");
1865
+ if (libcrypto.timingSafeEqual(Buffer.from(digest, "hex"), Buffer.from(signature, "hex")) !== true) {
1866
+ logger.log(0, log_source, "POST:/volt/payments/webhook: Error: Webhook signature verification failed.");
1867
+ return stream.error({status: Status.unauthorized, data: {error: "Webhook signature verification failed."}});
1868
+ }
1869
+
1870
+ // Process items.
1871
+ const event = JSON.parse(stream.body);
1872
+ switch (event.event_type) {
1873
+
1874
+ // Paid transaction.
1875
+ // https://developer.paddle.com/webhooks/transactions/transaction-paid
1876
+ case "transaction.paid":
1877
+ await this._payment_webhook(event.data);
1878
+ break;
1879
+
1880
+ // Subscription activated.
1881
+ // https://developer.paddle.com/webhooks/subscriptions/subscription-activated
1882
+ case "subscription.activated":
1883
+ case "subscription.trialing":
1884
+ case "subscription.resumed":
1885
+ await this._subscription_webhook(event.data);
1886
+ break;
1887
+
1888
+ // Subscription canceled.
1889
+ // https://developer.paddle.com/webhooks/subscriptions/subscription-canceled
1890
+ case "subscription.canceled":
1891
+ case "subscription.paused":
1892
+ await this._subscription_cancelled_webhook(event.data);
1893
+ break;
1894
+
1895
+ // Adjustment updated (refunds).
1896
+ // https://developer.paddle.com/webhooks/subscriptions/subscription-canceled
1897
+ case "adjustment.updated":
1898
+ await this._adjustment_webhook(event.data);
1899
+ break;
1900
+
1901
+ // Default.
1902
+ default: break;
1903
+ }
1904
+
1905
+ // Success.
1906
+ stream.success({data: {message: "OK"}});
1907
+
1908
+ },
1909
+ };
1910
+ }
1911
+
1912
+ // ---------------------------------------------------------
1913
+ // Public.
1914
+
1915
+ // Get a product by id.
1916
+ // Use async to keep it persistent with other functions.
1917
+ /* @docs
1918
+ * @title: Get Product
1919
+ * @description:
1920
+ * Get a product by id.
1921
+ * @parameter:
1922
+ * @name: id
1923
+ * @description: The id of the product to retrieve. The id may either be the id of a product or a the id from a plan of a subscription product.
1924
+ * @type: string
1925
+ * @parameter:
1926
+ * @name: throw_err
1927
+ * @type: boolean
1928
+ * @description: Throw an error when the product was not found.
1929
+ * @type: object.
1930
+ * @return: Returns the product object.
1931
+ * @usage:
1932
+ * ...
1933
+ * const product = server.get_product("prod_basic");
1934
+ * @funcs: 8
1935
+ */
1936
+
1937
+ public async get_product(id: string): Promise<ProductObject | null>;
1938
+ public async get_product(id: string, throw_err: true): Promise<ProductObject>;
1939
+ public async get_product(id: string, throw_err: boolean): Promise<ProductObject | null>;
1940
+ public async get_product(id: string, throw_err: boolean = false): Promise<ProductObject | null> {
1941
+ return this.get_product_sync(
1942
+ id,
1943
+ throw_err
1944
+ );
1945
+ }
1946
+ public get_product_sync(id: string): ProductObject | null;
1947
+ public get_product_sync(id: string, throw_err: true): ProductObject;
1948
+ public get_product_sync(id: string, throw_err: boolean): ProductObject | null;
1949
+ public get_product_sync(id: string, throw_err: boolean = false): ProductObject | null {
1950
+ const product = this.products.iterate((p) => {
1951
+ if (p.is_subscription) {
1952
+ if (p.id === id) {
1953
+ return p;
1954
+ }
1955
+ return p.plans?.iterate((plan) => {
1956
+ if (plan.id === id) {
1957
+ return plan;
1958
+ }
1959
+ })
1960
+ }
1961
+ else if (p.id === id) {
1962
+ return p;
1963
+ }
1964
+ })
1965
+ if (product == null && throw_err) {
1966
+ throw Error(`Unable to find product "${id}".`);
1967
+ }
1968
+ return product;
1969
+ }
1970
+
1971
+ // Get a payment by id.
1972
+ /* @docs:
1973
+ @title: Get Payment.
1974
+ @desc: Get a payment by id.
1975
+ @param:
1976
+ @name: id
1977
+ @required: true
1978
+ @type: string
1979
+ @desc: The id of the payment.
1980
+ */
1981
+ public async get_payment(id: string): Promise<Payment> {
1982
+ return await this._load_payment(id);
1983
+ }
1984
+
1985
+ // Get payments.
1986
+ /* @docs:
1987
+ @title: Get Refunded Payments.
1988
+ @desc:
1989
+ Get all payments.
1990
+
1991
+ All failed payments are no longer stored in the database.
1992
+ @param:
1993
+ @name: uid
1994
+ @cached: Users:uid:param
1995
+ @param:
1996
+ @name: days
1997
+ @type: number
1998
+ @desc: Retrieve payments from the last amount of days.
1999
+ @param:
2000
+ @name: limit
2001
+ @type: number
2002
+ @desc: Limit the amount of response payment objects.
2003
+ @param:
2004
+ @name: status
2005
+ @type: string, array[string]
2006
+ @desc: Filter the payments by status. Be aware that the line items of a payment also have a status with possible values of `open`, `cancelled`, `refunding` or `refunded.`
2007
+ @enum:
2008
+ @value: "open"
2009
+ @desc: Payments that are still open and unpaid.
2010
+ @enum:
2011
+ @value: "paid"
2012
+ @desc: Payments that are paid.
2013
+ */
2014
+ public async get_payments({
2015
+ uid,
2016
+ days = 30,
2017
+ limit = undefined,
2018
+ status = undefined,
2019
+ }: {
2020
+ uid: string;
2021
+ days?: number;
2022
+ limit?: number;
2023
+ status?: string | string[];
2024
+ }): Promise<Payment[]> {
2025
+
2026
+ // Get path.
2027
+ const list: Payment[] = await this._pay_db.list_query({_uid: uid});
2028
+
2029
+ // Get the since time.
2030
+ let since: any = null;
2031
+ if (days != null) {
2032
+ since = new Date();
2033
+ since.setHours(0, 0, 0, 0)
2034
+ since = Math.floor(since.getTime() - (3600 * 24 * 1000 * days));
2035
+ }
2036
+
2037
+ // Iterate list.
2038
+ const payments: Payment[] = [];
2039
+ const status_is_array = Array.isArray(status);
2040
+ list.iterate((payment) => {
2041
+ if ((since == null || payment.timestamp >= since)) {
2042
+ if (
2043
+ status == null ||
2044
+ (status_is_array === false && status === payment.status) ||
2045
+ (status_is_array && status.includes(payment.status))
2046
+ ) {
2047
+ payments.append(payment);
2048
+ }
2049
+ }
2050
+ if (limit != null && limit != -1 && payments.length >= limit) {
2051
+ return false;
2052
+ }
2053
+ })
2054
+
2055
+ // Sort.
2056
+ payments.sort((a, b) => b.timestamp - a.timestamp);
2057
+
2058
+ // Response.
2059
+ return payments;
2060
+ }
2061
+
2062
+ // Get all refundable payments.
2063
+ /* @docs:
2064
+ @title: Get Refundable Payments.
2065
+ @desc: Get all payments that are refundable.
2066
+ @param:
2067
+ @name: uid
2068
+ @cached: Users:uid:param
2069
+ @param:
2070
+ @name: days
2071
+ @type: number
2072
+ @desc: Retrieve payments from the last amount of days.
2073
+ @param:
2074
+ @name: limit
2075
+ @type: number
2076
+ @desc: Limit the amount of response payment objects.
2077
+ */
2078
+ public async get_refundable_payments({
2079
+ uid,
2080
+ days = 30,
2081
+ limit = undefined,
2082
+ }: {
2083
+ uid: string;
2084
+ days?: number;
2085
+ limit?: number;
2086
+ }): Promise<Payment[]> {
2087
+ const payments: Payment[] = [];
2088
+ const all_payments = await this.get_payments({uid, days, limit, status: "paid"});
2089
+ all_payments.iterate((payment) => {
2090
+ const line_items: LineItem[] = [];
2091
+ payment.line_items.iterate((item) => {
2092
+ if (item.status === "paid" && item.total > 0) { // skip total 0 for free trial.
2093
+ line_items.push(item);
2094
+ }
2095
+ })
2096
+ if (line_items.length > 0) {
2097
+ payment.line_items = line_items;
2098
+ payments.push(payment);
2099
+ }
2100
+ })
2101
+ return payments;
2102
+ }
2103
+
2104
+ // Get all refunded payments.
2105
+ /* @docs:
2106
+ @title: Get Refunded Payments.
2107
+ @desc: Get all payments that are successfully refunded.
2108
+ @param:
2109
+ @name: uid
2110
+ @cached: Users:uid:param
2111
+ @param:
2112
+ @name: days
2113
+ @type: number
2114
+ @desc: Retrieve payments from the last amount of days.
2115
+ @param:
2116
+ @name: limit
2117
+ @type: number
2118
+ @desc: Limit the amount of response payment objects.
2119
+ */
2120
+ public async get_refunded_payments({
2121
+ uid,
2122
+ days = 30,
2123
+ limit = undefined,
2124
+ }: {
2125
+ uid: string;
2126
+ days?: number;
2127
+ limit?: number;
2128
+ }): Promise<Payment[]> {
2129
+ const payments: Payment[] = [];
2130
+ const all_payments = await this.get_payments({uid, days, limit, status: "paid"});
2131
+ all_payments.iterate((payment) => {
2132
+ const line_items: LineItem[] = [];
2133
+ payment.line_items.iterate((item) => {
2134
+ if (item.status === "refunded") {
2135
+ line_items.push(item);
2136
+ }
2137
+ })
2138
+ if (line_items.length > 0) {
2139
+ payment.line_items = line_items;
2140
+ payments.push(payment);
2141
+ }
2142
+ })
2143
+ return payments;
2144
+ }
2145
+
2146
+ // Get all payments that are currently in the refunding process.
2147
+ /* @docs:
2148
+ @title: Get Refunding Payments.
2149
+ @desc: Get all payments that are currently in the refunding process.
2150
+ @param:
2151
+ @name: uid
2152
+ @cached: Users:uid:param
2153
+ @param:
2154
+ @name: days
2155
+ @type: number
2156
+ @desc: Retrieve payments from the last amount of days.
2157
+ @param:
2158
+ @name: limit
2159
+ @type: number
2160
+ @desc: Limit the amount of response payment objects.
2161
+ */
2162
+ public async get_refunding_payments({
2163
+ uid,
2164
+ days = undefined,
2165
+ limit = undefined,
2166
+ }: {
2167
+ uid: string;
2168
+ days?: number;
2169
+ limit?: number;
2170
+ }): Promise<Payment[]> {
2171
+ const payments: Payment[] = [];
2172
+ const all_payments = await this.get_payments({uid, days, limit, status: "paid"});
2173
+ all_payments.iterate((payment) => {
2174
+ const line_items: LineItem[] = [];
2175
+ payment.line_items.iterate((item) => {
2176
+ if (item.status === "refunding") {
2177
+ line_items.push(item);
2178
+ }
2179
+ })
2180
+ if (line_items.length > 0) {
2181
+ payment.line_items = line_items;
2182
+ payments.push(payment);
2183
+ }
2184
+ })
2185
+ return payments;
2186
+ }
2187
+
2188
+ // Refund a payment.
2189
+ /* @docs:
2190
+ @title: Refund Payment.
2191
+ @desc: Refund a payment based on the payment id.
2192
+ @warning: Refunding a subscription will also cancel all other subscriptions that were created by the same payment request.
2193
+ @param:
2194
+ @name: payment
2195
+ @required: true
2196
+ @type: number
2197
+ @desc: The id of the payment object or the payment object itself.
2198
+ @param:
2199
+ @name: line_items
2200
+ @type: array[object]
2201
+ @desc: The line items to refund, these must be retrieved from the original payment line items otherwise it may cause undefined behaviour. When undefined the entire payment will be refunded.
2202
+ @param:
2203
+ @name: reason
2204
+ @type: string
2205
+ @desc: The refund reason for internal analytics.
2206
+ */
2207
+ public async create_refund(
2208
+ payment: Payment | string,
2209
+ line_items: LineItem[] | undefined = undefined,
2210
+ reason: string = "refund"
2211
+ ): Promise<void> {
2212
+
2213
+ // Load payment.
2214
+ // The payment must be loaded from the database in case the line items or anything were edited by the user, such as dropping all non refundable line items.
2215
+ if (typeof payment === "string") {
2216
+ payment = await this._load_payment(payment);
2217
+ } else {
2218
+ payment = await this._load_payment(payment.id);
2219
+ }
2220
+
2221
+ // When no line items are defined than refund everything.
2222
+ if (line_items == null) {
2223
+ line_items = payment.line_items;
2224
+ }
2225
+
2226
+ // Check empty line items.
2227
+ if (line_items.length === 0) {
2228
+ throw Error("No refund line items array is empty.")
2229
+ }
2230
+
2231
+ // Parse line items.
2232
+ const items: {item_id: string, type: string}[] = [];
2233
+ const item_ids: string[] = [];
2234
+ line_items.iterate((item) => {
2235
+
2236
+ // Skip when the item is already being refunded.
2237
+ if (item.status === "refunded" || item.status === "refunding") { // || item.status === "cancelled" || item.status === "cancelling"
2238
+ return null;
2239
+ }
2240
+
2241
+ // Add to structured line items.
2242
+ item_ids.push(item.item_id);
2243
+ items.push({
2244
+ item_id: item.item_id,
2245
+ type: "full", // partial refudings are not supported per line item since there is no convenient way to keep track of how much is refunded.
2246
+ })
2247
+ })
2248
+
2249
+ // Check empty line items.
2250
+ if (items.length === 0) {
2251
+ throw Error("This payment no longer has any refundable line items.")
2252
+ }
2253
+
2254
+ // Make request.
2255
+ const response = await this._req("POST", `/adjustments`, {
2256
+ action: "refund",
2257
+ transaction_id: payment.tran_id,
2258
+ reason,
2259
+ items,
2260
+ custom_data: {
2261
+ uid: payment.uid,
2262
+ }
2263
+ });
2264
+ if (response.data.status === "rejected") {
2265
+ throw Error("This payment is no longer refundable.");
2266
+ } else if (response.data.status === "approved") {
2267
+ payment.line_items.iterate((item) => {
2268
+ if (line_items.find((i) => i.item_id === item.item_id)) {
2269
+ item.status = "refunded";
2270
+ }
2271
+ })
2272
+ } else {
2273
+ payment.line_items.iterate((item) => {
2274
+ if (line_items.find((i) => i.item_id === item.item_id)) {
2275
+ item.status = "refunding";
2276
+ }
2277
+ })
2278
+ }
2279
+
2280
+ // Update the payment object.
2281
+ await this._save_payment(payment);
2282
+ }
2283
+
2284
+ // Cancel a subscription.
2285
+ /* @docs:
2286
+ @title: Cancel Subscription.
2287
+ @desc: Cancel a subscription based on the retrieved payment object or id.
2288
+ @warning: Cancelling a subscription will also cancel all other subscriptions that were created by the same payment request.
2289
+ @param:
2290
+ @name: uid
2291
+ @cached: Users:uid:param
2292
+ @param:
2293
+ @name: products
2294
+ @required: true
2295
+ @type: string, array[string, object]
2296
+ @desc: The product to cancel, the product ids to cancel or the product objects to cancel.
2297
+ */
2298
+ public async cancel_subscription(
2299
+ uid: string,
2300
+ products: string | (string | ProductObject)[],
2301
+ _throw_no_cancelled_err: boolean = true
2302
+ ): Promise<void> {
2303
+ if (products == null) {
2304
+ throw new Error("Parameter \"products\" should be a defined value of type \"array[string, object]\".");
2305
+ }
2306
+ if (typeof products === "string") {
2307
+ products = [products];
2308
+ }
2309
+ let cancelled: string[] = [];
2310
+ await products.iterate_async_await(async (product) => {
2311
+ if (typeof product === "object") {
2312
+ product = product.id;
2313
+ }
2314
+ const {exists, sub_id} = await this._check_subscription(uid, product);
2315
+ if (exists && cancelled.includes(sub_id as string) === false) {
2316
+ await this._cancel_subscription(sub_id as string);
2317
+ cancelled.push(sub_id as string)
2318
+ }
2319
+ });
2320
+ if (_throw_no_cancelled_err && cancelled.length === 0) {
2321
+ throw new FrontendError("No cancellable subscriptions found.", Status.bad_request);
2322
+ }
2323
+ }
2324
+
2325
+ // Cancel subscription by subscription id.
2326
+ /* @docs:
2327
+ @title: Cancel subscription by subscription id.
2328
+ @desc: Cancel a subscription based on the retrieved subscription object or id.
2329
+ @warning: Cancelling a subscription will also cancel all other subscriptions that were created by the same payment request.
2330
+ @param:
2331
+ @name: subscription
2332
+ @required: true
2333
+ @type: string, object
2334
+ @desc: The retrieved subscription object or the subscription's id.
2335
+ @param:
2336
+ @name: immediate
2337
+ @type: boolean
2338
+ @desc: Immediately cancel the subscription, or wait till the end of the billing cycle.
2339
+ */
2340
+ public async cancel_subscription_by_id(
2341
+ subscription: string | Subscription,
2342
+ immediate: boolean = false
2343
+ ): Promise<void> {
2344
+ if (typeof subscription === "object") {
2345
+ subscription = subscription.id;
2346
+ }
2347
+ return await this._cancel_subscription(subscription, immediate);
2348
+ }
2349
+
2350
+ // Get subscriptioms.
2351
+ /* @docs:
2352
+ @title: Get active subscriptions
2353
+ @desc: Get the active subscriptions of a user.
2354
+ @param:
2355
+ @name: uid
2356
+ @cached: Users:uid:param
2357
+ */
2358
+ public async get_active_subscriptions(uid: string): Promise<string[]> {
2359
+ return await this._get_active_subscriptions(uid);
2360
+ }
2361
+ /* @docs:
2362
+ @title: Get all subscriptions
2363
+ @desc: Get all subscriptions of a user, active and inactive.
2364
+ @param:
2365
+ @name: uid
2366
+ @cached: Users:uid:param
2367
+ */
2368
+ public async get_subscriptions(uid: string): Promise<Subscription[]> {
2369
+ return await this._get_subscriptions(uid);
2370
+ }
2371
+
2372
+ // Is subscribed.
2373
+ /* @docs:
2374
+ @title: Is Subscribed
2375
+ @desc: Check if a user is subscribed to a product.
2376
+ @param:
2377
+ @name: uid
2378
+ @cached: Users:uid:param
2379
+ @param:
2380
+ @name: product
2381
+ @required: true
2382
+ @type: string
2383
+ @desc: The product id.
2384
+ */
2385
+ public async is_subscribed(uid: string, product: string): Promise<boolean> {
2386
+ return await this._check_subscription(uid, product, false);
2387
+ }
2388
+
2389
+ // Generate an invoice.
2390
+ /* @docs:
2391
+ @title: Generate Invoice
2392
+ @desc:
2393
+ Generate an invoice for a paid payment.
2394
+
2395
+ By default an invoice is already generated when a payment has been paid.
2396
+ @param:
2397
+ @name: payment
2398
+ @required: true
2399
+ @type: object
2400
+ @desc: The payment object.
2401
+ @return:
2402
+ @type: Promise
2403
+ @desc: This function returns a promise to the invoice pdf in bytes.
2404
+ */
2405
+ public async generate_invoice(payment: Payment | ExpandedPayment): Promise<Buffer> {
2406
+
2407
+ // Check arg..
2408
+ if (payment == null || typeof payment !== "object") {
2409
+ throw Error(`Parameter "payment" should be a defined value of type "object".`);
2410
+ }
2411
+
2412
+ // Vars.
2413
+ let currency!: string;
2414
+ let subtotal = 0;
2415
+ let subtotal_tax = 0;
2416
+ let total = 0;
2417
+ payment.line_items.iterate((item) => {
2418
+ if (typeof item.product === "string") {
2419
+ item.product = this.get_product_sync(item.product, true);
2420
+ }
2421
+ if (currency == null) {
2422
+ const c = Utils.get_currency_symbol((item.product as ProductObject).currency);
2423
+ if (c == null) {
2424
+ throw new Error(`Unable to determine the currency symbol for "${(item.product as ProductObject).currency}".`);
2425
+ }
2426
+ currency = c as string;
2427
+ }
2428
+ subtotal += item.subtotal;
2429
+ subtotal_tax += item.tax;
2430
+ total += item.total;
2431
+ })
2432
+ let total_due = payment.status === "open" ? total : 0;
2433
+ let doc = new PDFDocument({ size: "A4", margin: 50 });
2434
+ let expanded_payment = payment as ExpandedPayment;
2435
+
2436
+ /* Doc vars. */
2437
+ let top_offset = 57;
2438
+ let spacing = 10;
2439
+
2440
+ // Wrapper func.
2441
+ const gen_text = (text: string, x: number, y: number | null = null, opts: any = null, _spacing: any = null) => {
2442
+ if (y == null) {
2443
+ y = top_offset;
2444
+ } else {
2445
+ top_offset = y;
2446
+ }
2447
+ if (_spacing == null) {
2448
+ _spacing = spacing;
2449
+ }
2450
+ doc.text(text, x, y, opts);
2451
+ top_offset += doc.heightOfString(text, x, y, opts) + (_spacing == null ? spacing : _spacing);
2452
+ }
2453
+ const gen_col_text = (text: string, x: number, opts: any = null, is_last: any = false, _spacing: any = 2) => {
2454
+ doc.text(text, x, top_offset, opts);
2455
+ if (is_last) {
2456
+ top_offset += doc.heightOfString(text, x, top_offset, opts) + (_spacing == null ? spacing : _spacing);
2457
+ } else {
2458
+ return doc.heightOfString(text, x, top_offset, opts);
2459
+ }
2460
+ }
2461
+ const gen_divider = (_spacing: any = null) => {
2462
+ doc
2463
+ .strokeColor("#aaaaaa")
2464
+ .lineWidth(1)
2465
+ .moveTo(50, top_offset)
2466
+ .lineTo(550, top_offset)
2467
+ .stroke();
2468
+ top_offset += 1 + (_spacing == null ? spacing : _spacing);
2469
+ }
2470
+ const gen_line_item = ({name = "", desc = "", unit_cost = "", quantity = "", total_cost = ""}) => {
2471
+ const items: [number, string][] = [
2472
+ [0.25, name],
2473
+ [0.35, desc],
2474
+ [0.4 / 3, unit_cost],
2475
+ [0.4 / 3, quantity],
2476
+ [0.4 / 3, total_cost],
2477
+ ];
2478
+
2479
+ let x = 50;
2480
+ let max_height = 0;
2481
+ const full_width = (550 - 50) - (10 * 4);
2482
+
2483
+ // Get max height.
2484
+ items.iterate((item) => {
2485
+ max_height = Math.max(max_height, doc.heightOfString(item[1], x, top_offset, { width: full_width * item[0] as number, align: "left" }));
2486
+ x += (full_width * item[0]) + 10;
2487
+ })
2488
+
2489
+ // Check if a new page should be added.
2490
+ if (top_offset + max_height + 10 > doc.page.height - 50) {
2491
+ doc.addPage();
2492
+ top_offset = 50;
2493
+ }
2494
+
2495
+ // Add items.
2496
+ x = 50;
2497
+ items.iterate((item) => {
2498
+ gen_col_text(item[1], x, { width: full_width * item[0], align: "left" })
2499
+ x += (full_width * item[0]) + 10;
2500
+ })
2501
+
2502
+ // Add top offset.
2503
+ top_offset += max_height + spacing;
2504
+ }
2505
+ const format_date = (date: Date) => {
2506
+ return `${date.getDate()}/${date.getMonth() + 1}/${date.getFullYear()}`;
2507
+ }
2508
+
2509
+ // Header.
2510
+ doc.fillColor("#444444")
2511
+ doc.fontSize(20)
2512
+ if (this.server.company.stroke_icon_path != null) {
2513
+ doc.image(this.server.company.stroke_icon_path, 50, top_offset - 2, { width: 60 })
2514
+ } else {
2515
+ if (this.server.company.icon_path != null) {
2516
+ doc.image(this.server.company.icon_path, 50, top_offset - 2, { width: 18 })
2517
+ }
2518
+ gen_text(this.server.company.legal_name, 50 + 18 + 10)
2519
+ }
2520
+ top_offset += 15;
2521
+
2522
+ // From (left).
2523
+ const start_top_offset = top_offset;
2524
+ doc.fillColor("#444444")
2525
+ doc.fontSize(10)
2526
+ doc.font("Helvetica-Bold")
2527
+ gen_text("From", 50, null, null, 3);
2528
+ doc.font("Helvetica")
2529
+ gen_text(this.server.company.legal_name, 50, null, { align: "left" }, 2)
2530
+ gen_text(`${this.server.company.address}, ${this.server.company.postal_code}`, 50, null, { align: "left" }, 2)
2531
+ gen_text(`${this.server.company.city}, ${this.server.company.province}, ${this.server.company.country}`, 50, null, { align: "left" }, 2)
2532
+ gen_text(`VAT ID: ${this.server.company.tax_id}`, 50, null, { align: "left" }, 2)
2533
+ const left_top_offset = top_offset;
2534
+
2535
+ // Invoice details (right).
2536
+ top_offset = start_top_offset
2537
+ doc.fillColor("#444444")
2538
+ doc.fontSize(10)
2539
+ doc.font("Helvetica-Bold")
2540
+ gen_text("Invoice details", 550 - (150 + 10 + 80), null, null, 3);
2541
+ doc.font("Helvetica");
2542
+ [
2543
+ ["Invoice:", expanded_payment.id],
2544
+ ["Date of issue:", format_date(new Date())],
2545
+ ].iterate((item) => {
2546
+ gen_col_text(item[0], 550 - (150 + 10 + 80), {width: 80})
2547
+ gen_col_text(item[1], 550 - 150, {width: 150}, true)
2548
+ })
2549
+
2550
+ // Go down.
2551
+ top_offset = Math.max(top_offset, left_top_offset) + 25;
2552
+
2553
+ // Billing details.
2554
+ doc.fillColor("#444444")
2555
+ doc.fontSize(10)
2556
+ doc.font("Helvetica-Bold")
2557
+ gen_text("Billing Details", 50, null, null, 3);
2558
+ doc.font("Helvetica")
2559
+ if (expanded_payment.billing_details.business != null) {
2560
+ gen_text(`${expanded_payment.billing_details.business}`, 50, null, { align: "left" }, 2);
2561
+ } else {
2562
+ gen_text(`${expanded_payment.billing_details.name}`, 50, null, { align: "left" }, 2);
2563
+ }
2564
+ gen_text(expanded_payment.billing_details.email!, 50, null, { align: "left" }, 2);
2565
+ gen_text(`${expanded_payment.billing_details.address}`, 50, null, { align: "left" }, 2);
2566
+ gen_text(`${expanded_payment.billing_details.city}, ${expanded_payment.billing_details.province}, ${expanded_payment.billing_details.country}`, 50, null, { align: "left" }, 2);
2567
+ if (expanded_payment.billing_details.vat_id != null) {
2568
+ gen_text(`${expanded_payment.billing_details.vat_id}`, 50, null, { align: "left" }, 2);
2569
+ }
2570
+
2571
+ // Go down.
2572
+ top_offset += 35;
2573
+
2574
+ // Line items.
2575
+ doc.font("Helvetica-Bold");
2576
+ gen_line_item({
2577
+ name: "Item",
2578
+ desc: "Description",
2579
+ unit_cost: "Unit Cost",
2580
+ quantity: "Quantity",
2581
+ total_cost: "Line Total",
2582
+ });
2583
+ top_offset -= spacing * 0.5;
2584
+ doc.font("Helvetica");
2585
+ gen_divider();
2586
+ expanded_payment.line_items.iterate((item) => {
2587
+ gen_line_item({
2588
+ name: item.product.name,
2589
+ desc: item.product.description,
2590
+ unit_cost: `${currency} ${(item.subtotal / item.quantity).toFixed(2)}`,
2591
+ quantity: item.quantity.toString(),
2592
+ total_cost: `${currency} ${item.total.toFixed(2)}`,
2593
+ });
2594
+ top_offset += 10;
2595
+ gen_divider();
2596
+ });
2597
+ gen_line_item({unit_cost: "Subtotal:", total_cost: `${currency} ${subtotal.toFixed(2)}`});
2598
+ top_offset -= (spacing - 3);
2599
+ gen_line_item({unit_cost: "Taxes:", total_cost: `${currency} ${subtotal_tax.toFixed(2)}`});
2600
+ top_offset -= (spacing - 3);
2601
+ gen_line_item({unit_cost: "Total:", total_cost: `${currency} ${total.toFixed(2)}`});
2602
+ top_offset -= (spacing - 3);
2603
+ doc.font("Helvetica-Bold");
2604
+ gen_line_item({unit_cost: "Total Due:", total_cost: `${currency} ${total_due.toFixed(2)}`});
2605
+ top_offset -= (spacing - 3);
2606
+
2607
+ // Write to file.
2608
+ // doc.end();
2609
+ // doc.pipe(fs.createWriteStream(path.str()));
2610
+ // return path;
2611
+
2612
+ // Get as bytes.
2613
+ const stream = doc.pipe(blobstream());
2614
+ doc.end();
2615
+ return new Promise((resolve, reject) => {
2616
+ stream.on('finish', () => {
2617
+ const bytes = stream.toBuffer();
2618
+ resolve(bytes);
2619
+ });
2620
+ stream.on('error', (error) => {
2621
+ reject(error);
2622
+ });
2623
+ });
2624
+ }
2625
+
2626
+ // ---------------------------------------------------------
2627
+ // Development.
2628
+
2629
+ // Cancel all subscriptions to clear development environment.
2630
+ public async dev_cancel_all_subscriptions(): Promise<void> {
2631
+
2632
+ if (!this.sandbox) {
2633
+ throw new Error("This function is only for a sandbox environment.");
2634
+ }
2635
+
2636
+ // Fetch.
2637
+ let subs: any[] = [], after: string | null = null;
2638
+ while (true) {
2639
+ const response = await this._req("GET", "/subscriptions", after == null ? {per_page: 100} : {per_page: 100, after});
2640
+ subs = subs.concat(response.data);
2641
+ if (!response.meta.has_more) {
2642
+ break;
2643
+ }
2644
+ after = subs.last().id;
2645
+ }
2646
+
2647
+ // Cancel.
2648
+ await subs.iterate_async_await(async (sub: any) => {
2649
+ if (sub.status === "active") {
2650
+ console.log("Cancelling subscription", sub.id);
2651
+ await this._req("POST", `/subscriptions/${sub.id}/cancel`, {
2652
+ effective_from: "immediately",
2653
+ });
2654
+ }
2655
+ })
2656
+ }
2657
+
2658
+ }
2659
+ export default Paddle;