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