diff --git a/src/clanShop/clanShop.service.ts b/src/clanShop/clanShop.service.ts index 310b62d3a..1def1e4aa 100644 --- a/src/clanShop/clanShop.service.ts +++ b/src/clanShop/clanShop.service.ts @@ -17,12 +17,18 @@ import { VotingQueueParams } from '../fleaMarket/types/votingQueueParams.type'; import { ItemName } from '../clanInventory/item/enum/itemName.enum'; import { VotingQueueName } from '../voting/enum/VotingQueue.enum'; import { ClientSession, Connection } from 'mongoose'; -import { cancelTransaction } from '../common/function/cancelTransaction'; +import { Clan } from '../clan/clan.schema'; +import { + initializeSession, + cancelTransaction, + endTransaction, +} from '../common/function/Transactions'; import { InjectConnection } from '@nestjs/mongoose'; import { IServiceReturn } from '../common/service/basicService/IService'; @Injectable() export class ClanShopService { + constructor( private readonly clanService: ClanService, private readonly votingService: VotingService, @@ -37,17 +43,10 @@ export class ClanShopService { * This method performs several operations including validating the clan's funds, * reserving the required amount, initiating a voting process, and scheduling a voting check job. * All operations are executed within a transaction to ensure consistency. - * + * * @param playerId - The unique identifier of the player attempting to buy the item. * @param clanId - The unique identifier of the clan associated with the purchase. * @param item - The item being purchased, including its properties such as price. - * - * @throws Will cancel the transaction and throw errors if: - * - The clan cannot be retrieved or has insufficient game coins. - * - Funds cannot be reserved for the purchase. - * - The player cannot be retrieved. - * - The voting process cannot be initiated. - * * @returns A promise that resolves when the transaction is successfully committed. */ async buyItem( @@ -55,22 +54,29 @@ export class ClanShopService { clanId: string, item: ItemProperty, ): Promise> { - const session = await this.connection.startSession(); - session.startTransaction(); + const [session, sessionError] = await initializeSession(this.connection); + if (sessionError) return [null, sessionError]; const [clan, clanErrors] = await this.clanService.readOneById(clanId, { includeRefs: [ModelName.STOCK], }); - if (clanErrors) return await cancelTransaction(session, clanErrors); - if (clan.gameCoins < item.price) - return await cancelTransaction(session, [notEnoughCoinsError]); - const [, error] = await this.reserveFunds(clan._id, item.price, session); - if (error) return await cancelTransaction(session, error); + if (clanErrors) return cancelTransaction(session, clanErrors); + + if (clan.gameCoins < item.price) { + return cancelTransaction(session, [notEnoughCoinsError]); + } + + const [, reserveError] = await this.reserveFunds( + clan._id, + item.price, + session, + ); + if (reserveError) return cancelTransaction(session, reserveError); const [player, playerError] = await this.playerService.getPlayerById(playerId); - if (playerError) return await cancelTransaction(session, playerError); + if (playerError) return cancelTransaction(session, playerError); const [voting, votingErrors] = await this.votingService.startVoting( { @@ -82,20 +88,19 @@ export class ClanShopService { }, session, ); - if (votingErrors) { - return await cancelTransaction(session, votingErrors); - } + if (votingErrors) return cancelTransaction(session, votingErrors); + + const result = await endTransaction(session, true); await this.votingQueue.addVotingCheckJob({ voting, stockId: clan.Stock._id, price: item.price, queue: VotingQueueName.CLAN_SHOP, + clanId, }); - await session.commitTransaction(); - session.endSession(); - return [true, null]; + return result; } /** @@ -103,14 +108,13 @@ export class ClanShopService { * * @param clanId - The unique identifier of the clan whose funds are to be reserved. * @param price - The amount to be deducted from the clan's gameCoins. - * @returns A promise that resolves to the result of the update operation. + * @param session - mongoose ClientSession for transaction support. + * @returns A promise with the update result. */ async reserveFunds(clanId: string, price: number, session: ClientSession) { return await this.clanService.basicService.updateOneById( clanId, - { - $inc: { gameCoins: -price }, - }, + { $inc: { gameCoins: -price } }, { session }, ); } @@ -131,23 +135,34 @@ export class ClanShopService { * 4. Commits the transaction and ends the session. * * If any error occurs during the process, the transaction is canceled, and the session is ended. + * + * @returns A promise that resolves to a boolean indicating the success of the operation or an error if any step fails. */ - async checkVotingOnExpire(data: VotingQueueParams) { + async checkVotingOnExpire(data: VotingQueueParams): Promise> { + const { voting, price, clanId, stockId } = data; - const session = await this.connection.startSession(); - session.startTransaction(); + const [session, sessionError] = await initializeSession(this.connection); + if (sessionError) return [null, sessionError]; const votePassed = await this.votingService.checkVotingSuccess(voting); + if (votePassed) { - const [, passedError] = await this.handleVotePassed(voting, stockId); - if (passedError) await cancelTransaction(session, passedError); + const [, passedError] = await this.handleVotePassed( + voting, + stockId, + session, + ); + if (passedError) return cancelTransaction(session, passedError); } else { - const [, rejectError] = await this.handleVoteRejected(clanId, price); - if (rejectError) await cancelTransaction(session, rejectError); + const [, rejectError] = await this.handleVoteRejected( + clanId, + price, + session, + ); + if (rejectError) return cancelTransaction(session, rejectError); } - await session.commitTransaction(); - await session.endSession(); + return endTransaction(session, true); } /** @@ -156,12 +171,19 @@ export class ClanShopService { * * @param clanId - The unique identifier of the clan. * @param price - The amount to increment the clan's game coins by. + * @param session - mongoose ClientSession for transaction support. * @returns A promise that resolves with the result of the update operation. */ - private async handleVoteRejected(clanId, price) { - return this.clanService.basicService.updateOneById(clanId, { - $inc: { gameCoins: price }, - }); + private async handleVoteRejected( + clanId: string, + price: number, + session: ClientSession, + ) { + return await this.clanService.basicService.updateOneById( + clanId, + { $inc: { gameCoins: price } }, + { session }, + ); } /** @@ -172,11 +194,16 @@ export class ClanShopService { * * @param voting - The voting details containing information about the entity. * @param stockId - The identifier of the stock associated with the vote. + * @param session - mongoose ClientSession for transaction support. * @returns A promise that resolves to the created item. */ - private async handleVotePassed(voting: VotingDto, stockId: string) { + private async handleVotePassed( + voting: VotingDto, + stockId: string, + session: ClientSession, + ) { const newItem = this.getCreateItemDto(voting.shopItemName, stockId); - return await this.itemService.createOne(newItem); + return await this.itemService.createOne(newItem, { session }); } /** @@ -186,15 +213,14 @@ export class ClanShopService { * @param stockId - The unique identifier for the stock to associate with the item. * @returns A new `CreateItemDto` object containing the item's properties and additional metadata. */ - private getCreateItemDto(itemName: ItemName, stockId: string) { + private getCreateItemDto(itemName: ItemName, stockId: string): CreateItemDto { const item = itemProperties[itemName]; - const newItem: CreateItemDto = { + return { ...item, location: [0, 1], unityKey: item.name, stock_id: stockId, room_id: null, }; - return newItem; } } diff --git a/src/clanShop/clanShopVoting.processor.ts b/src/clanShop/clanShopVoting.processor.ts index ef9a88370..d3c06a142 100644 --- a/src/clanShop/clanShopVoting.processor.ts +++ b/src/clanShop/clanShopVoting.processor.ts @@ -6,6 +6,7 @@ import { VotingQueueName } from '../voting/enum/VotingQueue.enum'; @Processor(VotingQueueName.CLAN_SHOP) export class ClanShopVotingProcessor extends WorkerHost { + constructor(private readonly clanShopService: ClanShopService) { super(); }