├── .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 |
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 |
--------------------------------------------------------------------------------