diff --git a/src/app.module.ts b/src/app.module.ts index b89be6d..4bdc165 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,9 +1,9 @@ import { Module } from '@nestjs/common'; -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'; +import { AccessController } from './controller/access.controller'; +import { UserService } from './service/user.service'; +import { BuyController } from './controller/buy.controller'; +import { PurchaseService } from './service/purchase.service'; +import { StripeService } from './service/stripe.service'; @Module({ imports: [], diff --git a/src/buy.controller.ts b/src/buy.controller.ts deleted file mode 100644 index 136972e..0000000 --- a/src/buy.controller.ts +++ /dev/null @@ -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() {} -} diff --git a/src/access.controller.ts b/src/controller/access.controller.ts similarity index 94% rename from src/access.controller.ts rename to src/controller/access.controller.ts index 46f339c..a70e693 100644 --- a/src/access.controller.ts +++ b/src/controller/access.controller.ts @@ -5,7 +5,7 @@ import { Post, UnauthorizedException, } from '@nestjs/common'; -import { UserService } from './user.service'; +import { UserService } from '../service/user.service'; @Controller('/access') export class AccessController { diff --git a/src/controller/buy.controller.ts b/src/controller/buy.controller.ts new file mode 100644 index 0000000..0c4bfd7 --- /dev/null +++ b/src/controller/buy.controller.ts @@ -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' }; + } +} diff --git a/src/dto/purchase-status.ts b/src/dto/purchase-status.ts index 616718f..1cacea6 100644 --- a/src/dto/purchase-status.ts +++ b/src/dto/purchase-status.ts @@ -1,5 +1,6 @@ export enum PurchaseStatus { CREATED = 'CREATED', + IN_PROGRESS = 'IN_PROGRESS', COMPLETED = 'COMPLETED', FAILED = 'FAILED', } diff --git a/src/dto/purchase.ts b/src/dto/purchase.ts index 60805e5..b9580da 100644 --- a/src/dto/purchase.ts +++ b/src/dto/purchase.ts @@ -8,4 +8,5 @@ export class Purchase { purchasedProduct: PurchaseItem; purchasedUnits: number; status: PurchaseStatus; + stripeSessionId?: string; } diff --git a/src/main.ts b/src/main.ts index 17b88ad..75127a2 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,6 +1,6 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; -import dotenv from 'dotenv' +import dotenv from 'dotenv'; dotenv.config(); async function bootstrap() { diff --git a/src/purchase.service.ts b/src/purchase.service.ts deleted file mode 100644 index 9271b70..0000000 --- a/src/purchase.service.ts +++ /dev/null @@ -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), - ); - } -} diff --git a/src/service/purchase.service.ts b/src/service/purchase.service.ts new file mode 100644 index 0000000..b7381b0 --- /dev/null +++ b/src/service/purchase.service.ts @@ -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), + ); + } +} diff --git a/src/service/stripe.service.ts b/src/service/stripe.service.ts new file mode 100644 index 0000000..c274d24 --- /dev/null +++ b/src/service/stripe.service.ts @@ -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 = new Map(); + + constructor() { + const stripePrivateKey = process.env.STRIPE_PRIVATE_KEY!; + this.stripe = new Stripe(stripePrivateKey); + } + + public async createStripeSession( + user: User, + purchase: Purchase, + ): Promise> { + 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> { + const sessionId = this.stripeSessions.get(user.id); + if (!sessionId) { + throw new NotFoundException(); + } + return this.stripe.checkout.sessions.retrieve(sessionId); + } +} diff --git a/src/user.service.ts b/src/service/user.service.ts similarity index 100% rename from src/user.service.ts rename to src/service/user.service.ts diff --git a/src/stripe.service.ts b/src/stripe.service.ts deleted file mode 100644 index 62c2ff7..0000000 --- a/src/stripe.service.ts +++ /dev/null @@ -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> { - 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'], - }); - } -}