From ea124b597d347afb0609d1c2e8dfae504c7de726 Mon Sep 17 00:00:00 2001 From: MiguelMLorente Date: Sun, 23 Nov 2025 21:17:04 +0100 Subject: [PATCH] Refactor stripe session creation flow --- .gitignore | 3 +- package-lock.json | 19 +++++++-- package.json | 1 + src/access.controller.ts | 26 ++++++++---- src/app.module.ts | 3 +- src/buy.controller.ts | 82 +++++++++++++++++--------------------- src/dto/purchase-item.ts | 7 ++-- src/dto/purchase-status.ts | 11 +++-- src/dto/purchase.ts | 22 +++++----- src/main.ts | 2 + src/purchase.service.ts | 68 +++++++++++++++++-------------- src/stripe.service.ts | 28 +++++++++++++ src/user.service.ts | 8 ++-- 13 files changed, 167 insertions(+), 113 deletions(-) create mode 100644 src/stripe.service.ts diff --git a/.gitignore b/.gitignore index 8cdcaea..00911c7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ dist/ -node_modules/ \ No newline at end of file +node_modules/ +.env \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 75148b6..6a1d671 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@nestjs/common": "^11.0.1", "@nestjs/core": "^11.0.1", "@nestjs/platform-express": "^11.0.1", + "dotenv": "^17.2.3", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", "stripe": "^20.0.0", @@ -4968,9 +4969,9 @@ } }, "node_modules/dotenv": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -10052,6 +10053,18 @@ "ieee754": "^1.2.1" } }, + "node_modules/typeorm/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/typeorm/node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", diff --git a/package.json b/package.json index f1f424a..c76c96f 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "@nestjs/common": "^11.0.1", "@nestjs/core": "^11.0.1", "@nestjs/platform-express": "^11.0.1", + "dotenv": "^17.2.3", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", "stripe": "^20.0.0", diff --git a/src/access.controller.ts b/src/access.controller.ts index 22f0fd1..46f339c 100644 --- a/src/access.controller.ts +++ b/src/access.controller.ts @@ -1,4 +1,10 @@ -import { Body, Controller, Logger, Post, UnauthorizedException } from '@nestjs/common'; +import { + Body, + Controller, + Logger, + Post, + UnauthorizedException, +} from '@nestjs/common'; import { UserService } from './user.service'; @Controller('/access') @@ -8,23 +14,29 @@ export class AccessController { constructor(private readonly userService: UserService) {} @Post('/login') - public login(@Body("name") name: string, @Body("password") password: string): {userId: number} { - this.logger.debug("Received login request"); + public login( + @Body('name') name: string, + @Body('password') password: string, + ): { userId: number } { + this.logger.debug('Received login request'); const user = this.userService.getUserByName(name); if (user.password !== password) { throw new UnauthorizedException(); } return { - userId: user.id + userId: user.id, }; } @Post('/register') - public new(@Body("name") name: string, @Body("password") password: string): {userId: number} { - this.logger.debug("Received register request"); + public new( + @Body('name') name: string, + @Body('password') password: string, + ): { userId: number } { + this.logger.debug('Received register request'); const user = this.userService.createUser(name, password); return { - userId: user.id + userId: user.id, }; } } diff --git a/src/app.module.ts b/src/app.module.ts index 224902f..b89be6d 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -3,10 +3,11 @@ import { AccessController } from './access.controller'; import { UserService } from './user.service'; import { BuyController } from './buy.controller'; import { PurchaseService } from './purchase.service'; +import { StripeService } from './stripe.service'; @Module({ imports: [], controllers: [AccessController, BuyController], - providers: [UserService, PurchaseService], + providers: [UserService, PurchaseService, StripeService], }) export class AppModule {} diff --git a/src/buy.controller.ts b/src/buy.controller.ts index e6aebe7..136972e 100644 --- a/src/buy.controller.ts +++ b/src/buy.controller.ts @@ -1,45 +1,37 @@ -import { Body, Controller, Get, Post, Redirect } from "@nestjs/common"; -import Stripe from 'stripe' -import { PurchaseService } from "./purchase.service"; -import { UserService } from "./user.service"; -import { PurchaseItem } from "./dto/purchase-item"; - -@Controller('/buy') -export class BuyController { - private static TEST_KEY = "sk_test_51SVqfTBQui1OXGpt6kgnIuDpbwAqQBZqNoVXkn77UeoxfOMgjC3xFuXCet1h51REAq9XeP72qBDWdXlbaTvxb4yN00YWF6TWFm" - private readonly stripe = new Stripe(BuyController.TEST_KEY) - - constructor(private readonly purchaseService: PurchaseService, private readonly userService: UserService) {} - - @Post("/start") - @Redirect() - public async getStripeSessionUrl(@Body("userId") userId: number, @Body("quantity") quantity: number) { - const user = this.userService.getUserById(userId) - const purchase = this.purchaseService.recordPurchaseStart(user, PurchaseItem.PAELLA, quantity) - const session = await this.stripe.checkout.sessions.create({ - line_items: [ - { - price: purchase.purchasedProduct, - quantity: purchase.purchasedUnits - } - ], - mode: "payment", - success_url: "https://localhost:5173/buy/complete", - cancel_url: "https://localhost:5173/buy/cancel", - payment_method_types: ['card'], - }) - console.log(session) - - return {url: session.url} - } - - @Get("/complete") - public complete() { - - } - - @Get("/cancel") - public cancel() { - - } -} \ No newline at end of file +import { Body, Controller, Get, Post, Redirect } from '@nestjs/common'; +import { PurchaseService } from './purchase.service'; +import { UserService } from './user.service'; +import { PurchaseItem } from './dto/purchase-item'; +import { StripeService } from './stripe.service'; + +@Controller('/buy') +export class BuyController { + constructor( + private readonly purchaseService: PurchaseService, + private readonly userService: UserService, + private readonly stripeService: StripeService + ) {} + + @Post('/start') + @Redirect() + public async startPurchaseFlow( + @Body('userId') userId: number, + @Body('quantity') quantity: number, + ) { + const user = this.userService.getUserById(userId); + const purchase = this.purchaseService.recordPurchaseStart( + user, + PurchaseItem.PAELLA, + quantity, + ); + const session = await this.stripeService.createStripeSession(purchase); + console.log(session); + return { url: session.url }; + } + + @Get('/complete') + public complete() {} + + @Get('/cancel') + public cancel() {} +} diff --git a/src/dto/purchase-item.ts b/src/dto/purchase-item.ts index 479e2ec..1e00583 100644 --- a/src/dto/purchase-item.ts +++ b/src/dto/purchase-item.ts @@ -1,4 +1,3 @@ -export enum PurchaseItem { - PAELLA = "PAELLA", -} - +export enum PurchaseItem { + PAELLA = 'PAELLA', +} diff --git a/src/dto/purchase-status.ts b/src/dto/purchase-status.ts index 5cb3a02..616718f 100644 --- a/src/dto/purchase-status.ts +++ b/src/dto/purchase-status.ts @@ -1,6 +1,5 @@ -export enum PurchaseStatus { - CREATED = "CREATED", - COMPLETED = "COMPLETED", - FAILED = "FAILED", -} - +export enum PurchaseStatus { + CREATED = 'CREATED', + COMPLETED = 'COMPLETED', + FAILED = 'FAILED', +} diff --git a/src/dto/purchase.ts b/src/dto/purchase.ts index ef8eedb..60805e5 100644 --- a/src/dto/purchase.ts +++ b/src/dto/purchase.ts @@ -1,11 +1,11 @@ -import { PurchaseItem } from "./purchase-item"; -import { PurchaseStatus } from "./purchase-status"; -import { User } from "./user"; - -export class Purchase { - id: number; - purchasedBy: User; - purchasedProduct: PurchaseItem; - purchasedUnits: number; - status: PurchaseStatus -} \ No newline at end of file +import { PurchaseItem } from './purchase-item'; +import { PurchaseStatus } from './purchase-status'; +import { User } from './user'; + +export class Purchase { + id: number; + purchasedBy: User; + purchasedProduct: PurchaseItem; + purchasedUnits: number; + status: PurchaseStatus; +} diff --git a/src/main.ts b/src/main.ts index f76bc8d..17b88ad 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,6 +1,8 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; +import dotenv from 'dotenv' +dotenv.config(); async function bootstrap() { const app = await NestFactory.create(AppModule); await app.listen(process.env.PORT ?? 3000); diff --git a/src/purchase.service.ts b/src/purchase.service.ts index 18af01c..9271b70 100644 --- a/src/purchase.service.ts +++ b/src/purchase.service.ts @@ -1,31 +1,37 @@ -import { Injectable, Logger } from "@nestjs/common"; -import { Purchase } from "./dto/purchase"; -import { User } from "./dto/user"; -import { PurchaseItem } from "./dto/purchase-item"; -import { PurchaseStatus } from "./dto/purchase-status"; - -@Injectable() -export class PurchaseService { - private readonly purchases: Purchase[] = []; - private readonly logger = new Logger(PurchaseService.name); - - constructor() {} - - public recordPurchaseStart(user: User, purchasedItem: PurchaseItem, units: number): Purchase { - this.logger.debug(`Recording purchase for user ID: ${user.id}`) - const purchase: Purchase = { - id: this.purchases.length, - purchasedBy: user, - purchasedProduct: purchasedItem, - purchasedUnits: units, - status: PurchaseStatus.CREATED - } - this.purchases.push(purchase) - return purchase; - } - - public findPurchasesByUser(userId: number): Purchase[] { - this.logger.debug(`Searching purchases for user ID: ${userId}`) - return this.purchases.filter(purchase => purchase.purchasedBy.id = userId); - } -} \ No newline at end of file +import { Injectable, Logger } from '@nestjs/common'; +import { Purchase } from './dto/purchase'; +import { User } from './dto/user'; +import { PurchaseItem } from './dto/purchase-item'; +import { PurchaseStatus } from './dto/purchase-status'; + +@Injectable() +export class PurchaseService { + private readonly purchases: Purchase[] = []; + private readonly logger = new Logger(PurchaseService.name); + + constructor() {} + + public recordPurchaseStart( + user: User, + purchasedItem: PurchaseItem, + units: number, + ): Purchase { + this.logger.debug(`Recording purchase for user ID: ${user.id}`); + const purchase: Purchase = { + id: this.purchases.length, + purchasedBy: user, + purchasedProduct: purchasedItem, + purchasedUnits: units, + status: PurchaseStatus.CREATED, + }; + this.purchases.push(purchase); + return purchase; + } + + public findPurchasesByUser(userId: number): Purchase[] { + this.logger.debug(`Searching purchases for user ID: ${userId}`); + return this.purchases.filter( + (purchase) => (purchase.purchasedBy.id = userId), + ); + } +} diff --git a/src/stripe.service.ts b/src/stripe.service.ts new file mode 100644 index 0000000..62c2ff7 --- /dev/null +++ b/src/stripe.service.ts @@ -0,0 +1,28 @@ +import { Injectable } from '@nestjs/common'; +import { Purchase } from './dto/purchase'; +import Stripe from 'stripe'; + +@Injectable() +export class StripeService { + private readonly stripe: Stripe; + + constructor() { + const stripePrivateKey = process.env.STRIPE_PRIVATE_KEY!; + this.stripe = new Stripe(stripePrivateKey); + } + + public createStripeSession(purchase: Purchase): Promise> { + return this.stripe.checkout.sessions.create({ + line_items: [ + { + price: purchase.purchasedProduct, + quantity: purchase.purchasedUnits, + }, + ], + mode: 'payment', + success_url: 'https://localhost:5173/buy/complete', + cancel_url: 'https://localhost:5173/buy/cancel', + payment_method_types: ['card'], + }); + } +} diff --git a/src/user.service.ts b/src/user.service.ts index 8c070a5..ccf590a 100644 --- a/src/user.service.ts +++ b/src/user.service.ts @@ -4,12 +4,12 @@ import { User } from './dto/user'; @Injectable() export class UserService { private readonly users: User[] = []; - private readonly logger = new Logger(UserService.name) + private readonly logger = new Logger(UserService.name); constructor() {} public getUserByName(name: string): User { - this.logger.debug(`Get user by name: ${name}`) + this.logger.debug(`Get user by name: ${name}`); const user = this.users.find((u) => u.name === name); if (!user) { throw new NotFoundException(); @@ -18,7 +18,7 @@ export class UserService { } public getUserById(userId: number): User { - this.logger.debug(`Get user by id: ${userId}`) + this.logger.debug(`Get user by id: ${userId}`); const user = this.users.find((u) => u.id === userId); if (!user) { throw new NotFoundException(); @@ -34,7 +34,7 @@ export class UserService { if (!(e instanceof NotFoundException)) throw e; } - this.logger.debug(`Creating user with name: ${name}`) + this.logger.debug(`Creating user with name: ${name}`); const user: User = { id: this.users.length, name,