Introduce mutex for purchase fulfillment locking
parent
4a763acb48
commit
1fd8bfec89
|
|
@ -14,6 +14,7 @@
|
||||||
"@nestjs/jwt": "^11.0.2",
|
"@nestjs/jwt": "^11.0.2",
|
||||||
"@nestjs/platform-express": "^11.0.1",
|
"@nestjs/platform-express": "^11.0.1",
|
||||||
"@nestjs/typeorm": "^11.0.0",
|
"@nestjs/typeorm": "^11.0.0",
|
||||||
|
"async-lock": "^1.4.1",
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"pg": "^8.16.3",
|
"pg": "^8.16.3",
|
||||||
|
|
@ -4025,6 +4026,12 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/asynckit": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@
|
||||||
"@nestjs/jwt": "^11.0.2",
|
"@nestjs/jwt": "^11.0.2",
|
||||||
"@nestjs/platform-express": "^11.0.1",
|
"@nestjs/platform-express": "^11.0.1",
|
||||||
"@nestjs/typeorm": "^11.0.0",
|
"@nestjs/typeorm": "^11.0.0",
|
||||||
|
"async-lock": "^1.4.1",
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"pg": "^8.16.3",
|
"pg": "^8.16.3",
|
||||||
|
|
|
||||||
|
|
@ -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 { PurchaseService } from '../service/purchase.service';
|
||||||
import { UserService } from '../service/user.service';
|
import { UserService } from '../service/user.service';
|
||||||
import { PurchaseItem } from '../dto/purchase-item';
|
import { PurchaseItem } from '../dto/purchase-item';
|
||||||
import { StripeService } from '../service/stripe.service';
|
import { StripeService } from '../service/stripe.service';
|
||||||
import { PurchaseStatus } from 'src/dto/purchase-status';
|
import { PurchaseStatus } from 'src/dto/purchase-status';
|
||||||
import { TokenService } from 'src/service/token.service';
|
import { TokenService } from 'src/service/token.service';
|
||||||
|
import AsyncLock from 'async-lock';
|
||||||
|
|
||||||
@Controller('/buy')
|
@Controller('/buy')
|
||||||
export class BuyController {
|
export class BuyController {
|
||||||
|
|
@ -13,6 +21,7 @@ export class BuyController {
|
||||||
PurchaseStatus.IN_PROGRESS,
|
PurchaseStatus.IN_PROGRESS,
|
||||||
];
|
];
|
||||||
private readonly logger = new Logger(BuyController.name);
|
private readonly logger = new Logger(BuyController.name);
|
||||||
|
private readonly purchaseLock = new AsyncLock();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly purchaseService: PurchaseService,
|
private readonly purchaseService: PurchaseService,
|
||||||
|
|
@ -42,26 +51,34 @@ export class BuyController {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('/complete')
|
@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 userId = request.userId;
|
||||||
const user = await this.userService.getUserById(userId);
|
const user = await this.userService.getUserById(userId);
|
||||||
const purchases = await this.purchaseService.getPurchasesForUser(user);
|
await this.purchaseLock.acquire(purchaseId, async () => {
|
||||||
purchases
|
const purchase = await this.purchaseService.getPurchaseById(purchaseId);
|
||||||
.filter((purchase) =>
|
if (
|
||||||
BuyController.PURCHASE_STATUS_TO_REEVALUATE.includes(purchase.status),
|
!BuyController.PURCHASE_STATUS_TO_REEVALUATE.includes(purchase.status)
|
||||||
)
|
) {
|
||||||
.forEach(async (purchase) => {
|
this.logger.debug('Purchase is not open to re-evaluate');
|
||||||
const { isPaid } = await this.stripeService.getStripeSessionFromId(
|
return;
|
||||||
purchase.stripeSessionId!,
|
}
|
||||||
);
|
const { isPaid } = await this.stripeService.getStripeSessionFromId(
|
||||||
purchase.status = isPaid
|
purchase.stripeSessionId!,
|
||||||
? PurchaseStatus.COMPLETED
|
);
|
||||||
: PurchaseStatus.IN_PROGRESS;
|
purchase.status = isPaid
|
||||||
this.purchaseService.recordPurchase(purchase);
|
? PurchaseStatus.COMPLETED
|
||||||
if (purchase.status === PurchaseStatus.COMPLETED) {
|
: PurchaseStatus.IN_PROGRESS;
|
||||||
this.tokenService.addTokens(user, purchase);
|
await this.purchaseService.recordPurchase(purchase);
|
||||||
}
|
if (purchase.status === PurchaseStatus.COMPLETED) {
|
||||||
});
|
await this.tokenService.addTokens(user, purchase);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return { url: '/signup' };
|
return { url: '/signup' };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,13 +25,17 @@ export class PurchaseService {
|
||||||
purchasedProduct: purchasedItem,
|
purchasedProduct: purchasedItem,
|
||||||
purchasedUnits: units,
|
purchasedUnits: units,
|
||||||
status: PurchaseStatus.CREATED,
|
status: PurchaseStatus.CREATED,
|
||||||
purchaseDate: Date.now(),
|
purchaseDate: new Date(),
|
||||||
});
|
});
|
||||||
return this.purchaseRepo.save(purchase);
|
return this.purchaseRepo.save(purchase);
|
||||||
}
|
}
|
||||||
|
|
||||||
public recordPurchase(purchase: Purchase) {
|
public async recordPurchase(purchase: Purchase) {
|
||||||
this.purchaseRepo.save(purchase);
|
await this.purchaseRepo.save(purchase);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getPurchaseById(purchaseId: string) {
|
||||||
|
return this.purchaseRepo.findOneOrFail({ where: { id: purchaseId } });
|
||||||
}
|
}
|
||||||
|
|
||||||
public getPurchasesForUser(user: User) {
|
public getPurchasesForUser(user: User) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue