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/
node_modules/
.env

19
package-lock.json generated
View File

@ -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",

View File

@ -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",

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';
@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,
};
}
}

View File

@ -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 {}

View File

@ -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";
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 {
private static TEST_KEY = "sk_test_51SVqfTBQui1OXGpt6kgnIuDpbwAqQBZqNoVXkn77UeoxfOMgjC3xFuXCet1h51REAq9XeP72qBDWdXlbaTvxb4yN00YWF6TWFm"
private readonly stripe = new Stripe(BuyController.TEST_KEY)
constructor(
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")
@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)
@Get('/complete')
public complete() {}
return {url: session.url}
}
@Get("/complete")
public complete() {
}
@Get("/cancel")
public cancel() {
}
@Get('/cancel')
public cancel() {}
}

View File

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

View File

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

View File

@ -1,11 +1,11 @@
import { PurchaseItem } from "./purchase-item";
import { PurchaseStatus } from "./purchase-status";
import { User } from "./user";
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
id: number;
purchasedBy: User;
purchasedProduct: PurchaseItem;
purchasedUnits: number;
status: PurchaseStatus;
}

View File

@ -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);

View File

@ -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";
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);
private readonly purchases: Purchase[] = [];
private readonly logger = new Logger(PurchaseService.name);
constructor() {}
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 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);
}
public findPurchasesByUser(userId: number): Purchase[] {
this.logger.debug(`Searching purchases for user 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()
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,