diff --git a/package-lock.json b/package-lock.json index 4b93793..fec7848 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@nestjs/jwt": "^11.0.2", "@nestjs/platform-express": "^11.0.1", "@nestjs/typeorm": "^11.0.0", + "async-lock": "^1.4.1", "bcrypt": "^6.0.0", "dotenv": "^17.2.3", "pg": "^8.16.3", @@ -4025,6 +4026,12 @@ "dev": true, "license": "MIT" }, + "node_modules/async-lock": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.1.tgz", + "integrity": "sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==", + "license": "MIT" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", diff --git a/package.json b/package.json index ca7e74f..f5ad160 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@nestjs/jwt": "^11.0.2", "@nestjs/platform-express": "^11.0.1", "@nestjs/typeorm": "^11.0.0", + "async-lock": "^1.4.1", "bcrypt": "^6.0.0", "dotenv": "^17.2.3", "pg": "^8.16.3", diff --git a/src/controller/buy.controller.ts b/src/controller/buy.controller.ts index 71d027b..e7bb1de 100644 --- a/src/controller/buy.controller.ts +++ b/src/controller/buy.controller.ts @@ -1,10 +1,18 @@ -import { Body, Controller, Logger, Post, Req } from '@nestjs/common'; +import { + Body, + Controller, + Logger, + NotFoundException, + Post, + Req, +} 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'; import { TokenService } from 'src/service/token.service'; +import AsyncLock from 'async-lock'; @Controller('/buy') export class BuyController { @@ -13,6 +21,7 @@ export class BuyController { PurchaseStatus.IN_PROGRESS, ]; private readonly logger = new Logger(BuyController.name); + private readonly purchaseLock = new AsyncLock(); constructor( private readonly purchaseService: PurchaseService, @@ -42,26 +51,34 @@ export class BuyController { } @Post('/complete') - public async complete(@Req() request) { + public async complete( + @Req() request, + @Body('purchaseId') purchaseId: string, + ) { + if (!purchaseId) { + throw new NotFoundException(); + } const userId = request.userId; const user = await this.userService.getUserById(userId); - const purchases = await this.purchaseService.getPurchasesForUser(user); - purchases - .filter((purchase) => - BuyController.PURCHASE_STATUS_TO_REEVALUATE.includes(purchase.status), - ) - .forEach(async (purchase) => { - const { isPaid } = await this.stripeService.getStripeSessionFromId( - purchase.stripeSessionId!, - ); - purchase.status = isPaid - ? PurchaseStatus.COMPLETED - : PurchaseStatus.IN_PROGRESS; - this.purchaseService.recordPurchase(purchase); - if (purchase.status === PurchaseStatus.COMPLETED) { - this.tokenService.addTokens(user, purchase); - } - }); + await this.purchaseLock.acquire(purchaseId, async () => { + const purchase = await this.purchaseService.getPurchaseById(purchaseId); + if ( + !BuyController.PURCHASE_STATUS_TO_REEVALUATE.includes(purchase.status) + ) { + this.logger.debug('Purchase is not open to re-evaluate'); + return; + } + const { isPaid } = await this.stripeService.getStripeSessionFromId( + purchase.stripeSessionId!, + ); + purchase.status = isPaid + ? PurchaseStatus.COMPLETED + : PurchaseStatus.IN_PROGRESS; + await this.purchaseService.recordPurchase(purchase); + if (purchase.status === PurchaseStatus.COMPLETED) { + await this.tokenService.addTokens(user, purchase); + } + }); return { url: '/signup' }; } diff --git a/src/service/purchase.service.ts b/src/service/purchase.service.ts index da0a52d..b140d9f 100644 --- a/src/service/purchase.service.ts +++ b/src/service/purchase.service.ts @@ -25,13 +25,17 @@ export class PurchaseService { purchasedProduct: purchasedItem, purchasedUnits: units, status: PurchaseStatus.CREATED, - purchaseDate: Date.now(), + purchaseDate: new Date(), }); return this.purchaseRepo.save(purchase); } - public recordPurchase(purchase: Purchase) { - this.purchaseRepo.save(purchase); + public async recordPurchase(purchase: Purchase) { + await this.purchaseRepo.save(purchase); + } + + public async getPurchaseById(purchaseId: string) { + return this.purchaseRepo.findOneOrFail({ where: { id: purchaseId } }); } public getPurchasesForUser(user: User) {