Browse Source

Merge pull request #114 from vernu/improve-billing

prevent duplicate checkout sessions per user
pull/117/head
Israel Abebe 7 months ago
committed by GitHub
parent
commit
7a24c0cd2b
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      api/package.json
  2. 107
      api/pnpm-lock.yaml
  3. 2
      api/src/billing/billing.module.ts
  4. 51
      api/src/billing/billing.service.ts
  5. 27
      api/src/billing/schemas/checkout-session.schema.ts

2
api/package.json

@ -33,7 +33,7 @@
"@nestjs/schedule": "^4.1.1", "@nestjs/schedule": "^4.1.1",
"@nestjs/swagger": "^7.4.2", "@nestjs/swagger": "^7.4.2",
"@nestjs/throttler": "^6.2.1", "@nestjs/throttler": "^6.2.1",
"@polar-sh/sdk": "^0.32.3",
"@polar-sh/sdk": "^0.34.9",
"axios": "^1.8.2", "axios": "^1.8.2",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"bull": "^4.16.5", "bull": "^4.16.5",

107
api/pnpm-lock.yaml

@ -48,8 +48,8 @@ importers:
specifier: ^6.2.1 specifier: ^6.2.1
version: 6.2.1(@nestjs/common@10.4.5(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.5)(reflect-metadata@0.2.2) version: 6.2.1(@nestjs/common@10.4.5(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.5)(reflect-metadata@0.2.2)
'@polar-sh/sdk': '@polar-sh/sdk':
specifier: ^0.32.3
version: 0.32.3(zod@3.24.1)
specifier: ^0.34.9
version: 0.34.9
axios: axios:
specifier: ^1.8.2 specifier: ^1.8.2
version: 1.8.2 version: 1.8.2
@ -134,19 +134,19 @@ importers:
version: 6.0.2 version: 6.0.2
'@typescript-eslint/eslint-plugin': '@typescript-eslint/eslint-plugin':
specifier: ^8.10.0 specifier: ^8.10.0
version: 8.10.0(@typescript-eslint/parser@8.10.0(eslint@9.13.0)(typescript@5.6.3))(eslint@9.13.0)(typescript@5.6.3)
version: 8.10.0(@typescript-eslint/parser@8.10.0(eslint@9.13.0(jiti@2.4.2))(typescript@5.6.3))(eslint@9.13.0(jiti@2.4.2))(typescript@5.6.3)
'@typescript-eslint/parser': '@typescript-eslint/parser':
specifier: ^8.10.0 specifier: ^8.10.0
version: 8.10.0(eslint@9.13.0)(typescript@5.6.3)
version: 8.10.0(eslint@9.13.0(jiti@2.4.2))(typescript@5.6.3)
eslint: eslint:
specifier: ^9.13.0 specifier: ^9.13.0
version: 9.13.0
version: 9.13.0(jiti@2.4.2)
eslint-config-prettier: eslint-config-prettier:
specifier: ^9.1.0 specifier: ^9.1.0
version: 9.1.0(eslint@9.13.0)
version: 9.1.0(eslint@9.13.0(jiti@2.4.2))
eslint-plugin-prettier: eslint-plugin-prettier:
specifier: ^5.2.1 specifier: ^5.2.1
version: 5.2.1(@types/eslint@9.6.1)(eslint-config-prettier@9.1.0(eslint@9.13.0))(eslint@9.13.0)(prettier@3.3.3)
version: 5.2.1(@types/eslint@9.6.1)(eslint-config-prettier@9.1.0(eslint@9.13.0(jiti@2.4.2)))(eslint@9.13.0(jiti@2.4.2))(prettier@3.3.3)
jest: jest:
specifier: ^29.7.0 specifier: ^29.7.0
version: 29.7.0(@types/node@22.7.7)(ts-node@10.9.2(@types/node@22.7.7)(typescript@5.6.3)) version: 29.7.0(@types/node@22.7.7)(ts-node@10.9.2(@types/node@22.7.7)(typescript@5.6.3))
@ -317,6 +317,10 @@ packages:
resolution: {integrity: sha512-gYq0xCsqFfQaSL/yT1Gl1vIUjtsg7d7RhnUfsXaHt8xTxOKRTdH9GjbesBjXOzgOvB0W0vfssfreSNGFlOOMJg==} resolution: {integrity: sha512-gYq0xCsqFfQaSL/yT1Gl1vIUjtsg7d7RhnUfsXaHt8xTxOKRTdH9GjbesBjXOzgOvB0W0vfssfreSNGFlOOMJg==}
engines: {node: '>=16.0.0'} engines: {node: '>=16.0.0'}
'@aws-sdk/types@3.840.0':
resolution: {integrity: sha512-xliuHaUFZxEx1NSXeLLZ9Dyu6+EJVQKEoD+yM+zqUo3YDZ7medKJWY6fIOKiPX/N7XbLdBYwajb15Q7IL8KkeA==}
engines: {node: '>=18.0.0'}
'@aws-sdk/util-endpoints@3.667.0': '@aws-sdk/util-endpoints@3.667.0':
resolution: {integrity: sha512-X22SYDAuQJWnkF1/q17pkX3nGw5XMD9YEUbmt87vUnRq7iyJ3JOpl6UKOBeUBaL838wA5yzdbinmCITJ/VZ1QA==} resolution: {integrity: sha512-X22SYDAuQJWnkF1/q17pkX3nGw5XMD9YEUbmt87vUnRq7iyJ3JOpl6UKOBeUBaL838wA5yzdbinmCITJ/VZ1QA==}
engines: {node: '>=16.0.0'} engines: {node: '>=16.0.0'}
@ -975,12 +979,11 @@ packages:
resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==} resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==}
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
'@polar-sh/sdk@0.32.3':
resolution: {integrity: sha512-AW4CNIVIkLYmCy59ynkxGYz0/1m1w/L7WIC7+m0SW5j+k5HrLgTYK7/7jdwu8biddT5EcQOvmjTnHYtLFFs9sA==}
'@polar-sh/sdk@0.34.9':
resolution: {integrity: sha512-+kzQ3IlO67+/eSC4kmbZbmqdPjmvAr8zjTsavyx/u5lqwrerutvhpDzCCBQMlFnEVgeXArF+Rf6FQKkqKrgpxA==}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
'@modelcontextprotocol/sdk': ^1.5.0
zod: '>= 3'
'@modelcontextprotocol/sdk': '>=1.5.0 <1.10.0'
peerDependenciesMeta: peerDependenciesMeta:
'@modelcontextprotocol/sdk': '@modelcontextprotocol/sdk':
optional: true optional: true
@ -1122,6 +1125,10 @@ packages:
resolution: {integrity: sha512-QN0twHNfe8mNJdH9unwsCK13GURU7oEAZqkBI+rsvpv1jrmserO+WnLE7jidR9W/1dxwZ0u/CB01mV2Gms/K2Q==} resolution: {integrity: sha512-QN0twHNfe8mNJdH9unwsCK13GURU7oEAZqkBI+rsvpv1jrmserO+WnLE7jidR9W/1dxwZ0u/CB01mV2Gms/K2Q==}
engines: {node: '>=16.0.0'} engines: {node: '>=16.0.0'}
'@smithy/types@4.3.1':
resolution: {integrity: sha512-UqKOQBL2x6+HWl3P+3QqFD4ncKq0I8Nuz9QItGv5WuKuMHuuwlhvqcZCoXGfc+P1QmfJE7VieykoYYmrOoFJxA==}
engines: {node: '>=18.0.0'}
'@smithy/url-parser@3.0.7': '@smithy/url-parser@3.0.7':
resolution: {integrity: sha512-70UbSSR8J97c1rHZOWhl+VKiZDqHWxs/iW8ZHrHp5fCCPLSBE7GcUlUvKSle3Ca+J9LLbYCj/A79BxztBvAfpA==} resolution: {integrity: sha512-70UbSSR8J97c1rHZOWhl+VKiZDqHWxs/iW8ZHrHp5fCCPLSBE7GcUlUvKSle3Ca+J9LLbYCj/A79BxztBvAfpA==}
@ -2919,6 +2926,10 @@ packages:
node-notifier: node-notifier:
optional: true optional: true
jiti@2.4.2:
resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==}
hasBin: true
jose@4.15.9: jose@4.15.9:
resolution: {integrity: sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==} resolution: {integrity: sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==}
@ -4387,8 +4398,8 @@ packages:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'} engines: {node: '>=10'}
zod@3.24.1:
resolution: {integrity: sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==}
zod@3.25.76:
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
snapshots: snapshots:
@ -4455,7 +4466,7 @@ snapshots:
'@aws-crypto/sha256-js': 5.2.0 '@aws-crypto/sha256-js': 5.2.0
'@aws-crypto/supports-web-crypto': 5.2.0 '@aws-crypto/supports-web-crypto': 5.2.0
'@aws-crypto/util': 5.2.0 '@aws-crypto/util': 5.2.0
'@aws-sdk/types': 3.667.0
'@aws-sdk/types': 3.840.0
'@aws-sdk/util-locate-window': 3.568.0 '@aws-sdk/util-locate-window': 3.568.0
'@smithy/util-utf8': 2.3.0 '@smithy/util-utf8': 2.3.0
tslib: 2.8.1 tslib: 2.8.1
@ -4464,7 +4475,7 @@ snapshots:
'@aws-crypto/sha256-js@5.2.0': '@aws-crypto/sha256-js@5.2.0':
dependencies: dependencies:
'@aws-crypto/util': 5.2.0 '@aws-crypto/util': 5.2.0
'@aws-sdk/types': 3.667.0
'@aws-sdk/types': 3.840.0
tslib: 2.8.1 tslib: 2.8.1
optional: true optional: true
@ -4475,7 +4486,7 @@ snapshots:
'@aws-crypto/util@5.2.0': '@aws-crypto/util@5.2.0':
dependencies: dependencies:
'@aws-sdk/types': 3.667.0
'@aws-sdk/types': 3.840.0
'@smithy/util-utf8': 2.3.0 '@smithy/util-utf8': 2.3.0
tslib: 2.8.1 tslib: 2.8.1
optional: true optional: true
@ -4871,6 +4882,12 @@ snapshots:
tslib: 2.8.1 tslib: 2.8.1
optional: true optional: true
'@aws-sdk/types@3.840.0':
dependencies:
'@smithy/types': 4.3.1
tslib: 2.8.1
optional: true
'@aws-sdk/util-endpoints@3.667.0': '@aws-sdk/util-endpoints@3.667.0':
dependencies: dependencies:
'@aws-sdk/types': 3.667.0 '@aws-sdk/types': 3.667.0
@ -5109,9 +5126,9 @@ snapshots:
dependencies: dependencies:
'@jridgewell/trace-mapping': 0.3.9 '@jridgewell/trace-mapping': 0.3.9
'@eslint-community/eslint-utils@4.4.0(eslint@9.13.0)':
'@eslint-community/eslint-utils@4.4.0(eslint@9.13.0(jiti@2.4.2))':
dependencies: dependencies:
eslint: 9.13.0
eslint: 9.13.0(jiti@2.4.2)
eslint-visitor-keys: 3.4.3 eslint-visitor-keys: 3.4.3
'@eslint-community/regexpp@4.11.1': {} '@eslint-community/regexpp@4.11.1': {}
@ -5725,10 +5742,10 @@ snapshots:
'@pkgr/core@0.1.1': {} '@pkgr/core@0.1.1': {}
'@polar-sh/sdk@0.32.3(zod@3.24.1)':
'@polar-sh/sdk@0.34.9':
dependencies: dependencies:
standardwebhooks: 1.0.0 standardwebhooks: 1.0.0
zod: 3.24.1
zod: 3.25.76
'@protobufjs/aspromise@1.1.2': '@protobufjs/aspromise@1.1.2':
optional: true optional: true
@ -5967,6 +5984,11 @@ snapshots:
tslib: 2.8.1 tslib: 2.8.1
optional: true optional: true
'@smithy/types@4.3.1':
dependencies:
tslib: 2.8.1
optional: true
'@smithy/url-parser@3.0.7': '@smithy/url-parser@3.0.7':
dependencies: dependencies:
'@smithy/querystring-parser': 3.0.7 '@smithy/querystring-parser': 3.0.7
@ -6292,15 +6314,15 @@ snapshots:
dependencies: dependencies:
'@types/yargs-parser': 21.0.3 '@types/yargs-parser': 21.0.3
'@typescript-eslint/eslint-plugin@8.10.0(@typescript-eslint/parser@8.10.0(eslint@9.13.0)(typescript@5.6.3))(eslint@9.13.0)(typescript@5.6.3)':
'@typescript-eslint/eslint-plugin@8.10.0(@typescript-eslint/parser@8.10.0(eslint@9.13.0(jiti@2.4.2))(typescript@5.6.3))(eslint@9.13.0(jiti@2.4.2))(typescript@5.6.3)':
dependencies: dependencies:
'@eslint-community/regexpp': 4.11.1 '@eslint-community/regexpp': 4.11.1
'@typescript-eslint/parser': 8.10.0(eslint@9.13.0)(typescript@5.6.3)
'@typescript-eslint/parser': 8.10.0(eslint@9.13.0(jiti@2.4.2))(typescript@5.6.3)
'@typescript-eslint/scope-manager': 8.10.0 '@typescript-eslint/scope-manager': 8.10.0
'@typescript-eslint/type-utils': 8.10.0(eslint@9.13.0)(typescript@5.6.3)
'@typescript-eslint/utils': 8.10.0(eslint@9.13.0)(typescript@5.6.3)
'@typescript-eslint/type-utils': 8.10.0(eslint@9.13.0(jiti@2.4.2))(typescript@5.6.3)
'@typescript-eslint/utils': 8.10.0(eslint@9.13.0(jiti@2.4.2))(typescript@5.6.3)
'@typescript-eslint/visitor-keys': 8.10.0 '@typescript-eslint/visitor-keys': 8.10.0
eslint: 9.13.0
eslint: 9.13.0(jiti@2.4.2)
graphemer: 1.4.0 graphemer: 1.4.0
ignore: 5.3.2 ignore: 5.3.2
natural-compare: 1.4.0 natural-compare: 1.4.0
@ -6310,14 +6332,14 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@typescript-eslint/parser@8.10.0(eslint@9.13.0)(typescript@5.6.3)':
'@typescript-eslint/parser@8.10.0(eslint@9.13.0(jiti@2.4.2))(typescript@5.6.3)':
dependencies: dependencies:
'@typescript-eslint/scope-manager': 8.10.0 '@typescript-eslint/scope-manager': 8.10.0
'@typescript-eslint/types': 8.10.0 '@typescript-eslint/types': 8.10.0
'@typescript-eslint/typescript-estree': 8.10.0(typescript@5.6.3) '@typescript-eslint/typescript-estree': 8.10.0(typescript@5.6.3)
'@typescript-eslint/visitor-keys': 8.10.0 '@typescript-eslint/visitor-keys': 8.10.0
debug: 4.3.7 debug: 4.3.7
eslint: 9.13.0
eslint: 9.13.0(jiti@2.4.2)
optionalDependencies: optionalDependencies:
typescript: 5.6.3 typescript: 5.6.3
transitivePeerDependencies: transitivePeerDependencies:
@ -6328,10 +6350,10 @@ snapshots:
'@typescript-eslint/types': 8.10.0 '@typescript-eslint/types': 8.10.0
'@typescript-eslint/visitor-keys': 8.10.0 '@typescript-eslint/visitor-keys': 8.10.0
'@typescript-eslint/type-utils@8.10.0(eslint@9.13.0)(typescript@5.6.3)':
'@typescript-eslint/type-utils@8.10.0(eslint@9.13.0(jiti@2.4.2))(typescript@5.6.3)':
dependencies: dependencies:
'@typescript-eslint/typescript-estree': 8.10.0(typescript@5.6.3) '@typescript-eslint/typescript-estree': 8.10.0(typescript@5.6.3)
'@typescript-eslint/utils': 8.10.0(eslint@9.13.0)(typescript@5.6.3)
'@typescript-eslint/utils': 8.10.0(eslint@9.13.0(jiti@2.4.2))(typescript@5.6.3)
debug: 4.3.7 debug: 4.3.7
ts-api-utils: 1.3.0(typescript@5.6.3) ts-api-utils: 1.3.0(typescript@5.6.3)
optionalDependencies: optionalDependencies:
@ -6357,13 +6379,13 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@typescript-eslint/utils@8.10.0(eslint@9.13.0)(typescript@5.6.3)':
'@typescript-eslint/utils@8.10.0(eslint@9.13.0(jiti@2.4.2))(typescript@5.6.3)':
dependencies: dependencies:
'@eslint-community/eslint-utils': 4.4.0(eslint@9.13.0)
'@eslint-community/eslint-utils': 4.4.0(eslint@9.13.0(jiti@2.4.2))
'@typescript-eslint/scope-manager': 8.10.0 '@typescript-eslint/scope-manager': 8.10.0
'@typescript-eslint/types': 8.10.0 '@typescript-eslint/types': 8.10.0
'@typescript-eslint/typescript-estree': 8.10.0(typescript@5.6.3) '@typescript-eslint/typescript-estree': 8.10.0(typescript@5.6.3)
eslint: 9.13.0
eslint: 9.13.0(jiti@2.4.2)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
- typescript - typescript
@ -7196,19 +7218,19 @@ snapshots:
optionalDependencies: optionalDependencies:
source-map: 0.6.1 source-map: 0.6.1
eslint-config-prettier@9.1.0(eslint@9.13.0):
eslint-config-prettier@9.1.0(eslint@9.13.0(jiti@2.4.2)):
dependencies: dependencies:
eslint: 9.13.0
eslint: 9.13.0(jiti@2.4.2)
eslint-plugin-prettier@5.2.1(@types/eslint@9.6.1)(eslint-config-prettier@9.1.0(eslint@9.13.0))(eslint@9.13.0)(prettier@3.3.3):
eslint-plugin-prettier@5.2.1(@types/eslint@9.6.1)(eslint-config-prettier@9.1.0(eslint@9.13.0(jiti@2.4.2)))(eslint@9.13.0(jiti@2.4.2))(prettier@3.3.3):
dependencies: dependencies:
eslint: 9.13.0
eslint: 9.13.0(jiti@2.4.2)
prettier: 3.3.3 prettier: 3.3.3
prettier-linter-helpers: 1.0.0 prettier-linter-helpers: 1.0.0
synckit: 0.9.2 synckit: 0.9.2
optionalDependencies: optionalDependencies:
'@types/eslint': 9.6.1 '@types/eslint': 9.6.1
eslint-config-prettier: 9.1.0(eslint@9.13.0)
eslint-config-prettier: 9.1.0(eslint@9.13.0(jiti@2.4.2))
eslint-scope@5.1.1: eslint-scope@5.1.1:
dependencies: dependencies:
@ -7224,9 +7246,9 @@ snapshots:
eslint-visitor-keys@4.1.0: {} eslint-visitor-keys@4.1.0: {}
eslint@9.13.0:
eslint@9.13.0(jiti@2.4.2):
dependencies: dependencies:
'@eslint-community/eslint-utils': 4.4.0(eslint@9.13.0)
'@eslint-community/eslint-utils': 4.4.0(eslint@9.13.0(jiti@2.4.2))
'@eslint-community/regexpp': 4.11.1 '@eslint-community/regexpp': 4.11.1
'@eslint/config-array': 0.18.0 '@eslint/config-array': 0.18.0
'@eslint/core': 0.7.0 '@eslint/core': 0.7.0
@ -7261,6 +7283,8 @@ snapshots:
natural-compare: 1.4.0 natural-compare: 1.4.0
optionator: 0.9.4 optionator: 0.9.4
text-table: 0.2.0 text-table: 0.2.0
optionalDependencies:
jiti: 2.4.2
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@ -8373,6 +8397,9 @@ snapshots:
- supports-color - supports-color
- ts-node - ts-node
jiti@2.4.2:
optional: true
jose@4.15.9: {} jose@4.15.9: {}
js-stringify@1.0.2: js-stringify@1.0.2:
@ -9894,4 +9921,4 @@ snapshots:
yocto-queue@0.1.0: {} yocto-queue@0.1.0: {}
zod@3.24.1: {}
zod@3.25.76: {}

2
api/src/billing/billing.module.ts

@ -11,6 +11,7 @@ import { UsersModule } from 'src/users/users.module'
import { GatewayModule } from 'src/gateway/gateway.module' import { GatewayModule } from 'src/gateway/gateway.module'
import { PolarWebhookPayload, PolarWebhookPayloadSchema } from './schemas/polar-webhook-payload.schema' import { PolarWebhookPayload, PolarWebhookPayloadSchema } from './schemas/polar-webhook-payload.schema'
import { Device, DeviceSchema } from '../gateway/schemas/device.schema' import { Device, DeviceSchema } from '../gateway/schemas/device.schema'
import { CheckoutSession, CheckoutSessionSchema } from './schemas/checkout-session.schema'
@Module({ @Module({
imports: [ imports: [
@ -19,6 +20,7 @@ import { Device, DeviceSchema } from '../gateway/schemas/device.schema'
{ name: Subscription.name, schema: SubscriptionSchema }, { name: Subscription.name, schema: SubscriptionSchema },
{ name: PolarWebhookPayload.name, schema: PolarWebhookPayloadSchema }, { name: PolarWebhookPayload.name, schema: PolarWebhookPayloadSchema },
{ name: Device.name, schema: DeviceSchema }, { name: Device.name, schema: DeviceSchema },
{ name: CheckoutSession.name, schema: CheckoutSessionSchema },
]), ]),
forwardRef(() => AuthModule), forwardRef(() => AuthModule),
forwardRef(() => UsersModule), forwardRef(() => UsersModule),

51
api/src/billing/billing.service.ts

@ -1,4 +1,4 @@
import { HttpException, HttpStatus, Injectable } from '@nestjs/common'
import { BadRequestException, HttpException, HttpStatus, Injectable } from '@nestjs/common'
import { InjectModel } from '@nestjs/mongoose' import { InjectModel } from '@nestjs/mongoose'
import { Model, Types } from 'mongoose' import { Model, Types } from 'mongoose'
import { Plan, PlanDocument } from './schemas/plan.schema' import { Plan, PlanDocument } from './schemas/plan.schema'
@ -17,6 +17,7 @@ import {
PolarWebhookPayload, PolarWebhookPayload,
PolarWebhookPayloadDocument, PolarWebhookPayloadDocument,
} from './schemas/polar-webhook-payload.schema' } from './schemas/polar-webhook-payload.schema'
import { CheckoutSession, CheckoutSessionDocument } from './schemas/checkout-session.schema'
@Injectable() @Injectable()
export class BillingService { export class BillingService {
@ -31,6 +32,8 @@ export class BillingService {
@InjectModel(Device.name) private deviceModel: Model<DeviceDocument>, @InjectModel(Device.name) private deviceModel: Model<DeviceDocument>,
@InjectModel(PolarWebhookPayload.name) @InjectModel(PolarWebhookPayload.name)
private polarWebhookPayloadModel: Model<PolarWebhookPayloadDocument>, private polarWebhookPayloadModel: Model<PolarWebhookPayloadDocument>,
@InjectModel(CheckoutSession.name)
private checkoutSessionModel: Model<CheckoutSessionDocument>,
) { ) {
this.polarApi = new Polar({ this.polarApi = new Polar({
accessToken: process.env.POLAR_ACCESS_TOKEN ?? '', accessToken: process.env.POLAR_ACCESS_TOKEN ?? '',
@ -115,6 +118,15 @@ export class BillingService {
}): Promise<CheckoutResponseDTO> { }): Promise<CheckoutResponseDTO> {
const isYearly = payload.isYearly const isYearly = payload.isYearly
const existingCheckoutSession = await this.checkoutSessionModel.findOne({
user: user._id,
expiresAt: { $gt: new Date() },
})
if (existingCheckoutSession) {
return { redirectUrl: existingCheckoutSession.checkoutUrl }
}
const selectedPlan = await this.planModel.findOne({ const selectedPlan = await this.planModel.findOne({
name: payload.planName, name: payload.planName,
}) })
@ -123,13 +135,12 @@ export class BillingService {
!selectedPlan?.polarMonthlyProductId && !selectedPlan?.polarMonthlyProductId &&
!selectedPlan?.polarYearlyProductId !selectedPlan?.polarYearlyProductId
) { ) {
throw new Error('Plan cannot be purchased')
throw new BadRequestException('Plan cannot be purchased')
} }
// const product = await this.polarApi.products.get(selectedPlan.polarProductId) // const product = await this.polarApi.products.get(selectedPlan.polarProductId)
const discountId =
payload.discountId ?? '48f62ff7-3cd8-46ec-8ca7-2e570dc9c522'
const discountId = payload.discountId ?? process.env.POLAR_DEFAULT_DISCOUNT_ID
try { try {
const checkoutOptions: any = { const checkoutOptions: any = {
@ -146,16 +157,36 @@ export class BillingService {
metadata: { metadata: {
userId: user._id?.toString(), userId: user._id?.toString(),
}, },
externalCustomerId: user._id?.toString(),
} }
const discount = await this.polarApi.discounts.get({
id: discountId,
})
if (discount) {
checkoutOptions.discountId = discount.id
try {
const discount = await this.polarApi.discounts.get({
id: discountId,
})
if (discount) {
checkoutOptions.discountId = discount.id
}
} catch (error) {
console.error('failed to get discount', error)
} }
const checkout = await this.polarApi.checkouts.create(checkoutOptions) const checkout = await this.polarApi.checkouts.create(checkoutOptions)
console.log(checkout)
this.checkoutSessionModel.updateOne({
user: user._id,
},{
user: user._id,
checkoutSessionId: checkout.id,
checkoutUrl: checkout.url,
expiresAt: new Date(checkout.expiresAt),
payload: checkout,
}, { upsert: true }).catch((error) => {
console.error(error)
})
return { redirectUrl: checkout.url } return { redirectUrl: checkout.url }
} catch (error) { } catch (error) {
console.error(error) console.error(error)

27
api/src/billing/schemas/checkout-session.schema.ts

@ -0,0 +1,27 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'
import { Document, Types } from 'mongoose'
import { User } from '../../users/schemas/user.schema'
export type CheckoutSessionDocument = CheckoutSession & Document
@Schema({ timestamps: true })
export class CheckoutSession {
_id?: Types.ObjectId
@Prop({ type: Types.ObjectId, ref: User.name, required: true })
user: User
@Prop({ type: String, required: true })
checkoutSessionId: string
@Prop({ type: String, required: true })
checkoutUrl: string
@Prop({ type: Date, required: true })
expiresAt: Date
@Prop({ type: Object, required: true, default: {} })
payload: any
}
export const CheckoutSessionSchema = SchemaFactory.createForClass(CheckoutSession)
Loading…
Cancel
Save