Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 71 additions & 45 deletions src/clanShop/clanShop.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -37,40 +43,40 @@ 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(
playerId: string,
clanId: string,
item: ItemProperty,
): Promise<IServiceReturn<boolean>> {
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(
{
Expand All @@ -82,35 +88,33 @@ 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;
}

/**
* 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.
* @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 },
);
}
Expand All @@ -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<IServiceReturn<boolean>> {

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);
}

/**
Expand All @@ -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 },
);
}

/**
Expand All @@ -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 });
}

/**
Expand All @@ -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;
}
}
1 change: 1 addition & 0 deletions src/clanShop/clanShopVoting.processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down