Refactor stripe session creation flow

main
MiguelMLorente 2025-11-23 21:17:04 +01:00
parent 472ed8eb70
commit ea124b597d
13 changed files with 167 additions and 113 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
dist/ dist/
node_modules/ node_modules/
.env

19
package-lock.json generated
View File

@ -12,6 +12,7 @@
"@nestjs/common": "^11.0.1", "@nestjs/common": "^11.0.1",
"@nestjs/core": "^11.0.1", "@nestjs/core": "^11.0.1",
"@nestjs/platform-express": "^11.0.1", "@nestjs/platform-express": "^11.0.1",
"dotenv": "^17.2.3",
"reflect-metadata": "^0.2.2", "reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1", "rxjs": "^7.8.1",
"stripe": "^20.0.0", "stripe": "^20.0.0",
@ -4968,9 +4969,9 @@
} }
}, },
"node_modules/dotenv": { "node_modules/dotenv": {
"version": "16.6.1", "version": "17.2.3",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==",
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"engines": { "engines": {
"node": ">=12" "node": ">=12"
@ -10052,6 +10053,18 @@
"ieee754": "^1.2.1" "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": { "node_modules/typeorm/node_modules/glob": {
"version": "10.4.5", "version": "10.4.5",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",

View File

@ -18,6 +18,7 @@
"@nestjs/common": "^11.0.1", "@nestjs/common": "^11.0.1",
"@nestjs/core": "^11.0.1", "@nestjs/core": "^11.0.1",
"@nestjs/platform-express": "^11.0.1", "@nestjs/platform-express": "^11.0.1",
"dotenv": "^17.2.3",
"reflect-metadata": "^0.2.2", "reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1", "rxjs": "^7.8.1",
"stripe": "^20.0.0", "stripe": "^20.0.0",

View File

@ -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'; import { UserService } from './user.service';
@Controller('/access') @Controller('/access')
@ -8,23 +14,29 @@ export class AccessController {
constructor(private readonly userService: UserService) {} constructor(private readonly userService: UserService) {}
@Post('/login') @Post('/login')
public login(@Body("name") name: string, @Body("password") password: string): {userId: number} { public login(
this.logger.debug("Received login request"); @Body('name') name: string,
@Body('password') password: string,
): { userId: number } {
this.logger.debug('Received login request');
const user = this.userService.getUserByName(name); const user = this.userService.getUserByName(name);
if (user.password !== password) { if (user.password !== password) {
throw new UnauthorizedException(); throw new UnauthorizedException();
} }
return { return {
userId: user.id userId: user.id,
}; };
} }
@Post('/register') @Post('/register')
public new(@Body("name") name: string, @Body("password") password: string): {userId: number} { public new(
this.logger.debug("Received register request"); @Body('name') name: string,
@Body('password') password: string,
): { userId: number } {
this.logger.debug('Received register request');
const user = this.userService.createUser(name, password); const user = this.userService.createUser(name, password);
return { return {
userId: user.id userId: user.id,
}; };
} }
} }

View File

@ -3,10 +3,11 @@ import { AccessController } from './access.controller';
import { UserService } from './user.service'; import { UserService } from './user.service';
import { BuyController } from './buy.controller'; import { BuyController } from './buy.controller';
import { PurchaseService } from './purchase.service'; import { PurchaseService } from './purchase.service';
import { StripeService } from './stripe.service';
@Module({ @Module({
imports: [], imports: [],
controllers: [AccessController, BuyController], controllers: [AccessController, BuyController],
providers: [UserService, PurchaseService], providers: [UserService, PurchaseService, StripeService],
}) })
export class AppModule {} export class AppModule {}

View File

@ -1,45 +1,37 @@
import { Body, Controller, Get, Post, Redirect } from "@nestjs/common"; import { Body, Controller, Get, Post, Redirect } from '@nestjs/common';
import Stripe from 'stripe' import { PurchaseService } from './purchase.service';
import { PurchaseService } from "./purchase.service"; import { UserService } from './user.service';
import { UserService } from "./user.service"; import { PurchaseItem } from './dto/purchase-item';
import { PurchaseItem } from "./dto/purchase-item"; import { StripeService } from './stripe.service';
@Controller('/buy') @Controller('/buy')
export class BuyController { export class BuyController {
private static TEST_KEY = "sk_test_51SVqfTBQui1OXGpt6kgnIuDpbwAqQBZqNoVXkn77UeoxfOMgjC3xFuXCet1h51REAq9XeP72qBDWdXlbaTvxb4yN00YWF6TWFm" constructor(
private readonly stripe = new Stripe(BuyController.TEST_KEY) private readonly purchaseService: PurchaseService,
private readonly userService: UserService,
private readonly stripeService: StripeService
) {}
constructor(private readonly purchaseService: PurchaseService, private readonly userService: UserService) {} @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 };
}
@Post("/start") @Get('/complete')
@Redirect() public complete() {}
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('/cancel')
} public cancel() {}
@Get("/complete")
public complete() {
}
@Get("/cancel")
public cancel() {
}
} }

View File

@ -1,4 +1,3 @@
export enum PurchaseItem { export enum PurchaseItem {
PAELLA = "PAELLA", PAELLA = 'PAELLA',
} }

View File

@ -1,6 +1,5 @@
export enum PurchaseStatus { export enum PurchaseStatus {
CREATED = "CREATED", CREATED = 'CREATED',
COMPLETED = "COMPLETED", COMPLETED = 'COMPLETED',
FAILED = "FAILED", FAILED = 'FAILED',
} }

View File

@ -1,11 +1,11 @@
import { PurchaseItem } from "./purchase-item"; import { PurchaseItem } from './purchase-item';
import { PurchaseStatus } from "./purchase-status"; import { PurchaseStatus } from './purchase-status';
import { User } from "./user"; import { User } from './user';
export class Purchase { export class Purchase {
id: number; id: number;
purchasedBy: User; purchasedBy: User;
purchasedProduct: PurchaseItem; purchasedProduct: PurchaseItem;
purchasedUnits: number; purchasedUnits: number;
status: PurchaseStatus status: PurchaseStatus;
} }

View File

@ -1,6 +1,8 @@
import { NestFactory } from '@nestjs/core'; import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module'; import { AppModule } from './app.module';
import dotenv from 'dotenv'
dotenv.config();
async function bootstrap() { async function bootstrap() {
const app = await NestFactory.create(AppModule); const app = await NestFactory.create(AppModule);
await app.listen(process.env.PORT ?? 3000); await app.listen(process.env.PORT ?? 3000);

View File

@ -1,31 +1,37 @@
import { Injectable, Logger } from "@nestjs/common"; import { Injectable, Logger } from '@nestjs/common';
import { Purchase } from "./dto/purchase"; import { Purchase } from './dto/purchase';
import { User } from "./dto/user"; import { User } from './dto/user';
import { PurchaseItem } from "./dto/purchase-item"; import { PurchaseItem } from './dto/purchase-item';
import { PurchaseStatus } from "./dto/purchase-status"; import { PurchaseStatus } from './dto/purchase-status';
@Injectable() @Injectable()
export class PurchaseService { export class PurchaseService {
private readonly purchases: Purchase[] = []; private readonly purchases: Purchase[] = [];
private readonly logger = new Logger(PurchaseService.name); private readonly logger = new Logger(PurchaseService.name);
constructor() {} constructor() {}
public recordPurchaseStart(user: User, purchasedItem: PurchaseItem, units: number): Purchase { public recordPurchaseStart(
this.logger.debug(`Recording purchase for user ID: ${user.id}`) user: User,
const purchase: Purchase = { purchasedItem: PurchaseItem,
id: this.purchases.length, units: number,
purchasedBy: user, ): Purchase {
purchasedProduct: purchasedItem, this.logger.debug(`Recording purchase for user ID: ${user.id}`);
purchasedUnits: units, const purchase: Purchase = {
status: PurchaseStatus.CREATED id: this.purchases.length,
} purchasedBy: user,
this.purchases.push(purchase) purchasedProduct: purchasedItem,
return purchase; purchasedUnits: units,
} status: PurchaseStatus.CREATED,
};
this.purchases.push(purchase);
return purchase;
}
public findPurchasesByUser(userId: number): Purchase[] { public findPurchasesByUser(userId: number): Purchase[] {
this.logger.debug(`Searching purchases for user ID: ${userId}`) this.logger.debug(`Searching purchases for user ID: ${userId}`);
return this.purchases.filter(purchase => purchase.purchasedBy.id = userId); return this.purchases.filter(
} (purchase) => (purchase.purchasedBy.id = userId),
);
}
} }

28
src/stripe.service.ts Normal file
View File

@ -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<Stripe.Response<Stripe.Checkout.Session>> {
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'],
});
}
}

View File

@ -4,12 +4,12 @@ import { User } from './dto/user';
@Injectable() @Injectable()
export class UserService { export class UserService {
private readonly users: User[] = []; private readonly users: User[] = [];
private readonly logger = new Logger(UserService.name) private readonly logger = new Logger(UserService.name);
constructor() {} constructor() {}
public getUserByName(name: string): User { 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); const user = this.users.find((u) => u.name === name);
if (!user) { if (!user) {
throw new NotFoundException(); throw new NotFoundException();
@ -18,7 +18,7 @@ export class UserService {
} }
public getUserById(userId: number): User { 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); const user = this.users.find((u) => u.id === userId);
if (!user) { if (!user) {
throw new NotFoundException(); throw new NotFoundException();
@ -34,7 +34,7 @@ export class UserService {
if (!(e instanceof NotFoundException)) throw e; 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 = { const user: User = {
id: this.users.length, id: this.users.length,
name, name,