Skip to content
67 changes: 67 additions & 0 deletions database/migrations/20260927120000-add-player-clan-joindate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/**
* Migration: Add clan_joindate field to players
*
* Issue #1003: Player data should hold the date when the player joined the clan.
* - Players currently in a clan get 01-09-2026 as their join date
* - Players without a clan get null
*/

const DEFAULT_JOIN_DATE = new Date(Date.UTC(2026, 8, 1)); // "2026-09-01T00:00:00.000Z"

/**
* @param db {import('mongodb').Db}
* @param client {import('mongodb').MongoClient}
*/
module.exports.up = async (db, client) => {
const session = client.startSession();

try {
await session.withTransaction(async () => {
const players = db.collection('players');

const inClan = await players.updateMany(
{ clan_joindate: { $exists: false }, clan_id: { $ne: null } },
{ $set: { clan_joindate: DEFAULT_JOIN_DATE } },
{ session },
);

const withoutClan = await players.updateMany(
{ clan_joindate: { $exists: false } },
{ $set: { clan_joindate: null } },
{ session },
);

console.log(
`[migrate-mongo] Set clan_joindate for ${inClan.modifiedCount} players in a clan and ${withoutClan.modifiedCount} players without a clan`,
);
});
} finally {
await session.endSession();
}
};

/**
* @param db {import('mongodb').Db}
* @param client {import('mongodb').MongoClient}
*/
module.exports.down = async (db, client) => {
const session = client.startSession();

try {
await session.withTransaction(async () => {
const players = db.collection('players');

const result = await players.updateMany(
{ clan_joindate: { $exists: true } },
{ $unset: { clan_joindate: '' } },
{ session },
);

console.log(
`[migrate-mongo] Removed clan_joindate from ${result.modifiedCount} players`,
);
});
} finally {
await session.endSession();
}
};
14 changes: 14 additions & 0 deletions src/__tests__/clan/join/JoinService/handleJoinRequest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,20 @@ describe('JoinService.handleJoinRequest() test suite', () => {
expect(clanDto.name).toBe(openClan.name);
});

it('Should set clan_joindate of the joined player to the current date', async () => {
const joinToCreate = joinBuilder.setClanId(openClan._id).build();

const before = Date.now();
await joinService.handleJoinRequest(joinToCreate.clan_id, player._id);
const after = Date.now();

const playerInDB = await playerModel.findById(player._id);
const joinTime = playerInDB.clan_joindate.getTime();

expect(joinTime).toBeGreaterThanOrEqual(before);
expect(joinTime).toBeLessThanOrEqual(after);
});

it('Should throw NotFoundException if clan with that _id does not exists', async () => {
const nonExisting_id = getNonExisting_id();
const joinToCreate = joinBuilder.setClanId(nonExisting_id).build();
Expand Down
13 changes: 13 additions & 0 deletions src/__tests__/clan/join/JoinService/leaveClan.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,19 @@ describe('JoinService.leaveClan() test suite', () => {
expect(leftPlayer.clan_id).toBeNull();
});

it('Should clear clan_joindate of the player who left', async () => {
await playerModel.updateOne(
{ _id: player._id },
{ clan_joindate: new Date(Date.UTC(2026, 8, 1)) },
);

await joinService.leaveClan(player._id);

const leftPlayer = await playerModel.findById(player._id);

expect(leftPlayer.clan_joindate).toBeNull();
});

it('Should throw NotFoundException if clan does not exists', async () => {
const nonExisting_id = getNonExisting_id();
await playerModel.updateOne(
Expand Down
1 change: 1 addition & 0 deletions src/__tests__/player/data/player/playerBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export default class PlayerBuilder {
clanRole_id: null,
_id: undefined,
environment: Environment.TEACHING_DEMO,
clan_joindate: null,
};

build(): Player {
Expand Down
1 change: 1 addition & 0 deletions src/__tests__/player/data/player/playerDtoBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export default class PlayerDtoBuilder implements IDataBuilder<PlayerDto> {
battleCharacter_ids: [],
Clan: undefined,
CustomCharacter: [],
clan_joindate: null,
};

build(): PlayerDto {
Expand Down
1 change: 1 addition & 0 deletions src/box/accountClaimer/testerAccount.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ export class TesterAccountService {
{
clan_id: clanWithLeastPlayers._id,
clanRole_id: leaderRole._id,
clan_joindate: new Date(),
},
);

Expand Down
8 changes: 6 additions & 2 deletions src/clan/clan.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,11 @@ export class ClanService {

const [, playerErrors] = await this.playerService.updateOneById(
player_id,
{ clan_id: clan._id, clanRole_id: leaderRole?._id },
{
clan_id: clan._id,
clanRole_id: leaderRole?._id,
clan_joindate: new Date(),
},
{ session },
);
if (playerErrors) return await cancelTransaction(session, playerErrors);
Expand Down Expand Up @@ -432,7 +436,7 @@ export class ClanService {
for (const player of clan.Player) {
const [, upErrors] = await this.playerService.updateOneById(
player._id,
{ clan_id: null },
{ clan_id: null, clan_joindate: null },
{ session },
);
if (upErrors) {
Expand Down
4 changes: 3 additions & 1 deletion src/clan/join/join.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import {
cancelTransaction,
} from '../../common/function/Transactions';
import ClanNotifier from '../clan.notifier';

@Injectable()
export class JoinService {
private readonly logger = new Logger(JoinService.name);
Expand Down Expand Up @@ -178,6 +177,7 @@ export class JoinService {
{ _id: player_id },
{
clan_id: null,
clan_joindate: null,
},
{ session },
);
Expand Down Expand Up @@ -252,6 +252,7 @@ export class JoinService {
{ _id: player_id },
{
clan_id: null,
clan_joindate: null,
},
{ session },
); // update clan_id for the requested player;
Expand Down Expand Up @@ -315,6 +316,7 @@ export class JoinService {
{
clan_id,
clanRole_id: memberRole._id,
clan_joindate: new Date(),
},
{ session },
);
Expand Down
7 changes: 7 additions & 0 deletions src/player/dto/player.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,13 @@ export class PlayerDto {
@Expose()
clan_id: string;

/**
* Date when the player joined their current clan.
* @example "2026-09-01T12:00:00.000Z"
*/
@Expose()
clan_joindate?: Date;

/**
* Player's clan object
*/
Expand Down
3 changes: 3 additions & 0 deletions src/player/schemas/player.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ export class Player {
@Prop({ type: MongooseSchema.Types.ObjectId, ref: ModelName.CLAN })
clan_id?: string;

@Prop({ type: Date, default: null })
clan_joindate?: Date;

@ExtractField()
@Prop({ type: [ObjectId], default: [] })
battleCharacter_ids?: string[] | ObjectId[];
Expand Down
Loading