Refactor stripe session creation flow
parent
472ed8eb70
commit
ea124b597d
|
|
@ -1,2 +1,3 @@
|
||||||
dist/
|
dist/
|
||||||
node_modules/
|
node_modules/
|
||||||
|
.env
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
constructor(private readonly purchaseService: PurchaseService, private readonly userService: UserService) {}
|
private readonly stripeService: StripeService
|
||||||
|
) {}
|
||||||
@Post("/start")
|
|
||||||
@Redirect()
|
@Post('/start')
|
||||||
public async getStripeSessionUrl(@Body("userId") userId: number, @Body("quantity") quantity: number) {
|
@Redirect()
|
||||||
const user = this.userService.getUserById(userId)
|
public async startPurchaseFlow(
|
||||||
const purchase = this.purchaseService.recordPurchaseStart(user, PurchaseItem.PAELLA, quantity)
|
@Body('userId') userId: number,
|
||||||
const session = await this.stripe.checkout.sessions.create({
|
@Body('quantity') quantity: number,
|
||||||
line_items: [
|
) {
|
||||||
{
|
const user = this.userService.getUserById(userId);
|
||||||
price: purchase.purchasedProduct,
|
const purchase = this.purchaseService.recordPurchaseStart(
|
||||||
quantity: purchase.purchasedUnits
|
user,
|
||||||
}
|
PurchaseItem.PAELLA,
|
||||||
],
|
quantity,
|
||||||
mode: "payment",
|
);
|
||||||
success_url: "https://localhost:5173/buy/complete",
|
const session = await this.stripeService.createStripeSession(purchase);
|
||||||
cancel_url: "https://localhost:5173/buy/cancel",
|
console.log(session);
|
||||||
payment_method_types: ['card'],
|
return { url: session.url };
|
||||||
})
|
}
|
||||||
console.log(session)
|
|
||||||
|
@Get('/complete')
|
||||||
return {url: session.url}
|
public complete() {}
|
||||||
}
|
|
||||||
|
@Get('/cancel')
|
||||||
@Get("/complete")
|
public cancel() {}
|
||||||
public complete() {
|
}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get("/cancel")
|
|
||||||
public cancel() {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
export enum PurchaseItem {
|
export enum PurchaseItem {
|
||||||
PAELLA = "PAELLA",
|
PAELLA = 'PAELLA',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
export enum PurchaseStatus {
|
export enum PurchaseStatus {
|
||||||
CREATED = "CREATED",
|
CREATED = 'CREATED',
|
||||||
COMPLETED = "COMPLETED",
|
COMPLETED = 'COMPLETED',
|
||||||
FAILED = "FAILED",
|
FAILED = 'FAILED',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
};
|
||||||
public findPurchasesByUser(userId: number): Purchase[] {
|
this.purchases.push(purchase);
|
||||||
this.logger.debug(`Searching purchases for user ID: ${userId}`)
|
return purchase;
|
||||||
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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue