From e02e191be090be002efef048082be8009de6c54f Mon Sep 17 00:00:00 2001 From: CapoMK25 Date: Thu, 29 Jan 2026 21:54:33 +0200 Subject: [PATCH 1/8] Implemented transaction support for clanshop --- src/clanShop/clanShop.service.ts | 192 ++++++++++------------- src/clanShop/clanShopVoting.processor.ts | 7 + 2 files changed, 89 insertions(+), 110 deletions(-) diff --git a/src/clanShop/clanShop.service.ts b/src/clanShop/clanShop.service.ts index 310b62d3a..fe0f68514 100644 --- a/src/clanShop/clanShop.service.ts +++ b/src/clanShop/clanShop.service.ts @@ -17,9 +17,15 @@ 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 { + initializeSession, + cancelTransaction, + endTransaction +} from '../common/function/Transactions'; import { InjectConnection } from '@nestjs/mongoose'; import { IServiceReturn } from '../common/service/basicService/IService'; +import ServiceError from '../common/service/basicService/ServiceError'; +import { SEReason } from '../common/service/basicService/SEReason'; @Injectable() export class ClanShopService { @@ -34,76 +40,61 @@ export class ClanShopService { /** * Handles the process of purchasing an item from shop. - * 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. + * Uses centralized transaction utilities for consistency. */ async buyItem( playerId: string, 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]); + try { + 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); + const [, error] = await this.reserveFunds(clan._id, item.price, session); + if (error) return await cancelTransaction(session, error); + const [player, playerError] = await this.playerService.getPlayerById(playerId); + if (playerError) return await cancelTransaction(session, playerError); - const [player, playerError] = - await this.playerService.getPlayerById(playerId); - if (playerError) return await cancelTransaction(session, playerError); + const [voting, votingErrors] = await this.votingService.startVoting( + { + voterPlayer: player, + type: VotingType.SHOP_BUY_ITEM, + queue: VotingQueueName.CLAN_SHOP, + clanId, + shopItem: item.name, + }, + session, + ); + if (votingErrors) return await cancelTransaction(session, votingErrors); - const [voting, votingErrors] = await this.votingService.startVoting( - { - voterPlayer: player, - type: VotingType.SHOP_BUY_ITEM, + await this.votingQueue.addVotingCheckJob({ + voting, + stockId: clan.Stock._id, + price: item.price, queue: VotingQueueName.CLAN_SHOP, - clanId, - shopItem: item.name, - }, - session, - ); - if (votingErrors) { - return await cancelTransaction(session, votingErrors); - } - - await this.votingQueue.addVotingCheckJob({ - voting, - stockId: clan.Stock._id, - price: item.price, - queue: VotingQueueName.CLAN_SHOP, - }); + }); - await session.commitTransaction(); - session.endSession(); - return [true, null]; + return await endTransaction(session); + } catch (error) { + return await cancelTransaction(session, new ServiceError({ + reason: SEReason.UNEXPECTED, + message: error instanceof Error ? error.message : 'Buy item failed', + value: error + })); + } } /** - * Reserves funds from a clan by decrementing the specified price from the clan's gameCoins. - * - * @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. + * Reserves funds from a clan by decrementing gameCoins within a session. */ async reserveFunds(clanId: string, price: number, session: ClientSession) { return await this.clanService.basicService.updateOneById( @@ -116,75 +107,56 @@ export class ClanShopService { } /** - * Handles the expiration of a voting process by determining its outcome, - * performing the necessary actions based on the result, and cleaning up - * the associated voting record. - * - * @param data - An object containing the voting details, price, clan ID, and stock ID. - * - * The method performs the following steps: - * 1. Starts a database session and transaction. - * 2. Checks if the voting process was successful. - * - If successful, processes the passed vote and handles any errors. - * - If rejected, processes the rejected vote and handles any errors. - * 3. Deletes the voting record from the database and handles any errors. - * 4. Commits the transaction and ends the session. - * - * If any error occurs during the process, the transaction is canceled, and the session is ended. + * Handles the expiration of a voting process. + * Ensures that item creation or coin refunds happen atomically. */ - 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); - } else { - const [, rejectError] = await this.handleVoteRejected(clanId, price); - if (rejectError) await cancelTransaction(session, rejectError); - } + try { + const votePassed = await this.votingService.checkVotingSuccess(voting); + + if (votePassed) { + const [, passedError] = await this.handleVotePassed(voting, stockId, session); + if (passedError) return await cancelTransaction(session, passedError); + } else { + const [, rejectError] = await this.handleVoteRejected(clanId, price, session); + if (rejectError) return await cancelTransaction(session, rejectError); + } - await session.commitTransaction(); - await session.endSession(); + return await endTransaction(session); + } catch (error) { + return await cancelTransaction(session, new ServiceError({ + reason: SEReason.UNEXPECTED, + message: error instanceof Error ? error.message : 'Voting expiration failed', + value: error + })); + } } /** - * Handles the event when a vote is rejected. - * Return the reserved coin amount to clan. - * - * @param clanId - The unique identifier of the clan. - * @param price - The amount to increment the clan's game coins by. - * @returns A promise that resolves with the result of the update operation. + * Returns the reserved coin amount to the clan. */ - private async handleVoteRejected(clanId, price) { - return this.clanService.basicService.updateOneById(clanId, { - $inc: { gameCoins: price }, - }); + private async handleVoteRejected(clanId: string, price: number, session: ClientSession) { + return this.clanService.basicService.updateOneById( + clanId, + { $inc: { gameCoins: price } } as any, + { session } + ); } /** - * Handles the event when a vote has passed. - * - * This method creates a new item based on the voting details and stock ID, - * and then delegates the creation to the item service. - * - * @param voting - The voting details containing information about the entity. - * @param stockId - The identifier of the stock associated with the vote. - * @returns A promise that resolves to the created item. + * Creates the purchased item in the clan stock. */ - 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 } as any); } /** - * Creates a new `CreateItemDto` object based on the provided item name and stock ID. - * - * @param itemName - The name of the item to retrieve properties for. - * @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. + * Helper to map shop items to CreateItemDto. */ private getCreateItemDto(itemName: ItemName, stockId: string) { const item = itemProperties[itemName]; @@ -197,4 +169,4 @@ export class ClanShopService { }; return newItem; } -} +} \ No newline at end of file diff --git a/src/clanShop/clanShopVoting.processor.ts b/src/clanShop/clanShopVoting.processor.ts index ef9a88370..bb77f1080 100644 --- a/src/clanShop/clanShopVoting.processor.ts +++ b/src/clanShop/clanShopVoting.processor.ts @@ -16,5 +16,12 @@ export class ClanShopVotingProcessor extends WorkerHost { */ async process(job: Job): Promise { await this.clanShopService.checkVotingOnExpire(job.data); + const [, error] = await this.clanShopService.checkVotingOnExpire(job.data); + + if (error) { + throw new Error(`ClanShop Voting Job failed: ${JSON.stringify(error)}`); + } + return true; + } } From 7943511cd2c8d92ee42eba207908da266abd4618 Mon Sep 17 00:00:00 2001 From: CapoMK25 Date: Fri, 30 Jan 2026 09:40:15 +0200 Subject: [PATCH 2/8] Linter changes --- src/clanShop/clanShop.service.ts | 78 ++++++++++++++++-------- src/clanShop/clanShopVoting.processor.ts | 1 - 2 files changed, 52 insertions(+), 27 deletions(-) diff --git a/src/clanShop/clanShop.service.ts b/src/clanShop/clanShop.service.ts index fe0f68514..3fd50018f 100644 --- a/src/clanShop/clanShop.service.ts +++ b/src/clanShop/clanShop.service.ts @@ -17,10 +17,10 @@ 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 { - initializeSession, - cancelTransaction, - endTransaction +import { + initializeSession, + cancelTransaction, + endTransaction, } from '../common/function/Transactions'; import { InjectConnection } from '@nestjs/mongoose'; import { IServiceReturn } from '../common/service/basicService/IService'; @@ -55,13 +55,14 @@ export class ClanShopService { 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); - const [player, playerError] = await this.playerService.getPlayerById(playerId); + const [player, playerError] = + await this.playerService.getPlayerById(playerId); if (playerError) return await cancelTransaction(session, playerError); const [voting, votingErrors] = await this.votingService.startVoting( @@ -85,11 +86,14 @@ export class ClanShopService { return await endTransaction(session); } catch (error) { - return await cancelTransaction(session, new ServiceError({ - reason: SEReason.UNEXPECTED, - message: error instanceof Error ? error.message : 'Buy item failed', - value: error - })); + return await cancelTransaction( + session, + new ServiceError({ + reason: SEReason.UNEXPECTED, + message: error instanceof Error ? error.message : 'Buy item failed', + value: error, + }), + ); } } @@ -110,47 +114,69 @@ export class ClanShopService { * Handles the expiration of a voting process. * Ensures that item creation or coin refunds happen atomically. */ - async checkVotingOnExpire(data: VotingQueueParams): Promise> { + async checkVotingOnExpire( + data: VotingQueueParams, + ): Promise> { const { voting, price, clanId, stockId } = data; const [session, sessionError] = await initializeSession(this.connection); if (sessionError) return [null, sessionError]; try { const votePassed = await this.votingService.checkVotingSuccess(voting); - + if (votePassed) { - const [, passedError] = await this.handleVotePassed(voting, stockId, session); + const [, passedError] = await this.handleVotePassed( + voting, + stockId, + session, + ); if (passedError) return await cancelTransaction(session, passedError); } else { - const [, rejectError] = await this.handleVoteRejected(clanId, price, session); + const [, rejectError] = await this.handleVoteRejected( + clanId, + price, + session, + ); if (rejectError) return await cancelTransaction(session, rejectError); } return await endTransaction(session); } catch (error) { - return await cancelTransaction(session, new ServiceError({ - reason: SEReason.UNEXPECTED, - message: error instanceof Error ? error.message : 'Voting expiration failed', - value: error - })); + return await cancelTransaction( + session, + new ServiceError({ + reason: SEReason.UNEXPECTED, + message: + error instanceof Error ? error.message : 'Voting expiration failed', + value: error, + }), + ); } } /** * Returns the reserved coin amount to the clan. */ - private async handleVoteRejected(clanId: string, price: number, session: ClientSession) { + private async handleVoteRejected( + clanId: string, + price: number, + session: ClientSession, + ) { return this.clanService.basicService.updateOneById( - clanId, - { $inc: { gameCoins: price } } as any, - { session } + clanId, + { $inc: { gameCoins: price } } as any, + { session }, ); } /** * Creates the purchased item in the clan stock. */ - private async handleVotePassed(voting: VotingDto, stockId: string, session: ClientSession) { + private async handleVotePassed( + voting: VotingDto, + stockId: string, + session: ClientSession, + ) { const newItem = this.getCreateItemDto(voting.shopItemName, stockId); return await this.itemService.createOne(newItem, { session } as any); } @@ -169,4 +195,4 @@ export class ClanShopService { }; return newItem; } -} \ No newline at end of file +} diff --git a/src/clanShop/clanShopVoting.processor.ts b/src/clanShop/clanShopVoting.processor.ts index bb77f1080..cd82696b2 100644 --- a/src/clanShop/clanShopVoting.processor.ts +++ b/src/clanShop/clanShopVoting.processor.ts @@ -22,6 +22,5 @@ export class ClanShopVotingProcessor extends WorkerHost { throw new Error(`ClanShop Voting Job failed: ${JSON.stringify(error)}`); } return true; - } } From 9bae313b4691c9565d2a1564cd6dfdf401da29dc Mon Sep 17 00:00:00 2001 From: CapoMK25 Date: Mon, 2 Feb 2026 21:20:08 +0200 Subject: [PATCH 3/8] Refactored the clanShop transaction support --- src/clanShop/clanShop.service.ts | 151 ++++++++---------- src/clanShop/clanShopVoting.processor.ts | 14 +- .../types/votingQueueParams.type.ts | 1 + 3 files changed, 78 insertions(+), 88 deletions(-) diff --git a/src/clanShop/clanShop.service.ts b/src/clanShop/clanShop.service.ts index 3fd50018f..28fd36e70 100644 --- a/src/clanShop/clanShop.service.ts +++ b/src/clanShop/clanShop.service.ts @@ -24,8 +24,6 @@ import { } from '../common/function/Transactions'; import { InjectConnection } from '@nestjs/mongoose'; import { IServiceReturn } from '../common/service/basicService/IService'; -import ServiceError from '../common/service/basicService/ServiceError'; -import { SEReason } from '../common/service/basicService/SEReason'; @Injectable() export class ClanShopService { @@ -43,59 +41,55 @@ export class ClanShopService { * Uses centralized transaction utilities for consistency. */ async buyItem( - playerId: string, - clanId: string, - item: ItemProperty, - ): Promise> { + playerId: string, + clanId: string, + item: ItemProperty, +): Promise> { + const [session, sessionError] = await initializeSession(this.connection); if (sessionError) return [null, sessionError]; + if (!session) return [null, [{ message: 'Could not initialize session' } as any]]; - try { - 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); - const [player, playerError] = - await this.playerService.getPlayerById(playerId); - if (playerError) return await cancelTransaction(session, playerError); - - const [voting, votingErrors] = await this.votingService.startVoting( - { - voterPlayer: player, - type: VotingType.SHOP_BUY_ITEM, - queue: VotingQueueName.CLAN_SHOP, - clanId, - shopItem: item.name, - }, - session, - ); - if (votingErrors) return await cancelTransaction(session, votingErrors); - - await this.votingQueue.addVotingCheckJob({ - voting, - stockId: clan.Stock._id, - price: item.price, - queue: VotingQueueName.CLAN_SHOP, - }); - - return await endTransaction(session); - } catch (error) { - return await cancelTransaction( - session, - new ServiceError({ - reason: SEReason.UNEXPECTED, - message: error instanceof Error ? error.message : 'Buy item failed', - value: error, - }), - ); + const [clan, clanErrors] = await this.clanService.readOneById(clanId, { + includeRefs: [ModelName.STOCK], + session + } as any); + 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 cancelTransaction(session, playerError); + + const [voting, votingErrors] = await this.votingService.startVoting( + { + voterPlayer: player, + type: VotingType.SHOP_BUY_ITEM, + queue: VotingQueueName.CLAN_SHOP, + clanId, + shopItem: item.name, + }, + session, + ); + if (votingErrors) return cancelTransaction(session, votingErrors); + + await this.votingQueue.addVotingCheckJob({ + voting, + stockId: clan.Stock._id, + price: item.price, + queue: VotingQueueName.CLAN_SHOP, + clanId, + shopItem: item.name, + }); + + // 4. Clean Exit + return endTransaction(session, true); +} /** * Reserves funds from a clan by decrementing gameCoins within a session. @@ -117,42 +111,33 @@ export class ClanShopService { async checkVotingOnExpire( data: VotingQueueParams, ): Promise> { + const { voting, price, clanId, stockId } = data; const [session, sessionError] = await initializeSession(this.connection); + if (sessionError) return [null, sessionError]; + if (!session) return [null, [{ message: 'Session initialization failed' } as any]]; + + const votePassed = await this.votingService.checkVotingSuccess(voting); + + if (votePassed) { + const [, passedError] = await this.handleVotePassed( + voting, + stockId, + session, + ); + if (passedError) return cancelTransaction(session, passedError); + } else { + const [, rejectError] = await this.handleVoteRejected( + clanId, + price, + session, + ); + if (rejectError) return cancelTransaction(session, rejectError); + } - try { - const votePassed = await this.votingService.checkVotingSuccess(voting); - - if (votePassed) { - const [, passedError] = await this.handleVotePassed( - voting, - stockId, - session, - ); - if (passedError) return await cancelTransaction(session, passedError); - } else { - const [, rejectError] = await this.handleVoteRejected( - clanId, - price, - session, - ); - if (rejectError) return await cancelTransaction(session, rejectError); - } - - return await endTransaction(session); - } catch (error) { - return await cancelTransaction( - session, - new ServiceError({ - reason: SEReason.UNEXPECTED, - message: - error instanceof Error ? error.message : 'Voting expiration failed', - value: error, - }), - ); + return endTransaction(session, true); } - } /** * Returns the reserved coin amount to the clan. @@ -162,7 +147,7 @@ export class ClanShopService { price: number, session: ClientSession, ) { - return this.clanService.basicService.updateOneById( + return await this.clanService.basicService.updateOneById( clanId, { $inc: { gameCoins: price } } as any, { session }, diff --git a/src/clanShop/clanShopVoting.processor.ts b/src/clanShop/clanShopVoting.processor.ts index cd82696b2..cc5f56cb7 100644 --- a/src/clanShop/clanShopVoting.processor.ts +++ b/src/clanShop/clanShopVoting.processor.ts @@ -3,9 +3,12 @@ import { Job } from 'bullmq'; import { ClanShopService } from './clanShop.service'; import { VotingQueueParams } from '../fleaMarket/types/votingQueueParams.type'; import { VotingQueueName } from '../voting/enum/VotingQueue.enum'; +import { Logger } from '@nestjs/common'; @Processor(VotingQueueName.CLAN_SHOP) export class ClanShopVotingProcessor extends WorkerHost { + private readonly logger = new Logger(ClanShopVotingProcessor.name); + constructor(private readonly clanShopService: ClanShopService) { super(); } @@ -14,13 +17,14 @@ export class ClanShopVotingProcessor extends WorkerHost { * Processes the job when it is executed. * @param job - The job to be processed. */ - async process(job: Job): Promise { - await this.clanShopService.checkVotingOnExpire(job.data); - const [, error] = await this.clanShopService.checkVotingOnExpire(job.data); + async process(job: Job): Promise { + const [result, error] = await this.clanShopService.checkVotingOnExpire(job.data); if (error) { - throw new Error(`ClanShop Voting Job failed: ${JSON.stringify(error)}`); + this.logger.error(`ClanShop Voting Job ${job.id} failed`, JSON.stringify(error)); + + throw new Error(`ClanShop Voting Job failed: ${error[0]?.message || 'Unknown Error'}`); } - return true; + return result; } } diff --git a/src/fleaMarket/types/votingQueueParams.type.ts b/src/fleaMarket/types/votingQueueParams.type.ts index c5ac03e0c..3f1b574bf 100644 --- a/src/fleaMarket/types/votingQueueParams.type.ts +++ b/src/fleaMarket/types/votingQueueParams.type.ts @@ -8,4 +8,5 @@ export type VotingQueueParams = { stockId?: string; fleaMarketItemId?: string; queue: VotingQueueName; + shopItem?: string; }; From 889ebaef3a6f89b1de6140d82965afe5660519d0 Mon Sep 17 00:00:00 2001 From: CapoMK25 Date: Mon, 2 Feb 2026 21:23:15 +0200 Subject: [PATCH 4/8] Linter changes --- src/clanShop/clanShop.service.ts | 85 +++++++++++++----------- src/clanShop/clanShopVoting.processor.ts | 13 +++- 2 files changed, 55 insertions(+), 43 deletions(-) diff --git a/src/clanShop/clanShop.service.ts b/src/clanShop/clanShop.service.ts index 28fd36e70..968cd0c48 100644 --- a/src/clanShop/clanShop.service.ts +++ b/src/clanShop/clanShop.service.ts @@ -41,18 +41,18 @@ export class ClanShopService { * Uses centralized transaction utilities for consistency. */ async buyItem( - playerId: string, - clanId: string, - item: ItemProperty, -): Promise> { - + playerId: string, + clanId: string, + item: ItemProperty, + ): Promise> { const [session, sessionError] = await initializeSession(this.connection); if (sessionError) return [null, sessionError]; - if (!session) return [null, [{ message: 'Could not initialize session' } as any]]; + if (!session) + return [null, [{ message: 'Could not initialize session' } as any]]; const [clan, clanErrors] = await this.clanService.readOneById(clanId, { includeRefs: [ModelName.STOCK], - session + session, } as any); if (clanErrors) return cancelTransaction(session, clanErrors); @@ -60,36 +60,41 @@ export class ClanShopService { return cancelTransaction(session, [notEnoughCoinsError]); } - const [, reserveError] = await this.reserveFunds(clan._id, item.price, session); + const [, reserveError] = await this.reserveFunds( + clan._id, + item.price, + session, + ); if (reserveError) return cancelTransaction(session, reserveError); - const [player, playerError] = await this.playerService.getPlayerById(playerId); + const [player, playerError] = + await this.playerService.getPlayerById(playerId); if (playerError) return cancelTransaction(session, playerError); const [voting, votingErrors] = await this.votingService.startVoting( - { - voterPlayer: player, - type: VotingType.SHOP_BUY_ITEM, - queue: VotingQueueName.CLAN_SHOP, - clanId, - shopItem: item.name, - }, - session, + { + voterPlayer: player, + type: VotingType.SHOP_BUY_ITEM, + queue: VotingQueueName.CLAN_SHOP, + clanId, + shopItem: item.name, + }, + session, ); if (votingErrors) return cancelTransaction(session, votingErrors); await this.votingQueue.addVotingCheckJob({ - voting, - stockId: clan.Stock._id, - price: item.price, - queue: VotingQueueName.CLAN_SHOP, - clanId, - shopItem: item.name, + voting, + stockId: clan.Stock._id, + price: item.price, + queue: VotingQueueName.CLAN_SHOP, + clanId, + shopItem: item.name, }); // 4. Clean Exit return endTransaction(session, true); -} + } /** * Reserves funds from a clan by decrementing gameCoins within a session. @@ -111,33 +116,33 @@ export class ClanShopService { async checkVotingOnExpire( data: VotingQueueParams, ): Promise> { - const { voting, price, clanId, stockId } = data; const [session, sessionError] = await initializeSession(this.connection); - + if (sessionError) return [null, sessionError]; - if (!session) return [null, [{ message: 'Session initialization failed' } as any]]; + if (!session) + return [null, [{ message: 'Session initialization failed' } as any]]; const votePassed = await this.votingService.checkVotingSuccess(voting); if (votePassed) { - const [, passedError] = await this.handleVotePassed( - voting, - stockId, - session, - ); - if (passedError) return 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, - session, - ); - if (rejectError) return cancelTransaction(session, rejectError); + const [, rejectError] = await this.handleVoteRejected( + clanId, + price, + session, + ); + if (rejectError) return cancelTransaction(session, rejectError); } return endTransaction(session, true); - } + } /** * Returns the reserved coin amount to the clan. diff --git a/src/clanShop/clanShopVoting.processor.ts b/src/clanShop/clanShopVoting.processor.ts index cc5f56cb7..819cbefed 100644 --- a/src/clanShop/clanShopVoting.processor.ts +++ b/src/clanShop/clanShopVoting.processor.ts @@ -18,12 +18,19 @@ export class ClanShopVotingProcessor extends WorkerHost { * @param job - The job to be processed. */ async process(job: Job): Promise { - const [result, error] = await this.clanShopService.checkVotingOnExpire(job.data); + const [result, error] = await this.clanShopService.checkVotingOnExpire( + job.data, + ); if (error) { - this.logger.error(`ClanShop Voting Job ${job.id} failed`, JSON.stringify(error)); + this.logger.error( + `ClanShop Voting Job ${job.id} failed`, + JSON.stringify(error), + ); - throw new Error(`ClanShop Voting Job failed: ${error[0]?.message || 'Unknown Error'}`); + throw new Error( + `ClanShop Voting Job failed: ${error[0]?.message || 'Unknown Error'}`, + ); } return result; } From 58f9096c3ae1a3a3c58a1df9dfed53ae270dfdd0 Mon Sep 17 00:00:00 2001 From: CapoMK25 Date: Tue, 3 Feb 2026 08:56:38 +0200 Subject: [PATCH 5/8] Removed comment --- src/clanShop/clanShop.service.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/clanShop/clanShop.service.ts b/src/clanShop/clanShop.service.ts index 968cd0c48..eb1b82472 100644 --- a/src/clanShop/clanShop.service.ts +++ b/src/clanShop/clanShop.service.ts @@ -91,8 +91,7 @@ export class ClanShopService { clanId, shopItem: item.name, }); - - // 4. Clean Exit + return endTransaction(session, true); } From 800c33cd659ebe34a2499236184b8685b7ce8ed4 Mon Sep 17 00:00:00 2001 From: CapoMK25 Date: Tue, 3 Feb 2026 20:42:07 +0200 Subject: [PATCH 6/8] Addressing all feedback here --- .../handleExpiredVotingCleanup.test.ts | 1 + src/clanShop/clanShop.service.ts | 78 +++++++++++-------- src/clanShop/clanShopVoting.processor.ts | 11 +-- 3 files changed, 49 insertions(+), 41 deletions(-) diff --git a/src/__tests__/voting/ExpiredVotingCleanupService/handleExpiredVotingCleanup.test.ts b/src/__tests__/voting/ExpiredVotingCleanupService/handleExpiredVotingCleanup.test.ts index 52975af17..e8bf7189d 100644 --- a/src/__tests__/voting/ExpiredVotingCleanupService/handleExpiredVotingCleanup.test.ts +++ b/src/__tests__/voting/ExpiredVotingCleanupService/handleExpiredVotingCleanup.test.ts @@ -12,6 +12,7 @@ describe('ExpiredVotingCleanupService.handleExpiredVotingCleanup() test suite', let expiredVotingCleanupService: ExpiredVotingCleanupService; beforeEach(async () => { + await votingModel.deleteMany({}); expiredVotingCleanupService = await VotingModule.getExpiredVotingCleanupService(); }); diff --git a/src/clanShop/clanShop.service.ts b/src/clanShop/clanShop.service.ts index eb1b82472..23add4db9 100644 --- a/src/clanShop/clanShop.service.ts +++ b/src/clanShop/clanShop.service.ts @@ -1,8 +1,5 @@ -import { Injectable } from '@nestjs/common'; -import { - itemProperties, - ItemProperty, -} from '../clanInventory/item/const/itemProperties'; +import { Injectable, Logger } from '@nestjs/common'; +import { itemProperties, ItemProperty } from '../clanInventory/item/const/itemProperties'; import { ClanService } from '../clan/clan.service'; import { ModelName } from '../common/enum/modelName.enum'; import { notEnoughCoinsError } from '../fleaMarket/errors/notEnoughCoins.error'; @@ -16,7 +13,8 @@ import { VotingQueue } from '../voting/voting.queue'; 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 { ClientSession, Connection, UpdateQuery } from 'mongoose'; +import { Clan } from '../clan/clan.schema'; import { initializeSession, cancelTransaction, @@ -27,6 +25,8 @@ import { IServiceReturn } from '../common/service/basicService/IService'; @Injectable() export class ClanShopService { + private readonly logger = new Logger(ClanShopService.name); + constructor( private readonly clanService: ClanService, private readonly votingService: VotingService, @@ -37,8 +37,12 @@ export class ClanShopService { ) {} /** - * Handles the process of purchasing an item from shop. - * Uses centralized transaction utilities for consistency. + * Handles the process of purchasing an item from the shop. + * Initializes a transaction, reserves clan funds, and starts a voting process. + * * @param playerId - The ID of the player initiating the purchase. + * @param clanId - The ID of the clan the player belongs to. + * @param item - The properties of the item being purchased. + * @returns A promise resolving to an IServiceReturn indicating success or containing errors. */ async buyItem( playerId: string, @@ -47,13 +51,11 @@ export class ClanShopService { ): Promise> { const [session, sessionError] = await initializeSession(this.connection); if (sessionError) return [null, sessionError]; - if (!session) - return [null, [{ message: 'Could not initialize session' } as any]]; - + const [clan, clanErrors] = await this.clanService.readOneById(clanId, { includeRefs: [ModelName.STOCK], - session, - } as any); + }); + if (clanErrors) return cancelTransaction(session, clanErrors); if (clan.gameCoins < item.price) { @@ -67,8 +69,7 @@ export class ClanShopService { ); if (reserveError) return cancelTransaction(session, reserveError); - const [player, playerError] = - await this.playerService.getPlayerById(playerId); + const [player, playerError] = await this.playerService.getPlayerById(playerId); if (playerError) return cancelTransaction(session, playerError); const [voting, votingErrors] = await this.votingService.startVoting( @@ -83,44 +84,47 @@ export class ClanShopService { ); 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, - shopItem: item.name, }); - return endTransaction(session, true); + return result; } /** - * Reserves funds from a clan by decrementing gameCoins within a session. + * Reserves funds from a clan by decrementing gameCoins. + * * @param clanId - The ID of the clan. + * @param price - The amount to deduct. + * @param session - The active database session for the transaction. + * @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 }, - }, + } as UpdateQuery, { session }, ); } /** - * Handles the expiration of a voting process. - * Ensures that item creation or coin refunds happen atomically. + * Processes the result of a finished vote when the queue job expires. + * * @param data - The parameters passed from the background job. + * @returns A promise resolving to an IServiceReturn. */ async checkVotingOnExpire( data: VotingQueueParams, ): Promise> { const { voting, price, clanId, stockId } = data; const [session, sessionError] = await initializeSession(this.connection); - if (sessionError) return [null, sessionError]; - if (!session) - return [null, [{ message: 'Session initialization failed' } as any]]; const votePassed = await this.votingService.checkVotingSuccess(voting); @@ -144,7 +148,10 @@ export class ClanShopService { } /** - * Returns the reserved coin amount to the clan. + * Returns the reserved coin amount to the clan if a vote fails. + * * @param clanId - The ID of the clan. + * @param price - The amount to return. + * @param session - The active database session. */ private async handleVoteRejected( clanId: string, @@ -153,13 +160,16 @@ export class ClanShopService { ) { return await this.clanService.basicService.updateOneById( clanId, - { $inc: { gameCoins: price } } as any, + { $inc: { gameCoins: price } } as UpdateQuery, { session }, ); } /** - * Creates the purchased item in the clan stock. + * Finalizes the purchase by creating the item in the clan stock. + * * @param voting - The voting record containing item details. + * @param stockId - The ID of the stock where the item will be placed. + * @param session - The active database session. */ private async handleVotePassed( voting: VotingDto, @@ -167,21 +177,23 @@ export class ClanShopService { session: ClientSession, ) { const newItem = this.getCreateItemDto(voting.shopItemName, stockId); - return await this.itemService.createOne(newItem, { session } as any); + return await this.itemService.createOne(newItem, { session }); } /** - * Helper to map shop items to CreateItemDto. + * Helper to map shop items to a CreateItemDto for the ItemService. + * * @param itemName - The name of the item. + * @param stockId - The ID of the destination stock. + * @returns A populated CreateItemDto. */ - 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; } -} +} \ No newline at end of file diff --git a/src/clanShop/clanShopVoting.processor.ts b/src/clanShop/clanShopVoting.processor.ts index 819cbefed..b352ee242 100644 --- a/src/clanShop/clanShopVoting.processor.ts +++ b/src/clanShop/clanShopVoting.processor.ts @@ -24,14 +24,9 @@ export class ClanShopVotingProcessor extends WorkerHost { if (error) { this.logger.error( - `ClanShop Voting Job ${job.id} failed`, + `ClanShop Voting Job ${job.id} stopped due to an error`, JSON.stringify(error), ); - - throw new Error( - `ClanShop Voting Job failed: ${error[0]?.message || 'Unknown Error'}`, - ); - } - return result; + return; } -} +}} From 93acf5c6f34c3edaee78adaad267060916fe5018 Mon Sep 17 00:00:00 2001 From: CapoMK25 Date: Tue, 3 Feb 2026 20:44:12 +0200 Subject: [PATCH 7/8] linter changes again... --- src/clanShop/clanShop.service.ts | 14 +++++++++----- src/clanShop/clanShopVoting.processor.ts | 3 ++- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/clanShop/clanShop.service.ts b/src/clanShop/clanShop.service.ts index 23add4db9..cd19936d8 100644 --- a/src/clanShop/clanShop.service.ts +++ b/src/clanShop/clanShop.service.ts @@ -1,5 +1,8 @@ import { Injectable, Logger } from '@nestjs/common'; -import { itemProperties, ItemProperty } from '../clanInventory/item/const/itemProperties'; +import { + itemProperties, + ItemProperty, +} from '../clanInventory/item/const/itemProperties'; import { ClanService } from '../clan/clan.service'; import { ModelName } from '../common/enum/modelName.enum'; import { notEnoughCoinsError } from '../fleaMarket/errors/notEnoughCoins.error'; @@ -51,7 +54,7 @@ export class ClanShopService { ): Promise> { const [session, sessionError] = await initializeSession(this.connection); if (sessionError) return [null, sessionError]; - + const [clan, clanErrors] = await this.clanService.readOneById(clanId, { includeRefs: [ModelName.STOCK], }); @@ -69,7 +72,8 @@ export class ClanShopService { ); if (reserveError) return cancelTransaction(session, reserveError); - const [player, playerError] = await this.playerService.getPlayerById(playerId); + const [player, playerError] = + await this.playerService.getPlayerById(playerId); if (playerError) return cancelTransaction(session, playerError); const [voting, votingErrors] = await this.votingService.startVoting( @@ -93,7 +97,7 @@ export class ClanShopService { queue: VotingQueueName.CLAN_SHOP, clanId, }); - + return result; } @@ -196,4 +200,4 @@ export class ClanShopService { room_id: null, }; } -} \ No newline at end of file +} diff --git a/src/clanShop/clanShopVoting.processor.ts b/src/clanShop/clanShopVoting.processor.ts index b352ee242..a9aba27ca 100644 --- a/src/clanShop/clanShopVoting.processor.ts +++ b/src/clanShop/clanShopVoting.processor.ts @@ -28,5 +28,6 @@ export class ClanShopVotingProcessor extends WorkerHost { JSON.stringify(error), ); return; + } } -}} +} From 432874290fd156cdcb74fc528ee90bbeedb14e4a Mon Sep 17 00:00:00 2001 From: hoan301298 Date: Tue, 3 Feb 2026 22:42:56 +0200 Subject: [PATCH 8/8] re-store documentation comments, vote type and test --- .../handleExpiredVotingCleanup.test.ts | 1 - src/clanShop/clanShop.service.ts | 93 ++++++++++++------- src/clanShop/clanShopVoting.processor.ts | 16 +--- .../types/votingQueueParams.type.ts | 1 - 4 files changed, 60 insertions(+), 51 deletions(-) diff --git a/src/__tests__/voting/ExpiredVotingCleanupService/handleExpiredVotingCleanup.test.ts b/src/__tests__/voting/ExpiredVotingCleanupService/handleExpiredVotingCleanup.test.ts index e8bf7189d..52975af17 100644 --- a/src/__tests__/voting/ExpiredVotingCleanupService/handleExpiredVotingCleanup.test.ts +++ b/src/__tests__/voting/ExpiredVotingCleanupService/handleExpiredVotingCleanup.test.ts @@ -12,7 +12,6 @@ describe('ExpiredVotingCleanupService.handleExpiredVotingCleanup() test suite', let expiredVotingCleanupService: ExpiredVotingCleanupService; beforeEach(async () => { - await votingModel.deleteMany({}); expiredVotingCleanupService = await VotingModule.getExpiredVotingCleanupService(); }); diff --git a/src/clanShop/clanShop.service.ts b/src/clanShop/clanShop.service.ts index cd19936d8..1def1e4aa 100644 --- a/src/clanShop/clanShop.service.ts +++ b/src/clanShop/clanShop.service.ts @@ -1,4 +1,4 @@ -import { Injectable, Logger } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { itemProperties, ItemProperty, @@ -16,7 +16,7 @@ import { VotingQueue } from '../voting/voting.queue'; 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, UpdateQuery } from 'mongoose'; +import { ClientSession, Connection } from 'mongoose'; import { Clan } from '../clan/clan.schema'; import { initializeSession, @@ -28,7 +28,6 @@ import { IServiceReturn } from '../common/service/basicService/IService'; @Injectable() export class ClanShopService { - private readonly logger = new Logger(ClanShopService.name); constructor( private readonly clanService: ClanService, @@ -40,12 +39,15 @@ export class ClanShopService { ) {} /** - * Handles the process of purchasing an item from the shop. - * Initializes a transaction, reserves clan funds, and starts a voting process. - * * @param playerId - The ID of the player initiating the purchase. - * @param clanId - The ID of the clan the player belongs to. - * @param item - The properties of the item being purchased. - * @returns A promise resolving to an IServiceReturn indicating success or containing errors. + * Handles the process of purchasing an item from shop. + * 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. + * @returns A promise that resolves when the transaction is successfully committed. */ async buyItem( playerId: string, @@ -102,30 +104,42 @@ export class ClanShopService { } /** - * Reserves funds from a clan by decrementing gameCoins. - * * @param clanId - The ID of the clan. - * @param price - The amount to deduct. - * @param session - The active database session for the transaction. + * Reserves funds from a clan by decrementing the specified price from the clan's gameCoins. + * + * @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. + * @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 }, - } as UpdateQuery, + { $inc: { gameCoins: -price } }, { session }, ); } /** - * Processes the result of a finished vote when the queue job expires. - * * @param data - The parameters passed from the background job. - * @returns A promise resolving to an IServiceReturn. + * Handles the expiration of a voting process by determining its outcome, + * performing the necessary actions based on the result, and cleaning up + * the associated voting record. + * + * @param data - An object containing the voting details, price, clan ID, and stock ID. + * + * The method performs the following steps: + * 1. Starts a database session and transaction. + * 2. Checks if the voting process was successful. + * - If successful, processes the passed vote and handles any errors. + * - If rejected, processes the rejected vote and handles any errors. + * 3. Deletes the voting record from the database and handles any errors. + * 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, - ): Promise> { + async checkVotingOnExpire(data: VotingQueueParams): Promise> { + const { voting, price, clanId, stockId } = data; const [session, sessionError] = await initializeSession(this.connection); if (sessionError) return [null, sessionError]; @@ -152,10 +166,13 @@ export class ClanShopService { } /** - * Returns the reserved coin amount to the clan if a vote fails. - * * @param clanId - The ID of the clan. - * @param price - The amount to return. - * @param session - The active database session. + * Handles the event when a vote is rejected. + * Return the reserved coin amount to clan. + * + * @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: string, @@ -164,16 +181,21 @@ export class ClanShopService { ) { return await this.clanService.basicService.updateOneById( clanId, - { $inc: { gameCoins: price } } as UpdateQuery, + { $inc: { gameCoins: price } }, { session }, ); } /** - * Finalizes the purchase by creating the item in the clan stock. - * * @param voting - The voting record containing item details. - * @param stockId - The ID of the stock where the item will be placed. - * @param session - The active database session. + * Handles the event when a vote has passed. + * + * This method creates a new item based on the voting details and stock ID, + * and then delegates the creation to the item service. + * + * @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, @@ -185,10 +207,11 @@ export class ClanShopService { } /** - * Helper to map shop items to a CreateItemDto for the ItemService. - * * @param itemName - The name of the item. - * @param stockId - The ID of the destination stock. - * @returns A populated CreateItemDto. + * Creates a new `CreateItemDto` object based on the provided item name and stock ID. + * + * @param itemName - The name of the item to retrieve properties for. + * @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): CreateItemDto { const item = itemProperties[itemName]; diff --git a/src/clanShop/clanShopVoting.processor.ts b/src/clanShop/clanShopVoting.processor.ts index a9aba27ca..d3c06a142 100644 --- a/src/clanShop/clanShopVoting.processor.ts +++ b/src/clanShop/clanShopVoting.processor.ts @@ -3,11 +3,9 @@ import { Job } from 'bullmq'; import { ClanShopService } from './clanShop.service'; import { VotingQueueParams } from '../fleaMarket/types/votingQueueParams.type'; import { VotingQueueName } from '../voting/enum/VotingQueue.enum'; -import { Logger } from '@nestjs/common'; @Processor(VotingQueueName.CLAN_SHOP) export class ClanShopVotingProcessor extends WorkerHost { - private readonly logger = new Logger(ClanShopVotingProcessor.name); constructor(private readonly clanShopService: ClanShopService) { super(); @@ -17,17 +15,7 @@ export class ClanShopVotingProcessor extends WorkerHost { * Processes the job when it is executed. * @param job - The job to be processed. */ - async process(job: Job): Promise { - const [result, error] = await this.clanShopService.checkVotingOnExpire( - job.data, - ); - - if (error) { - this.logger.error( - `ClanShop Voting Job ${job.id} stopped due to an error`, - JSON.stringify(error), - ); - return; - } + async process(job: Job): Promise { + await this.clanShopService.checkVotingOnExpire(job.data); } } diff --git a/src/fleaMarket/types/votingQueueParams.type.ts b/src/fleaMarket/types/votingQueueParams.type.ts index 3f1b574bf..c5ac03e0c 100644 --- a/src/fleaMarket/types/votingQueueParams.type.ts +++ b/src/fleaMarket/types/votingQueueParams.type.ts @@ -8,5 +8,4 @@ export type VotingQueueParams = { stockId?: string; fleaMarketItemId?: string; queue: VotingQueueName; - shopItem?: string; };