Skip to content

Commit 8d3ab89

Browse files
authored
Merge pull request #739 from SprocketBot/cursor/issue-728-sprocket-authoritative-roster-426f
Make Sprocket authoritative for franchise roster/staff reads (GH-728)
2 parents a05bbc5 + e08a0ae commit 8d3ab89

5 files changed

Lines changed: 673 additions & 3 deletions

File tree

core/src/franchise/franchise.module.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,29 @@
11
import {forwardRef, Module} from "@nestjs/common";
22
import {JwtModule} from "@nestjs/jwt";
3+
import {TypeOrmModule} from "@nestjs/typeorm";
34
import {
45
AnalyticsModule, config, EventsModule, NotificationModule,
56
} from "@sprocketbot/common";
67

8+
import {FranchiseLeadershipRole} from "../database/authorization/franchise_leadership_role";
9+
import {FranchiseLeadershipSeat} from "../database/authorization/franchise_leadership_seat";
10+
import {FranchiseStaffRole} from "../database/authorization/franchise_staff_role";
11+
import {FranchiseStaffSeat} from "../database/authorization/franchise_staff_seat";
12+
import {FranchiseLeadershipAppointment} from "../database/franchise/franchise_leadership_appointment";
13+
import {FranchiseStaffAppointment} from "../database/franchise/franchise_staff_appointment";
14+
import {Player} from "../database/franchise/player/player.model";
15+
import {RosterRole} from "../database/franchise/roster_role";
16+
import {RosterSlot} from "../database/franchise/roster_slot";
17+
import {Team} from "../database/franchise/team/team.model";
718
import {DatabaseModule} from "../database";
819
import {EloConnectorModule} from "../elo/elo-connector";
920
import {GameModule} from "../game";
21+
import {MLE_Player} from "../database/mledb/Player.model";
22+
import {MLE_Team} from "../database/mledb/Team.model";
23+
import {MLE_TeamToCaptain} from "../database/mledb/TeamToCaptain.model";
24+
import {LeagueToSkillGroup} from "../database/mledb-bridge/league_to_skill_group.model";
25+
import {PlayerToPlayer} from "../database/mledb-bridge/player_to_player.model";
26+
import {TeamToFranchise} from "../database/mledb-bridge/team_to_franchise.model";
1027
import {MledbInterfaceModule} from "../mledb";
1128
import {OrganizationModule} from "../organization/organization.module";
1229
import {SchedulingModule} from "../scheduling/scheduling.module";
@@ -23,11 +40,32 @@ import {
2340
import {PlayerService} from "./player";
2441
import {PlayerController} from "./player/player.controller";
2542
import {PlayerResolver} from "./player/player.resolver";
43+
import {RosterAuthorityService} from "./roster-authority.service";
2644
import {TeamService} from "./team/team.service";
2745

46+
const rosterAuthorityOrm = TypeOrmModule.forFeature([
47+
MLE_Player,
48+
MLE_Team,
49+
MLE_TeamToCaptain,
50+
TeamToFranchise,
51+
LeagueToSkillGroup,
52+
PlayerToPlayer,
53+
Player,
54+
RosterRole,
55+
RosterSlot,
56+
Team,
57+
FranchiseStaffAppointment,
58+
FranchiseLeadershipAppointment,
59+
FranchiseStaffRole,
60+
FranchiseStaffSeat,
61+
FranchiseLeadershipRole,
62+
FranchiseLeadershipSeat,
63+
]);
64+
2865
@Module({
2966
imports: [
3067
DatabaseModule,
68+
rosterAuthorityOrm,
3169
UtilModule,
3270
NotificationModule,
3371
EventsModule,
@@ -43,6 +81,7 @@ import {TeamService} from "./team/team.service";
4381
],
4482
providers: [
4583
PlayerService,
84+
RosterAuthorityService,
4685
GameSkillGroupService,
4786
GameSkillGroupResolver,
4887
FranchiseService,
@@ -51,7 +90,7 @@ import {TeamService} from "./team/team.service";
5190
PlayerResolver,
5291
TeamService,
5392
],
54-
exports: [PlayerService, FranchiseService, GameSkillGroupService, TeamService],
93+
exports: [PlayerService, FranchiseService, GameSkillGroupService, TeamService, RosterAuthorityService],
5594
controllers: [FranchiseController, GameSkillGroupController, PlayerController],
5695
})
5796
export class FranchiseModule {}

core/src/franchise/franchise/franchise.service.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {FranchiseProfile} from "$db/franchise/franchise_profile/franchise_profil
1212
import {MledbPlayerService} from "../../mledb";
1313
import {MemberService} from "../../organization";
1414
import {PlayerService} from "../player";
15+
import {RosterAuthorityService} from "../roster-authority.service";
1516

1617
@Injectable()
1718
export class FranchiseService {
@@ -23,6 +24,7 @@ export class FranchiseService {
2324
private readonly sprocketMemberService: MemberService,
2425
@InjectRepository(FranchiseProfile)
2526
private readonly franchiseProfileRepository: Repository<FranchiseProfile>,
27+
private readonly rosterAuthorityService: RosterAuthorityService,
2628
) {}
2729

2830
async getFranchiseProfile(franchiseId: number): Promise<FranchiseProfile> {
@@ -62,6 +64,26 @@ export class FranchiseService {
6264
}
6365

6466
async getPlayerFranchisesByUserId(userId: number): Promise<CoreOutput<CoreEndpoint.GetPlayerFranchises>> {
67+
const fromSprocket = await this.rosterAuthorityService.getPlayerFranchisesFromSprocket(userId);
68+
69+
let fromMle: CoreOutput<CoreEndpoint.GetPlayerFranchises> = [];
70+
try {
71+
fromMle = await this.getPlayerFranchisesFromMle(userId);
72+
} catch {
73+
// No linked MLE player (or other read failure): Sprocket-only is fine.
74+
}
75+
76+
if (fromMle.length === 0) {
77+
return fromSprocket;
78+
}
79+
80+
return this.mergePlayerFranchiseRows(fromSprocket, fromMle);
81+
}
82+
83+
/**
84+
* Legacy MLE read path (dual-read with Sprocket until projection/backfill is complete everywhere).
85+
*/
86+
private async getPlayerFranchisesFromMle(userId: number): Promise<CoreOutput<CoreEndpoint.GetPlayerFranchises>> {
6587
const mlePlayer = await this.mledbPlayerService.getMlePlayerBySprocketUser(userId);
6688

6789
const playerId = mlePlayer.id;
@@ -112,6 +134,46 @@ export class FranchiseService {
112134
}
113135
}
114136

137+
private mergePlayerFranchiseRows(
138+
sprocket: CoreOutput<CoreEndpoint.GetPlayerFranchises>,
139+
mle: CoreOutput<CoreEndpoint.GetPlayerFranchises>,
140+
): CoreOutput<CoreEndpoint.GetPlayerFranchises> {
141+
type Acc = {id: number; name: string; staffByCode: Map<string, {id: number; name: string;}>;};
142+
const byFranchiseId = new Map<number, Acc>();
143+
144+
const mergeRow = (row: {id: number; name: string; staffPositions: Array<{id: number; name: string;}>;}) => {
145+
let acc = byFranchiseId.get(row.id);
146+
if (!acc) {
147+
acc = {
148+
id: row.id,
149+
name: row.name,
150+
staffByCode: new Map(),
151+
};
152+
byFranchiseId.set(row.id, acc);
153+
} else if (!acc.name && row.name) {
154+
acc.name = row.name;
155+
}
156+
for (const sp of row.staffPositions) {
157+
if (!acc.staffByCode.has(sp.name)) {
158+
acc.staffByCode.set(sp.name, sp);
159+
}
160+
}
161+
};
162+
163+
for (const row of sprocket) {
164+
mergeRow(row);
165+
}
166+
for (const row of mle) {
167+
mergeRow(row);
168+
}
169+
170+
return [...byFranchiseId.values()].map(acc => ({
171+
id: acc.id,
172+
name: acc.name,
173+
staffPositions: [...acc.staffByCode.values()],
174+
}));
175+
}
176+
115177
/**
116178
* Helper method to get franchises based on staff assignments
117179
* Used for FP/FA players and as a fallback when team franchise lookup fails

core/src/franchise/player/player.service.ts

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import {MemberPlatformAccountService} from "../../organization/member-platform-a
5252
import {OrganizationService} from "../../organization";
5353
import {MemberService} from "../../organization/member/member.service";
5454
import {GameSkillGroupService} from "../game-skill-group";
55+
import {RosterAuthorityService} from "../roster-authority.service";
5556
import type {RankdownJwtPayload} from "./player.types";
5657
import type {CreatePlayerTuple} from "./player.types";
5758

@@ -87,12 +88,23 @@ export class PlayerService {
8788
private readonly eloConnectorService: EloConnectorService,
8889
private readonly platformService: PlatformService,
8990
private readonly analyticsService: AnalyticsService,
91+
private readonly rosterAuthorityService: RosterAuthorityService,
9092
@Inject(forwardRef(() => MemberPlatformAccountService))
9193
private readonly memberPlatformAccountService: MemberPlatformAccountService,
9294
@Inject(forwardRef(() => MledbPlayerAccountService))
9395
private readonly mledbPlayerAccountService: MledbPlayerAccountService,
9496
) {}
9597

98+
private async syncRosterAuthorityAfterMleSave(mlePlayerId: number): Promise<void> {
99+
try {
100+
await this.rosterAuthorityService.syncFromMlePlayerId(mlePlayerId);
101+
} catch (err) {
102+
this.logger.warn(
103+
`Roster authority sync failed for MLE player ${mlePlayerId}: ${err instanceof Error ? err.message : String(err)}`,
104+
);
105+
}
106+
}
107+
96108
async getPlayer(query: FindOneOptions<Player>): Promise<Player> {
97109
this.logger.debug(`getPlayer: ${JSON.stringify(query)}`);
98110
return this.playerRepository.findOneOrFail(query);
@@ -405,6 +417,11 @@ export class PlayerService {
405417
throw new Error(`Tried updating player with MLEID: ${mleid}, but that MLEID does not yet exist.`);
406418
}
407419
await runner.commitTransaction();
420+
421+
const mleAfter = await this.mle_playerRepository.findOne({where: {mleid} });
422+
if (mleAfter) {
423+
await this.syncRosterAuthorityAfterMleSave(mleAfter.id);
424+
}
408425
} catch (e) {
409426
await runner.rollbackTransaction();
410427
this.logger.error(e);
@@ -441,9 +458,10 @@ export class PlayerService {
441458
});
442459

443460
if (runner) {
444-
await runner.manager.save(player);
461+
await runner.manager.save(MLE_Player, updatedPlayer);
445462
} else {
446-
await this.mle_playerRepository.save(player);
463+
await this.mle_playerRepository.save(updatedPlayer);
464+
await this.syncRosterAuthorityAfterMleSave(updatedPlayer.id);
447465
}
448466

449467
return updatedPlayer;
@@ -500,6 +518,7 @@ export class PlayerService {
500518
await runner.manager.save(ptpBridge);
501519
} else {
502520
await this.ptpRepo.save(ptpBridge);
521+
await this.syncRosterAuthorityAfterMleSave(player.id);
503522
}
504523

505524
return player;
@@ -854,6 +873,7 @@ export class PlayerService {
854873
});
855874

856875
await this.mle_playerRepository.save(newMlePlayer);
876+
await this.syncRosterAuthorityAfterMleSave(newMlePlayer.id);
857877

858878
this.logger.debug(`Player ${playerDelta.playerId}: Salary update complete`);
859879
}
@@ -908,6 +928,7 @@ export class PlayerService {
908928
const mlePlayer = await this.getMlePlayerBySprocketPlayer(sprocPlayerId);
909929
mlePlayer.salary = salary;
910930
await this.mle_playerRepository.save(mlePlayer);
931+
await this.syncRosterAuthorityAfterMleSave(mlePlayer.id);
911932
return mlePlayer;
912933
}
913934

@@ -938,6 +959,7 @@ export class PlayerService {
938959
}
939960

940961
await this.mle_playerRepository.save(player);
962+
await this.syncRosterAuthorityAfterMleSave(player.id);
941963

942964
return player;
943965
}
@@ -973,6 +995,7 @@ export class PlayerService {
973995
});
974996

975997
await this.mle_playerRepository.save(player);
998+
await this.syncRosterAuthorityAfterMleSave(player.id);
976999

9771000
// Move player to Waiver Wire
9781001
// TODO fix later when we abstract away from MLE
@@ -1020,6 +1043,7 @@ export class PlayerService {
10201043
});
10211044

10221045
await this.mle_playerRepository.save(player);
1046+
await this.syncRosterAuthorityAfterMleSave(player.id);
10231047
return player;
10241048
}
10251049

@@ -1342,6 +1366,19 @@ export class PlayerService {
13421366
await runner.commitTransaction();
13431367
this.logger.log(`Transaction committed successfully`);
13441368

1369+
const rlPlayers = await this.playerRepository.find({
1370+
where: {
1371+
member: {user: {id: user.id} },
1372+
skillGroup: {game: {id: 7} },
1373+
},
1374+
});
1375+
for (const pl of rlPlayers) {
1376+
const bridge = await this.ptpRepo.findOne({where: {sprocketPlayerId: pl.id} });
1377+
if (bridge) {
1378+
await this.syncRosterAuthorityAfterMleSave(bridge.mledPlayerId);
1379+
}
1380+
}
1381+
13451382
const result = `Successfully created/updated user with ID ${user.id}.`;
13461383
this.logger.log(`=== INTAKE USER COMPLETED ===`);
13471384
this.logger.log(`Result: ${result}`);
@@ -1406,6 +1443,7 @@ export class PlayerService {
14061443
});
14071444
mlePlayer.discordId = newAcct;
14081445
await this.mle_playerRepository.save(mlePlayer);
1446+
await this.syncRosterAuthorityAfterMleSave(mlePlayer.id);
14091447

14101448
// Then, follow up with Sprocket.
14111449
const uaa = await this.userAuthRepository.findOneOrFail({
@@ -1425,6 +1463,7 @@ export class PlayerService {
14251463
});
14261464
mlePlayer.teamName = newTeam;
14271465
await this.mle_playerRepository.save(mlePlayer);
1466+
await this.syncRosterAuthorityAfterMleSave(mlePlayer.id);
14281467
}
14291468

14301469
async changePlayerName(mleid: number, newName: string): Promise<void> {

0 commit comments

Comments
 (0)