From 16258f09b88469037a6af09a6de19ffbfa6cda73 Mon Sep 17 00:00:00 2001 From: constf03 Date: Sat, 26 Sep 2026 18:25:17 +0300 Subject: [PATCH 1/8] add new Player field clan_joindate --- src/player/dto/player.dto.ts | 7 +++++++ src/player/schemas/player.schema.ts | 3 +++ 2 files changed, 10 insertions(+) diff --git a/src/player/dto/player.dto.ts b/src/player/dto/player.dto.ts index b7edf7fa1..8b37aee28 100644 --- a/src/player/dto/player.dto.ts +++ b/src/player/dto/player.dto.ts @@ -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 */ diff --git a/src/player/schemas/player.schema.ts b/src/player/schemas/player.schema.ts index da1399bc9..5679410ee 100644 --- a/src/player/schemas/player.schema.ts +++ b/src/player/schemas/player.schema.ts @@ -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[]; From 24a8ecfbf1316d41f55ecd9dfb736ac163f7faf9 Mon Sep 17 00:00:00 2001 From: constf03 Date: Sat, 26 Sep 2026 18:34:45 +0300 Subject: [PATCH 2/8] handle clan_joindate when player joins/leaves clan --- src/clan/clan.service.ts | 8 ++++++-- src/clan/join/join.service.ts | 4 +++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/clan/clan.service.ts b/src/clan/clan.service.ts index e9c1ef4e0..a952e198d 100644 --- a/src/clan/clan.service.ts +++ b/src/clan/clan.service.ts @@ -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); @@ -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) { diff --git a/src/clan/join/join.service.ts b/src/clan/join/join.service.ts index f78451e4e..0a46869be 100644 --- a/src/clan/join/join.service.ts +++ b/src/clan/join/join.service.ts @@ -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); @@ -178,6 +177,7 @@ export class JoinService { { _id: player_id }, { clan_id: null, + clan_joindate: null, }, { session }, ); @@ -252,6 +252,7 @@ export class JoinService { { _id: player_id }, { clan_id: null, + clan_joindate: null, }, { session }, ); // update clan_id for the requested player; @@ -315,6 +316,7 @@ export class JoinService { { clan_id, clanRole_id: memberRole._id, + clan_joindate: new Date(), }, { session }, ); From ddbc777859db4340da61fe77a0a88d661b4208e6 Mon Sep 17 00:00:00 2001 From: constf03 Date: Sun, 27 Sep 2026 17:14:30 +0300 Subject: [PATCH 3/8] update player clan_joindate also in the old update method (just in case) --- src/player/player.service.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/player/player.service.ts b/src/player/player.service.ts index 50171544a..ca165892c 100644 --- a/src/player/player.service.ts +++ b/src/player/player.service.ts @@ -30,7 +30,6 @@ import { PlayerEmotion } from './enum/playerEmotion.enum'; import { prizePool } from '../rewarder/const/prizePool'; import { PlayerObject } from '../common/type/playerObject.type'; import { EmotionCheckResult } from './dto/emotionCheckResult.dto'; - @Injectable() @AddBasicService() export class PlayerService @@ -186,6 +185,12 @@ export class PlayerService ): Promise => { if (!input?.clan_id) return true; + if (oldDoc.clan_id?.toString() !== input.clan_id.toString()) + await this.model.updateOne( + { _id: oldDoc._id }, + { clan_joindate: new Date() }, + ); + const changeCounterValue = this.requestHelperService.changeCounterValue; //decrease playerCounter from old clan From e50549ee1aa8e89497d260948c6e057d4a0f7bdb Mon Sep 17 00:00:00 2001 From: constf03 Date: Sun, 27 Sep 2026 17:16:51 +0300 Subject: [PATCH 4/8] set clan_joindate in tester account --- src/box/accountClaimer/testerAccount.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/box/accountClaimer/testerAccount.service.ts b/src/box/accountClaimer/testerAccount.service.ts index 407722a48..f2e23ef9a 100644 --- a/src/box/accountClaimer/testerAccount.service.ts +++ b/src/box/accountClaimer/testerAccount.service.ts @@ -119,6 +119,7 @@ export class TesterAccountService { { clan_id: clanWithLeastPlayers._id, clanRole_id: leaderRole._id, + clan_joindate: new Date(), }, ); From 9dc1b9c0e804c46e10bf59fe34f4eb3a59851fdd Mon Sep 17 00:00:00 2001 From: constf03 Date: Sun, 27 Sep 2026 17:17:56 +0300 Subject: [PATCH 5/8] update join tests for clan_joindate --- .../join/JoinService/handleJoinRequest.test.ts | 14 ++++++++++++++ .../clan/join/JoinService/leaveClan.test.ts | 13 +++++++++++++ 2 files changed, 27 insertions(+) diff --git a/src/__tests__/clan/join/JoinService/handleJoinRequest.test.ts b/src/__tests__/clan/join/JoinService/handleJoinRequest.test.ts index 6bdea4316..480ee3627 100644 --- a/src/__tests__/clan/join/JoinService/handleJoinRequest.test.ts +++ b/src/__tests__/clan/join/JoinService/handleJoinRequest.test.ts @@ -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(); diff --git a/src/__tests__/clan/join/JoinService/leaveClan.test.ts b/src/__tests__/clan/join/JoinService/leaveClan.test.ts index 93b5515ab..d9f9a20c3 100644 --- a/src/__tests__/clan/join/JoinService/leaveClan.test.ts +++ b/src/__tests__/clan/join/JoinService/leaveClan.test.ts @@ -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( From 9d9b3f36848b4ecbc42048d035462f391e89ab7e Mon Sep 17 00:00:00 2001 From: constf03 Date: Sun, 27 Sep 2026 17:23:40 +0300 Subject: [PATCH 6/8] include clan_joindate in playerbuilders --- src/__tests__/player/data/player/playerBuilder.ts | 1 + src/__tests__/player/data/player/playerDtoBuilder.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/src/__tests__/player/data/player/playerBuilder.ts b/src/__tests__/player/data/player/playerBuilder.ts index 3e6dafcab..70eaf80a8 100644 --- a/src/__tests__/player/data/player/playerBuilder.ts +++ b/src/__tests__/player/data/player/playerBuilder.ts @@ -44,6 +44,7 @@ export default class PlayerBuilder { clanRole_id: null, _id: undefined, environment: Environment.TEACHING_DEMO, + clan_joindate: null, }; build(): Player { diff --git a/src/__tests__/player/data/player/playerDtoBuilder.ts b/src/__tests__/player/data/player/playerDtoBuilder.ts index 3129a642d..be1d40dbd 100644 --- a/src/__tests__/player/data/player/playerDtoBuilder.ts +++ b/src/__tests__/player/data/player/playerDtoBuilder.ts @@ -44,6 +44,7 @@ export default class PlayerDtoBuilder implements IDataBuilder { battleCharacter_ids: [], Clan: undefined, CustomCharacter: [], + clan_joindate: null, }; build(): PlayerDto { From bf98aff420807d9bb956b54ba23897eb28b71a4a Mon Sep 17 00:00:00 2001 From: constf03 Date: Mon, 28 Sep 2026 16:20:47 +0300 Subject: [PATCH 7/8] add clan_joindate migration file --- ...20260927120000-add-player-clan-joindate.js | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 database/migrations/20260927120000-add-player-clan-joindate.js diff --git a/database/migrations/20260927120000-add-player-clan-joindate.js b/database/migrations/20260927120000-add-player-clan-joindate.js new file mode 100644 index 000000000..dedd110ea --- /dev/null +++ b/database/migrations/20260927120000-add-player-clan-joindate.js @@ -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(); + } +}; From 4041ecb8e06508addcfde67dfcc6cc3353d36a44 Mon Sep 17 00:00:00 2001 From: constf03 Date: Tue, 29 Sep 2026 12:30:23 +0300 Subject: [PATCH 8/8] Revert "update player clan_joindate also in the old update method (just in case)" This reverts commit ddbc777859db4340da61fe77a0a88d661b4208e6. --- src/player/player.service.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/player/player.service.ts b/src/player/player.service.ts index ca165892c..50171544a 100644 --- a/src/player/player.service.ts +++ b/src/player/player.service.ts @@ -30,6 +30,7 @@ import { PlayerEmotion } from './enum/playerEmotion.enum'; import { prizePool } from '../rewarder/const/prizePool'; import { PlayerObject } from '../common/type/playerObject.type'; import { EmotionCheckResult } from './dto/emotionCheckResult.dto'; + @Injectable() @AddBasicService() export class PlayerService @@ -185,12 +186,6 @@ export class PlayerService ): Promise => { if (!input?.clan_id) return true; - if (oldDoc.clan_id?.toString() !== input.clan_id.toString()) - await this.model.updateOne( - { _id: oldDoc._id }, - { clan_joindate: new Date() }, - ); - const changeCounterValue = this.requestHelperService.changeCounterValue; //decrease playerCounter from old clan