volleyballsimtypes 0.0.462 → 0.0.464

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/dist/cjs/src/api/index.d.ts +14 -1
  2. package/dist/cjs/src/data/init-models.js +17 -0
  3. package/dist/cjs/src/data/models/currency-transaction.d.ts +4 -2
  4. package/dist/cjs/src/data/models/currency-transaction.js +2 -0
  5. package/dist/cjs/src/data/models/index.d.ts +3 -0
  6. package/dist/cjs/src/data/models/index.js +3 -0
  7. package/dist/cjs/src/data/models/match-preset.d.ts +28 -0
  8. package/dist/cjs/src/data/models/match-preset.js +65 -0
  9. package/dist/cjs/src/data/models/match-set.d.ts +2 -0
  10. package/dist/cjs/src/data/models/match-set.js +5 -0
  11. package/dist/cjs/src/data/models/match.d.ts +5 -2
  12. package/dist/cjs/src/data/models/match.js +4 -0
  13. package/dist/cjs/src/data/models/physician.d.ts +25 -0
  14. package/dist/cjs/src/data/models/physician.js +57 -0
  15. package/dist/cjs/src/data/models/physiotherapist.d.ts +25 -0
  16. package/dist/cjs/src/data/models/physiotherapist.js +57 -0
  17. package/dist/cjs/src/data/models/player.d.ts +8 -0
  18. package/dist/cjs/src/data/models/player.js +17 -0
  19. package/dist/cjs/src/data/models/tactics.d.ts +9 -1
  20. package/dist/cjs/src/data/models/tactics.js +9 -0
  21. package/dist/cjs/src/data/transformers/division.js +1 -1
  22. package/dist/cjs/src/data/transformers/lineup-preset.js +7 -1
  23. package/dist/cjs/src/data/transformers/lineup-preset.test.js +15 -2
  24. package/dist/cjs/src/data/transformers/match-preset-override.test.d.ts +1 -0
  25. package/dist/cjs/src/data/transformers/match-preset-override.test.js +275 -0
  26. package/dist/cjs/src/data/transformers/match-set.js +2 -0
  27. package/dist/cjs/src/data/transformers/match.d.ts +2 -1
  28. package/dist/cjs/src/data/transformers/match.js +21 -5
  29. package/dist/cjs/src/data/transformers/player.js +15 -1
  30. package/dist/cjs/src/data/transformers/qualifier.js +1 -1
  31. package/dist/cjs/src/data/transformers/rally-event-injury.test.d.ts +1 -0
  32. package/dist/cjs/src/data/transformers/rally-event-injury.test.js +33 -0
  33. package/dist/cjs/src/data/transformers/rally-event.d.ts +1 -0
  34. package/dist/cjs/src/data/transformers/rally-event.js +4 -1
  35. package/dist/cjs/src/data/transformers/season.js +1 -1
  36. package/dist/cjs/src/data/transformers/tactics.d.ts +3 -2
  37. package/dist/cjs/src/data/transformers/tactics.js +106 -3
  38. package/dist/cjs/src/data/transformers/team.d.ts +7 -1
  39. package/dist/cjs/src/data/transformers/team.js +22 -4
  40. package/dist/cjs/src/data/transformers/tournament.js +1 -1
  41. package/dist/cjs/src/service/event/schemas/substitution.z.d.ts +1 -0
  42. package/dist/cjs/src/service/event/schemas/substitution.z.js +2 -1
  43. package/dist/cjs/src/service/event/substitution.d.ts +1 -0
  44. package/dist/cjs/src/service/event/substitution.js +4 -2
  45. package/dist/cjs/src/service/match/match-set-forfeit.test.d.ts +1 -0
  46. package/dist/cjs/src/service/match/match-set-forfeit.test.js +76 -0
  47. package/dist/cjs/src/service/match/match-set.d.ts +8 -0
  48. package/dist/cjs/src/service/match/match-set.js +23 -1
  49. package/dist/cjs/src/service/match/match.d.ts +2 -0
  50. package/dist/cjs/src/service/match/match.js +2 -1
  51. package/dist/cjs/src/service/match/schemas/match-set.z.d.ts +1 -0
  52. package/dist/cjs/src/service/match/schemas/match-set.z.js +30 -2
  53. package/dist/cjs/src/service/match/schemas/match.z.d.ts +4 -0
  54. package/dist/cjs/src/service/match/schemas/match.z.js +4 -1
  55. package/dist/cjs/src/service/player/index.d.ts +1 -0
  56. package/dist/cjs/src/service/player/index.js +1 -0
  57. package/dist/cjs/src/service/player/injury.d.ts +14 -0
  58. package/dist/cjs/src/service/player/injury.js +39 -0
  59. package/dist/cjs/src/service/player/injury.test.d.ts +1 -0
  60. package/dist/cjs/src/service/player/injury.test.js +78 -0
  61. package/dist/cjs/src/service/player/player-generator.js +3 -1
  62. package/dist/cjs/src/service/player/player.d.ts +3 -0
  63. package/dist/cjs/src/service/player/player.js +3 -1
  64. package/dist/cjs/src/service/player/schemas/player.z.d.ts +6 -0
  65. package/dist/cjs/src/service/player/schemas/player.z.js +7 -0
  66. package/dist/cjs/src/service/team/index.d.ts +1 -0
  67. package/dist/cjs/src/service/team/index.js +1 -0
  68. package/dist/cjs/src/service/team/reserve-priority.d.ts +6 -0
  69. package/dist/cjs/src/service/team/reserve-priority.js +2 -0
  70. package/dist/cjs/src/service/team/schemas/reserve-priority.z.test.d.ts +1 -0
  71. package/dist/cjs/src/service/team/schemas/reserve-priority.z.test.js +125 -0
  72. package/dist/cjs/src/service/team/schemas/tactics.z.d.ts +5 -0
  73. package/dist/cjs/src/service/team/schemas/tactics.z.js +32 -1
  74. package/dist/cjs/src/service/team/schemas/team.z.d.ts +5 -0
  75. package/dist/cjs/src/service/team/tactics.d.ts +5 -0
  76. package/dist/cjs/src/service/team/tactics.js +3 -1
  77. package/dist/esm/src/api/index.d.ts +14 -1
  78. package/dist/esm/src/data/init-models.js +18 -1
  79. package/dist/esm/src/data/models/currency-transaction.d.ts +4 -2
  80. package/dist/esm/src/data/models/currency-transaction.js +2 -0
  81. package/dist/esm/src/data/models/index.d.ts +3 -0
  82. package/dist/esm/src/data/models/index.js +3 -0
  83. package/dist/esm/src/data/models/match-preset.d.ts +28 -0
  84. package/dist/esm/src/data/models/match-preset.js +61 -0
  85. package/dist/esm/src/data/models/match-set.d.ts +2 -0
  86. package/dist/esm/src/data/models/match-set.js +5 -0
  87. package/dist/esm/src/data/models/match.d.ts +5 -2
  88. package/dist/esm/src/data/models/match.js +4 -0
  89. package/dist/esm/src/data/models/physician.d.ts +25 -0
  90. package/dist/esm/src/data/models/physician.js +53 -0
  91. package/dist/esm/src/data/models/physiotherapist.d.ts +25 -0
  92. package/dist/esm/src/data/models/physiotherapist.js +53 -0
  93. package/dist/esm/src/data/models/player.d.ts +8 -0
  94. package/dist/esm/src/data/models/player.js +17 -0
  95. package/dist/esm/src/data/models/tactics.d.ts +9 -1
  96. package/dist/esm/src/data/models/tactics.js +9 -0
  97. package/dist/esm/src/data/transformers/division.js +1 -1
  98. package/dist/esm/src/data/transformers/lineup-preset.js +7 -1
  99. package/dist/esm/src/data/transformers/lineup-preset.test.js +15 -2
  100. package/dist/esm/src/data/transformers/match-preset-override.test.d.ts +1 -0
  101. package/dist/esm/src/data/transformers/match-preset-override.test.js +273 -0
  102. package/dist/esm/src/data/transformers/match-set.js +2 -0
  103. package/dist/esm/src/data/transformers/match.d.ts +2 -1
  104. package/dist/esm/src/data/transformers/match.js +21 -5
  105. package/dist/esm/src/data/transformers/player.js +15 -1
  106. package/dist/esm/src/data/transformers/qualifier.js +1 -1
  107. package/dist/esm/src/data/transformers/rally-event-injury.test.d.ts +1 -0
  108. package/dist/esm/src/data/transformers/rally-event-injury.test.js +31 -0
  109. package/dist/esm/src/data/transformers/rally-event.d.ts +1 -0
  110. package/dist/esm/src/data/transformers/rally-event.js +4 -1
  111. package/dist/esm/src/data/transformers/season.js +1 -1
  112. package/dist/esm/src/data/transformers/tactics.d.ts +3 -2
  113. package/dist/esm/src/data/transformers/tactics.js +106 -4
  114. package/dist/esm/src/data/transformers/team.d.ts +7 -1
  115. package/dist/esm/src/data/transformers/team.js +20 -5
  116. package/dist/esm/src/data/transformers/tournament.js +1 -1
  117. package/dist/esm/src/service/event/schemas/substitution.z.d.ts +1 -0
  118. package/dist/esm/src/service/event/schemas/substitution.z.js +2 -1
  119. package/dist/esm/src/service/event/substitution.d.ts +1 -0
  120. package/dist/esm/src/service/event/substitution.js +4 -2
  121. package/dist/esm/src/service/match/match-set-forfeit.test.d.ts +1 -0
  122. package/dist/esm/src/service/match/match-set-forfeit.test.js +74 -0
  123. package/dist/esm/src/service/match/match-set.d.ts +8 -0
  124. package/dist/esm/src/service/match/match-set.js +23 -1
  125. package/dist/esm/src/service/match/match.d.ts +2 -0
  126. package/dist/esm/src/service/match/match.js +2 -1
  127. package/dist/esm/src/service/match/schemas/match-set.z.d.ts +1 -0
  128. package/dist/esm/src/service/match/schemas/match-set.z.js +30 -2
  129. package/dist/esm/src/service/match/schemas/match.z.d.ts +4 -0
  130. package/dist/esm/src/service/match/schemas/match.z.js +4 -1
  131. package/dist/esm/src/service/player/index.d.ts +1 -0
  132. package/dist/esm/src/service/player/index.js +1 -0
  133. package/dist/esm/src/service/player/injury.d.ts +14 -0
  134. package/dist/esm/src/service/player/injury.js +33 -0
  135. package/dist/esm/src/service/player/injury.test.d.ts +1 -0
  136. package/dist/esm/src/service/player/injury.test.js +76 -0
  137. package/dist/esm/src/service/player/player-generator.js +3 -1
  138. package/dist/esm/src/service/player/player.d.ts +3 -0
  139. package/dist/esm/src/service/player/player.js +3 -1
  140. package/dist/esm/src/service/player/schemas/player.z.d.ts +6 -0
  141. package/dist/esm/src/service/player/schemas/player.z.js +7 -0
  142. package/dist/esm/src/service/team/index.d.ts +1 -0
  143. package/dist/esm/src/service/team/index.js +1 -0
  144. package/dist/esm/src/service/team/reserve-priority.d.ts +6 -0
  145. package/dist/esm/src/service/team/reserve-priority.js +1 -0
  146. package/dist/esm/src/service/team/schemas/reserve-priority.z.test.d.ts +1 -0
  147. package/dist/esm/src/service/team/schemas/reserve-priority.z.test.js +123 -0
  148. package/dist/esm/src/service/team/schemas/tactics.z.d.ts +5 -0
  149. package/dist/esm/src/service/team/schemas/tactics.z.js +32 -1
  150. package/dist/esm/src/service/team/schemas/team.z.d.ts +5 -0
  151. package/dist/esm/src/service/team/tactics.d.ts +5 -0
  152. package/dist/esm/src/service/team/tactics.js +3 -1
  153. package/package.json +1 -1
@@ -0,0 +1,25 @@
1
+ import * as Sequelize from 'sequelize';
2
+ import { Model } from 'sequelize';
3
+ import { TeamId, TeamModel } from '.';
4
+ export interface PhysiotherapistAttributes {
5
+ physiotherapist_id: string;
6
+ team_id: string;
7
+ name: string;
8
+ rarity: string;
9
+ currency_invested: number;
10
+ }
11
+ export type PhysiotherapistPk = 'physiotherapist_id';
12
+ export type PhysiotherapistId = PhysiotherapistModel[PhysiotherapistPk];
13
+ export type PhysiotherapistCreationAttributes = PhysiotherapistAttributes;
14
+ export declare class PhysiotherapistModel extends Model<PhysiotherapistAttributes, PhysiotherapistCreationAttributes> implements PhysiotherapistAttributes {
15
+ physiotherapist_id: string;
16
+ team_id: string;
17
+ name: string;
18
+ rarity: string;
19
+ currency_invested: number;
20
+ team: TeamModel;
21
+ getTeam: Sequelize.BelongsToGetAssociationMixin<TeamModel>;
22
+ setTeam: Sequelize.BelongsToSetAssociationMixin<TeamModel, TeamId>;
23
+ createTeam: Sequelize.BelongsToCreateAssociationMixin<TeamModel>;
24
+ static initModel(sequelize: Sequelize.Sequelize): typeof PhysiotherapistModel;
25
+ }
@@ -0,0 +1,53 @@
1
+ import { DataTypes, Model } from 'sequelize';
2
+ export class PhysiotherapistModel extends Model {
3
+ static initModel(sequelize) {
4
+ return PhysiotherapistModel.init({
5
+ physiotherapist_id: {
6
+ type: DataTypes.UUID,
7
+ allowNull: false,
8
+ primaryKey: true
9
+ },
10
+ team_id: {
11
+ type: DataTypes.UUID,
12
+ allowNull: false,
13
+ unique: true,
14
+ references: {
15
+ model: 'Team',
16
+ key: 'team_id'
17
+ }
18
+ },
19
+ name: {
20
+ type: DataTypes.STRING,
21
+ allowNull: false,
22
+ defaultValue: ''
23
+ },
24
+ rarity: {
25
+ type: DataTypes.STRING,
26
+ allowNull: false,
27
+ defaultValue: 'COMMON'
28
+ },
29
+ currency_invested: {
30
+ type: DataTypes.INTEGER,
31
+ allowNull: false,
32
+ defaultValue: 0
33
+ }
34
+ }, {
35
+ sequelize,
36
+ tableName: 'Physiotherapist',
37
+ schema: 'public',
38
+ timestamps: false,
39
+ indexes: [
40
+ {
41
+ name: 'Physiotherapist_pk',
42
+ unique: true,
43
+ fields: [{ name: 'physiotherapist_id' }]
44
+ },
45
+ {
46
+ name: 'Physiotherapist_team_id_unique',
47
+ unique: true,
48
+ fields: [{ name: 'team_id' }]
49
+ }
50
+ ]
51
+ });
52
+ }
53
+ }
@@ -15,6 +15,10 @@ export interface PlayerAttributes {
15
15
  birth_iteration: number;
16
16
  last_declined_iteration: number | null;
17
17
  decline_profile: DeclineProfile;
18
+ injury_prone_profile?: number | string;
19
+ injury_severity?: number | null;
20
+ injured_until?: Date | string | null;
21
+ injury_debuff?: number | string | null;
18
22
  PerformanceStat?: PerformanceStatsAttributes;
19
23
  BoxScores?: BoxScoreAttributes[];
20
24
  }
@@ -34,6 +38,10 @@ export declare class PlayerModel extends Model<PlayerAttributes, PlayerCreationA
34
38
  birth_iteration: number;
35
39
  last_declined_iteration: number | null;
36
40
  decline_profile: DeclineProfile;
41
+ injury_prone_profile: number | string;
42
+ injury_severity: number | null;
43
+ injured_until: Date | string | null;
44
+ injury_debuff: number | string | null;
37
45
  country: CountryModel;
38
46
  getCountry: Sequelize.BelongsToGetAssociationMixin<CountryModel>;
39
47
  setCountry: Sequelize.BelongsToSetAssociationMixin<CountryModel, CountryId>;
@@ -59,6 +59,23 @@ export class PlayerModel extends Model {
59
59
  type: DataTypes.ENUM(...Object.values(DeclineProfileEnum)),
60
60
  allowNull: false,
61
61
  defaultValue: DeclineProfileEnum.STANDARD
62
+ },
63
+ injury_prone_profile: {
64
+ type: DataTypes.DECIMAL(3, 2),
65
+ allowNull: false,
66
+ defaultValue: 0.5
67
+ },
68
+ injury_severity: {
69
+ type: DataTypes.INTEGER,
70
+ allowNull: true
71
+ },
72
+ injured_until: {
73
+ type: DataTypes.DATE,
74
+ allowNull: true
75
+ },
76
+ injury_debuff: {
77
+ type: DataTypes.DECIMAL(3, 2),
78
+ allowNull: true
62
79
  }
63
80
  }, {
64
81
  sequelize,
@@ -39,6 +39,10 @@ export interface SystemSetAttributes {
39
39
  rotationSystem: RotationSystemEnum;
40
40
  designatedSetters: PlayerId[];
41
41
  }
42
+ export interface ReservePriorityAttributes {
43
+ position: CourtPosition;
44
+ reserves: PlayerId[];
45
+ }
42
46
  export interface LiberoSetSubConfigAttributes {
43
47
  mode: LiberoSubMode;
44
48
  fatigueBand?: EnergyBand;
@@ -61,10 +65,12 @@ export interface TacticsAttributes {
61
65
  offensive_preferences: OffensivePreferenceAttributes[];
62
66
  system_sets?: SystemSetAttributes[];
63
67
  offensive_preference_sets?: OffensivePreferenceAttributes[][];
68
+ replace_injured_immediately?: boolean;
69
+ reserve_priorities?: ReservePriorityAttributes[];
64
70
  }
65
71
  export type TacticsPk = 'team_id';
66
72
  export type TacticsId = TacticsModel[TacticsPk];
67
- export type TacticsOptionalAttributes = 'lineup' | 'libero_replacements' | 'receive_rotation_offset' | 'designated_subs' | 'substitution_band' | 'second_libero' | 'libero_sub' | 'libero_replacement_sets' | 'rotation_system' | 'designated_setters' | 'offensive_preferences' | 'system_sets' | 'offensive_preference_sets';
73
+ export type TacticsOptionalAttributes = 'lineup' | 'libero_replacements' | 'receive_rotation_offset' | 'designated_subs' | 'substitution_band' | 'second_libero' | 'libero_sub' | 'libero_replacement_sets' | 'rotation_system' | 'designated_setters' | 'offensive_preferences' | 'system_sets' | 'offensive_preference_sets' | 'replace_injured_immediately' | 'reserve_priorities';
68
74
  export type TacticsCreationAttributes = Optional<TacticsAttributes, TacticsOptionalAttributes>;
69
75
  export declare class TacticsModel extends Model<TacticsAttributes, TacticsCreationAttributes> implements TacticsAttributes {
70
76
  team_id: string;
@@ -81,6 +87,8 @@ export declare class TacticsModel extends Model<TacticsAttributes, TacticsCreati
81
87
  offensive_preferences: OffensivePreferenceAttributes[];
82
88
  system_sets?: SystemSetAttributes[];
83
89
  offensive_preference_sets?: OffensivePreferenceAttributes[][];
90
+ replace_injured_immediately?: boolean;
91
+ reserve_priorities?: ReservePriorityAttributes[];
84
92
  team: TeamModel;
85
93
  getTeam: Sequelize.BelongsToGetAssociationMixin<TeamModel>;
86
94
  setTeam: Sequelize.BelongsToSetAssociationMixin<TeamModel, TeamId>;
@@ -71,6 +71,15 @@ export class TacticsModel extends Model {
71
71
  offensive_preference_sets: {
72
72
  type: DataTypes.JSONB,
73
73
  allowNull: true
74
+ },
75
+ replace_injured_immediately: {
76
+ type: DataTypes.BOOLEAN,
77
+ allowNull: false,
78
+ defaultValue: true
79
+ },
80
+ reserve_priorities: {
81
+ type: DataTypes.JSONB,
82
+ allowNull: true
74
83
  }
75
84
  }, {
76
85
  sequelize,
@@ -23,7 +23,7 @@ function transformToObject(model) {
23
23
  subdivision: model.subdivision,
24
24
  leagueId: model.league_id,
25
25
  seasons: (model.DivisionSeasons ?? []).map((ds) => transformToSeason(ds.competition)),
26
- teams: model.Teams != null ? model.Teams.map(transformToTeam) : undefined
26
+ teams: model.Teams != null ? model.Teams.map(team => transformToTeam(team)) : undefined
27
27
  });
28
28
  }
29
29
  export { transformToObject as transformToDivision, transformToAttributes as transformFromDivision };
@@ -1,11 +1,17 @@
1
1
  // `config` is already stored as the API Tactics DTO, so a preset row maps to its API shape with no hydration.
2
+ // Presets saved before the injuries fields exist lack replaceInjuredImmediately: heal it to the effective
3
+ // default (true, matching transformToTactics' hydration) so the DTO's required field is honest and a UI
4
+ // toggle seeded from an old preset cannot silently flip the user's effective setting on re-save.
2
5
  function transformToObject(model) {
3
6
  return {
4
7
  presetId: model.preset_id,
5
8
  name: model.name,
6
9
  isActive: model.is_active,
7
10
  orderIndex: model.order_index,
8
- config: model.config
11
+ config: {
12
+ ...model.config,
13
+ replaceInjuredImmediately: model.config.replaceInjuredImmediately ?? true
14
+ }
9
15
  };
10
16
  }
11
17
  export { transformToObject as transformToLineupPreset };
@@ -1,7 +1,7 @@
1
1
  import { describe, it, expect } from '@jest/globals';
2
2
  import { transformToLineupPreset } from './lineup-preset';
3
3
  describe('transformToLineupPreset()', () => {
4
- it('maps preset columns to the API shape and passes config through by reference', () => {
4
+ it('maps preset columns to the API shape and heals a pre-injuries config to the effective default', () => {
5
5
  const config = { lineup: { bench: [] }, substitutionBand: 'TIRED' };
6
6
  const preset = transformToLineupPreset({
7
7
  preset_id: 'p1',
@@ -16,8 +16,21 @@ describe('transformToLineupPreset()', () => {
16
16
  name: 'Starters',
17
17
  isActive: true,
18
18
  orderIndex: 2,
19
+ // A config stored before the injuries fields lacks replaceInjuredImmediately; the transform heals it
20
+ // to true (the effective hydration default) so the DTO's required field is honest.
21
+ config: { ...config, replaceInjuredImmediately: true }
22
+ });
23
+ });
24
+ it('keeps an explicit replaceInjuredImmediately: false', () => {
25
+ const config = { lineup: { bench: [] }, substitutionBand: 'TIRED', replaceInjuredImmediately: false };
26
+ const preset = transformToLineupPreset({
27
+ preset_id: 'p2',
28
+ team_id: 't1',
29
+ name: 'Risky',
30
+ is_active: false,
31
+ order_index: 3,
19
32
  config
20
33
  });
21
- expect(preset.config).toBe(config);
34
+ expect(preset.config.replaceInjuredImmediately).toBe(false);
22
35
  });
23
36
  });
@@ -0,0 +1,273 @@
1
+ import { describe, it, expect, jest } from '@jest/globals';
2
+ import { v4 as uuidv4 } from 'uuid';
3
+ import { apiTacticsConfigToTacticsAttributes, tacticsColumnsToApiDto, transformToTactics } from './tactics';
4
+ import { transformToTeam } from './team';
5
+ import { transformToMatch } from './match';
6
+ import { CourtPosition, DeclineProfileEnum, RarityEnum, RoleEnum } from '../../service';
7
+ // ─── shared mocks (model-shaped plain objects, cast through unknown) ──────────
8
+ const countryFixture = {
9
+ country_id: uuidv4(),
10
+ name: 'Test Country',
11
+ alpha_2: 'TC',
12
+ alpha_3: 'TCN',
13
+ country_code: '999',
14
+ iso_3166_2: 'TC-TEST',
15
+ region: '',
16
+ region_code: '',
17
+ sub_region: '',
18
+ sub_region_code: '',
19
+ intermediate_region: '',
20
+ intermediate_region_code: ''
21
+ };
22
+ function makePlayerModel(id) {
23
+ const stat = 50;
24
+ return {
25
+ player_id: id,
26
+ first_name: 'Test',
27
+ last_name: `P-${id.slice(0, 4)}`,
28
+ rarity: RarityEnum.COMMON,
29
+ roles: [RoleEnum.OUTSIDE_HITTER],
30
+ traits: [],
31
+ birth_age: 18,
32
+ birth_iteration: 1,
33
+ last_declined_iteration: null,
34
+ decline_profile: DeclineProfileEnum.STANDARD,
35
+ BoxScores: [],
36
+ BoxScoreTotals: null,
37
+ country: countryFixture,
38
+ PerformanceStat: {
39
+ setting: stat,
40
+ serve: stat,
41
+ spike: stat,
42
+ quick: stat,
43
+ power: stat,
44
+ awareness: stat,
45
+ attack: stat,
46
+ back_attack: stat,
47
+ reception: stat,
48
+ overhand: stat,
49
+ bump: stat,
50
+ block: stat,
51
+ read: stat,
52
+ commit: stat,
53
+ focus: stat,
54
+ defense: stat,
55
+ stamina: stat,
56
+ reflex: stat
57
+ }
58
+ };
59
+ }
60
+ // Six starters; index order is the row lineup's zone 1..6 order.
61
+ const ids = Array.from({ length: 6 }, () => uuidv4());
62
+ const teamId = uuidv4();
63
+ function rowLineup(order) {
64
+ return {
65
+ [CourtPosition.RIGHT_BACK]: order[0],
66
+ [CourtPosition.RIGHT_FRONT]: order[1],
67
+ [CourtPosition.MIDDLE_FRONT]: order[2],
68
+ [CourtPosition.LEFT_FRONT]: order[3],
69
+ [CourtPosition.LEFT_BACK]: order[4],
70
+ [CourtPosition.MIDDLE_BACK]: order[5],
71
+ bench: []
72
+ };
73
+ }
74
+ function makeTacticsRow(order) {
75
+ return {
76
+ team_id: teamId,
77
+ lineup: rowLineup(order),
78
+ libero_replacements: [],
79
+ receive_rotation_offset: false,
80
+ designated_subs: [],
81
+ substitution_band: 'TIRED',
82
+ rotation_system: '6-0',
83
+ designated_setters: [],
84
+ offensive_preferences: []
85
+ };
86
+ }
87
+ function makeTeamModel(activeOrder) {
88
+ return {
89
+ team_id: teamId,
90
+ name: 'Test Team',
91
+ short_name: 'TST',
92
+ rating: 0,
93
+ division_id: uuidv4(),
94
+ active: true,
95
+ country: countryFixture,
96
+ tactics: makeTacticsRow(activeOrder),
97
+ PlayerTeams: ids.map(id => ({ player: makePlayerModel(id) }))
98
+ };
99
+ }
100
+ // The API-DTO config for a preset whose starters are the REVERSE of the active order, so an applied
101
+ // override is visible through the hydrated lineup.
102
+ function makeConfig(order) {
103
+ return {
104
+ lineup: {
105
+ [CourtPosition.RIGHT_BACK]: order[0],
106
+ [CourtPosition.RIGHT_FRONT]: order[1],
107
+ [CourtPosition.MIDDLE_FRONT]: order[2],
108
+ [CourtPosition.LEFT_FRONT]: order[3],
109
+ [CourtPosition.LEFT_BACK]: order[4],
110
+ [CourtPosition.MIDDLE_BACK]: order[5],
111
+ bench: []
112
+ },
113
+ liberoReplacements: [],
114
+ receiveRotationOffset: false,
115
+ designatedSubs: [],
116
+ substitutionBand: 'TIRED',
117
+ rotationSystem: '6-0',
118
+ designatedSetters: [],
119
+ offensivePreferences: []
120
+ };
121
+ }
122
+ const activeOrder = ids;
123
+ const presetOrder = [...ids].reverse();
124
+ function starterAt(tactics, zone) {
125
+ return tactics.lineup[zone]?.id;
126
+ }
127
+ // ─── apiTacticsConfigToTacticsAttributes ──────────────────────────────────────
128
+ describe('apiTacticsConfigToTacticsAttributes()', () => {
129
+ it('round-trips a fully-loaded Tactics row through tacticsColumnsToApiDto', () => {
130
+ const full = {
131
+ team_id: teamId,
132
+ lineup: { ...rowLineup(activeOrder), [CourtPosition.LIBERO_ZONE]: undefined },
133
+ libero_replacements: [ids[5]],
134
+ receive_rotation_offset: true,
135
+ designated_subs: [{ starterId: ids[0], benchId: undefined, mode: 'FATIGUE', fatigueBand: 'WORN', sets: [{ mode: 'NEVER' }] }],
136
+ substitution_band: 'TIRED',
137
+ second_libero: undefined,
138
+ libero_sub: undefined,
139
+ libero_replacement_sets: [[ids[0]], [ids[1]]],
140
+ rotation_system: '5-1',
141
+ designated_setters: [ids[0]],
142
+ offensive_preferences: [{ rotation: 1, order: [ids[1], ids[2]] }],
143
+ system_sets: [{ rotationSystem: '5-1', designatedSetters: [ids[0]] }],
144
+ offensive_preference_sets: [[{ rotation: 2, order: [ids[3]] }]]
145
+ };
146
+ const dto = tacticsColumnsToApiDto(full);
147
+ const back = apiTacticsConfigToTacticsAttributes(dto, teamId);
148
+ expect(back.lineup).toEqual(full.lineup);
149
+ expect(back.libero_replacements).toEqual(full.libero_replacements);
150
+ expect(back.receive_rotation_offset).toBe(true);
151
+ expect(back.designated_subs).toEqual(full.designated_subs);
152
+ expect(back.libero_replacement_sets).toEqual(full.libero_replacement_sets);
153
+ expect(back.rotation_system).toBe('5-1');
154
+ expect(back.designated_setters).toEqual(full.designated_setters);
155
+ expect(back.offensive_preferences).toEqual(full.offensive_preferences);
156
+ expect(back.system_sets).toEqual(full.system_sets);
157
+ expect(back.offensive_preference_sets).toEqual(full.offensive_preference_sets);
158
+ });
159
+ it('hydrates through transformToTactics identically to the row path', () => {
160
+ const roster = ids.map(id => transformToTeam(makeTeamModel(activeOrder)).roster.find(p => p.id === id));
161
+ expect(roster.every(p => p != null)).toBe(true);
162
+ const teamModel = makeTeamModel(activeOrder);
163
+ const players = transformToTeam(teamModel).roster;
164
+ const fromRow = transformToTactics(makeTacticsRow(activeOrder), players);
165
+ const fromConfig = transformToTactics(apiTacticsConfigToTacticsAttributes(makeConfig(activeOrder), teamId), players);
166
+ for (const zone of [1, 2, 3, 4, 5, 6]) {
167
+ expect(starterAt(fromConfig, zone)).toBe(starterAt(fromRow, zone));
168
+ }
169
+ });
170
+ it('throws PLAYER_NOT_FOUND when the config references an off-roster player', () => {
171
+ const players = transformToTeam(makeTeamModel(activeOrder)).roster;
172
+ const drifted = makeConfig([uuidv4(), ...ids.slice(1)]);
173
+ expect(() => transformToTactics(apiTacticsConfigToTacticsAttributes(drifted, teamId), players)).toThrow('PLAYER_NOT_FOUND');
174
+ });
175
+ });
176
+ // ─── transformToTeam override / fallback ──────────────────────────────────────
177
+ describe('transformToTeam() tacticsOverride', () => {
178
+ it('uses the override config when it hydrates', () => {
179
+ const team = transformToTeam(makeTeamModel(activeOrder), undefined, {
180
+ presetId: 'preset-1',
181
+ config: makeConfig(presetOrder)
182
+ });
183
+ expect(starterAt(team.tactics, CourtPosition.RIGHT_BACK)).toBe(presetOrder[0]);
184
+ });
185
+ it('falls back to the active tactics and notifies onFallback when the override fails', () => {
186
+ const onFallback = jest.fn();
187
+ const drifted = makeConfig([uuidv4(), ...ids.slice(1)]);
188
+ const team = transformToTeam(makeTeamModel(activeOrder), undefined, {
189
+ presetId: 'preset-1',
190
+ config: drifted,
191
+ onFallback
192
+ });
193
+ expect(onFallback).toHaveBeenCalledTimes(1);
194
+ expect(onFallback.mock.calls[0][0]).toBe('preset-1');
195
+ expect(starterAt(team.tactics, CourtPosition.RIGHT_BACK)).toBe(activeOrder[0]);
196
+ });
197
+ it('keeps the active tactics when no override is given', () => {
198
+ const team = transformToTeam(makeTeamModel(activeOrder));
199
+ expect(starterAt(team.tactics, CourtPosition.RIGHT_BACK)).toBe(activeOrder[0]);
200
+ });
201
+ });
202
+ // ─── transformToMatch per-team override ───────────────────────────────────────
203
+ describe('transformToMatch() MatchPresets', () => {
204
+ const awayIds = Array.from({ length: 6 }, () => uuidv4());
205
+ const awayTeamId = uuidv4();
206
+ function makeAwayModel() {
207
+ return {
208
+ team_id: awayTeamId,
209
+ name: 'Away Team',
210
+ short_name: 'AWY',
211
+ rating: 0,
212
+ division_id: uuidv4(),
213
+ active: true,
214
+ country: countryFixture,
215
+ tactics: {
216
+ team_id: awayTeamId,
217
+ lineup: {
218
+ [CourtPosition.RIGHT_BACK]: awayIds[0],
219
+ [CourtPosition.RIGHT_FRONT]: awayIds[1],
220
+ [CourtPosition.MIDDLE_FRONT]: awayIds[2],
221
+ [CourtPosition.LEFT_FRONT]: awayIds[3],
222
+ [CourtPosition.LEFT_BACK]: awayIds[4],
223
+ [CourtPosition.MIDDLE_BACK]: awayIds[5],
224
+ bench: []
225
+ },
226
+ libero_replacements: [],
227
+ receive_rotation_offset: false,
228
+ designated_subs: [],
229
+ substitution_band: 'TIRED',
230
+ rotation_system: '6-0',
231
+ designated_setters: [],
232
+ offensive_preferences: []
233
+ },
234
+ PlayerTeams: awayIds.map(id => ({ player: makePlayerModel(id) }))
235
+ };
236
+ }
237
+ function makeMatchModel(withPreset) {
238
+ return {
239
+ match_id: uuidv4(),
240
+ home_team: teamId,
241
+ away_team: awayTeamId,
242
+ scheduled_date: new Date(),
243
+ status: 'PENDING',
244
+ HomeTeam: makeTeamModel(activeOrder),
245
+ AwayTeam: makeAwayModel(),
246
+ MatchSets: [],
247
+ VPERs: [],
248
+ MatchPresets: withPreset
249
+ ? [{ match_id: 'm', team_id: teamId, preset_id: 'preset-1', preset: { config: makeConfig(presetOrder) } }]
250
+ : undefined
251
+ };
252
+ }
253
+ it('applies the home override and leaves the away team on active tactics', () => {
254
+ const match = transformToMatch(makeMatchModel(true));
255
+ expect(starterAt(match.homeTeam.tactics, CourtPosition.RIGHT_BACK)).toBe(presetOrder[0]);
256
+ expect(starterAt(match.awayTeam.tactics, CourtPosition.RIGHT_BACK)).toBe(awayIds[0]);
257
+ });
258
+ it('behaves exactly as before when the association is absent', () => {
259
+ const match = transformToMatch(makeMatchModel(false));
260
+ expect(starterAt(match.homeTeam.tactics, CourtPosition.RIGHT_BACK)).toBe(activeOrder[0]);
261
+ expect(starterAt(match.awayTeam.tactics, CourtPosition.RIGHT_BACK)).toBe(awayIds[0]);
262
+ });
263
+ it('routes the fallback callback with the failing team id', () => {
264
+ const onPresetFallback = jest.fn();
265
+ const model = makeMatchModel(true);
266
+ model.MatchPresets[0].preset.config = makeConfig([uuidv4(), ...ids.slice(1)]);
267
+ const match = transformToMatch(model, undefined, onPresetFallback);
268
+ expect(onPresetFallback).toHaveBeenCalledTimes(1);
269
+ expect(onPresetFallback.mock.calls[0][0]).toBe(teamId);
270
+ expect(onPresetFallback.mock.calls[0][1]).toBe('preset-1');
271
+ expect(starterAt(match.homeTeam.tactics, CourtPosition.RIGHT_BACK)).toBe(activeOrder[0]);
272
+ });
273
+ });
@@ -23,6 +23,7 @@ function transformToAttributes(set, matchId) {
23
23
  is_tie_break: set.isTieBreak,
24
24
  home_score: set.homeScore,
25
25
  away_score: set.awayScore,
26
+ forfeited: set.forfeited,
26
27
  player_positions: [...homeSetPositions, ...awaySetPositions],
27
28
  BoxScores: (set.boxScores ?? []).map(transformFromBoxScore),
28
29
  Rallies: (set.rallies ?? []).map((rally) => transformFromRally(rally, set.id))
@@ -39,6 +40,7 @@ function transformToObject(model) {
39
40
  homeScore: model.home_score,
40
41
  awayScore: model.away_score,
41
42
  isTieBreak: model.is_tie_break,
43
+ forfeited: model.forfeited ?? false,
42
44
  boxScores: (model.BoxScores ?? []).map(transformToBoxScore),
43
45
  homePlayerPosition,
44
46
  awayPlayerPosition,
@@ -1,5 +1,6 @@
1
1
  import { MatchAttributes, MatchModel } from '../models';
2
2
  import { Match } from '../../service';
3
+ export type PresetFallbackHandler = (teamId: string, presetId: string, err: unknown) => void;
3
4
  declare function transformToAttributes(match: Match): MatchAttributes;
4
- declare function transformToObject(model: MatchModel, currentIteration?: number): Match;
5
+ declare function transformToObject(model: MatchModel, currentIteration?: number, onPresetFallback?: PresetFallbackHandler): Match;
5
6
  export { transformToObject as transformToMatch, transformToAttributes as transformFromMatch };
@@ -8,10 +8,25 @@ function transformToAttributes(match) {
8
8
  away_team: match.awayTeam.id,
9
9
  home_team: match.homeTeam.id,
10
10
  scheduled_date: match.scheduledDate,
11
- status: match.status
11
+ status: match.status,
12
+ // Both directions on purpose (the read is in transformToObject below). NOTE for the Sim: updateMatch's
13
+ // completion write uses an explicit column list and must include forfeit_type or the marker is lost.
14
+ forfeit_type: match.forfeitType ?? null
12
15
  };
13
16
  }
14
- function transformToObject(model, currentIteration) {
17
+ function transformToObject(model, currentIteration, onPresetFallback) {
18
+ // The (match, team)-chosen preset override for one side, when the loader included MatchPresets with the
19
+ // preset loaded (only the simulator's match query does). Association absent = active tactics, as always.
20
+ const overrideFor = (teamId) => {
21
+ const row = (model.MatchPresets ?? []).find(mp => mp.team_id === teamId);
22
+ if (row?.preset == null)
23
+ return undefined;
24
+ return {
25
+ presetId: row.preset_id,
26
+ config: row.preset.config,
27
+ onFallback: (presetId, err) => onPresetFallback?.(teamId, presetId, err)
28
+ };
29
+ };
15
30
  const sets = (model.MatchSets ?? []).map(transformToMatchSet)
16
31
  .sort((s1, s2) => s1.order - s2.order);
17
32
  let homeScore = 0;
@@ -43,15 +58,16 @@ function transformToObject(model, currentIteration) {
43
58
  const VPERs = (model.VPERs ?? []).map(transformToVPER);
44
59
  return Match.create({
45
60
  id: model.match_id,
46
- homeTeam: transformToTeam(model.HomeTeam, currentIteration),
47
- awayTeam: transformToTeam(model.AwayTeam, currentIteration),
61
+ homeTeam: transformToTeam(model.HomeTeam, currentIteration, overrideFor(model.home_team)),
62
+ awayTeam: transformToTeam(model.AwayTeam, currentIteration, overrideFor(model.away_team)),
48
63
  scheduledDate: new Date(model.scheduled_date),
49
64
  sets,
50
65
  status: model.status,
51
66
  VPERs,
52
67
  homeScore,
53
68
  awayScore,
54
- winner: model.MatchResult?.Winner != null ? transformToTeam(model.MatchResult.Winner, currentIteration) : undefined
69
+ winner: model.MatchResult?.Winner != null ? transformToTeam(model.MatchResult.Winner, currentIteration) : undefined,
70
+ forfeitType: model.forfeit_type ?? undefined
55
71
  });
56
72
  }
57
73
  export { transformToObject as transformToMatch, transformToAttributes as transformFromMatch };
@@ -21,7 +21,11 @@ function transformToAttributes(player) {
21
21
  birth_age: player.birthAge,
22
22
  birth_iteration: player.birthIteration,
23
23
  last_declined_iteration: null,
24
- decline_profile: player.declineProfile
24
+ decline_profile: player.declineProfile,
25
+ injury_prone_profile: player.injuryProneProfile,
26
+ injury_severity: player.injury?.severity ?? null,
27
+ injured_until: player.injury?.injuredUntil ?? null,
28
+ injury_debuff: player.injury?.debuff ?? null
25
29
  };
26
30
  }
27
31
  function transformToBoxScoreTotals(model) {
@@ -87,6 +91,16 @@ function transformToObject(model, currentIteration) {
87
91
  birthAge: model.birth_age,
88
92
  birthIteration: model.birth_iteration,
89
93
  declineProfile: model.decline_profile,
94
+ // DECIMAL comes back as a string from pg; schema-defaulted when the column is absent (pre-migration rows).
95
+ injuryProneProfile: model.injury_prone_profile != null ? Number(model.injury_prone_profile) : undefined,
96
+ // Severity defends against a partial write: an injured_until without severity reads as MINOR.
97
+ injury: model.injured_until != null
98
+ ? {
99
+ severity: model.injury_severity != null ? Number(model.injury_severity) : 1,
100
+ injuredUntil: model.injured_until,
101
+ debuff: model.injury_debuff != null ? Number(model.injury_debuff) : undefined
102
+ }
103
+ : undefined,
90
104
  boxScores: (model.BoxScores ?? []).map(transformToBoxScore),
91
105
  boxScoreTotals
92
106
  });
@@ -40,7 +40,7 @@ function transformToObject(model) {
40
40
  id: model.competition_id,
41
41
  iteration: transformToIteration(model.Iteration),
42
42
  matches: (model.CompetitionMatches ?? []).map(transformToQualifierMatch),
43
- teams: sortTeamsByCompetitionIndex(model.Teams ?? []).map(transformToTeam),
43
+ teams: sortTeamsByCompetitionIndex(model.Teams ?? []).map(team => transformToTeam(team)),
44
44
  status: model.status,
45
45
  region: transformToRegion(regionQualifier),
46
46
  champion
@@ -0,0 +1,31 @@
1
+ import { describe, it, expect } from '@jest/globals';
2
+ import { v4 as uuidv4 } from 'uuid';
3
+ import { EventTypeEnum, Substitution } from '../../service';
4
+ import { transformToCompact, transformToSubstitution } from './rally-event';
5
+ describe('rally-event compact round trip — injury substitution marker', () => {
6
+ const playerIn = uuidv4();
7
+ const playerOut = uuidv4();
8
+ const roster = [playerIn, playerOut];
9
+ const playerIndex = new Map(roster.map((id, i) => [id, i]));
10
+ it('packs the injury flag as j=1 and round-trips it', () => {
11
+ const sub = Substitution.create({ playerId: playerIn, playerOut, injury: true });
12
+ const compact = transformToCompact(sub, playerIndex);
13
+ expect(compact.j).toBe(1);
14
+ const decoded = transformToSubstitution(compact, roster);
15
+ expect(decoded.injury).toBe(true);
16
+ expect(decoded.playerId).toBe(playerIn);
17
+ expect(decoded.playerOut).toBe(playerOut);
18
+ });
19
+ it('omits j entirely for a normal substitution', () => {
20
+ const sub = Substitution.create({ playerId: playerIn, playerOut });
21
+ const compact = transformToCompact(sub, playerIndex);
22
+ expect('j' in compact).toBe(false);
23
+ const decoded = transformToSubstitution(compact, roster);
24
+ expect(decoded.injury).toBeUndefined();
25
+ });
26
+ it('decodes a legacy compact event (no j key) as a non-injury substitution', () => {
27
+ const legacy = { p: 0, e: EventTypeEnum.SUBSTITUTION, o: 1 };
28
+ const decoded = transformToSubstitution(legacy, roster);
29
+ expect(decoded.injury).toBeUndefined();
30
+ });
31
+ });