Skip to content

Commit

Permalink
feat: transact between players (#712)
Browse files Browse the repository at this point in the history
* feat: transact between players

fixes #674

* chore: regenerate apiclient
  • Loading branch information
niekcandaele authored Nov 24, 2023
1 parent 544272b commit f388f5e
Show file tree
Hide file tree
Showing 6 changed files with 333 additions and 2 deletions.
20 changes: 20 additions & 0 deletions packages/app-api/src/controllers/PlayerOnGameserverController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,14 @@ class PlayerOnGameServerSetCurrencyInputDTO {
currency!: number;
}

class ParamSenderReceiver {
@IsUUID(4)
sender!: string;

@IsUUID(4)
receiver!: string;
}

@OpenAPI({
security: [{ domainAuth: [] }],
})
Expand Down Expand Up @@ -101,4 +109,16 @@ export class PlayerOnGameServerController {
const service = new PlayerOnGameServerService(req.domainId);
return apiResponse(await service.setCurrency(params.id, body.currency));
}

@UseBefore(AuthService.getAuthMiddleware([PERMISSIONS.MANAGE_PLAYERS]), onlyIfEconomyEnabledMiddleware)
@ResponseSchema(PlayerOnGameserverOutputDTOAPI)
@Post('/gameserver/player/:sender/:receiver/transfer')
async transactBetweenPlayers(
@Req() req: AuthenticatedRequest,
@Params() params: ParamSenderReceiver,
@Body() body: PlayerOnGameServerSetCurrencyInputDTO
) {
const service = new PlayerOnGameServerService(req.domainId);
return apiResponse(await service.transact(params.sender, params.receiver, body.currency));
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { IntegrationTest, SetupGameServerPlayers, expect } from '@takaro/test';
import { isAxiosError } from 'axios';

const group = 'PlayerOnGameserverController';

Expand Down Expand Up @@ -102,6 +103,132 @@ const tests = [
return rejectedRes;
},
}),
new IntegrationTest<SetupGameServerPlayers.ISetupData>({
group,
snapshot: false,
name: 'Can send money between two players',
setup: SetupGameServerPlayers.setup,
test: async function () {
const res = await this.client.playerOnGameserver.playerOnGameServerControllerSearch({
filters: {
gameServerId: [this.setupData.gameServer1.id],
},
});

const player1 = res.data.data[0];
const player2 = res.data.data[1];

await this.client.settings.settingsControllerSet('economyEnabled', {
gameServerId: player1.gameServerId,
value: 'true',
});
await this.client.playerOnGameserver.playerOnGameServerControllerSetCurrency(player1.id, {
currency: 100,
});
await this.client.playerOnGameserver.playerOnGameServerControllerSetCurrency(player2.id, {
currency: 100,
});

await this.client.playerOnGameserver.playerOnGameServerControllerTransactBetweenPlayers(player1.id, player2.id, {
currency: 50,
});

const player1Res = await this.client.playerOnGameserver.playerOnGameServerControllerGetOne(player1.id);
const player2Res = await this.client.playerOnGameserver.playerOnGameServerControllerGetOne(player2.id);

expect(player1Res.data.data.currency).to.be.eq(50);
expect(player2Res.data.data.currency).to.be.eq(150);
},
}),
new IntegrationTest<SetupGameServerPlayers.ISetupData>({
group,
snapshot: false,
name: 'Safely aborts transaction when sender does not have enough money',
setup: SetupGameServerPlayers.setup,
expectedStatus: 400,
test: async function () {
const res = await this.client.playerOnGameserver.playerOnGameServerControllerSearch({
filters: {
gameServerId: [this.setupData.gameServer1.id],
},
});

const player1 = res.data.data[0];
const player2 = res.data.data[1];

await this.client.settings.settingsControllerSet('economyEnabled', {
gameServerId: player1.gameServerId,
value: 'true',
});
await this.client.playerOnGameserver.playerOnGameServerControllerSetCurrency(player1.id, {
currency: 100,
});
await this.client.playerOnGameserver.playerOnGameServerControllerSetCurrency(player2.id, {
currency: 100,
});

try {
await this.client.playerOnGameserver.playerOnGameServerControllerTransactBetweenPlayers(
player1.id,
player2.id,
{
currency: 150,
}
);
} catch (error) {
if (!isAxiosError(error)) throw error;
if (!error.response) throw error;
expect(error.response.data.meta.error.message).to.be.eq('Insufficient funds');
}

const player1Res = await this.client.playerOnGameserver.playerOnGameServerControllerGetOne(player1.id);
const player2Res = await this.client.playerOnGameserver.playerOnGameServerControllerGetOne(player2.id);

expect(player1Res.data.data.currency).to.be.eq(100);
expect(player2Res.data.data.currency).to.be.eq(100);
},
}),
new IntegrationTest<SetupGameServerPlayers.ISetupData>({
group,
snapshot: false,
name: 'Does not allow transacting currency for players on different gameservers',
setup: SetupGameServerPlayers.setup,
expectedStatus: 400,
test: async function () {
const res1 = await this.client.playerOnGameserver.playerOnGameServerControllerSearch({
filters: {
gameServerId: [this.setupData.gameServer1.id],
},
});
const res2 = await this.client.playerOnGameserver.playerOnGameServerControllerSearch({
filters: {
gameServerId: [this.setupData.gameServer2.id],
},
});

const player1 = res1.data.data[0];
const player2 = res2.data.data[0];

await this.client.settings.settingsControllerSet('economyEnabled', {
gameServerId: player1.gameServerId,
value: 'true',
});

try {
await this.client.playerOnGameserver.playerOnGameServerControllerTransactBetweenPlayers(
player1.id,
player2.id,
{
currency: 150,
}
);
} catch (error) {
if (!isAxiosError(error)) throw error;
if (!error.response) throw error;
expect(error.response.data.meta.error.message).to.be.eq('Players are not on the same game server');
}
},
}),
];

describe(group, function () {
Expand Down
37 changes: 37 additions & 0 deletions packages/app-api/src/db/playerOnGameserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,4 +182,41 @@ export class PlayerOnGameServerRepo extends ITakaroRepo<

return new IPlayerReferenceDTO().construct(foundProfiles[0]);
}

async transact(senderId: string, receiverId: string, amount: number) {
const { model } = await this.getModel();

await model.transaction(async (trx) => {
const txQuery = () => model.query(trx).modify('domainScoped', this.domainId);

const sender = txQuery().findById(senderId);
const receiver = txQuery().findById(receiverId);

const [senderData, receiverData] = await Promise.all([sender, receiver]);

if (!senderData || !receiverData) {
throw new errors.NotFoundError();
}

if (senderData.gameServerId !== receiverData.gameServerId) {
throw new errors.BadRequestError('Players are not on the same game server');
}

if (senderData.currency < amount) {
throw new errors.BadRequestError('Insufficient funds');
}

await txQuery()
.findById(senderId)
.patch({ currency: senderData.currency - amount })
.returning('*');

await txQuery()
.findById(receiverId)
.patch({ currency: receiverData.currency + amount })
.returning('*');

return;
});
}
}
2 changes: 1 addition & 1 deletion packages/app-api/src/middlewares/onlyIfEconomyEnabled.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export async function onlyIfEconomyEnabledMiddleware(

// Could be a playerOnGameServer route
const playerOnGameServerService = new PlayerOnGameServerService(req.domainId);
const possiblePlayerId = req.params.id;
const possiblePlayerId = req.params.id || req.params.sender || req.params.receiver;
const maybePlayer = await playerOnGameServerService.findOne(possiblePlayerId);

if (maybePlayer) {
Expand Down
4 changes: 4 additions & 0 deletions packages/app-api/src/service/PlayerOnGameserverService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,4 +166,8 @@ export class PlayerOnGameServerService extends TakaroService<
throw error;
}
}

async transact(senderId: string, receiverId: string, amount: number) {
return this.repo.transact(senderId, receiverId, amount);
}
}
Loading

0 comments on commit f388f5e

Please sign in to comment.