Track stripe sessions and fulfill purchase status

main
MiguelMLorente 2025-11-24 20:33:34 +01:00
parent ea124b597d
commit c411143d24
12 changed files with 167 additions and 109 deletions

View File

@ -1,9 +1,9 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { AccessController } from './access.controller'; import { AccessController } from './controller/access.controller';
import { UserService } from './user.service'; import { UserService } from './service/user.service';
import { BuyController } from './buy.controller'; import { BuyController } from './controller/buy.controller';
import { PurchaseService } from './purchase.service'; import { PurchaseService } from './service/purchase.service';
import { StripeService } from './stripe.service'; import { StripeService } from './service/stripe.service';
@Module({ @Module({
imports: [], imports: [],

View File

@ -1,37 +0,0 @@
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() {}
}

View File

@ -5,7 +5,7 @@ import {
Post, Post,
UnauthorizedException, UnauthorizedException,
} from '@nestjs/common'; } from '@nestjs/common';
import { UserService } from './user.service'; import { UserService } from '../service/user.service';
@Controller('/access') @Controller('/access')
export class AccessController { export class AccessController {

View File

@ -0,0 +1,47 @@
import { Body, Controller, Get, Post, Redirect } from '@nestjs/common';
import { PurchaseService } from '../service/purchase.service';
import { UserService } from '../service/user.service';
import { PurchaseItem } from '../dto/purchase-item';
import { StripeService } from '../service/stripe.service';
import { PurchaseStatus } from 'src/dto/purchase-status';
@Controller('/buy')
export class BuyController {
constructor(
private readonly purchaseService: PurchaseService,
private readonly userService: UserService,
private readonly stripeService: StripeService,
) {}
@Post('/start')
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(
user,
purchase,
);
this.purchaseService.attachStripeSessionToPurchase(session.id, purchase.id);
console.log(session);
return { url: session.url };
}
@Post('/complete')
public async complete(@Body('userId') userId: number) {
const user = this.userService.getUserById(userId);
const session = await this.stripeService.getStripeSessionForCustomer(user);
const isPaid = session.payment_status == 'paid';
this.purchaseService.findAndTransitionPurchaseStatus(
session.id,
isPaid ? PurchaseStatus.COMPLETED : PurchaseStatus.IN_PROGRESS,
);
return { url: '/signup' };
}
}

View File

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

View File

@ -8,4 +8,5 @@ export class Purchase {
purchasedProduct: PurchaseItem; purchasedProduct: PurchaseItem;
purchasedUnits: number; purchasedUnits: number;
status: PurchaseStatus; status: PurchaseStatus;
stripeSessionId?: string;
} }

View File

@ -1,6 +1,6 @@
import { NestFactory } from '@nestjs/core'; import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module'; import { AppModule } from './app.module';
import dotenv from 'dotenv' import dotenv from 'dotenv';
dotenv.config(); dotenv.config();
async function bootstrap() { async function bootstrap() {

View File

@ -1,37 +0,0 @@
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),
);
}
}

View File

@ -0,0 +1,65 @@
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
import { Purchase } from 'src/dto/purchase';
import { PurchaseItem } from 'src/dto/purchase-item';
import { PurchaseStatus } from 'src/dto/purchase-status';
import { User } from 'src/dto/user';
@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 attachStripeSessionToPurchase(
sessionId: string,
purchaseId: number,
): void {
const purchase = this.purchases.find(
(purchase) => purchase.id === purchaseId,
);
if (!purchase) {
throw new NotFoundException();
}
purchase.stripeSessionId = sessionId;
}
public findAndTransitionPurchaseStatus(
sessionId: string,
purchaseStatus: PurchaseStatus,
) {
const purchase = this.purchases.find(
(purchase) => purchase.stripeSessionId === sessionId,
);
if (!purchase) {
throw new NotFoundException();
}
purchase.status = purchaseStatus;
}
public findPurchasesByUser(userId: number): Purchase[] {
this.logger.debug(`Searching purchases for user ID: ${userId}`);
return this.purchases.filter(
(purchase) => (purchase.purchasedBy.id = userId),
);
}
}

View File

@ -0,0 +1,46 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { Purchase } from '../dto/purchase';
import Stripe from 'stripe';
import { User } from '../dto/user';
import { NotFoundError } from 'rxjs';
@Injectable()
export class StripeService {
private readonly stripe: Stripe;
private readonly stripeSessions: Map<number, string> = new Map();
constructor() {
const stripePrivateKey = process.env.STRIPE_PRIVATE_KEY!;
this.stripe = new Stripe(stripePrivateKey);
}
public async createStripeSession(
user: User,
purchase: Purchase,
): Promise<Stripe.Response<Stripe.Checkout.Session>> {
const session = await this.stripe.checkout.sessions.create({
line_items: [
{
price: 'price_1SWLunBQui1OXGptCHHnBvLX',
quantity: purchase.purchasedUnits,
},
],
mode: 'payment',
success_url: 'http://localhost:5173/buy/return',
cancel_url: 'http://localhost:5173/buy/return',
payment_method_types: ['card', 'paypal'],
});
this.stripeSessions.set(user.id, session.id);
return session;
}
public getStripeSessionForCustomer(
user: User,
): Promise<Stripe.Response<Stripe.Checkout.Session>> {
const sessionId = this.stripeSessions.get(user.id);
if (!sessionId) {
throw new NotFoundException();
}
return this.stripe.checkout.sessions.retrieve(sessionId);
}
}

View File

@ -1,28 +0,0 @@
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'],
});
}
}