├── .env.example ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── nest-cli.json ├── package-lock.json ├── package.json ├── src ├── app.controller.spec.ts ├── app.controller.ts ├── app.module.ts ├── app.service.ts ├── config │ ├── base.config.ts │ ├── checkout.config.ts │ ├── fixed-price.config.ts │ ├── metered-usage.config.ts │ ├── multi-plan.config.ts │ └── per-seat.config.ts ├── loading-module │ └── strategy.module.ts ├── main.ts ├── stripe-checkout │ ├── dto │ │ ├── create-portal-session.dto.ts │ │ ├── create-subscription.dto.ts │ │ └── get-checkout-session.dto.ts │ ├── stripe-checkout.controller.ts │ ├── stripe-checkout.module.ts │ └── stripe-checkout.service.ts ├── stripe-fixed-price │ ├── dto │ │ ├── cancel-subscription.dto.ts │ │ ├── create-customer.dto.ts │ │ ├── create-subscription.dto.ts │ │ ├── get-invoice-preview.dto.ts │ │ └── update-subscription.dto.ts │ ├── stripe-fixed-price.controller.ts │ ├── stripe-fixed-price.module.ts │ └── stripe-fixed-price.service.ts ├── stripe-metered-usage │ ├── dto │ │ ├── cancel-subscription.dto.ts │ │ ├── create-subscription.dto.ts │ │ ├── retrieveCustomerPM.dto.ts │ │ ├── retry-invoice.dto.ts │ │ ├── retry-upcoming-invoice.dto.ts │ │ └── update-subscription.dto.ts │ ├── stripe-metered-usage.controller.ts │ ├── stripe-metered-usage.module.ts │ └── stripe-metered-usage.service.ts ├── stripe-multi-plan │ ├── dto │ │ ├── create-customer.dto.ts │ │ └── get-subscription.dto.ts │ ├── stripe-multiple-plan.controller.ts │ ├── stripe-multiple-plan.module.ts │ └── stripe-multiple-plan.service.ts └── stripe-per-seat │ ├── dto │ ├── cancel-subscription.dto.ts │ ├── create-customer.dto.ts │ ├── create-subscription.dto.ts │ ├── retrieve-subs-info.dto.ts │ ├── retry-invoice.dto.ts │ ├── retry-upcoming-invoice.dto.ts │ └── update-subscription.dto.ts │ ├── stripe-per-seat.controller.ts │ ├── stripe-per-seat.module.ts │ └── stripe-per-seat.service.ts ├── stripe-template-code ├── checkout-single-subscription.js ├── fixed-price-subscriptions.js ├── multiple-plan-subscriptions.js ├── per-seat-subscription.js └── usage-based-subscriptions.js ├── test ├── app.e2e-spec.ts └── jest-e2e.json ├── tsconfig.build.json └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | PORT=3000 2 | # Stripe common 3 | STRIPE_STRATEGY=Checkout 4 | STRIPE_API_KEY=sk_test_1234 5 | STRIPE_PUBLISHABLE_KEY=pk_test_1234 6 | STRIPE_WEBHOOK_SECRET=whsec_1234 7 | # --------------------------------- 8 | # stripe Checkout 9 | # --------------------------------- 10 | DOMAIN=http://localhost 11 | BASIC_PRICE_ID=price_123 12 | PRO_PRICE_ID=price_456 13 | PORTAL_RETURN_URL=/return.html 14 | 15 | # --------------------------------- 16 | # stripe Fixed Price - Metered Usage - Per Seat 17 | # --------------------------------- 18 | # Billing variables 19 | BASIC=price_12345 20 | PREMIUM=price_7890 21 | 22 | # --------------------------------- 23 | # stripe Multiple Plan 24 | # --------------------------------- 25 | # Merchant variables 26 | ANIMALS=bear,koala,panda,dog,cat,tiger,horse,rabbit,dragon 27 | COUPON_ID=STRIPE_SAMPLE_MULTI_PLAN_DISCOUNT_20OFF 28 | MIN_PRODUCTS_FOR_DISCOUNT = 2 29 | DISCOUNT_FACTOR=.2 30 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: ['@typescript-eslint/eslint-plugin'], 8 | extends: [ 9 | 'plugin:@typescript-eslint/recommended', 10 | 'plugin:prettier/recommended', 11 | ], 12 | root: true, 13 | env: { 14 | node: true, 15 | jest: true, 16 | }, 17 | ignorePatterns: ['.eslintrc.js'], 18 | rules: { 19 | '@typescript-eslint/interface-name-prefix': 'off', 20 | '@typescript-eslint/explicit-function-return-type': 'off', 21 | '@typescript-eslint/explicit-module-boundary-types': 'off', 22 | '@typescript-eslint/no-explicit-any': 'off', 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | 3 | # compiled output 4 | /dist 5 | /node_modules 6 | 7 | # Logs 8 | logs 9 | *.log 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | lerna-debug.log* 14 | 15 | # OS 16 | .DS_Store 17 | 18 | # Tests 19 | /coverage 20 | /.nyc_output 21 | 22 | # IDEs and editors 23 | /.idea 24 | .project 25 | .classpath 26 | .c9/ 27 | *.launch 28 | .settings/ 29 | *.sublime-workspace 30 | 31 | # IDE - VSCode 32 | .vscode/* 33 | !.vscode/settings.json 34 | !.vscode/tasks.json 35 | !.vscode/launch.json 36 | !.vscode/extensions.json -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) {{ year }}, {{ author }} 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Just a little test to try to implement Stripe with Nestjs 2 | 3 | Part of a personal [list](https://github.com/audiBookning/samples-code-ressource-list) of random samples code 4 | 5 | - Code Still not complete and not presentable. 😎 6 | 7 | - And more importantly, there is no tests. 8 | 9 | - Some manual tests were done for the webhooks. 10 | 11 | - This is a basic Nestjs project, so to run it just use `npm run start:dev`. 12 | 13 | ## Libs used 14 | 15 | - "[Nestjs](https://github.com/nestjs/nest)": "^7.6.15", 16 | 17 | - "[Stripe Node.js Library](https://github.com/stripe/stripe-node)": "^8.148.0" 18 | 19 | - "[@golevelup/nestjs-stripe](https://github.com/golevelup/nestjs/tree/master/packages/stripe)": "^0.2.0", 20 | 21 | - "[@golevelup/nestjs-webhooks](https://github.com/golevelup/nestjs/tree/master/packages/webhooks)": "^0.2.7", 22 | 23 | ## Notes 24 | 25 | - The stripe code of the different subscription strategies is in 5 different modules, each with a controller managing some "main" routes and with its own service (used at this time only for the Webhooks). 26 | 27 | - The StrategyModule is used just to dynamically load the different strategy modules depending on the env variable `STRIPE_STRATEGY`, so as to avoid well deserved instabilities when instantiating multiple times the @golevelup/nestjs-stripe module. 28 | 29 | - Their controllers are: 30 | 31 | - The code used in the `StripeCheckoutController` is directly based from [Using Checkout for subscriptions](https://github.com/stripe-samples/checkout-single-subscription/). 32 | 33 | - The code used in the `StripeFixedPriceController` is directly based from [Subscriptions with fixed price](https://github.com/stripe-samples/subscription-use-cases/tree/master/fixed-price-subscriptions/). 34 | 35 | - The code used in the `StripeMeteredUsageController` is directly based from [Subscriptions with metered usage](https://github.com/stripe-samples/subscription-use-cases/tree/master/usage-based-subscriptions). 36 | 37 | - The code used in the `StripePerSeatController` is directly based from [Subscriptions with per seat pricing](https://github.com/stripe-samples/subscription-use-cases/tree/master/per-seat-subscriptions). 38 | 39 | - The code used in the `StripeMultiplePlanController` is directly based from [Stripe Billing sample subscribing a customer to multiple products](https://github.com/stripe-samples/charging-for-multiple-plan-subscriptions). 40 | 41 | - The Webhooks are consumed in the stripe services with the help of the `@StripeWebhookHandler` decorator. 42 | 43 | - The route for the Webhooks is `stripe/webhook`. 44 | 45 | - Rename `.env.example` to `.env` and change the stripe keys. 46 | 47 | - Since this is just a test (with minimal effort ...) and in with the objective of separating a little the code of the different subscription strategies without creating different repo or more complexity, much of the code or config is repeated. It will also give the easiness of latter simply copying a specific module folder to use as a quick boilerplate. 48 | 49 | - Each strategy has a separate config file. Many times the config is the same between them... 50 | 51 | ## TODO 52 | 53 | - Add tests. 54 | 55 | - Errors are too generic and many try catch missing, although Nestjs catch them by default... 56 | 57 | - Integrate with a database. Maybe do it in a different repo in order to separate the basic "routing" implementation on its own? 58 | 59 | ## BuyMeACoffee 60 | 61 | Buy Me A Coffee 62 | 63 | ## Disclaimer 64 | 65 | This code is not and will never be maintained. It is just some random sample code. 66 | 67 | Feel free to copy and make any change you like. 68 | 69 | ## 70 | 71 | License 72 | ISC © 2021 AudiBookning 73 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src" 4 | } 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sample-stripe-backend", 3 | "version": "0.0.1", 4 | "description": "just testing some code", 5 | "author": "audiBookning", 6 | "private": true, 7 | "license": "ISC", 8 | "scripts": { 9 | "prebuild": "rimraf dist", 10 | "build": "nest build", 11 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 12 | "start": "nest start", 13 | "start:dev": "nest start --watch", 14 | "start:debug": "nest start --debug --watch", 15 | "start:prod": "node dist/main", 16 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 17 | "test": "jest", 18 | "test:watch": "jest --watch", 19 | "test:cov": "jest --coverage", 20 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 21 | "test:e2e": "jest --config ./test/jest-e2e.json" 22 | }, 23 | "dependencies": { 24 | "@golevelup/nestjs-stripe": "^0.2.0", 25 | "@golevelup/nestjs-webhooks": "^0.2.7", 26 | "@nestjs/common": "^7.6.15", 27 | "@nestjs/config": "^0.6.3", 28 | "@nestjs/core": "^7.6.15", 29 | "@nestjs/platform-express": "^7.6.15", 30 | "reflect-metadata": "^0.1.13", 31 | "rimraf": "^3.0.2", 32 | "rxjs": "^6.6.6", 33 | "stripe": "^8.148.0" 34 | }, 35 | "devDependencies": { 36 | "@nestjs/cli": "^7.6.0", 37 | "@nestjs/schematics": "^7.3.0", 38 | "@nestjs/testing": "^7.6.15", 39 | "@types/express": "^4.17.11", 40 | "@types/jest": "^26.0.22", 41 | "@types/node": "^14.14.36", 42 | "@types/supertest": "^2.0.10", 43 | "@typescript-eslint/eslint-plugin": "^4.19.0", 44 | "@typescript-eslint/parser": "^4.19.0", 45 | "eslint": "^7.22.0", 46 | "eslint-config-prettier": "^8.1.0", 47 | "eslint-plugin-prettier": "^3.3.1", 48 | "jest": "^26.6.3", 49 | "prettier": "^2.2.1", 50 | "supertest": "^6.1.3", 51 | "ts-jest": "^26.5.4", 52 | "ts-loader": "^8.0.18", 53 | "ts-node": "^9.1.1", 54 | "tsconfig-paths": "^3.9.0", 55 | "typescript": "^4.2.3" 56 | }, 57 | "jest": { 58 | "moduleFileExtensions": [ 59 | "js", 60 | "json", 61 | "ts" 62 | ], 63 | "rootDir": "src", 64 | "testRegex": ".*\\.spec\\.ts$", 65 | "transform": { 66 | "^.+\\.(t|j)s$": "ts-jest" 67 | }, 68 | "collectCoverageFrom": [ 69 | "**/*.(t|j)s" 70 | ], 71 | "coverageDirectory": "../coverage", 72 | "testEnvironment": "node" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/app.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | 5 | describe('AppController', () => { 6 | let appController: AppController; 7 | 8 | beforeEach(async () => { 9 | const app: TestingModule = await Test.createTestingModule({ 10 | controllers: [AppController], 11 | providers: [AppService], 12 | }).compile(); 13 | 14 | appController = app.get(AppController); 15 | }); 16 | 17 | describe('root', () => { 18 | it('should return "Hello World!"', () => { 19 | expect(appController.getHello()).toBe('Hello World!'); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { AppService } from './app.service'; 3 | 4 | @Controller() 5 | export class AppController { 6 | constructor(private readonly appService: AppService) {} 7 | 8 | @Get() 9 | getHello(): string { 10 | return this.appService.getHello(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ConfigModule } from '@nestjs/config'; 3 | import { AppController } from './app.controller'; 4 | import { AppService } from './app.service'; 5 | import baseConfig from './config/base.config'; 6 | import checkoutConfig from './config/checkout.config'; 7 | import fixedPriceConfig from './config/fixed-price.config'; 8 | import meteredUsageConfig from './config/metered-usage.config'; 9 | import multiPlanConfig from './config/multi-plan.config'; 10 | import perSeatConfig from './config/per-seat.config'; 11 | import { StrategyModule } from './loading-module/strategy.module'; 12 | 13 | @Module({ 14 | imports: [ 15 | ConfigModule.forRoot({ 16 | load: [ 17 | baseConfig, 18 | checkoutConfig, 19 | fixedPriceConfig, 20 | meteredUsageConfig, 21 | multiPlanConfig, 22 | perSeatConfig, 23 | ], 24 | isGlobal: true, 25 | }), 26 | StrategyModule.forRoot(), 27 | ], 28 | controllers: [AppController], 29 | providers: [AppService], 30 | }) 31 | export class AppModule {} 32 | -------------------------------------------------------------------------------- /src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService { 5 | getHello(): string { 6 | return 'Hello World!'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/config/base.config.ts: -------------------------------------------------------------------------------- 1 | export default () => ({ 2 | port: parseInt(process.env.PORT, 10) || 3000, 3 | domain: process.env.DOMAIN, 4 | // Common stripe env 5 | stripe: { 6 | apiKey: process.env.STRIPE_API_KEY, 7 | publishablekey: process.env.STRIPE_PUBLISHABLE_KEY, 8 | webhookSecret: process.env.STRIPE_WEBHOOK_SECRET, 9 | }, 10 | stripeCheckout: { 11 | basicPriceId: process.env.BASIC_PRICE_ID, 12 | proPriceId: process.env.PRO_PRICE_ID, 13 | portalReturnUrl: process.env.PORTAL_RETURN_URL, 14 | }, 15 | stripeFixedPrice: { 16 | // "dynamic" newPriceLookupKey 17 | basic: process.env.BASIC, 18 | premium: process.env.PREMIUM, 19 | }, 20 | // INFO: In this case it is the same as stripeFixedPrice 21 | stripeMeteredUsage: { 22 | // "dynamic" priceId 23 | basic: process.env.BASIC, 24 | premium: process.env.PREMIUM, 25 | }, 26 | // INFO: In this case it is the same as stripeFixedPrice 27 | stripePerSeat: { 28 | // "dynamic" priceId 29 | basic: process.env.BASIC, 30 | premium: process.env.PREMIUM, 31 | }, 32 | stripeMultiPlan: { 33 | animals: process.env.ANIMALS, 34 | couponId: process.env.COUPON_ID, 35 | minProdDiscount: process.env.MIN_PRODUCTS_FOR_DISCOUNT, 36 | discountFactor: process.env.DISCOUNT_FACTOR, 37 | }, 38 | }); 39 | -------------------------------------------------------------------------------- /src/config/checkout.config.ts: -------------------------------------------------------------------------------- 1 | export default () => ({ 2 | stripeCheckout: { 3 | basicPriceId: process.env.BASIC_PRICE_ID, 4 | proPriceId: process.env.PRO_PRICE_ID, 5 | portalReturnUrl: process.env.PORTAL_RETURN_URL, 6 | }, 7 | }); 8 | -------------------------------------------------------------------------------- /src/config/fixed-price.config.ts: -------------------------------------------------------------------------------- 1 | export default () => ({ 2 | stripeFixedPrice: { 3 | // "dynamic" newPriceLookupKey 4 | basic: process.env.BASIC, 5 | premium: process.env.PREMIUM, 6 | }, 7 | }); 8 | -------------------------------------------------------------------------------- /src/config/metered-usage.config.ts: -------------------------------------------------------------------------------- 1 | export default () => ({ 2 | // INFO: In this case it is the same as stripeFixedPrice 3 | stripeMeteredUsage: { 4 | // "dynamic" priceId 5 | basic: process.env.BASIC, 6 | premium: process.env.PREMIUM, 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /src/config/multi-plan.config.ts: -------------------------------------------------------------------------------- 1 | export default () => ({ 2 | stripeMultiPlan: { 3 | animals: process.env.ANIMALS, 4 | couponId: process.env.COUPON_ID, 5 | minProdDiscount: process.env.MIN_PRODUCTS_FOR_DISCOUNT, 6 | discountFactor: process.env.DISCOUNT_FACTOR, 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /src/config/per-seat.config.ts: -------------------------------------------------------------------------------- 1 | export default () => ({ 2 | // INFO: In this case it is the same as stripeFixedPrice 3 | stripePerSeat: { 4 | // "dynamic" priceId 5 | basic: process.env.BASIC, 6 | premium: process.env.PREMIUM, 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /src/loading-module/strategy.module.ts: -------------------------------------------------------------------------------- 1 | import { DynamicModule, Module } from '@nestjs/common'; 2 | import { config } from 'dotenv'; 3 | import { StripeCheckoutModule } from '../stripe-checkout/stripe-checkout.module'; 4 | import { StripeFixedPriceModule } from '../stripe-fixed-price/stripe-fixed-price.module'; 5 | import { StripeMeteredUsageModule } from '../stripe-metered-usage/stripe-metered-usage.module'; 6 | import { StripeMultiPlanModule } from '../stripe-multi-plan/stripe-multiple-plan.module'; 7 | import { StripePerSeatModule } from '../stripe-per-seat/stripe-per-seat.module'; 8 | config(); 9 | 10 | enum StrategyEnum { 11 | Checkout = 'Checkout', 12 | Fixed = 'Fixed', 13 | Metered = 'Metered', 14 | Multi = 'Multi', 15 | Seat = 'Seat', 16 | } 17 | 18 | @Module({}) 19 | export class StrategyModule { 20 | public static forRoot(): DynamicModule { 21 | const strategy = process.env.STRIPE_STRATEGY; 22 | const modulesImports = { 23 | Checkout: StripeCheckoutModule, 24 | Fixed: StripeFixedPriceModule, 25 | Metered: StripeMeteredUsageModule, 26 | Multi: StripeMultiPlanModule, 27 | Seat: StripePerSeatModule, 28 | }; 29 | 30 | if (!modulesImports[strategy]) { 31 | throw new Error('Wrong module string'); 32 | } 33 | return { 34 | module: StrategyModule, 35 | imports: [modulesImports[strategy]], 36 | providers: [], 37 | exports: [], 38 | }; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { ConfigService } from '@nestjs/config'; 2 | import { NestFactory } from '@nestjs/core'; 3 | import { AppModule } from './app.module'; 4 | 5 | async function bootstrap() { 6 | // const app = await NestFactory.create(AppModule); 7 | const app = await NestFactory.create(AppModule, { 8 | bodyParser: false, 9 | }); 10 | const configService = app.get(ConfigService); 11 | 12 | const port = configService.get('port'); 13 | 14 | await app.listen(port); 15 | console.log(`App started at http://localhost:${port}/`); 16 | } 17 | bootstrap(); 18 | -------------------------------------------------------------------------------- /src/stripe-checkout/dto/create-portal-session.dto.ts: -------------------------------------------------------------------------------- 1 | export class CreatePortalSessionDto { 2 | sessionId: string; 3 | } 4 | -------------------------------------------------------------------------------- /src/stripe-checkout/dto/create-subscription.dto.ts: -------------------------------------------------------------------------------- 1 | export class CreateCheckoutSessionDto { 2 | priceId: string; 3 | } 4 | -------------------------------------------------------------------------------- /src/stripe-checkout/dto/get-checkout-session.dto.ts: -------------------------------------------------------------------------------- 1 | export class GetCheckoutSessionDto { 2 | sessionId: string; 3 | } 4 | -------------------------------------------------------------------------------- /src/stripe-checkout/stripe-checkout.controller.ts: -------------------------------------------------------------------------------- 1 | import { InjectStripeClient } from '@golevelup/nestjs-stripe'; 2 | import { Body, Controller, Get, Post, Query } from '@nestjs/common'; 3 | import { ConfigService } from '@nestjs/config'; 4 | import { Stripe } from 'stripe'; 5 | import { CreatePortalSessionDto } from './dto/create-portal-session.dto'; 6 | import { CreateCheckoutSessionDto } from './dto/create-subscription.dto'; 7 | import { GetCheckoutSessionDto } from './dto/get-checkout-session.dto'; 8 | // REF: https://github.com/stripe-samples/checkout-single-subscription/blob/master/server/node/server.js 9 | 10 | @Controller('stripe-checkout') 11 | export class StripeCheckoutController { 12 | constructor( 13 | private readonly configSvc: ConfigService, 14 | @InjectStripeClient() private readonly stripeClient: Stripe, 15 | ) {} 16 | 17 | // Fetch the Checkout Session to display the JSON result on the success page 18 | @Get('checkout-session') 19 | checkoutSession( 20 | @Query() { sessionId }: GetCheckoutSessionDto, 21 | ): Promise> { 22 | return this.stripeClient.checkout.sessions.retrieve(sessionId); 23 | } 24 | 25 | // TODO: Implement with Config 26 | // TODO: Change to @Post and Body payload 27 | @Post('create-checkout-session') 28 | async createCheckoutSession(@Body() { priceId }: CreateCheckoutSessionDto) { 29 | const domainURL = this.configSvc.get('domain'); 30 | // Create new Checkout Session for the order 31 | // Other optional params include: 32 | // [billing_address_collection] - to display billing address details on the page 33 | // [customer] - if you have an existing Stripe Customer ID 34 | // [customer_email] - lets you prefill the email input in the form 35 | // For full details see https://stripe.com/docs/api/checkout/sessions/create 36 | try { 37 | const session = await this.stripeClient.checkout.sessions.create({ 38 | mode: 'subscription', 39 | payment_method_types: ['card'], 40 | line_items: [ 41 | { 42 | price: priceId, 43 | quantity: 1, 44 | }, 45 | ], 46 | // ?session_id={CHECKOUT_SESSION_ID} means the redirect will have the session ID set as a query param 47 | success_url: `${domainURL}/success.html?session_id={CHECKOUT_SESSION_ID}`, 48 | cancel_url: `${domainURL}/canceled.html`, 49 | }); 50 | 51 | return { 52 | sessionId: session.id, 53 | }; 54 | } catch (error) { 55 | console.log(error); 56 | throw error; 57 | } 58 | } 59 | 60 | // TODO: Implement with Config 61 | @Get('setup') 62 | getSetup() { 63 | return { 64 | publishableKey: this.configSvc.get('stripe.publishablekey'), 65 | // TODO: These should come from the Databse or from the stripe API 66 | basicPrice: this.configSvc.get('stripeCheckout.basicPriceId'), 67 | proPrice: this.configSvc.get('stripeCheckout.proPriceId'), 68 | }; 69 | } 70 | 71 | // TODO: Implement with Config 72 | // TODO: Change to @Post and Body payload 73 | @Post('customer-portal') 74 | async getCustomerPortal( 75 | @Body('sessionId') { sessionId }: CreatePortalSessionDto, 76 | ) { 77 | const checkoutsession = await this.stripeClient.checkout.sessions.retrieve( 78 | sessionId, 79 | ); 80 | // This is the url to which the customer will be redirected when they are done 81 | // managing their billing with the portal. 82 | const domain = this.configSvc.get('domain'); 83 | const port = this.configSvc.get('port'); 84 | const returnUrl = this.configSvc.get( 85 | 'stripeCheckout.portalReturnUrl', 86 | ); 87 | const portalsession = await this.stripeClient.billingPortal.sessions.create( 88 | { 89 | customer: checkoutsession.customer as string, 90 | return_url: `${domain}:${port}${returnUrl}`, 91 | }, 92 | ); 93 | 94 | return { 95 | url: portalsession.url, 96 | }; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/stripe-checkout/stripe-checkout.module.ts: -------------------------------------------------------------------------------- 1 | import { StripeModule } from '@golevelup/nestjs-stripe'; 2 | import { 3 | applyRawBodyOnlyTo, 4 | JsonBodyMiddleware, 5 | RawBodyMiddleware, 6 | } from '@golevelup/nestjs-webhooks'; 7 | import { 8 | MiddlewareConsumer, 9 | Module, 10 | NestModule, 11 | RequestMethod, 12 | } from '@nestjs/common'; 13 | import { ConfigModule, ConfigService } from '@nestjs/config'; 14 | import { config } from 'dotenv'; 15 | import { StripeCheckoutController } from './stripe-checkout.controller'; 16 | import { StripeCheckoutService } from './stripe-checkout.service'; 17 | config(); 18 | 19 | @Module({ 20 | imports: [ 21 | JsonBodyMiddleware, 22 | RawBodyMiddleware, 23 | 24 | StripeModule.forRootAsync(StripeModule, { 25 | imports: [ConfigModule], 26 | useFactory: (configSvc: ConfigService) => ({ 27 | apiKey: configSvc.get('stripe.apiKey'), 28 | webhookConfig: { 29 | stripeWebhookSecret: configSvc.get('stripe.webhookSecret'), 30 | }, 31 | }), 32 | inject: [ConfigService], 33 | }), 34 | ], 35 | controllers: [StripeCheckoutController], 36 | providers: [StripeCheckoutService], 37 | }) 38 | export class StripeCheckoutModule implements NestModule { 39 | configure(consumer: MiddlewareConsumer) { 40 | applyRawBodyOnlyTo(consumer, { 41 | method: RequestMethod.ALL, 42 | path: 'stripe/webhook', 43 | }); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/stripe-checkout/stripe-checkout.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | InjectStripeClient, 3 | StripeWebhookHandler, 4 | } from '@golevelup/nestjs-stripe'; 5 | import { Injectable } from '@nestjs/common'; 6 | import Stripe from 'stripe'; 7 | 8 | @Injectable() 9 | export class StripeCheckoutService { 10 | constructor(@InjectStripeClient() private stripeClient: Stripe) {} 11 | 12 | /* **************************************** 13 | * STRIPE WebHooks 14 | * ****************************************/ 15 | 16 | @StripeWebhookHandler('checkout.session.completed') 17 | handleCheckoutSessionCompleted(evt: Stripe.Event) { 18 | console.log('StripeCheckoutService checkout.session.completed: ', evt); 19 | const dataObject = evt.data.object as Stripe.Checkout.Session; 20 | console.log(`🔔 Payment received!`); 21 | return true; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/stripe-fixed-price/dto/cancel-subscription.dto.ts: -------------------------------------------------------------------------------- 1 | export class CancelSubscriptionDto { 2 | subscriptionId: string; 3 | } 4 | -------------------------------------------------------------------------------- /src/stripe-fixed-price/dto/create-customer.dto.ts: -------------------------------------------------------------------------------- 1 | export class CreateCustomerDto { 2 | email: string; 3 | } 4 | -------------------------------------------------------------------------------- /src/stripe-fixed-price/dto/create-subscription.dto.ts: -------------------------------------------------------------------------------- 1 | export class FPCreateSubscriptionDto { 2 | priceId: string; 3 | } 4 | -------------------------------------------------------------------------------- /src/stripe-fixed-price/dto/get-invoice-preview.dto.ts: -------------------------------------------------------------------------------- 1 | export class FPGetInvoicePreviewDto { 2 | subscriptionId: string; 3 | newPriceLookupKey: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/stripe-fixed-price/dto/update-subscription.dto.ts: -------------------------------------------------------------------------------- 1 | export class FPUpdateSubscriptionDto { 2 | subscriptionId: string; 3 | newPriceLookupKey: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/stripe-fixed-price/stripe-fixed-price.controller.ts: -------------------------------------------------------------------------------- 1 | import { InjectStripeClient } from '@golevelup/nestjs-stripe'; 2 | import { Body, Controller, Get, Post, Query } from '@nestjs/common'; 3 | import { ConfigService } from '@nestjs/config'; 4 | import Stripe from 'stripe'; 5 | import { CancelSubscriptionDto } from './dto/cancel-subscription.dto'; 6 | import { CreateCustomerDto } from './dto/create-customer.dto'; 7 | import { FPCreateSubscriptionDto } from './dto/create-subscription.dto'; 8 | import { FPGetInvoicePreviewDto } from './dto/get-invoice-preview.dto'; 9 | import { FPUpdateSubscriptionDto } from './dto/update-subscription.dto'; 10 | 11 | // REF: https://github.com/stripe-samples/subscription-use-cases/blob/master/fixed-price-subscriptions/server/node/server.js 12 | 13 | @Controller('stripe-fixed-price') 14 | export class StripeFixedPriceController { 15 | constructor( 16 | private readonly configSvc: ConfigService, 17 | @InjectStripeClient() private stripeClient: Stripe, 18 | ) {} 19 | 20 | // TODO: Implement with Config 21 | @Get('setup') 22 | async getSetup() { 23 | const prices = await this.stripeClient.prices.list({ 24 | lookup_keys: ['sample_basic', 'sample_premium'], 25 | expand: ['data.product'], 26 | }); 27 | return { 28 | publishableKey: this.configSvc.get('stripe.publishablekey'), 29 | proPrice: prices.data, 30 | }; 31 | } 32 | 33 | @Post('create-customer') 34 | async createCustomer(@Body() { email }: CreateCustomerDto) { 35 | try { 36 | // Create a new customer object 37 | const customer = await this.stripeClient.customers.create({ 38 | email, 39 | }); 40 | 41 | // Save the customer.id in your database alongside your user. 42 | // We're simulating authentication with a cookie. 43 | // res.cookie('customer', customer.id, { maxAge: 900000, httpOnly: true }); 44 | 45 | return { 46 | customer, 47 | }; 48 | } catch (error) { 49 | console.log(error); 50 | throw error; 51 | } 52 | } 53 | 54 | // TODO: implement retriving of customerID from DB or Auth 55 | @Post('create-subscription') 56 | async createSubscription(@Body() { priceId }: FPCreateSubscriptionDto) { 57 | // Simulate authenticated user. In practice this will be the 58 | // Stripe Customer ID related to the authenticated user. 59 | // const customerId = req.cookies['customer']; 60 | const customerId = ''; 61 | 62 | // Create the subscription 63 | try { 64 | const subscription = await this.stripeClient.subscriptions.create({ 65 | customer: customerId, 66 | items: [ 67 | { 68 | price: priceId, 69 | }, 70 | ], 71 | payment_behavior: 'default_incomplete', 72 | expand: ['latest_invoice.payment_intent'], 73 | }); 74 | 75 | // Type checking the type unions 76 | const latest_invoice = subscription.latest_invoice; 77 | if (typeof latest_invoice === 'string') { 78 | throw new Error('latest_invoice not expanded'); 79 | } 80 | const payment_intent = latest_invoice.payment_intent; 81 | if (typeof payment_intent === 'string') { 82 | throw new Error('payment_intent not expanded'); 83 | } 84 | 85 | return { 86 | subscriptionId: subscription.id, 87 | clientSecret: payment_intent.client_secret, 88 | }; 89 | } catch (error) { 90 | console.log('createSubscription: ', error); 91 | throw error; 92 | } 93 | } 94 | 95 | // TODO: Implement with Config 96 | // TODO: implement retriving of customerID from DB or Auth 97 | @Get('invoice-preview') 98 | async getInvoicePreview( 99 | @Query() { subscriptionId, newPriceLookupKey }: FPGetInvoicePreviewDto, 100 | ) { 101 | const customerId = 'customer'; 102 | const priceId = this.configSvc.get( 103 | `stripeFixedPrice.${newPriceLookupKey}`, 104 | ); 105 | 106 | const subscription = await this.stripeClient.subscriptions.retrieve( 107 | subscriptionId, 108 | ); 109 | 110 | const invoice = await this.stripeClient.invoices.retrieveUpcoming({ 111 | customer: customerId, 112 | subscription: subscriptionId, 113 | subscription_items: [ 114 | { 115 | id: subscription.items.data[0].id, 116 | price: priceId, 117 | }, 118 | ], 119 | }); 120 | 121 | return { invoice }; 122 | } 123 | 124 | @Post('cancel-subscription') 125 | async cancelSubscription(@Body() { subscriptionId }: CancelSubscriptionDto) { 126 | // Cancel the subscription 127 | try { 128 | const deletedSubscription = await this.stripeClient.subscriptions.del( 129 | subscriptionId, 130 | ); 131 | 132 | return { subscription: deletedSubscription }; 133 | } catch (error) { 134 | throw error; 135 | } 136 | } 137 | 138 | // TODO: Implement with Config 139 | @Post('update-subscription') 140 | async updateSubscription( 141 | @Body() { subscriptionId, newPriceLookupKey }: FPUpdateSubscriptionDto, 142 | ) { 143 | try { 144 | const subscription = await this.stripeClient.subscriptions.retrieve( 145 | subscriptionId, 146 | ); 147 | const updatedSubscription = await this.stripeClient.subscriptions.update( 148 | subscriptionId, 149 | { 150 | items: [ 151 | { 152 | id: subscription.items.data[0].id, 153 | price: this.configSvc.get( 154 | `stripeFixedPrice.${newPriceLookupKey}`, 155 | ), 156 | }, 157 | ], 158 | }, 159 | ); 160 | 161 | return { subscription: updatedSubscription }; 162 | } catch (error) { 163 | throw error; 164 | } 165 | } 166 | 167 | // TODO: implement retriving of customerID from DB or Auth 168 | @Get('subscriptions') 169 | async getSubscriptions() { 170 | // Simulate authenticated user. In practice this will be the 171 | // Stripe Customer ID related to the authenticated user. 172 | const customerId = 'customer'; 173 | 174 | const subscriptions = await this.stripeClient.subscriptions.list({ 175 | customer: customerId, 176 | status: 'all', 177 | expand: ['data.default_payment_method'], 178 | }); 179 | 180 | return { subscriptions }; 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/stripe-fixed-price/stripe-fixed-price.module.ts: -------------------------------------------------------------------------------- 1 | import { StripeModule } from '@golevelup/nestjs-stripe'; 2 | import { 3 | applyRawBodyOnlyTo, 4 | JsonBodyMiddleware, 5 | RawBodyMiddleware, 6 | } from '@golevelup/nestjs-webhooks'; 7 | import { 8 | MiddlewareConsumer, 9 | Module, 10 | NestModule, 11 | RequestMethod, 12 | } from '@nestjs/common'; 13 | import { ConfigModule, ConfigService } from '@nestjs/config'; 14 | import { config } from 'dotenv'; 15 | import { StripeFixedPriceController } from './stripe-fixed-price.controller'; 16 | import { StripeFixedPriceService } from './stripe-fixed-price.service'; 17 | config(); 18 | 19 | @Module({ 20 | imports: [ 21 | JsonBodyMiddleware, 22 | RawBodyMiddleware, 23 | StripeModule.forRootAsync(StripeModule, { 24 | imports: [ConfigModule], 25 | useFactory: (configSvc: ConfigService) => ({ 26 | apiKey: configSvc.get('stripe.apiKey'), 27 | webhookConfig: { 28 | stripeWebhookSecret: configSvc.get('stripe.webhookSecret'), 29 | }, 30 | }), 31 | inject: [ConfigService], 32 | }), 33 | ], 34 | controllers: [StripeFixedPriceController], 35 | providers: [StripeFixedPriceService], 36 | }) 37 | export class StripeFixedPriceModule implements NestModule { 38 | configure(consumer: MiddlewareConsumer) { 39 | applyRawBodyOnlyTo(consumer, { 40 | method: RequestMethod.ALL, 41 | path: 'stripe/webhook', 42 | }); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/stripe-fixed-price/stripe-fixed-price.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | InjectStripeClient, 3 | StripeWebhookHandler, 4 | } from '@golevelup/nestjs-stripe'; 5 | import { Injectable } from '@nestjs/common'; 6 | import Stripe from 'stripe'; 7 | 8 | @Injectable() 9 | export class StripeFixedPriceService { 10 | constructor(@InjectStripeClient() private stripeClient: Stripe) {} 11 | 12 | /* **************************************** 13 | * STRIPE WebHooks 14 | * ****************************************/ 15 | 16 | @StripeWebhookHandler('invoice.payment_succeeded') 17 | async handleInvoicePaymentSucceeded(evt: Stripe.Event) { 18 | console.log('StripeFixedPriceService invoice.payment_succeeded: ', evt); 19 | const dataObject = evt.data.object as Stripe.Invoice; 20 | 21 | const payment_intent_id = dataObject['payment_intent']; 22 | if (typeof payment_intent_id !== 'string') { 23 | throw new Error('payment_intent_id should be of type string'); 24 | } 25 | await this.stripeClient.paymentIntents.retrieve(payment_intent_id); 26 | 27 | try { 28 | if (dataObject['billing_reason'] == 'subscription_create') { 29 | // The subscription automatically activates after successful payment 30 | // Set the payment method used to pay the first invoice 31 | // as the default payment method for that subscription 32 | const subscription_id = dataObject['subscription']; 33 | const payment_intent_id = dataObject['payment_intent']; 34 | 35 | if (typeof subscription_id !== 'string') { 36 | throw new Error('subscription_id should be of type string'); 37 | } 38 | 39 | if (typeof payment_intent_id !== 'string') { 40 | throw new Error('payment_intent_id should be of type string'); 41 | } 42 | 43 | // Retrieve the payment intent used to pay the subscription 44 | const payment_intent = await this.stripeClient.paymentIntents.retrieve( 45 | payment_intent_id, 46 | ); 47 | 48 | if (typeof payment_intent.payment_method !== 'string') { 49 | throw new Error('payment_method should be of type string'); 50 | } 51 | 52 | await this.stripeClient.subscriptions.update(subscription_id, { 53 | default_payment_method: payment_intent.payment_method, 54 | }); 55 | 56 | console.log( 57 | 'Default payment method set for subscription:' + 58 | payment_intent.payment_method, 59 | ); 60 | } 61 | } catch (error) { 62 | console.log(error); 63 | throw error; 64 | } 65 | } 66 | 67 | @StripeWebhookHandler('invoice.payment_failed') 68 | handleInvoicePaymentFailed(evt: Stripe.Event) { 69 | console.log('StripeFixedPriceService invoice.payment_failed: ', evt); 70 | const dataObject = evt.data.object as Stripe.Invoice; 71 | // If the payment fails or the customer does not have a valid payment method, 72 | // an invoice.payment_failed event is sent, the subscription becomes past_due. 73 | // Use this webhook to notify your user that their payment has 74 | // failed and to retrieve new card details. 75 | return true; 76 | } 77 | 78 | @StripeWebhookHandler('invoice.finalized') 79 | handleInvoiceFinalized(evt: Stripe.Event) { 80 | console.log('StripeFixedPriceService invoice.finalized: ', evt); 81 | const dataObject = evt.data.object as Stripe.Invoice; 82 | // If you want to manually send out invoices to your customers 83 | // or store them locally to reference to avoid hitting Stripe rate limits. 84 | return true; 85 | } 86 | 87 | @StripeWebhookHandler('customer.subscription.deleted') 88 | handleCustomerSubsDeleted(evt: Stripe.Event) { 89 | console.log('StripeFixedPriceService customer.subscription.deleted: ', evt); 90 | const dataObject = evt.data.object as Stripe.Subscription; 91 | if (evt.request != null) { 92 | // handle a subscription cancelled by your request 93 | // from above. 94 | } else { 95 | // handle subscription cancelled automatically based 96 | // upon your subscription settings. 97 | } 98 | return true; 99 | } 100 | 101 | @StripeWebhookHandler('customer.subscription.trial_will_end') 102 | handleCustomerSubsTrialEnd(evt: Stripe.Event) { 103 | console.log( 104 | 'StripeFixedPriceService customer.subscription.trial_will_end: ', 105 | evt, 106 | ); 107 | const dataObject = evt.data.object as Stripe.Subscription; 108 | // Send notification to your user that the trial will end 109 | return true; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/stripe-metered-usage/dto/cancel-subscription.dto.ts: -------------------------------------------------------------------------------- 1 | export class MUCancelSubscriptionDto { 2 | subscriptionId: string; 3 | } 4 | -------------------------------------------------------------------------------- /src/stripe-metered-usage/dto/create-subscription.dto.ts: -------------------------------------------------------------------------------- 1 | export class MUCreateSubscriptionDto { 2 | paymentMethodId: string; 3 | customerId: string; 4 | priceId: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/stripe-metered-usage/dto/retrieveCustomerPM.dto.ts: -------------------------------------------------------------------------------- 1 | export class MURetrieveCustomerPM { 2 | paymentMethodId: string; 3 | } 4 | -------------------------------------------------------------------------------- /src/stripe-metered-usage/dto/retry-invoice.dto.ts: -------------------------------------------------------------------------------- 1 | export class MURetryInvoice { 2 | customerId: string; 3 | paymentMethodId: string; 4 | invoiceId: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/stripe-metered-usage/dto/retry-upcoming-invoice.dto.ts: -------------------------------------------------------------------------------- 1 | export class MURetryUpcomingInvoice { 2 | subscriptionId: string; 3 | customerId: string; 4 | newPriceId: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/stripe-metered-usage/dto/update-subscription.dto.ts: -------------------------------------------------------------------------------- 1 | export class MUUpdateSubscriptionDto { 2 | subscriptionId: string; 3 | newPriceId: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/stripe-metered-usage/stripe-metered-usage.controller.ts: -------------------------------------------------------------------------------- 1 | import { InjectStripeClient } from '@golevelup/nestjs-stripe'; 2 | import { Body, Controller, Get, Post } from '@nestjs/common'; 3 | import { ConfigService } from '@nestjs/config'; 4 | import Stripe from 'stripe'; 5 | import { CreateCustomerDto } from '../stripe-fixed-price/dto/create-customer.dto'; 6 | import { MUCancelSubscriptionDto } from './dto/cancel-subscription.dto'; 7 | import { MUCreateSubscriptionDto } from './dto/create-subscription.dto'; 8 | import { MURetrieveCustomerPM } from './dto/retrieveCustomerPM.dto'; 9 | import { MURetryInvoice } from './dto/retry-invoice.dto'; 10 | import { MURetryUpcomingInvoice } from './dto/retry-upcoming-invoice.dto'; 11 | import { MUUpdateSubscriptionDto } from './dto/update-subscription.dto'; 12 | 13 | // REF: https://github.com/stripe-samples/subscription-use-cases/blob/master/usage-based-subscriptions/server/node/server.js 14 | 15 | @Controller('stripe-metered-usage') 16 | export class StripeMeteredUsageController { 17 | constructor( 18 | private readonly configSvc: ConfigService, 19 | @InjectStripeClient() private stripeClient: Stripe, 20 | ) {} 21 | 22 | // TODO: Implement with Config 23 | @Get('setup') 24 | getSetup() { 25 | return { 26 | publishableKey: this.configSvc.get('stripe.publishablekey}'), 27 | }; 28 | } 29 | 30 | @Post('create-customer') 31 | async createCustomer(@Body() { email }: CreateCustomerDto) { 32 | try { 33 | // Create a new customer object 34 | const customer = await this.stripeClient.customers.create({ 35 | email, 36 | }); 37 | 38 | // Save the customer.id in your database alongside your user. 39 | // We're simulating authentication with a cookie. 40 | // res.cookie('customer', customer.id, { maxAge: 900000, httpOnly: true }); 41 | 42 | return { 43 | customer, 44 | }; 45 | } catch (error) { 46 | console.log(error); 47 | throw error; 48 | } 49 | } 50 | 51 | // TODO: implement retriving of customerID from DB or Auth 52 | @Post('create-subscription') 53 | async createSubscription( 54 | @Body() { paymentMethodId, customerId, priceId }: MUCreateSubscriptionDto, 55 | ) { 56 | // Set the default payment method on the customer 57 | try { 58 | await this.stripeClient.paymentMethods.attach(paymentMethodId, { 59 | customer: customerId, 60 | }); 61 | } catch (error) { 62 | throw error; 63 | } 64 | 65 | let updateCustomerDefaultPaymentMethod = 66 | await this.stripeClient.customers.update(customerId, { 67 | invoice_settings: { 68 | default_payment_method: paymentMethodId, 69 | }, 70 | }); 71 | 72 | // Create the subscription 73 | const subscription = await this.stripeClient.subscriptions.create({ 74 | customer: customerId, 75 | // TODO: this dynamic 'getter' of configSvc is a security issue 76 | // The plans should not come from the env, get them from the database for example 77 | items: [ 78 | { price: this.configSvc.get(`stripeMeteredUsage.${priceId}`) }, 79 | ], 80 | expand: ['latest_invoice.payment_intent', 'pending_setup_intent'], 81 | }); 82 | 83 | return subscription; 84 | } 85 | 86 | @Post('retry-invoice') 87 | async retrySubscription( 88 | @Body() { customerId, paymentMethodId, invoiceId }: MURetryInvoice, 89 | ) { 90 | // Set the default payment method on the customer 91 | 92 | try { 93 | await this.stripeClient.paymentMethods.attach(paymentMethodId, { 94 | customer: customerId, 95 | }); 96 | await this.stripeClient.customers.update(customerId, { 97 | invoice_settings: { 98 | default_payment_method: paymentMethodId, 99 | }, 100 | }); 101 | } catch (error) { 102 | // in case card_decline error 103 | throw error; // .status('402') 104 | } 105 | 106 | const invoice = await this.stripeClient.invoices.retrieve(invoiceId, { 107 | expand: ['payment_intent'], 108 | }); 109 | return invoice; 110 | } 111 | 112 | @Post('retrieve-upcoming-invoice') 113 | async retrieveIncomingInvoice( 114 | @Body() { subscriptionId, customerId, newPriceId }: MURetryUpcomingInvoice, 115 | ) { 116 | const subscription = await this.stripeClient.subscriptions.retrieve( 117 | subscriptionId, 118 | ); 119 | 120 | const invoice = await this.stripeClient.invoices.retrieveUpcoming({ 121 | // INFO: No longer exist. 122 | // was renamed to 'subscription_proration_behavior' with an enum type 123 | // subscription_prorate: true, 124 | subscription_proration_behavior: 'always_invoice', 125 | customer: customerId, 126 | subscription: subscriptionId, 127 | subscription_items: [ 128 | { 129 | id: subscription.items.data[0].id, 130 | clear_usage: true, 131 | deleted: true, 132 | }, 133 | { 134 | price: this.configSvc.get(`stripeMeteredUsage.${newPriceId}`), 135 | deleted: false, 136 | }, 137 | ], 138 | }); 139 | return invoice; 140 | } 141 | 142 | @Post('cancel-subscription') 143 | async cancelSubscription( 144 | @Body() { subscriptionId }: MUCancelSubscriptionDto, 145 | ) { 146 | // Cancel the subscription 147 | try { 148 | const deletedSubscription = await this.stripeClient.subscriptions.del( 149 | subscriptionId, 150 | ); 151 | 152 | return { subscription: deletedSubscription }; 153 | } catch (error) { 154 | throw error; 155 | } 156 | } 157 | 158 | // TODO: Implement with Config 159 | @Post('update-subscription') 160 | async updateSubscription( 161 | @Body() { subscriptionId, newPriceId }: MUUpdateSubscriptionDto, 162 | ) { 163 | try { 164 | const subscription = await this.stripeClient.subscriptions.retrieve( 165 | subscriptionId, 166 | ); 167 | const updatedSubscription = await this.stripeClient.subscriptions.update( 168 | subscriptionId, 169 | { 170 | cancel_at_period_end: false, 171 | items: [ 172 | { 173 | id: subscription.items.data[0].id, 174 | price: this.configSvc.get( 175 | `stripeMeteredUsage.${newPriceId}`, 176 | ), 177 | }, 178 | ], 179 | }, 180 | ); 181 | 182 | return updatedSubscription; 183 | } catch (error) { 184 | throw error; 185 | } 186 | } 187 | 188 | @Post('retrieve-customer-payment-method') 189 | async retrieveCustomerPaymentMethod( 190 | @Body() { paymentMethodId }: MURetrieveCustomerPM, 191 | ) { 192 | const paymentMethod = await this.stripeClient.paymentMethods.retrieve( 193 | paymentMethodId, 194 | ); 195 | 196 | return paymentMethod; 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /src/stripe-metered-usage/stripe-metered-usage.module.ts: -------------------------------------------------------------------------------- 1 | import { StripeModule } from '@golevelup/nestjs-stripe'; 2 | import { 3 | applyRawBodyOnlyTo, 4 | JsonBodyMiddleware, 5 | RawBodyMiddleware, 6 | } from '@golevelup/nestjs-webhooks'; 7 | import { 8 | MiddlewareConsumer, 9 | Module, 10 | NestModule, 11 | RequestMethod, 12 | } from '@nestjs/common'; 13 | import { ConfigModule, ConfigService } from '@nestjs/config'; 14 | import { config } from 'dotenv'; 15 | import { StripeMeteredUsageController } from './stripe-metered-usage.controller'; 16 | import { StripeMeteredUsageService } from './stripe-metered-usage.service'; 17 | config(); 18 | 19 | @Module({ 20 | imports: [ 21 | JsonBodyMiddleware, 22 | RawBodyMiddleware, 23 | StripeModule.forRootAsync(StripeModule, { 24 | imports: [ConfigModule], 25 | useFactory: (configSvc: ConfigService) => ({ 26 | apiKey: configSvc.get('stripe.apiKey'), 27 | webhookConfig: { 28 | stripeWebhookSecret: configSvc.get('stripe.webhookSecret'), 29 | }, 30 | }), 31 | inject: [ConfigService], 32 | }), 33 | ], 34 | controllers: [StripeMeteredUsageController], 35 | providers: [StripeMeteredUsageService], 36 | }) 37 | export class StripeMeteredUsageModule implements NestModule { 38 | configure(consumer: MiddlewareConsumer) { 39 | applyRawBodyOnlyTo(consumer, { 40 | method: RequestMethod.ALL, 41 | path: 'stripe/webhook', 42 | }); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/stripe-metered-usage/stripe-metered-usage.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | InjectStripeClient, 3 | StripeWebhookHandler, 4 | } from '@golevelup/nestjs-stripe'; 5 | import { Injectable } from '@nestjs/common'; 6 | import Stripe from 'stripe'; 7 | 8 | @Injectable() 9 | export class StripeMeteredUsageService { 10 | constructor(@InjectStripeClient() private stripeClient: Stripe) {} 11 | 12 | /* **************************************** 13 | * STRIPE WebHooks 14 | * ****************************************/ 15 | 16 | @StripeWebhookHandler('invoice.paid') 17 | handleInvoicePaid(evt: Stripe.Event) { 18 | console.log('StripeMeteredUsageService invoice.paid: ', evt); 19 | const dataObject = evt.data.object as Stripe.Invoice; 20 | console.log(`🔔 Invoice paid!`); 21 | // Used to provision services after the trial has ended. 22 | // The status of the invoice will show up as paid. Store the status in your 23 | // database to reference when a user accesses your service to avoid hitting rate limits. 24 | return true; 25 | } 26 | 27 | @StripeWebhookHandler('invoice.payment_failed') 28 | handleInvoicePaymentFailed(evt: Stripe.Event) { 29 | console.log('StripeMeteredUsageService invoice.payment_failed: ', evt); 30 | const dataObject = evt.data.object as Stripe.Invoice; 31 | // If the payment fails or the customer does not have a valid payment method, 32 | // an invoice.payment_failed event is sent, the subscription becomes past_due. 33 | // Use this webhook to notify your user that their payment has 34 | // failed and to retrieve new card details. 35 | return true; 36 | } 37 | 38 | @StripeWebhookHandler('invoice.finalized') 39 | handleInvoiceFinalized(evt: Stripe.Event) { 40 | console.log('StripeMeteredUsageService invoice.finalized: ', evt); 41 | const dataObject = evt.data.object as Stripe.Invoice; 42 | // If you want to manually send out invoices to your customers 43 | // or store them locally to reference to avoid hitting Stripe rate limits. 44 | return true; 45 | } 46 | 47 | @StripeWebhookHandler('customer.subscription.deleted') 48 | handleCustomerSubsDeleted(evt: Stripe.Event) { 49 | console.log( 50 | 'StripeMeteredUsageService customer.subscription.deleted: ', 51 | evt, 52 | ); 53 | const dataObject = evt.data.object as Stripe.Subscription; 54 | if (evt.request != null) { 55 | // handle a subscription cancelled by your request 56 | // from above. 57 | } else { 58 | // handle subscription cancelled automatically based 59 | // upon your subscription settings. 60 | } 61 | return true; 62 | } 63 | 64 | @StripeWebhookHandler('customer.subscription.trial_will_end') 65 | handleCustomerSubsTrialEnd(evt: Stripe.Event) { 66 | console.log( 67 | 'StripeMeteredUsageService customer.subscription.trial_will_end: ', 68 | evt, 69 | ); 70 | const dataObject = evt.data.object as Stripe.Subscription; 71 | // Send notification to your user that the trial will end 72 | return true; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/stripe-multi-plan/dto/create-customer.dto.ts: -------------------------------------------------------------------------------- 1 | export class MPCreateCustomerDto { 2 | email: string; 3 | payment_method: string; 4 | priceIds: string[]; 5 | } 6 | -------------------------------------------------------------------------------- /src/stripe-multi-plan/dto/get-subscription.dto.ts: -------------------------------------------------------------------------------- 1 | export class MPGetSubscriptionDto { 2 | subscriptionId: string; 3 | } 4 | -------------------------------------------------------------------------------- /src/stripe-multi-plan/stripe-multiple-plan.controller.ts: -------------------------------------------------------------------------------- 1 | import { InjectStripeClient } from '@golevelup/nestjs-stripe'; 2 | import { Body, Controller, Get, Post, Query } from '@nestjs/common'; 3 | import { ConfigService } from '@nestjs/config'; 4 | import Stripe from 'stripe'; 5 | import { MPCreateCustomerDto } from './dto/create-customer.dto'; 6 | import { MPGetSubscriptionDto } from './dto/get-subscription.dto'; 7 | 8 | // REF: https://github.com/stripe-samples/charging-for-multiple-plan-subscriptions/blob/master/server/node/server.js 9 | 10 | @Controller('stripe-multiple-plan') 11 | export class StripeMultiplePlanController { 12 | constructor( 13 | private readonly configSvc: ConfigService, 14 | @InjectStripeClient() private stripeClient: Stripe, 15 | ) {} 16 | 17 | @Get('setup') 18 | async getSetup() { 19 | const animals = this.configSvc 20 | .get('stripeMultiPlan.animals') 21 | .split(','); 22 | 23 | const lookup_keys = []; 24 | animals.forEach((animal) => lookup_keys.push(animal + '-monthly-usd')); 25 | 26 | const prices = await this.stripeClient.prices.list({ 27 | lookup_keys: lookup_keys, 28 | expand: ['data.product'], 29 | }); 30 | 31 | const products = []; 32 | prices.data.forEach((price) => { 33 | // INFO: Type checking 34 | if (typeof price.product === 'string') { 35 | throw new Error('Product not expanded'); 36 | } 37 | if (!('metadata' in price.product)) { 38 | throw new Error('Product not expanded'); 39 | } 40 | const product = { 41 | price: { id: price.id, unit_amount: price.unit_amount }, 42 | title: price.product.metadata.title, 43 | emoji: price.product.metadata.emoji, 44 | }; 45 | products.push(product); 46 | }); 47 | 48 | return { 49 | publicKey: this.configSvc.get('stripe.publishablekey'), 50 | minProductsForDiscount: this.configSvc.get( 51 | 'stripeMultiPlan.minProdDiscount', 52 | ), 53 | discountFactor: this.configSvc.get( 54 | 'stripeMultiPlan.discountFactor', 55 | ), 56 | products: products, 57 | }; 58 | } 59 | 60 | @Post('create-customer') 61 | async createCustomer( 62 | @Body() { payment_method, email, priceIds }: MPCreateCustomerDto, 63 | ) { 64 | // This creates a new Customer and attaches 65 | // the PaymentMethod to be default for invoice in one API call. 66 | const customer = await this.stripeClient.customers.create({ 67 | payment_method: payment_method, 68 | email: email, 69 | invoice_settings: { 70 | default_payment_method: payment_method, 71 | }, 72 | }); 73 | 74 | // In this example, we apply the coupon if the number of plans purchased 75 | // meets or exceeds the threshold. 76 | const minProdDiscount = Number( 77 | this.configSvc.get('stripeMultiPlan.minProdDiscount'), 78 | ); 79 | const eligibleForDiscount = priceIds.length >= minProdDiscount; 80 | const coupon = eligibleForDiscount 81 | ? this.configSvc.get('stripeMultiPlan.couponId') 82 | : null; 83 | 84 | // At this point, associate the ID of the Customer object with your 85 | // own internal representation of a customer, if you have one. 86 | const subscription = await this.stripeClient.subscriptions.create({ 87 | customer: customer.id, 88 | items: priceIds.map((priceId) => { 89 | return { price: priceId }; 90 | }), 91 | expand: ['latest_invoice.payment_intent'], 92 | coupon: coupon, 93 | }); 94 | 95 | subscription; 96 | } 97 | 98 | @Get('subscription') 99 | getSubscription(@Query() { subscriptionId }: MPGetSubscriptionDto) { 100 | return this.stripeClient.subscriptions.retrieve(subscriptionId); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/stripe-multi-plan/stripe-multiple-plan.module.ts: -------------------------------------------------------------------------------- 1 | import { StripeModule } from '@golevelup/nestjs-stripe'; 2 | import { 3 | applyRawBodyOnlyTo, 4 | JsonBodyMiddleware, 5 | RawBodyMiddleware, 6 | } from '@golevelup/nestjs-webhooks'; 7 | import { 8 | MiddlewareConsumer, 9 | Module, 10 | NestModule, 11 | RequestMethod, 12 | } from '@nestjs/common'; 13 | import { ConfigModule, ConfigService } from '@nestjs/config'; 14 | import { config } from 'dotenv'; 15 | import { StripeMultiplePlanController } from './stripe-multiple-plan.controller'; 16 | import { StripeMultiplePlanService } from './stripe-multiple-plan.service'; 17 | config(); 18 | 19 | @Module({ 20 | imports: [ 21 | JsonBodyMiddleware, 22 | RawBodyMiddleware, 23 | StripeModule.forRootAsync(StripeModule, { 24 | imports: [ConfigModule], 25 | useFactory: (configSvc: ConfigService) => ({ 26 | apiKey: configSvc.get('stripe.apiKey'), 27 | webhookConfig: { 28 | stripeWebhookSecret: configSvc.get('stripe.webhookSecret'), 29 | }, 30 | }), 31 | inject: [ConfigService], 32 | }), 33 | ], 34 | controllers: [StripeMultiplePlanController], 35 | providers: [StripeMultiplePlanService], 36 | }) 37 | export class StripeMultiPlanModule implements NestModule { 38 | configure(consumer: MiddlewareConsumer) { 39 | applyRawBodyOnlyTo(consumer, { 40 | method: RequestMethod.ALL, 41 | path: 'stripe/webhook', 42 | }); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/stripe-multi-plan/stripe-multiple-plan.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | InjectStripeClient, 3 | StripeWebhookHandler, 4 | } from '@golevelup/nestjs-stripe'; 5 | import { Injectable } from '@nestjs/common'; 6 | import Stripe from 'stripe'; 7 | 8 | @Injectable() 9 | export class StripeMultiplePlanService { 10 | constructor(@InjectStripeClient() private stripeClient: Stripe) {} 11 | 12 | /* **************************************** 13 | * STRIPE WebHooks 14 | * ****************************************/ 15 | 16 | @StripeWebhookHandler('customer.updated') 17 | handleCustomerUpdated(evt: Stripe.Event) { 18 | console.log('StripeMultiplePlanService customer.updated: ', evt); 19 | const dataObject = evt.data.object as Stripe.Customer; 20 | return true; 21 | } 22 | 23 | @StripeWebhookHandler('customer.subscription.created') 24 | handleCustomerSubsCreated(evt: Stripe.Event) { 25 | console.log( 26 | 'StripeMultiplePlanService customer.subscription.created: ', 27 | evt, 28 | ); 29 | const dataObject = evt.data.object as Stripe.Subscription; 30 | return true; 31 | } 32 | 33 | @StripeWebhookHandler('customer.created') 34 | handleCustomerCreated(evt: Stripe.Event) { 35 | console.log('StripeMultiplePlanService customer.created: ', evt); 36 | const dataObject = evt.data.object as Stripe.Customer; 37 | 38 | return true; 39 | } 40 | 41 | @StripeWebhookHandler('invoice.upcoming') 42 | handleInvoiceUpcoming(evt: Stripe.Event) { 43 | console.log('StripeMultiplePlanService invoice.upcoming: ', evt); 44 | const dataObject = evt.data.object as Stripe.Invoice; 45 | return true; 46 | } 47 | 48 | @StripeWebhookHandler('invoice.created') 49 | handleInvoiceCreated(evt: Stripe.Event) { 50 | console.log('StripeMultiplePlanService invoice.created: ', evt); 51 | const dataObject = evt.data.object as Stripe.Invoice; 52 | return true; 53 | } 54 | 55 | @StripeWebhookHandler('invoice.finalized') 56 | handleInvoiceFinalized(evt: Stripe.Event) { 57 | console.log('StripeMultiplePlanService invoice.finalized: ', evt); 58 | const dataObject = evt.data.object as Stripe.Invoice; 59 | // If you want to manually send out invoices to your customers 60 | // or store them locally to reference to avoid hitting Stripe rate limits. 61 | return true; 62 | } 63 | 64 | @StripeWebhookHandler('invoice.payment_succeeded') 65 | async handleInvoicePaymentSucceeded(evt: Stripe.Event) { 66 | console.log('StripeMultiplePlanService invoice.payment_succeeded: ', evt); 67 | const dataObject = evt.data.object as Stripe.Invoice; 68 | 69 | // If you want to manually send out invoices to your customers 70 | // or store them locally to reference to avoid hitting Stripe rate limits. 71 | return true; 72 | } 73 | 74 | @StripeWebhookHandler('invoice.payment_failed') 75 | handleInvoicePaymentFailed(evt: Stripe.Event) { 76 | console.log('StripeMultiplePlanService invoice.payment_failed: ', evt); 77 | const dataObject = evt.data.object as Stripe.Invoice; 78 | // If the payment fails or the customer does not have a valid payment method, 79 | // an invoice.payment_failed event is sent, the subscription becomes past_due. 80 | // Use this webhook to notify your user that their payment has 81 | // failed and to retrieve new card details. 82 | return true; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/stripe-per-seat/dto/cancel-subscription.dto.ts: -------------------------------------------------------------------------------- 1 | export class PSCancelSubscriptionDto { 2 | subscriptionId: string; 3 | } 4 | -------------------------------------------------------------------------------- /src/stripe-per-seat/dto/create-customer.dto.ts: -------------------------------------------------------------------------------- 1 | export class PSCreateCustomerDto { 2 | email: string; 3 | } 4 | -------------------------------------------------------------------------------- /src/stripe-per-seat/dto/create-subscription.dto.ts: -------------------------------------------------------------------------------- 1 | export class PSCreateSubscriptionDto { 2 | paymentMethodId: string; 3 | customerId: string; 4 | priceId: string; 5 | quantity: number; 6 | } 7 | -------------------------------------------------------------------------------- /src/stripe-per-seat/dto/retrieve-subs-info.dto.ts: -------------------------------------------------------------------------------- 1 | export class PSRetrieveSubsInfoDto { 2 | subscriptionId: string; 3 | } 4 | -------------------------------------------------------------------------------- /src/stripe-per-seat/dto/retry-invoice.dto.ts: -------------------------------------------------------------------------------- 1 | export class PSRetryInvoice { 2 | customerId: string; 3 | paymentMethodId: string; 4 | invoiceId: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/stripe-per-seat/dto/retry-upcoming-invoice.dto.ts: -------------------------------------------------------------------------------- 1 | export class PSRetryUpcomingInvoice { 2 | subscriptionId: string; 3 | customerId: string; 4 | newPriceId: string; 5 | quantity: number; 6 | } 7 | -------------------------------------------------------------------------------- /src/stripe-per-seat/dto/update-subscription.dto.ts: -------------------------------------------------------------------------------- 1 | export class PSUpdateSubscriptionDto { 2 | subscriptionId: string; 3 | newPriceId: string; 4 | quantity: number; 5 | } 6 | -------------------------------------------------------------------------------- /src/stripe-per-seat/stripe-per-seat.controller.ts: -------------------------------------------------------------------------------- 1 | import { InjectStripeClient } from '@golevelup/nestjs-stripe'; 2 | import { Body, Controller, Get, Post } from '@nestjs/common'; 3 | import { ConfigService } from '@nestjs/config'; 4 | import Stripe from 'stripe'; 5 | import { PSCancelSubscriptionDto } from './dto/cancel-subscription.dto'; 6 | import { PSCreateCustomerDto } from './dto/create-customer.dto'; 7 | import { PSCreateSubscriptionDto } from './dto/create-subscription.dto'; 8 | import { PSRetrieveSubsInfoDto } from './dto/retrieve-subs-info.dto'; 9 | import { PSRetryInvoice } from './dto/retry-invoice.dto'; 10 | import { PSRetryUpcomingInvoice } from './dto/retry-upcoming-invoice.dto'; 11 | import { PSUpdateSubscriptionDto } from './dto/update-subscription.dto'; 12 | 13 | // REF: https://github.com/stripe-samples/subscription-use-cases/blob/master/per-seat-subscriptions/server/node/server.js 14 | 15 | @Controller('stripe-per-seat') 16 | export class StripePerSeatController { 17 | constructor( 18 | private readonly configSvc: ConfigService, 19 | @InjectStripeClient() private stripeClient: Stripe, 20 | ) {} 21 | 22 | // TODO: Implement with Config 23 | @Get('setup') 24 | getSetup() { 25 | return { 26 | publishableKey: this.configSvc.get('stripe.publishablekey}'), 27 | }; 28 | } 29 | 30 | @Post('retrieve-subscription-information') 31 | async retrieveSubsInfo(@Body() { subscriptionId }: PSRetrieveSubsInfoDto) { 32 | const subscription = await this.stripeClient.subscriptions.retrieve( 33 | subscriptionId, 34 | { 35 | expand: [ 36 | 'latest_invoice', 37 | 'customer.invoice_settings.default_payment_method', 38 | 'items.data.price.product', 39 | ], 40 | }, 41 | ); 42 | 43 | const upcoming_invoice = await this.stripeClient.invoices.retrieveUpcoming({ 44 | subscription: subscriptionId, 45 | }); 46 | 47 | const item = subscription.items.data[0]; 48 | 49 | // INFO: Type checking of the type unions. Because of the expand. 50 | if (typeof subscription.customer === 'string') { 51 | throw new Error('Customer not expanded'); 52 | } 53 | if (!('invoice_settings' in subscription.customer)) { 54 | throw new Error('Customer not expanded'); 55 | } 56 | 57 | if ( 58 | typeof subscription.customer.invoice_settings.default_payment_method === 59 | 'string' 60 | ) { 61 | throw new Error('Default_payment_method not expanded'); 62 | } 63 | 64 | if (typeof item.price.product === 'string') { 65 | throw new Error('Product not expanded'); 66 | } 67 | if (!('name' in item.price.product)) { 68 | throw new Error('Product not expanded'); 69 | } 70 | 71 | return { 72 | card: subscription.customer.invoice_settings.default_payment_method.card, 73 | product_description: item.price.product.name, 74 | current_price: item.price.id, 75 | current_quantity: item.quantity, 76 | latest_invoice: subscription.latest_invoice, 77 | upcoming_invoice: upcoming_invoice, 78 | }; 79 | } 80 | 81 | @Post('create-customer') 82 | async createCustomer(@Body() { email }: PSCreateCustomerDto) { 83 | try { 84 | // Create a new customer object 85 | const customer = await this.stripeClient.customers.create({ 86 | email, 87 | }); 88 | 89 | // save the customer.id as stripeCustomerId 90 | // in your database. 91 | 92 | return { 93 | customer, 94 | }; 95 | } catch (error) { 96 | console.log(error); 97 | throw error; 98 | } 99 | } 100 | 101 | // TODO: implement retriving of customerID from DB or Auth 102 | @Post('create-subscription') 103 | async createSubscription( 104 | @Body() 105 | { paymentMethodId, customerId, priceId, quantity }: PSCreateSubscriptionDto, 106 | ) { 107 | // Set the default payment method on the customer 108 | try { 109 | const payment_method = await this.stripeClient.paymentMethods.attach( 110 | paymentMethodId, 111 | { 112 | customer: customerId, 113 | }, 114 | ); 115 | 116 | await this.stripeClient.customers.update(customerId, { 117 | invoice_settings: { 118 | default_payment_method: payment_method.id, 119 | }, 120 | }); 121 | 122 | // Create the subscription 123 | const subscription = await this.stripeClient.subscriptions.create({ 124 | customer: customerId, 125 | items: [ 126 | { 127 | price: this.configSvc.get(`stripePerSeat.${priceId}`), 128 | quantity: quantity, 129 | }, 130 | ], 131 | expand: ['latest_invoice.payment_intent', 'plan.product'], 132 | }); 133 | 134 | return subscription; 135 | } catch (error) { 136 | // return res.status(400).send({ error: { message: error.message } }); 137 | throw error; 138 | } 139 | } 140 | 141 | @Post('retry-invoice') 142 | async retrySubscription( 143 | @Body() { customerId, paymentMethodId, invoiceId }: PSRetryInvoice, 144 | ) { 145 | // Set the default payment method on the customer 146 | 147 | try { 148 | const payment_method = await this.stripeClient.paymentMethods.attach( 149 | paymentMethodId, 150 | { 151 | customer: customerId, 152 | }, 153 | ); 154 | await this.stripeClient.customers.update(customerId, { 155 | invoice_settings: { 156 | default_payment_method: payment_method.id, 157 | }, 158 | }); 159 | } catch (error) { 160 | // in case card_decline error 161 | throw error; 162 | /* return res 163 | .status(400) 164 | .send({ result: { error: { message: error.message } } }); */ 165 | } 166 | 167 | const invoice = await this.stripeClient.invoices.retrieve(invoiceId, { 168 | expand: ['payment_intent'], 169 | }); 170 | return invoice; 171 | } 172 | 173 | @Post('retrieve-upcoming-invoice') 174 | async retrieveIncomingInvoice( 175 | @Body() 176 | { 177 | subscriptionId, 178 | customerId, 179 | newPriceId, 180 | quantity, 181 | }: PSRetryUpcomingInvoice, 182 | ) { 183 | const new_price = this.configSvc.get(`stripePerSeat.${newPriceId}`); 184 | 185 | const params = {}; 186 | params['customer'] = customerId; 187 | let subscription; 188 | 189 | if (subscriptionId != null) { 190 | params['subscription'] = subscriptionId; 191 | subscription = await this.stripeClient.subscriptions.retrieve( 192 | subscriptionId, 193 | ); 194 | 195 | const current_price = subscription.items.data[0].price.id; 196 | 197 | if (current_price == new_price) { 198 | params['subscription_items'] = [ 199 | { 200 | id: subscription.items.data[0].id, 201 | quantity: quantity, 202 | }, 203 | ]; 204 | } else { 205 | params['subscription_items'] = [ 206 | { 207 | id: subscription.items.data[0].id, 208 | deleted: true, 209 | }, 210 | { 211 | price: new_price, 212 | quantity: quantity, 213 | }, 214 | ]; 215 | } 216 | } else { 217 | params['subscription_items'] = [ 218 | { 219 | price: new_price, 220 | quantity: quantity, 221 | }, 222 | ]; 223 | } 224 | 225 | const invoice = await this.stripeClient.invoices.retrieveUpcoming(params); 226 | 227 | let response = {}; 228 | 229 | if (subscriptionId != null) { 230 | const current_period_end = subscription.current_period_end; 231 | let immediate_total = 0; 232 | let next_invoice_sum = 0; 233 | 234 | invoice.lines.data.forEach((invoiceLineItem) => { 235 | if (invoiceLineItem.period.end == current_period_end) { 236 | immediate_total += invoiceLineItem.amount; 237 | } else { 238 | next_invoice_sum += invoiceLineItem.amount; 239 | } 240 | }); 241 | 242 | response = { 243 | immediate_total: immediate_total, 244 | next_invoice_sum: next_invoice_sum, 245 | invoice: invoice, 246 | }; 247 | } else { 248 | response = { 249 | invoice: invoice, 250 | }; 251 | } 252 | 253 | return response; 254 | } 255 | 256 | @Post('cancel-subscription') 257 | async cancelSubscription( 258 | @Body() { subscriptionId }: PSCancelSubscriptionDto, 259 | ) { 260 | // Cancel the subscription 261 | try { 262 | const deletedSubscription = await this.stripeClient.subscriptions.del( 263 | subscriptionId, 264 | ); 265 | 266 | return { subscription: deletedSubscription }; 267 | } catch (error) { 268 | throw error; 269 | } 270 | } 271 | 272 | // TODO: Implement with Config 273 | @Post('update-subscription') 274 | async updateSubscription( 275 | @Body() { subscriptionId, newPriceId, quantity }: PSUpdateSubscriptionDto, 276 | ) { 277 | const subscription = await this.stripeClient.subscriptions.retrieve( 278 | subscriptionId, 279 | ); 280 | 281 | const current_price = subscription.items.data[0].price.id; 282 | const new_price = this.configSvc.get(`stripePerSeat.${newPriceId}`); 283 | 284 | let updatedSubscription; 285 | 286 | if (current_price == new_price) { 287 | updatedSubscription = await this.stripeClient.subscriptions.update( 288 | subscriptionId, 289 | { 290 | items: [ 291 | { 292 | id: subscription.items.data[0].id, 293 | quantity: quantity, 294 | }, 295 | ], 296 | }, 297 | ); 298 | } else { 299 | updatedSubscription = await this.stripeClient.subscriptions.update( 300 | subscriptionId, 301 | { 302 | items: [ 303 | { 304 | id: subscription.items.data[0].id, 305 | deleted: true, 306 | }, 307 | { 308 | price: new_price, 309 | quantity: quantity, 310 | }, 311 | ], 312 | expand: ['plan.product'], 313 | }, 314 | ); 315 | } 316 | 317 | // type checking the unions 318 | if (typeof subscription.customer !== 'string') { 319 | throw new Error('Customer should be of type string'); 320 | } 321 | 322 | const invoice = await this.stripeClient.invoices.create({ 323 | customer: subscription.customer, 324 | subscription: subscription.id, 325 | description: 326 | 'Change to ' + 327 | quantity + 328 | ' seat(s) on the ' + 329 | updatedSubscription.plan.product.name + 330 | ' plan', 331 | }); 332 | 333 | await this.stripeClient.invoices.pay(invoice.id); 334 | return { subscription: updatedSubscription }; 335 | } 336 | } 337 | -------------------------------------------------------------------------------- /src/stripe-per-seat/stripe-per-seat.module.ts: -------------------------------------------------------------------------------- 1 | import { StripeModule } from '@golevelup/nestjs-stripe'; 2 | import { 3 | applyRawBodyOnlyTo, 4 | JsonBodyMiddleware, 5 | RawBodyMiddleware, 6 | } from '@golevelup/nestjs-webhooks'; 7 | import { 8 | MiddlewareConsumer, 9 | Module, 10 | NestModule, 11 | RequestMethod, 12 | } from '@nestjs/common'; 13 | import { ConfigModule, ConfigService } from '@nestjs/config'; 14 | import { config } from 'dotenv'; 15 | import { StripePerSeatController } from './stripe-per-seat.controller'; 16 | import { StripePerSeatService } from './stripe-per-seat.service'; 17 | config(); 18 | 19 | @Module({ 20 | imports: [ 21 | JsonBodyMiddleware, 22 | RawBodyMiddleware, 23 | StripeModule.forRootAsync(StripeModule, { 24 | imports: [ConfigModule], 25 | useFactory: (configSvc: ConfigService) => ({ 26 | apiKey: configSvc.get('stripe.apiKey'), 27 | webhookConfig: { 28 | stripeWebhookSecret: configSvc.get('stripe.webhookSecret'), 29 | }, 30 | }), 31 | inject: [ConfigService], 32 | }), 33 | ], 34 | controllers: [StripePerSeatController], 35 | providers: [StripePerSeatService], 36 | }) 37 | export class StripePerSeatModule implements NestModule { 38 | configure(consumer: MiddlewareConsumer) { 39 | applyRawBodyOnlyTo(consumer, { 40 | method: RequestMethod.ALL, 41 | path: 'stripe/webhook', 42 | }); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/stripe-per-seat/stripe-per-seat.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | InjectStripeClient, 3 | StripeWebhookHandler, 4 | } from '@golevelup/nestjs-stripe'; 5 | import { Injectable } from '@nestjs/common'; 6 | import Stripe from 'stripe'; 7 | 8 | @Injectable() 9 | export class StripePerSeatService { 10 | constructor(@InjectStripeClient() private stripeClient: Stripe) {} 11 | 12 | /* **************************************** 13 | * STRIPE WebHooks 14 | * ****************************************/ 15 | 16 | @StripeWebhookHandler('invoice.paid') 17 | handleInvoicePaid(evt: Stripe.Event) { 18 | console.log('StripePerSeatService invoice.paid: ', evt); 19 | const dataObject = evt.data.object as Stripe.Invoice; 20 | console.log(`🔔 Invoice paid!`); 21 | // Used to provision services after the trial has ended. 22 | // The status of the invoice will show up as paid. Store the status in your 23 | // database to reference when a user accesses your service to avoid hitting rate limits. 24 | return true; 25 | } 26 | 27 | @StripeWebhookHandler('invoice.payment_failed') 28 | handleInvoicePaymentFailed(evt: Stripe.Event) { 29 | console.log('StripePerSeatService invoice.payment_failed: ', evt); 30 | const dataObject = evt.data.object as Stripe.Invoice; 31 | // If the payment fails or the customer does not have a valid payment method, 32 | // an invoice.payment_failed event is sent, the subscription becomes past_due. 33 | // Use this webhook to notify your user that their payment has 34 | // failed and to retrieve new card details. 35 | return true; 36 | } 37 | 38 | @StripeWebhookHandler('invoice.finalized') 39 | handleInvoiceFinalized(evt: Stripe.Event) { 40 | console.log('StripePerSeatService invoice.finalized: ', evt); 41 | const dataObject = evt.data.object as Stripe.Invoice; 42 | // If you want to manually send out invoices to your customers 43 | // or store them locally to reference to avoid hitting Stripe rate limits. 44 | return true; 45 | } 46 | 47 | @StripeWebhookHandler('customer.subscription.deleted') 48 | handleCustomerSubsDeleted(evt: Stripe.Event) { 49 | console.log('StripePerSeatService customer.subscription.deleted: ', evt); 50 | const dataObject = evt.data.object as Stripe.Subscription; 51 | if (evt.request != null) { 52 | // handle a subscription cancelled by your request 53 | // from above. 54 | } else { 55 | // handle subscription cancelled automatically based 56 | // upon your subscription settings. 57 | } 58 | return true; 59 | } 60 | 61 | @StripeWebhookHandler('customer.subscription.trial_will_end') 62 | handleCustomerSubsTrialEnd(evt: Stripe.Event) { 63 | console.log( 64 | 'StripePerSeatService customer.subscription.trial_will_end: ', 65 | evt, 66 | ); 67 | const dataObject = evt.data.object as Stripe.Subscription; 68 | // Send notification to your user that the trial will end 69 | return true; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /stripe-template-code/checkout-single-subscription.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const app = express(); 3 | const path = require('path'); 4 | 5 | // Copy the .env.example in the root into a .env file in this folder 6 | const envFilePath = path.resolve(__dirname, './.env'); 7 | const env = require("dotenv").config({ path: envFilePath }); 8 | if (env.error) { 9 | throw new Error(`Unable to load the .env file from ${envFilePath}. Please copy .env.example to ${envFilePath}`); 10 | } 11 | 12 | const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY, { 13 | apiVersion: '2020-08-27', 14 | appInfo: { // For sample support and debugging, not required for production: 15 | name: "stripe-samples/checkout-single-subscription", 16 | version: "0.0.1", 17 | url: "https://github.com/stripe-samples/checkout-single-subscription" 18 | } 19 | }); 20 | 21 | app.use(express.static(process.env.STATIC_DIR)); 22 | app.use( 23 | express.json({ 24 | // We need the raw body to verify webhook signatures. 25 | // Let's compute it only when hitting the Stripe webhook endpoint. 26 | verify: function (req, res, buf) { 27 | if (req.originalUrl.startsWith("/webhook")) { 28 | req.rawBody = buf.toString(); 29 | } 30 | }, 31 | }) 32 | ); 33 | 34 | app.get("/", (req, res) => { 35 | const filePath = path.resolve(process.env.STATIC_DIR + "/index.html"); 36 | res.sendFile(filePath); 37 | }); 38 | 39 | // Fetch the Checkout Session to display the JSON result on the success page 40 | app.get("/checkout-session", async (req, res) => { 41 | const { sessionId } = req.query; 42 | const session = await stripe.checkout.sessions.retrieve(sessionId); 43 | res.send(session); 44 | }); 45 | 46 | app.post("/create-checkout-session", async (req, res) => { 47 | const domainURL = process.env.DOMAIN; 48 | const { priceId } = req.body; 49 | 50 | // Create new Checkout Session for the order 51 | // Other optional params include: 52 | // [billing_address_collection] - to display billing address details on the page 53 | // [customer] - if you have an existing Stripe Customer ID 54 | // [customer_email] - lets you prefill the email input in the form 55 | // For full details see https://stripe.com/docs/api/checkout/sessions/create 56 | try { 57 | const session = await stripe.checkout.sessions.create({ 58 | mode: "subscription", 59 | payment_method_types: ["card"], 60 | line_items: [ 61 | { 62 | price: priceId, 63 | quantity: 1, 64 | }, 65 | ], 66 | // ?session_id={CHECKOUT_SESSION_ID} means the redirect will have the session ID set as a query param 67 | success_url: `${domainURL}/success.html?session_id={CHECKOUT_SESSION_ID}`, 68 | cancel_url: `${domainURL}/canceled.html`, 69 | }); 70 | 71 | res.send({ 72 | sessionId: session.id, 73 | }); 74 | } catch (e) { 75 | res.status(400); 76 | return res.send({ 77 | error: { 78 | message: e.message, 79 | } 80 | }); 81 | } 82 | }); 83 | 84 | app.get("/setup", (req, res) => { 85 | res.send({ 86 | publishableKey: process.env.STRIPE_PUBLISHABLE_KEY, 87 | basicPrice: process.env.BASIC_PRICE_ID, 88 | proPrice: process.env.PRO_PRICE_ID, 89 | }); 90 | }); 91 | 92 | app.post('/customer-portal', async (req, res) => { 93 | // For demonstration purposes, we're using the Checkout session to retrieve the customer ID. 94 | // Typically this is stored alongside the authenticated user in your database. 95 | const { sessionId } = req.body; 96 | const checkoutsession = await stripe.checkout.sessions.retrieve(sessionId); 97 | 98 | // This is the url to which the customer will be redirected when they are done 99 | // managing their billing with the portal. 100 | const returnUrl = process.env.DOMAIN; 101 | 102 | const portalsession = await stripe.billingPortal.sessions.create({ 103 | customer: checkoutsession.customer, 104 | return_url: returnUrl, 105 | }); 106 | 107 | res.send({ 108 | url: portalsession.url, 109 | }); 110 | }); 111 | 112 | // Webhook handler for asynchronous events. 113 | app.post("/webhook", async (req, res) => { 114 | let data; 115 | let eventType; 116 | // Check if webhook signing is configured. 117 | if (process.env.STRIPE_WEBHOOK_SECRET) { 118 | // Retrieve the event by verifying the signature using the raw body and secret. 119 | let event; 120 | let signature = req.headers["stripe-signature"]; 121 | 122 | try { 123 | event = stripe.webhooks.constructEvent( 124 | req.rawBody, 125 | signature, 126 | process.env.STRIPE_WEBHOOK_SECRET 127 | ); 128 | } catch (err) { 129 | console.log(`⚠️ Webhook signature verification failed.`); 130 | return res.sendStatus(400); 131 | } 132 | // Extract the object from the event. 133 | data = event.data; 134 | eventType = event.type; 135 | } else { 136 | // Webhook signing is recommended, but if the secret is not configured in `config.js`, 137 | // retrieve the event data directly from the request body. 138 | data = req.body.data; 139 | eventType = req.body.type; 140 | } 141 | 142 | if (eventType === "checkout.session.completed") { 143 | console.log(`🔔 Payment received!`); 144 | } 145 | 146 | res.sendStatus(200); 147 | }); 148 | 149 | app.listen(4242, () => console.log(`Node server listening at http://localhost:${4242}/`)); 150 | -------------------------------------------------------------------------------- /stripe-template-code/fixed-price-subscriptions.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const app = express(); 3 | const { resolve } = require('path'); 4 | const bodyParser = require('body-parser'); 5 | const cookieParser = require('cookie-parser'); 6 | // Replace if using a different env file or config 7 | require('dotenv').config({ path: './.env' }); 8 | // REF: https://github.com/stripe-samples/subscription-use-cases/blob/master/fixed-price-subscriptions/server/node/server.js 9 | 10 | if ( 11 | !process.env.STRIPE_SECRET_KEY || 12 | !process.env.STRIPE_PUBLISHABLE_KEY || 13 | !process.env.STATIC_DIR 14 | ) { 15 | console.log( 16 | 'The .env file is not configured. Follow the instructions in the readme to configure the .env file. https://github.com/stripe-samples/subscription-use-cases' 17 | ); 18 | console.log(''); 19 | process.env.STRIPE_SECRET_KEY 20 | ? '' 21 | : console.log('Add STRIPE_SECRET_KEY to your .env file.'); 22 | 23 | process.env.STRIPE_PUBLISHABLE_KEY 24 | ? '' 25 | : console.log('Add STRIPE_PUBLISHABLE_KEY to your .env file.'); 26 | 27 | process.env.STATIC_DIR 28 | ? '' 29 | : console.log( 30 | 'Add STATIC_DIR to your .env file. Check .env.example in the root folder for an example' 31 | ); 32 | 33 | process.exit(); 34 | } 35 | 36 | const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY, { 37 | apiVersion: '2020-08-27', 38 | appInfo: { // For sample support and debugging, not required for production: 39 | name: "stripe-samples/subscription-use-cases/fixed-price", 40 | version: "0.0.1", 41 | url: "https://github.com/stripe-samples/subscription-use-cases/fixed-price" 42 | } 43 | }); 44 | 45 | // Use static to serve static assets. 46 | app.use(express.static(process.env.STATIC_DIR)); 47 | 48 | // Use cookies to simulate logged in user. 49 | app.use(cookieParser()); 50 | 51 | // Use JSON parser for parsing payloads as JSON on all non-webhook routes. 52 | app.use((req, res, next) => { 53 | if (req.originalUrl === '/webhook') { 54 | next(); 55 | } else { 56 | bodyParser.json()(req, res, next); 57 | } 58 | }); 59 | 60 | app.get('/', (req, res) => { 61 | const path = resolve(process.env.STATIC_DIR + '/register.html'); 62 | res.sendFile(path); 63 | }); 64 | 65 | app.get('/config', async (req, res) => { 66 | const prices = await stripe.prices.list({ 67 | lookup_keys: ['sample_basic', 'sample_premium'], 68 | expand: ['data.product'] 69 | }); 70 | 71 | res.send({ 72 | publishableKey: process.env.STRIPE_PUBLISHABLE_KEY, 73 | prices: prices.data, 74 | }); 75 | }); 76 | 77 | app.post('/create-customer', async (req, res) => { 78 | // Create a new customer object 79 | const customer = await stripe.customers.create({ 80 | email: req.body.email, 81 | }); 82 | 83 | // Save the customer.id in your database alongside your user. 84 | // We're simulating authentication with a cookie. 85 | res.cookie('customer', customer.id, { maxAge: 900000, httpOnly: true }); 86 | 87 | res.send({ customer: customer }); 88 | }); 89 | 90 | app.post('/create-subscription', async (req, res) => { 91 | // Simulate authenticated user. In practice this will be the 92 | // Stripe Customer ID related to the authenticated user. 93 | const customerId = req.cookies['customer']; 94 | 95 | // Create the subscription 96 | const priceId = req.body.priceId; 97 | 98 | try { 99 | const subscription = await stripe.subscriptions.create({ 100 | customer: customerId, 101 | items: [{ 102 | price: priceId, 103 | }], 104 | payment_behavior: 'default_incomplete', 105 | expand: ['latest_invoice.payment_intent'], 106 | }); 107 | 108 | res.send({ 109 | subscriptionId: subscription.id, 110 | clientSecret: subscription.latest_invoice.payment_intent.client_secret, 111 | }); 112 | } catch (error) { 113 | return res.status(400).send({ error: { message: error.message } }); 114 | } 115 | }); 116 | 117 | app.get('/invoice-preview', async (req, res) => { 118 | const customerId = req.cookies['customer']; 119 | const priceId = process.env[req.query.newPriceLookupKey.toUpperCase()]; 120 | 121 | const subscription = await stripe.subscriptions.retrieve( 122 | req.query.subscriptionId 123 | ); 124 | 125 | const invoice = await stripe.invoices.retrieveUpcoming({ 126 | customer: customerId, 127 | subscription: req.query.subscriptionId, 128 | subscription_items: [ { 129 | id: subscription.items.data[0].id, 130 | price: priceId, 131 | }], 132 | }); 133 | 134 | res.send({ invoice }); 135 | }); 136 | 137 | app.post('/cancel-subscription', async (req, res) => { 138 | // Cancel the subscription 139 | try { 140 | const deletedSubscription = await stripe.subscriptions.del( 141 | req.body.subscriptionId 142 | ); 143 | 144 | res.send({ subscription: deletedSubscription }); 145 | } catch (error) { 146 | return res.status(400).send({ error: { message: error.message } }); 147 | } 148 | }); 149 | 150 | app.post('/update-subscription', async (req, res) => { 151 | try { 152 | const subscription = await stripe.subscriptions.retrieve( 153 | req.body.subscriptionId 154 | ); 155 | const updatedSubscription = await stripe.subscriptions.update( 156 | req.body.subscriptionId, { 157 | items: [{ 158 | id: subscription.items.data[0].id, 159 | price: process.env[req.body.newPriceLookupKey.toUpperCase()], 160 | }], 161 | } 162 | ); 163 | 164 | res.send({ subscription: updatedSubscription }); 165 | } catch (error) { 166 | return res.status(400).send({ error: { message: error.message } }); 167 | } 168 | }); 169 | 170 | app.get('/subscriptions', async (req, res) => { 171 | // Simulate authenticated user. In practice this will be the 172 | // Stripe Customer ID related to the authenticated user. 173 | const customerId = req.cookies['customer']; 174 | 175 | const subscriptions = await stripe.subscriptions.list({ 176 | customer: customerId, 177 | status: 'all', 178 | expand: ['data.default_payment_method'], 179 | }); 180 | 181 | res.json({subscriptions}); 182 | }); 183 | 184 | app.post( 185 | '/webhook', 186 | bodyParser.raw({ type: 'application/json' }), 187 | async (req, res) => { 188 | // Retrieve the event by verifying the signature using the raw body and secret. 189 | let event; 190 | 191 | try { 192 | event = stripe.webhooks.constructEvent( 193 | req.body, 194 | req.header('Stripe-Signature'), 195 | process.env.STRIPE_WEBHOOK_SECRET 196 | ); 197 | } catch (err) { 198 | console.log(err); 199 | console.log(`⚠️ Webhook signature verification failed.`); 200 | console.log( 201 | `⚠️ Check the env file and enter the correct webhook secret.` 202 | ); 203 | return res.sendStatus(400); 204 | } 205 | 206 | // Extract the object from the event. 207 | const dataObject = event.data.object; 208 | 209 | // Handle the event 210 | // Review important events for Billing webhooks 211 | // https://stripe.com/docs/billing/webhooks 212 | // Remove comment to see the various objects sent for this sample 213 | switch (event.type) { 214 | case 'invoice.payment_succeeded': 215 | if(dataObject['billing_reason'] == 'subscription_create') { 216 | // The subscription automatically activates after successful payment 217 | // Set the payment method used to pay the first invoice 218 | // as the default payment method for that subscription 219 | const subscription_id = dataObject['subscription'] 220 | const payment_intent_id = dataObject['payment_intent'] 221 | 222 | // Retrieve the payment intent used to pay the subscription 223 | const payment_intent = await stripe.paymentIntents.retrieve(payment_intent_id); 224 | 225 | const subscription = await stripe.subscriptions.update( 226 | subscription_id, 227 | { 228 | default_payment_method: payment_intent.payment_method, 229 | }, 230 | ); 231 | 232 | console.log("Default payment method set for subscription:" + payment_intent.payment_method); 233 | }; 234 | 235 | break; 236 | case 'invoice.payment_failed': 237 | // If the payment fails or the customer does not have a valid payment method, 238 | // an invoice.payment_failed event is sent, the subscription becomes past_due. 239 | // Use this webhook to notify your user that their payment has 240 | // failed and to retrieve new card details. 241 | break; 242 | case 'invoice.finalized': 243 | // If you want to manually send out invoices to your customers 244 | // or store them locally to reference to avoid hitting Stripe rate limits. 245 | break; 246 | case 'customer.subscription.deleted': 247 | if (event.request != null) { 248 | // handle a subscription cancelled by your request 249 | // from above. 250 | } else { 251 | // handle subscription cancelled automatically based 252 | // upon your subscription settings. 253 | } 254 | break; 255 | case 'customer.subscription.trial_will_end': 256 | // Send notification to your user that the trial will end 257 | break; 258 | default: 259 | // Unexpected event type 260 | } 261 | res.sendStatus(200); 262 | } 263 | ); 264 | 265 | app.listen(4242, () => console.log(`Node server listening on port http://localhost:${4242}!`)); -------------------------------------------------------------------------------- /stripe-template-code/multiple-plan-subscriptions.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const app = express(); 3 | const { resolve } = require("path"); 4 | // Replace if using a different env file or config 5 | const env = require("dotenv").config({ path: "./.env" }); 6 | const stripe = require("stripe")(process.env.STRIPE_SECRET_KEY); 7 | 8 | app.use(express.static(process.env.STATIC_DIR)); 9 | 10 | app.use( 11 | express.json({ 12 | // We need the raw body to verify webhook signatures. 13 | // Let's compute it only when hitting the Stripe webhook endpoint. 14 | verify: function(req, res, buf) { 15 | if (req.originalUrl.startsWith("/webhook")) { 16 | req.rawBody = buf.toString(); 17 | } 18 | } 19 | }) 20 | ); 21 | 22 | const asyncMiddleware = fn => (req, res, next) => { 23 | Promise.resolve(fn(req, res, next)).catch(next); 24 | }; 25 | 26 | app.get("/", (req, res) => { 27 | const path = resolve(process.env.STATIC_DIR + "/index.html"); 28 | res.sendFile(path); 29 | }); 30 | 31 | app.get("/setup-page", asyncMiddleware(async (req, res, next) => { 32 | animals = process.env.ANIMALS.split(','); 33 | 34 | var lookup_keys = []; 35 | animals.forEach(animal => lookup_keys.push(animal + "-monthly-usd")); 36 | 37 | const prices = await stripe.prices.list({ 38 | lookup_keys: lookup_keys, 39 | expand:["data.product"]}); 40 | 41 | var products = []; 42 | prices.data.forEach(price => { 43 | var product = { 44 | price: {id: price.id, unit_amount: price.unit_amount}, 45 | title: price.product.metadata.title, 46 | emoji: price.product.metadata.emoji 47 | }; 48 | products.push(product); 49 | }); 50 | 51 | res.send({ 52 | publicKey: process.env.STRIPE_PUBLISHABLE_KEY, 53 | minProductsForDiscount: process.env.MIN_PRODUCTS_FOR_DISCOUNT, 54 | discountFactor: process.env.DISCOUNT_FACTOR, 55 | products: products 56 | }); 57 | }) 58 | ); 59 | 60 | app.post( 61 | "/create-customer", 62 | asyncMiddleware(async (req, res, next) => { 63 | // This creates a new Customer and attaches 64 | // the PaymentMethod to be default for invoice in one API call. 65 | const customer = await stripe.customers.create({ 66 | payment_method: req.body.payment_method, 67 | email: req.body.email, 68 | invoice_settings: { 69 | default_payment_method: req.body.payment_method 70 | } 71 | }); 72 | 73 | // In this example, we apply the coupon if the number of plans purchased 74 | // meets or exceeds the threshold. 75 | priceIds = req.body.price_ids; 76 | const eligibleForDiscount = priceIds.length >= process.env.MIN_PRODUCTS_FOR_DISCOUNT; 77 | const coupon = eligibleForDiscount ? process.env.COUPON_ID : null; 78 | 79 | // At this point, associate the ID of the Customer object with your 80 | // own internal representation of a customer, if you have one. 81 | const subscription = await stripe.subscriptions.create({ 82 | customer: customer.id, 83 | items: priceIds.map(priceId => { 84 | return { price: priceId }; 85 | }), 86 | expand: ["latest_invoice.payment_intent"], 87 | coupon: coupon 88 | }); 89 | 90 | res.send(subscription); 91 | }) 92 | ); 93 | 94 | app.post( 95 | "/subscription", 96 | asyncMiddleware(async (req, res) => { 97 | let subscription = await stripe.subscriptions.retrieve( 98 | req.body.subscriptionId 99 | ); 100 | res.send(subscription); 101 | }) 102 | ); 103 | 104 | // Webhook handler for asynchronous events. 105 | app.post("/webhook", async (req, res) => { 106 | let data; 107 | let eventType; 108 | // Check if webhook signing is configured. 109 | if (process.env.STRIPE_WEBHOOK_SECRET) { 110 | // Retrieve the event by verifying the signature using the raw body and secret. 111 | let event; 112 | let signature = req.headers["stripe-signature"]; 113 | 114 | try { 115 | event = stripe.webhooks.constructEvent( 116 | req.rawBody, 117 | signature, 118 | process.env.STRIPE_WEBHOOK_SECRET 119 | ); 120 | } catch (err) { 121 | console.log(`⚠️ Webhook signature verification failed.`); 122 | return res.sendStatus(400); 123 | } 124 | // Extract the object from the event. 125 | dataObject = event.data.object; 126 | eventType = event.type; 127 | 128 | // Handle the event 129 | // Review important events for Billing webhooks 130 | // https://stripe.com/docs/billing/webhooks 131 | // Remove comment to see the various objects sent for this sample 132 | switch (event.type) { 133 | case "customer.created": 134 | // console.log(dataObject); 135 | break; 136 | case "customer.updated": 137 | // console.log(dataObject); 138 | break; 139 | case "invoice.upcoming": 140 | // console.log(dataObject); 141 | break; 142 | case "invoice.created": 143 | // console.log(dataObject); 144 | break; 145 | case "invoice.finalized": 146 | // console.log(dataObject); 147 | break; 148 | case "invoice.payment_succeeded": 149 | // console.log(dataObject); 150 | break; 151 | case "invoice.payment_failed": 152 | // console.log(dataObject); 153 | break; 154 | case "customer.subscription.created": 155 | // console.log(dataObject); 156 | break; 157 | // ... handle other event types 158 | default: 159 | // Unexpected event type 160 | return res.status(400).end(); 161 | } 162 | } else { 163 | // Webhook signing is recommended, but if the secret is not configured in `config.js`, 164 | // retrieve the event data directly from the request body. 165 | data = req.body.data; 166 | eventType = req.body.type; 167 | } 168 | 169 | res.sendStatus(200); 170 | }); 171 | 172 | function errorHandler(err, req, res, next) { 173 | res.status(500).send({ error: { message: err.message } }); 174 | } 175 | 176 | app.use(errorHandler); 177 | 178 | app.listen(4242, () => console.log(`Node server listening on port ${4242}!`)); -------------------------------------------------------------------------------- /stripe-template-code/per-seat-subscription.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const app = express(); 3 | const { resolve } = require('path'); 4 | const bodyParser = require('body-parser'); 5 | // Replace if using a different env file or config 6 | require('dotenv').config({ path: './.env' }); 7 | 8 | if ( 9 | !process.env.STRIPE_SECRET_KEY || 10 | !process.env.STRIPE_PUBLISHABLE_KEY || 11 | !process.env.BASIC || 12 | !process.env.PREMIUM || 13 | !process.env.STATIC_DIR 14 | ) { 15 | console.log( 16 | 'The .env file is not configured. Follow the instructions in the readme to configure the .env file. https://github.com/stripe-samples/subscription-use-cases' 17 | ); 18 | console.log(''); 19 | process.env.STRIPE_SECRET_KEY 20 | ? '' 21 | : console.log('Add STRIPE_SECRET_KEY to your .env file.'); 22 | 23 | process.env.STRIPE_PUBLISHABLE_KEY 24 | ? '' 25 | : console.log('Add STRIPE_PUBLISHABLE_KEY to your .env file.'); 26 | 27 | process.env.BASIC 28 | ? '' 29 | : console.log( 30 | 'Add BASIC priceID to your .env file. See repo readme for setup instructions.' 31 | ); 32 | 33 | process.env.PREMIUM 34 | ? '' 35 | : console.log( 36 | 'Add PREMIUM priceID to your .env file. See repo readme for setup instructions.' 37 | ); 38 | 39 | process.env.STATIC_DIR 40 | ? '' 41 | : console.log( 42 | 'Add STATIC_DIR to your .env file. Check .env.example in the root folder for an example' 43 | ); 44 | 45 | process.exit(); 46 | } 47 | 48 | const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY, { 49 | apiVersion: '2020-08-27', 50 | appInfo: { // For sample support and debugging, not required for production: 51 | name: "stripe-samples/subscription-use-cases/per-seat-subscriptions", 52 | version: "0.0.1", 53 | url: "https://github.com/stripe-samples/subscription-use-cases/per-seat-subscriptions" 54 | } 55 | }); 56 | 57 | app.use(express.static(process.env.STATIC_DIR)); 58 | // Use JSON parser for all non-webhook routes. 59 | app.use((req, res, next) => { 60 | if (req.originalUrl === '/webhook') { 61 | next(); 62 | } else { 63 | bodyParser.json()(req, res, next); 64 | } 65 | }); 66 | 67 | app.get('/', (req, res) => { 68 | const path = resolve(process.env.STATIC_DIR + '/index.html'); 69 | res.sendFile(path); 70 | }); 71 | 72 | app.get('/config', async (req, res) => { 73 | res.send({ 74 | publishableKey: process.env.STRIPE_PUBLISHABLE_KEY, 75 | }); 76 | }); 77 | 78 | app.post('/retrieve-subscription-information', async (req, res) => { 79 | const subscriptionId = req.body.subscriptionId; 80 | 81 | const subscription = await stripe.subscriptions.retrieve(subscriptionId, { 82 | expand: [ 83 | 'latest_invoice', 84 | 'customer.invoice_settings.default_payment_method', 85 | 'items.data.price.product', 86 | ], 87 | }); 88 | 89 | const upcoming_invoice = await stripe.invoices.retrieveUpcoming({ 90 | subscription: subscriptionId, 91 | }); 92 | 93 | const item = subscription.items.data[0]; 94 | 95 | res.send({ 96 | card: subscription.customer.invoice_settings.default_payment_method.card, 97 | product_description: item.price.product.name, 98 | current_price: item.price.id, 99 | current_quantity: item.quantity, 100 | latest_invoice: subscription.latest_invoice, 101 | upcoming_invoice: upcoming_invoice, 102 | }); 103 | }); 104 | 105 | app.post('/create-customer', async (req, res) => { 106 | // Create a new customer object 107 | const customer = await stripe.customers.create({ 108 | email: req.body.email, 109 | }); 110 | 111 | // save the customer.id as stripeCustomerId 112 | // in your database. 113 | 114 | res.send({ customer }); 115 | }); 116 | 117 | app.post('/create-subscription', async (req, res) => { 118 | // Set the default payment method on the customer 119 | try { 120 | const payment_method = await stripe.paymentMethods.attach(req.body.paymentMethodId, { 121 | customer: req.body.customerId, 122 | }); 123 | 124 | await stripe.customers.update( 125 | req.body.customerId, 126 | { 127 | invoice_settings: { 128 | default_payment_method: payment_method.id, 129 | }, 130 | } 131 | ); 132 | 133 | // Create the subscription 134 | const subscription = await stripe.subscriptions.create({ 135 | customer: req.body.customerId, 136 | items: [ 137 | { price: process.env[req.body.priceId], quantity: req.body.quantity }, 138 | ], 139 | expand: ['latest_invoice.payment_intent', 'plan.product'], 140 | }); 141 | 142 | return res.send(subscription); 143 | } catch (error) { 144 | return res.status(400).send({ error: { message: error.message } }); 145 | } 146 | 147 | 148 | }); 149 | 150 | app.post('/retry-invoice', async (req, res) => { 151 | // Set the default payment method on the customer 152 | 153 | try { 154 | const payment_method = await stripe.paymentMethods.attach(req.body.paymentMethodId, { 155 | customer: req.body.customerId, 156 | }); 157 | await stripe.customers.update(req.body.customerId, { 158 | invoice_settings: { 159 | default_payment_method: payment_method.id, 160 | }, 161 | }); 162 | } catch (error) { 163 | // in case card_decline error 164 | return res 165 | .status(400) 166 | .send({ result: { error: { message: error.message } } }); 167 | } 168 | 169 | const invoice = await stripe.invoices.retrieve(req.body.invoiceId, { 170 | expand: ['payment_intent'], 171 | }); 172 | res.send(invoice); 173 | }); 174 | 175 | app.post('/retrieve-upcoming-invoice', async (req, res) => { 176 | const new_price = process.env[req.body.newPriceId.toUpperCase()]; 177 | const quantity = req.body.quantity; 178 | const subscriptionId = req.body.subscriptionId; 179 | 180 | var params = {}; 181 | params['customer'] = req.body.customerId; 182 | var subscription; 183 | 184 | if (subscriptionId != null) { 185 | params['subscription'] = subscriptionId; 186 | subscription = await stripe.subscriptions.retrieve(subscriptionId); 187 | 188 | const current_price = subscription.items.data[0].price.id; 189 | 190 | if (current_price == new_price) { 191 | params['subscription_items'] = [ 192 | { 193 | id: subscription.items.data[0].id, 194 | quantity: quantity, 195 | }, 196 | ]; 197 | } else { 198 | params['subscription_items'] = [ 199 | { 200 | id: subscription.items.data[0].id, 201 | deleted: true, 202 | }, 203 | { 204 | price: new_price, 205 | quantity: quantity, 206 | }, 207 | ]; 208 | } 209 | } else { 210 | params['subscription_items'] = [ 211 | { 212 | price: new_price, 213 | quantity: quantity, 214 | }, 215 | ]; 216 | } 217 | 218 | const invoice = await stripe.invoices.retrieveUpcoming(params); 219 | 220 | response = {}; 221 | 222 | if (subscriptionId != null) { 223 | const current_period_end = subscription.current_period_end; 224 | var immediate_total = 0; 225 | var next_invoice_sum = 0; 226 | 227 | invoice.lines.data.forEach((invoiceLineItem) => { 228 | if (invoiceLineItem.period.end == current_period_end) { 229 | immediate_total += invoiceLineItem.amount; 230 | } else { 231 | next_invoice_sum += invoiceLineItem.amount; 232 | } 233 | }); 234 | 235 | response = { 236 | immediate_total: immediate_total, 237 | next_invoice_sum: next_invoice_sum, 238 | invoice: invoice, 239 | }; 240 | } else { 241 | response = { 242 | invoice: invoice, 243 | }; 244 | } 245 | 246 | res.send(response); 247 | }); 248 | 249 | app.post('/cancel-subscription', async (req, res) => { 250 | // Delete the subscription 251 | const deletedSubscription = await stripe.subscriptions.del( 252 | req.body.subscriptionId 253 | ); 254 | res.send(deletedSubscription); 255 | }); 256 | 257 | app.post('/update-subscription', async (req, res) => { 258 | const subscriptionId = req.body.subscriptionId; 259 | 260 | const subscription = await stripe.subscriptions.retrieve(subscriptionId); 261 | 262 | const current_price = subscription.items.data[0].price.id; 263 | const new_price = process.env[req.body.newPriceId.toUpperCase()]; 264 | const quantity = req.body.quantity; 265 | var updatedSubscription; 266 | 267 | if (current_price == new_price) { 268 | updatedSubscription = await stripe.subscriptions.update(subscriptionId, { 269 | items: [ 270 | { 271 | id: subscription.items.data[0].id, 272 | quantity: quantity, 273 | }, 274 | ], 275 | }); 276 | } else { 277 | updatedSubscription = await stripe.subscriptions.update(subscriptionId, { 278 | items: [ 279 | { 280 | id: subscription.items.data[0].id, 281 | deleted: true, 282 | }, 283 | { 284 | price: new_price, 285 | quantity: quantity, 286 | }, 287 | ], 288 | expand: ['plan.product'], 289 | }); 290 | } 291 | 292 | var invoice = await stripe.invoices.create({ 293 | customer: subscription.customer, 294 | subscription: subscription.id, 295 | description: 296 | 'Change to ' + 297 | quantity + 298 | ' seat(s) on the ' + 299 | updatedSubscription.plan.product.name + 300 | ' plan', 301 | }); 302 | 303 | invoice = await stripe.invoices.pay(invoice.id); 304 | res.send({subscription: updatedSubscription}); 305 | }); 306 | 307 | // Webhook handler for asynchronous events. 308 | app.post( 309 | '/webhook', 310 | bodyParser.raw({ type: 'application/json' }), 311 | async (req, res) => { 312 | // Retrieve the event by verifying the signature using the raw body and secret. 313 | let event; 314 | 315 | try { 316 | event = stripe.webhooks.constructEvent( 317 | req.body, 318 | req.headers['stripe-signature'], 319 | process.env.STRIPE_WEBHOOK_SECRET 320 | ); 321 | } catch (err) { 322 | console.log(err); 323 | console.log(`⚠️ Webhook signature verification failed.`); 324 | console.log( 325 | `⚠️ Check the env file and enter the correct webhook secret.` 326 | ); 327 | return res.sendStatus(400); 328 | } 329 | // Extract the object from the event. 330 | const dataObject = event.data.object; 331 | 332 | // Handle the event 333 | // Review important events for Billing webhooks 334 | // https://stripe.com/docs/billing/webhooks 335 | // Remove comment to see the various objects sent for this sample 336 | switch (event.type) { 337 | case 'invoice.paid': 338 | // Used to provision services after the trial has ended. 339 | // The status of the invoice will show up as paid. Store the status in your 340 | // database to reference when a user accesses your service to avoid hitting rate limits. 341 | break; 342 | case 'invoice.payment_failed': 343 | // If the payment fails or the customer does not have a valid payment method, 344 | // an invoice.payment_failed event is sent, the subscription becomes past_due. 345 | // Use this webhook to notify your user that their payment has 346 | // failed and to retrieve new card details. 347 | break; 348 | case 'invoice.finalized': 349 | // If you want to manually send out invoices to your customers 350 | // or store them locally to reference to avoid hitting Stripe rate limits. 351 | break; 352 | case 'customer.subscription.deleted': 353 | if (event.request != null) { 354 | // handle a subscription cancelled by your request 355 | // from above. 356 | } else { 357 | // handle subscription cancelled automatically based 358 | // upon your subscription settings. 359 | } 360 | break; 361 | case 'customer.subscription.trial_will_end': 362 | // Send notification to your user that the trial will end 363 | break; 364 | default: 365 | // Unexpected event type 366 | } 367 | res.sendStatus(200); 368 | } 369 | ); 370 | 371 | app.listen(4242, () => console.log(`Node server listening on port http://localhost:${4242}!`)); -------------------------------------------------------------------------------- /stripe-template-code/usage-based-subscriptions.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const app = express(); 3 | const { resolve } = require('path'); 4 | const bodyParser = require('body-parser'); 5 | // Replace if using a different env file or config 6 | require('dotenv').config({ path: './.env' }); 7 | 8 | if ( 9 | !process.env.STRIPE_SECRET_KEY || 10 | !process.env.STRIPE_PUBLISHABLE_KEY || 11 | !process.env.BASIC || 12 | !process.env.PREMIUM || 13 | !process.env.STATIC_DIR 14 | ) { 15 | console.log( 16 | 'The .env file is not configured. Follow the instructions in the readme to configure the .env file. https://github.com/stripe-samples/subscription-use-cases' 17 | ); 18 | console.log(''); 19 | process.env.STRIPE_SECRET_KEY 20 | ? '' 21 | : console.log('Add STRIPE_SECRET_KEY to your .env file.'); 22 | 23 | process.env.STRIPE_PUBLISHABLE_KEY 24 | ? '' 25 | : console.log('Add STRIPE_PUBLISHABLE_KEY to your .env file.'); 26 | 27 | process.env.BASIC 28 | ? '' 29 | : console.log( 30 | 'Add BASIC priceID to your .env file. See repo readme for setup instructions.' 31 | ); 32 | 33 | process.env.PREMIUM 34 | ? '' 35 | : console.log( 36 | 'Add PREMIUM priceID to your .env file. See repo readme for setup instructions.' 37 | ); 38 | 39 | process.env.STATIC_DIR 40 | ? '' 41 | : console.log( 42 | 'Add STATIC_DIR to your .env file. Check .env.example in the root folder for an example' 43 | ); 44 | 45 | process.exit(); 46 | } 47 | 48 | const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY, { 49 | apiVersion: '2020-08-27', 50 | appInfo: { // For sample support and debugging, not required for production: 51 | name: "stripe-samples/subscription-use-cases/usage-based-subscriptions", 52 | version: "0.0.1", 53 | url: "https://github.com/stripe-samples/subscription-use-cases/usage-based-subscriptions" 54 | } 55 | }); 56 | 57 | app.use(express.static(process.env.STATIC_DIR)); 58 | // Use JSON parser for all non-webhook routes. 59 | app.use((req, res, next) => { 60 | if (req.originalUrl === '/webhook') { 61 | next(); 62 | } else { 63 | bodyParser.json()(req, res, next); 64 | } 65 | }); 66 | 67 | app.get('/', (req, res) => { 68 | const path = resolve(process.env.STATIC_DIR + '/index.html'); 69 | res.sendFile(path); 70 | }); 71 | 72 | app.get('/config', async (req, res) => { 73 | res.send({ 74 | publishableKey: process.env.STRIPE_PUBLISHABLE_KEY, 75 | }); 76 | }); 77 | 78 | app.post('/create-customer', async (req, res) => { 79 | // Create a new customer object 80 | const customer = await stripe.customers.create({ 81 | email: req.body.email, 82 | }); 83 | 84 | // save the customer.id as stripeCustomerId 85 | // in your database. 86 | 87 | res.send({ customer }); 88 | }); 89 | 90 | app.post('/create-subscription', async (req, res) => { 91 | // Set the default payment method on the customer 92 | try { 93 | await stripe.paymentMethods.attach(req.body.paymentMethodId, { 94 | customer: req.body.customerId, 95 | }); 96 | } catch (error) { 97 | return res.status('402').send({ error: { message: error.message } }); 98 | } 99 | 100 | let updateCustomerDefaultPaymentMethod = await stripe.customers.update( 101 | req.body.customerId, 102 | { 103 | invoice_settings: { 104 | default_payment_method: req.body.paymentMethodId, 105 | }, 106 | } 107 | ); 108 | 109 | // Create the subscription 110 | const subscription = await stripe.subscriptions.create({ 111 | customer: req.body.customerId, 112 | items: [{ price: process.env[req.body.priceId] }], 113 | expand: ['latest_invoice.payment_intent', 'pending_setup_intent'], 114 | }); 115 | 116 | res.send(subscription); 117 | }); 118 | 119 | app.post('/retry-invoice', async (req, res) => { 120 | // Set the default payment method on the customer 121 | 122 | try { 123 | await stripe.paymentMethods.attach(req.body.paymentMethodId, { 124 | customer: req.body.customerId, 125 | }); 126 | await stripe.customers.update(req.body.customerId, { 127 | invoice_settings: { 128 | default_payment_method: req.body.paymentMethodId, 129 | }, 130 | }); 131 | } catch (error) { 132 | // in case card_decline error 133 | return res 134 | .status('402') 135 | .send({ result: { error: { message: error.message } } }); 136 | } 137 | 138 | const invoice = await stripe.invoices.retrieve(req.body.invoiceId, { 139 | expand: ['payment_intent'], 140 | }); 141 | res.send(invoice); 142 | }); 143 | 144 | app.post('/retrieve-upcoming-invoice', async (req, res) => { 145 | const subscription = await stripe.subscriptions.retrieve( 146 | req.body.subscriptionId 147 | ); 148 | 149 | const invoice = await stripe.invoices.retrieveUpcoming({ 150 | subscription_prorate: true, 151 | customer: req.body.customerId, 152 | subscription: req.body.subscriptionId, 153 | subscription_items: [ 154 | { 155 | id: subscription.items.data[0].id, 156 | clear_usage: true, 157 | deleted: true, 158 | }, 159 | { 160 | price: process.env[req.body.newPriceId], 161 | deleted: false, 162 | }, 163 | ], 164 | }); 165 | res.send(invoice); 166 | }); 167 | 168 | app.post('/cancel-subscription', async (req, res) => { 169 | // Delete the subscription 170 | const deletedSubscription = await stripe.subscriptions.del( 171 | req.body.subscriptionId 172 | ); 173 | res.send(deletedSubscription); 174 | }); 175 | 176 | app.post('/update-subscription', async (req, res) => { 177 | const subscription = await stripe.subscriptions.retrieve( 178 | req.body.subscriptionId 179 | ); 180 | const updatedSubscription = await stripe.subscriptions.update( 181 | req.body.subscriptionId, 182 | { 183 | cancel_at_period_end: false, 184 | items: [ 185 | { 186 | id: subscription.items.data[0].id, 187 | price: process.env[req.body.newPriceId], 188 | }, 189 | ], 190 | } 191 | ); 192 | 193 | res.send(updatedSubscription); 194 | }); 195 | 196 | app.post('/retrieve-customer-payment-method', async (req, res) => { 197 | const paymentMethod = await stripe.paymentMethods.retrieve( 198 | req.body.paymentMethodId 199 | ); 200 | 201 | res.send(paymentMethod); 202 | }); 203 | // Webhook handler for asynchronous events. 204 | app.post( 205 | '/webhook', 206 | bodyParser.raw({ type: 'application/json' }), 207 | async (req, res) => { 208 | // Retrieve the event by verifying the signature using the raw body and secret. 209 | let event; 210 | 211 | try { 212 | event = stripe.webhooks.constructEvent( 213 | req.body, 214 | req.headers['stripe-signature'], 215 | process.env.STRIPE_WEBHOOK_SECRET 216 | ); 217 | } catch (err) { 218 | console.log(err); 219 | console.log(`⚠️ Webhook signature verification failed.`); 220 | console.log( 221 | `⚠️ Check the env file and enter the correct webhook secret.` 222 | ); 223 | return res.sendStatus(400); 224 | } 225 | // Extract the object from the event. 226 | const dataObject = event.data.object; 227 | 228 | // Handle the event 229 | // Review important events for Billing webhooks 230 | // https://stripe.com/docs/billing/webhooks 231 | // Remove comment to see the various objects sent for this sample 232 | switch (event.type) { 233 | case 'invoice.paid': 234 | // Used to provision services after the trial has ended. 235 | // The status of the invoice will show up as paid. Store the status in your 236 | // database to reference when a user accesses your service to avoid hitting rate limits. 237 | break; 238 | case 'invoice.payment_failed': 239 | // If the payment fails or the customer does not have a valid payment method, 240 | // an invoice.payment_failed event is sent, the subscription becomes past_due. 241 | // Use this webhook to notify your user that their payment has 242 | // failed and to retrieve new card details. 243 | break; 244 | case 'invoice.finalized': 245 | // If you want to manually send out invoices to your customers 246 | // or store them locally to reference to avoid hitting Stripe rate limits. 247 | break; 248 | case 'customer.subscription.deleted': 249 | if (event.request != null) { 250 | // handle a subscription cancelled by your request 251 | // from above. 252 | } else { 253 | // handle subscription cancelled automatically based 254 | // upon your subscription settings. 255 | } 256 | break; 257 | case 'customer.subscription.trial_will_end': 258 | // Send notification to your user that the trial will end 259 | break; 260 | default: 261 | // Unexpected event type 262 | } 263 | res.sendStatus(200); 264 | } 265 | ); 266 | 267 | app.listen(4242, () => console.log(`Node server listening on port ${4242}!`)); -------------------------------------------------------------------------------- /test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true 14 | } 15 | } 16 | --------------------------------------------------------------------------------