volleyballsimtypes 0.0.478 → 0.0.480
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.
- package/dist/cjs/src/api/index.d.ts +1 -0
- package/dist/cjs/src/data/models/tactics.d.ts +1 -0
- package/dist/cjs/src/data/transformers/rally-event-tempo.test.d.ts +1 -0
- package/dist/cjs/src/data/transformers/rally-event-tempo.test.js +40 -0
- package/dist/cjs/src/data/transformers/rally-event.d.ts +1 -0
- package/dist/cjs/src/data/transformers/rally-event.js +5 -1
- package/dist/cjs/src/data/transformers/tactics.js +8 -4
- package/dist/cjs/src/data/transformers/tactics.test.js +13 -0
- package/dist/cjs/src/service/event/schemas/set.z.d.ts +1 -0
- package/dist/cjs/src/service/event/schemas/set.z.js +4 -1
- package/dist/cjs/src/service/event/schemas/set.z.test.d.ts +1 -0
- package/dist/cjs/src/service/event/schemas/set.z.test.js +29 -0
- package/dist/cjs/src/service/event/set.d.ts +7 -0
- package/dist/cjs/src/service/event/set.js +15 -3
- package/dist/cjs/src/service/index.d.ts +1 -0
- package/dist/cjs/src/service/index.js +1 -0
- package/dist/cjs/src/service/notification/catalog/en.json +41 -0
- package/dist/cjs/src/service/notification/catalog/es.json +41 -0
- package/dist/cjs/src/service/notification/catalog/fr.json +41 -0
- package/dist/cjs/src/service/notification/catalog/index.d.ts +3 -0
- package/dist/cjs/src/service/notification/catalog/index.js +22 -0
- package/dist/cjs/src/service/notification/catalog/it.json +41 -0
- package/dist/cjs/src/service/notification/catalog/pl.json +41 -0
- package/dist/cjs/src/service/notification/catalog/pt-BR.json +41 -0
- package/dist/cjs/src/service/notification/catalog/tr.json +41 -0
- package/dist/cjs/src/service/notification/index.d.ts +5 -0
- package/dist/cjs/src/service/notification/index.js +23 -0
- package/dist/cjs/src/service/notification/notification-type.d.ts +6 -0
- package/dist/cjs/src/service/notification/notification-type.js +30 -0
- package/dist/cjs/src/service/notification/notification.test.d.ts +1 -0
- package/dist/cjs/src/service/notification/notification.test.js +128 -0
- package/dist/cjs/src/service/notification/payloads.d.ts +34 -0
- package/dist/cjs/src/service/notification/payloads.js +5 -0
- package/dist/cjs/src/service/notification/registry.d.ts +13 -0
- package/dist/cjs/src/service/notification/registry.js +112 -0
- package/dist/cjs/src/service/notification/render.d.ts +10 -0
- package/dist/cjs/src/service/notification/render.js +64 -0
- package/dist/cjs/src/service/team/offensive-preference.d.ts +2 -0
- package/dist/cjs/src/service/team/schemas/tactics.z.d.ts +8 -0
- package/dist/cjs/src/service/team/schemas/tactics.z.js +12 -2
- package/dist/cjs/src/service/team/schemas/tactics.z.test.js +24 -0
- package/dist/cjs/src/service/team/schemas/team.z.d.ts +8 -0
- package/dist/esm/src/api/index.d.ts +1 -0
- package/dist/esm/src/data/models/tactics.d.ts +1 -0
- package/dist/esm/src/data/transformers/rally-event-tempo.test.d.ts +1 -0
- package/dist/esm/src/data/transformers/rally-event-tempo.test.js +38 -0
- package/dist/esm/src/data/transformers/rally-event.d.ts +1 -0
- package/dist/esm/src/data/transformers/rally-event.js +5 -1
- package/dist/esm/src/data/transformers/tactics.js +8 -4
- package/dist/esm/src/data/transformers/tactics.test.js +13 -0
- package/dist/esm/src/service/event/schemas/set.z.d.ts +1 -0
- package/dist/esm/src/service/event/schemas/set.z.js +4 -1
- package/dist/esm/src/service/event/schemas/set.z.test.d.ts +1 -0
- package/dist/esm/src/service/event/schemas/set.z.test.js +27 -0
- package/dist/esm/src/service/event/set.d.ts +7 -0
- package/dist/esm/src/service/event/set.js +14 -2
- package/dist/esm/src/service/index.d.ts +1 -0
- package/dist/esm/src/service/index.js +1 -0
- package/dist/esm/src/service/notification/catalog/en.json +41 -0
- package/dist/esm/src/service/notification/catalog/es.json +41 -0
- package/dist/esm/src/service/notification/catalog/fr.json +41 -0
- package/dist/esm/src/service/notification/catalog/index.d.ts +3 -0
- package/dist/esm/src/service/notification/catalog/index.js +16 -0
- package/dist/esm/src/service/notification/catalog/it.json +41 -0
- package/dist/esm/src/service/notification/catalog/pl.json +41 -0
- package/dist/esm/src/service/notification/catalog/pt-BR.json +41 -0
- package/dist/esm/src/service/notification/catalog/tr.json +41 -0
- package/dist/esm/src/service/notification/index.d.ts +5 -0
- package/dist/esm/src/service/notification/index.js +5 -0
- package/dist/esm/src/service/notification/notification-type.d.ts +6 -0
- package/dist/esm/src/service/notification/notification-type.js +26 -0
- package/dist/esm/src/service/notification/notification.test.d.ts +1 -0
- package/dist/esm/src/service/notification/notification.test.js +126 -0
- package/dist/esm/src/service/notification/payloads.d.ts +34 -0
- package/dist/esm/src/service/notification/payloads.js +4 -0
- package/dist/esm/src/service/notification/registry.d.ts +13 -0
- package/dist/esm/src/service/notification/registry.js +109 -0
- package/dist/esm/src/service/notification/render.d.ts +10 -0
- package/dist/esm/src/service/notification/render.js +58 -0
- package/dist/esm/src/service/team/offensive-preference.d.ts +2 -0
- package/dist/esm/src/service/team/schemas/tactics.z.d.ts +8 -0
- package/dist/esm/src/service/team/schemas/tactics.z.js +12 -2
- package/dist/esm/src/service/team/schemas/tactics.z.test.js +24 -0
- package/dist/esm/src/service/team/schemas/team.z.d.ts +8 -0
- package/package.json +6 -1
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare const NOTIFICATION_TYPES: readonly ["MATCH_RESULT", "INJURY", "INJURY_RECOVERED", "SEASON_END", "GRANT_RECEIVED", "PROMOTION", "DAILY_LOGIN", "WELCOME", "COMPENSATION"];
|
|
2
|
+
export type NotificationType = typeof NOTIFICATION_TYPES[number];
|
|
3
|
+
export declare const NOTIFICATION_LOCALES: readonly ["en", "es", "fr", "it", "pl", "pt-BR", "tr"];
|
|
4
|
+
export type NotificationLocale = typeof NOTIFICATION_LOCALES[number];
|
|
5
|
+
export declare const DEFAULT_NOTIFICATION_LOCALE: NotificationLocale;
|
|
6
|
+
export declare function normalizeNotificationLocale(locale: string | null | undefined): NotificationLocale;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Single source of truth for the set of notification types and the locales the catalog ships.
|
|
3
|
+
// Adding a notification type: add it here, add a NOTIFICATION_REGISTRY entry, and add its catalog
|
|
4
|
+
// strings to every locale file. The completeness test (notification.test.ts) fails the build if any
|
|
5
|
+
// of those three is missing. See README.md in this folder.
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.DEFAULT_NOTIFICATION_LOCALE = exports.NOTIFICATION_LOCALES = exports.NOTIFICATION_TYPES = void 0;
|
|
8
|
+
exports.normalizeNotificationLocale = normalizeNotificationLocale;
|
|
9
|
+
exports.NOTIFICATION_TYPES = [
|
|
10
|
+
'MATCH_RESULT',
|
|
11
|
+
'INJURY',
|
|
12
|
+
'INJURY_RECOVERED',
|
|
13
|
+
'SEASON_END',
|
|
14
|
+
'GRANT_RECEIVED',
|
|
15
|
+
'PROMOTION',
|
|
16
|
+
'DAILY_LOGIN',
|
|
17
|
+
'WELCOME',
|
|
18
|
+
'COMPENSATION'
|
|
19
|
+
];
|
|
20
|
+
// The 7 supported locales. Must stay in sync with the API VALID_LOCALES set and the UI src/locales/*.json.
|
|
21
|
+
exports.NOTIFICATION_LOCALES = ['en', 'es', 'fr', 'it', 'pl', 'pt-BR', 'tr'];
|
|
22
|
+
exports.DEFAULT_NOTIFICATION_LOCALE = 'en';
|
|
23
|
+
// Coalesce an arbitrary/untrusted locale string (e.g. UserSettings.locale, which is nullable) to a
|
|
24
|
+
// supported locale, defaulting to English. Push senders MUST call this before renderPush.
|
|
25
|
+
function normalizeNotificationLocale(locale) {
|
|
26
|
+
if (locale != null && exports.NOTIFICATION_LOCALES.includes(locale)) {
|
|
27
|
+
return locale;
|
|
28
|
+
}
|
|
29
|
+
return exports.DEFAULT_NOTIFICATION_LOCALE;
|
|
30
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const globals_1 = require("@jest/globals");
|
|
4
|
+
const notification_type_1 = require("./notification-type");
|
|
5
|
+
const registry_1 = require("./registry");
|
|
6
|
+
const catalog_1 = require("./catalog");
|
|
7
|
+
const render_1 = require("./render");
|
|
8
|
+
function readNested(node, dotKey) {
|
|
9
|
+
let current = node;
|
|
10
|
+
for (const part of dotKey.split('.')) {
|
|
11
|
+
if (typeof current !== 'object' || current == null)
|
|
12
|
+
return undefined;
|
|
13
|
+
current = current[part];
|
|
14
|
+
}
|
|
15
|
+
return typeof current === 'string' ? current : undefined;
|
|
16
|
+
}
|
|
17
|
+
function placeholders(value) {
|
|
18
|
+
return (value.match(/\{\{(\w+)\}\}/g) ?? []).sort((a, b) => a.localeCompare(b));
|
|
19
|
+
}
|
|
20
|
+
function flattenStrings(node, prefix = '') {
|
|
21
|
+
const out = [];
|
|
22
|
+
if (typeof node !== 'object' || node == null)
|
|
23
|
+
return out;
|
|
24
|
+
for (const [key, value] of Object.entries(node)) {
|
|
25
|
+
if (typeof value === 'string')
|
|
26
|
+
out.push([prefix + key, value]);
|
|
27
|
+
else if (typeof value === 'object' && value != null)
|
|
28
|
+
out.push(...flattenStrings(value, `${prefix}${key}.`));
|
|
29
|
+
}
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
(0, globals_1.describe)('notification registry + catalog completeness', () => {
|
|
33
|
+
(0, globals_1.it)('has exactly one descriptor per declared type', () => {
|
|
34
|
+
(0, globals_1.expect)(Object.keys(registry_1.NOTIFICATION_REGISTRY).sort((a, b) => a.localeCompare(b)))
|
|
35
|
+
.toEqual([...notification_type_1.NOTIFICATION_TYPES].sort((a, b) => a.localeCompare(b)));
|
|
36
|
+
});
|
|
37
|
+
(0, globals_1.it)('resolves every descriptor catalog key to a non-empty string in all 7 locales', () => {
|
|
38
|
+
for (const type of notification_type_1.NOTIFICATION_TYPES) {
|
|
39
|
+
for (const key of registry_1.NOTIFICATION_REGISTRY[type].catalogKeys) {
|
|
40
|
+
for (const locale of notification_type_1.NOTIFICATION_LOCALES) {
|
|
41
|
+
const value = readNested(catalog_1.NOTIFICATION_CATALOG[locale], key);
|
|
42
|
+
(0, globals_1.expect)([locale, key, (value ?? '').length > 0]).toEqual([locale, key, true]);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
(0, globals_1.it)('ships an identical key set across all 7 locales (no drift)', () => {
|
|
48
|
+
const enKeys = flattenStrings(catalog_1.NOTIFICATION_CATALOG.en).map(([key]) => key).sort((a, b) => a.localeCompare(b));
|
|
49
|
+
for (const locale of notification_type_1.NOTIFICATION_LOCALES) {
|
|
50
|
+
const keys = flattenStrings(catalog_1.NOTIFICATION_CATALOG[locale]).map(([key]) => key).sort((a, b) => a.localeCompare(b));
|
|
51
|
+
(0, globals_1.expect)([locale, keys]).toEqual([locale, enKeys]);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
(0, globals_1.it)('keeps placeholders identical across locales', () => {
|
|
55
|
+
for (const [key, enValue] of flattenStrings(catalog_1.NOTIFICATION_CATALOG.en)) {
|
|
56
|
+
const expected = placeholders(enValue);
|
|
57
|
+
for (const locale of notification_type_1.NOTIFICATION_LOCALES) {
|
|
58
|
+
const value = readNested(catalog_1.NOTIFICATION_CATALOG[locale], key);
|
|
59
|
+
if (value == null)
|
|
60
|
+
continue;
|
|
61
|
+
(0, globals_1.expect)([locale, key, placeholders(value)]).toEqual([locale, key, expected]);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
(0, globals_1.it)('contains no em or en dashes', () => {
|
|
66
|
+
for (const locale of notification_type_1.NOTIFICATION_LOCALES) {
|
|
67
|
+
for (const [key, value] of flattenStrings(catalog_1.NOTIFICATION_CATALOG[locale])) {
|
|
68
|
+
(0, globals_1.expect)([locale, key, value.includes('—') || value.includes('–')]).toEqual([locale, key, false]);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
(0, globals_1.describe)('renderPush', () => {
|
|
74
|
+
(0, globals_1.it)('renders a localized MATCH_RESULT win with a named opponent', () => {
|
|
75
|
+
const payload = { matchId: 'm1', result: 'WIN', side: 'HOME', awayTeam: { name: 'Rivals' } };
|
|
76
|
+
const en = (0, render_1.renderPush)('MATCH_RESULT', payload, 'en');
|
|
77
|
+
(0, globals_1.expect)(en?.title).toBe('Match result');
|
|
78
|
+
(0, globals_1.expect)(en?.body).toBe('You won against Rivals');
|
|
79
|
+
(0, globals_1.expect)(en?.data.path).toBe('/match/m1');
|
|
80
|
+
(0, globals_1.expect)((0, render_1.renderPush)('MATCH_RESULT', payload, 'es')?.body).toBe('Ganaste contra Rivals');
|
|
81
|
+
});
|
|
82
|
+
(0, globals_1.it)('uses the unnamed variant and root path when the opponent is unknown', () => {
|
|
83
|
+
const en = (0, render_1.renderPush)('MATCH_RESULT', { result: 'LOSS' }, 'en');
|
|
84
|
+
(0, globals_1.expect)(en?.body).toBe('You lost your match');
|
|
85
|
+
(0, globals_1.expect)(en?.data.path).toBe('/');
|
|
86
|
+
});
|
|
87
|
+
(0, globals_1.it)('interpolates the injured player name and links to the infirmary', () => {
|
|
88
|
+
const en = (0, render_1.renderPush)('INJURY', { playerNickname: 'Ace', teamId: 't1' }, 'en');
|
|
89
|
+
(0, globals_1.expect)(en?.title).toBe('Player injured');
|
|
90
|
+
(0, globals_1.expect)(en?.body).toContain('Ace');
|
|
91
|
+
(0, globals_1.expect)(en?.data.path).toBe('/infirmary');
|
|
92
|
+
});
|
|
93
|
+
(0, globals_1.it)('uses a generic subject when the injury payload has no player name', () => {
|
|
94
|
+
const en = (0, render_1.renderPush)('INJURY', { teamId: 't1' }, 'en');
|
|
95
|
+
(0, globals_1.expect)(en?.body.startsWith('A player')).toBe(true);
|
|
96
|
+
});
|
|
97
|
+
(0, globals_1.it)('returns undefined for in-app-only types (COMPENSATION)', () => {
|
|
98
|
+
(0, globals_1.expect)((0, render_1.renderPush)('COMPENSATION', { currency: 100 }, 'en')).toBeUndefined();
|
|
99
|
+
});
|
|
100
|
+
(0, globals_1.it)('falls back to generic copy and root path for an unknown type', () => {
|
|
101
|
+
const en = (0, render_1.renderPush)('SOMETHING_NEW', {}, 'en');
|
|
102
|
+
(0, globals_1.expect)(en?.title).toBe('World Aces');
|
|
103
|
+
(0, globals_1.expect)(en?.body).toBe('You have a new notification.');
|
|
104
|
+
(0, globals_1.expect)(en?.data.path).toBe('/');
|
|
105
|
+
});
|
|
106
|
+
(0, globals_1.it)('serializes the payload into the FCM data block', () => {
|
|
107
|
+
const en = (0, render_1.renderPush)('GRANT_RECEIVED', { pullType: 'MYTHIC' }, 'en');
|
|
108
|
+
(0, globals_1.expect)(en?.data.type).toBe('GRANT_RECEIVED');
|
|
109
|
+
(0, globals_1.expect)(JSON.parse(en?.data.payload ?? '{}')).toEqual({ pullType: 'MYTHIC' });
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
(0, globals_1.describe)('translateNotification + interpolate + notificationPath', () => {
|
|
113
|
+
(0, globals_1.it)('falls back to English then to the raw key', () => {
|
|
114
|
+
(0, globals_1.expect)((0, render_1.translateNotification)('fr', 'promotion.title')).toBe('Promotion');
|
|
115
|
+
(0, globals_1.expect)((0, render_1.translateNotification)('en', 'does.not.exist')).toBe('does.not.exist');
|
|
116
|
+
});
|
|
117
|
+
(0, globals_1.it)('replaces placeholders and drops unknown ones', () => {
|
|
118
|
+
(0, globals_1.expect)((0, render_1.interpolate)('Hi {{name}}!', { name: 'X' })).toBe('Hi X!');
|
|
119
|
+
(0, globals_1.expect)((0, render_1.interpolate)('Hi {{name}}!', {})).toBe('Hi !');
|
|
120
|
+
});
|
|
121
|
+
(0, globals_1.it)('derives the deep-link path per type', () => {
|
|
122
|
+
(0, globals_1.expect)((0, render_1.notificationPath)('SEASON_END', { leagueId: 'l1' })).toBe('/league/l1');
|
|
123
|
+
(0, globals_1.expect)((0, render_1.notificationPath)('SEASON_END', {})).toBe('/league');
|
|
124
|
+
(0, globals_1.expect)((0, render_1.notificationPath)('PROMOTION', { teamId: 't1' })).toBe('/team/t1');
|
|
125
|
+
(0, globals_1.expect)((0, render_1.notificationPath)('GRANT_RECEIVED', {})).toBe('/recruit');
|
|
126
|
+
(0, globals_1.expect)((0, render_1.notificationPath)('COMPENSATION', {})).toBe('/');
|
|
127
|
+
});
|
|
128
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export interface MatchResultTeam {
|
|
2
|
+
id?: string;
|
|
3
|
+
name?: string;
|
|
4
|
+
}
|
|
5
|
+
export interface MatchResultPayload {
|
|
6
|
+
matchId?: string;
|
|
7
|
+
homeTeam?: MatchResultTeam;
|
|
8
|
+
awayTeam?: MatchResultTeam;
|
|
9
|
+
result?: 'WIN' | 'LOSS';
|
|
10
|
+
side?: 'HOME' | 'AWAY';
|
|
11
|
+
}
|
|
12
|
+
export interface InjuryPayload {
|
|
13
|
+
playerId?: string;
|
|
14
|
+
playerName?: string;
|
|
15
|
+
playerNickname?: string;
|
|
16
|
+
teamId?: string;
|
|
17
|
+
matchId?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface InjuryRecoveredPayload {
|
|
20
|
+
playerId?: string;
|
|
21
|
+
playerName?: string;
|
|
22
|
+
playerNickname?: string;
|
|
23
|
+
teamId?: string;
|
|
24
|
+
}
|
|
25
|
+
export interface SeasonEndPayload {
|
|
26
|
+
competitionId?: string;
|
|
27
|
+
leagueId?: string;
|
|
28
|
+
position?: number;
|
|
29
|
+
}
|
|
30
|
+
export interface PromotionPayload {
|
|
31
|
+
teamId?: string;
|
|
32
|
+
amount?: number;
|
|
33
|
+
}
|
|
34
|
+
export type NotificationPayload = MatchResultPayload | InjuryPayload | InjuryRecoveredPayload | SeasonEndPayload | PromotionPayload | Record<string, unknown>;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Typed payload shapes per notification type, for call-site safety in the Sim/API. The DB `Notification.payload`
|
|
3
|
+
// column is jsonb (free-form), so the registry reads payloads defensively (Record<string, unknown>); these
|
|
4
|
+
// interfaces document the intended shape and can be used by emitters when constructing the payload.
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type NotificationType } from './notification-type';
|
|
2
|
+
export type CatalogKeyResolver = string | ((payload: Record<string, unknown>) => string);
|
|
3
|
+
export interface NotificationDescriptor {
|
|
4
|
+
emitsPush: boolean;
|
|
5
|
+
deepLink?: (payload: Record<string, unknown>) => string;
|
|
6
|
+
pushTitleKey?: CatalogKeyResolver;
|
|
7
|
+
pushBodyKey?: CatalogKeyResolver;
|
|
8
|
+
inAppMessageKey?: CatalogKeyResolver;
|
|
9
|
+
richRender?: NotificationType;
|
|
10
|
+
params?: (payload: Record<string, unknown>) => Record<string, string>;
|
|
11
|
+
catalogKeys: string[];
|
|
12
|
+
}
|
|
13
|
+
export declare const NOTIFICATION_REGISTRY: Record<NotificationType, NotificationDescriptor>;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NOTIFICATION_REGISTRY = void 0;
|
|
4
|
+
function readString(value) {
|
|
5
|
+
return typeof value === 'string' && value !== '' ? value : null;
|
|
6
|
+
}
|
|
7
|
+
function teamPath(payload) {
|
|
8
|
+
const teamId = readString(payload.teamId);
|
|
9
|
+
return teamId != null ? `/team/${encodeURIComponent(teamId)}` : '/';
|
|
10
|
+
}
|
|
11
|
+
// MATCH_RESULT copy is recipient-relative: `result` is WIN/LOSS from the recipient's view and `side` marks
|
|
12
|
+
// which team is theirs, so the OTHER team is the opponent. When side/names are missing, do not name a winner.
|
|
13
|
+
function matchOpponent(payload) {
|
|
14
|
+
const homeTeam = typeof payload.homeTeam === 'object' && payload.homeTeam != null ? payload.homeTeam : {};
|
|
15
|
+
const awayTeam = typeof payload.awayTeam === 'object' && payload.awayTeam != null ? payload.awayTeam : {};
|
|
16
|
+
const homeName = readString(homeTeam.name);
|
|
17
|
+
const awayName = readString(awayTeam.name);
|
|
18
|
+
if (payload.side === 'HOME')
|
|
19
|
+
return awayName;
|
|
20
|
+
if (payload.side === 'AWAY')
|
|
21
|
+
return homeName;
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
function injuredName(payload) {
|
|
25
|
+
// Matches the old Sim injuredPlayerName fallback so a name-less payload never renders a subjectless sentence.
|
|
26
|
+
return readString(payload.playerNickname) ?? readString(payload.playerName) ?? 'A player';
|
|
27
|
+
}
|
|
28
|
+
exports.NOTIFICATION_REGISTRY = {
|
|
29
|
+
MATCH_RESULT: {
|
|
30
|
+
emitsPush: true,
|
|
31
|
+
deepLink: (p) => {
|
|
32
|
+
const matchId = readString(p.matchId);
|
|
33
|
+
return matchId != null ? `/match/${encodeURIComponent(matchId)}` : '/';
|
|
34
|
+
},
|
|
35
|
+
pushTitleKey: 'matchResult.title',
|
|
36
|
+
pushBodyKey: (p) => {
|
|
37
|
+
const outcome = p.result === 'WIN' ? 'won' : 'lost';
|
|
38
|
+
const named = matchOpponent(p) != null ? 'Named' : 'Unnamed';
|
|
39
|
+
return `matchResult.${outcome}${named}`;
|
|
40
|
+
},
|
|
41
|
+
params: (p) => ({ opponent: matchOpponent(p) ?? '' }),
|
|
42
|
+
richRender: 'MATCH_RESULT',
|
|
43
|
+
catalogKeys: ['matchResult.title', 'matchResult.wonNamed', 'matchResult.wonUnnamed', 'matchResult.lostNamed', 'matchResult.lostUnnamed']
|
|
44
|
+
},
|
|
45
|
+
INJURY: {
|
|
46
|
+
emitsPush: true,
|
|
47
|
+
deepLink: () => '/infirmary',
|
|
48
|
+
pushTitleKey: 'injury.title',
|
|
49
|
+
pushBodyKey: 'injury.body',
|
|
50
|
+
inAppMessageKey: 'injury.body',
|
|
51
|
+
params: (p) => ({ name: injuredName(p) }),
|
|
52
|
+
catalogKeys: ['injury.title', 'injury.body']
|
|
53
|
+
},
|
|
54
|
+
INJURY_RECOVERED: {
|
|
55
|
+
emitsPush: true,
|
|
56
|
+
deepLink: teamPath,
|
|
57
|
+
pushTitleKey: 'injuryRecovered.title',
|
|
58
|
+
pushBodyKey: 'injuryRecovered.body',
|
|
59
|
+
inAppMessageKey: 'injuryRecovered.body',
|
|
60
|
+
params: (p) => ({ name: injuredName(p) }),
|
|
61
|
+
catalogKeys: ['injuryRecovered.title', 'injuryRecovered.body']
|
|
62
|
+
},
|
|
63
|
+
SEASON_END: {
|
|
64
|
+
emitsPush: true,
|
|
65
|
+
deepLink: (p) => {
|
|
66
|
+
const leagueId = readString(p.leagueId);
|
|
67
|
+
return leagueId != null ? `/league/${encodeURIComponent(leagueId)}` : '/league';
|
|
68
|
+
},
|
|
69
|
+
pushTitleKey: 'seasonEnd.title',
|
|
70
|
+
pushBodyKey: 'seasonEnd.body',
|
|
71
|
+
inAppMessageKey: 'seasonEnd.body',
|
|
72
|
+
catalogKeys: ['seasonEnd.title', 'seasonEnd.body']
|
|
73
|
+
},
|
|
74
|
+
GRANT_RECEIVED: {
|
|
75
|
+
emitsPush: true,
|
|
76
|
+
deepLink: () => '/recruit',
|
|
77
|
+
pushTitleKey: 'grant.title',
|
|
78
|
+
pushBodyKey: 'grant.body',
|
|
79
|
+
inAppMessageKey: 'grant.body',
|
|
80
|
+
catalogKeys: ['grant.title', 'grant.body']
|
|
81
|
+
},
|
|
82
|
+
PROMOTION: {
|
|
83
|
+
emitsPush: true,
|
|
84
|
+
deepLink: teamPath,
|
|
85
|
+
pushTitleKey: 'promotion.title',
|
|
86
|
+
pushBodyKey: 'promotion.body',
|
|
87
|
+
inAppMessageKey: 'promotion.body',
|
|
88
|
+
catalogKeys: ['promotion.title', 'promotion.body']
|
|
89
|
+
},
|
|
90
|
+
DAILY_LOGIN: {
|
|
91
|
+
emitsPush: true,
|
|
92
|
+
deepLink: () => '/recruit',
|
|
93
|
+
pushTitleKey: 'dailyLogin.title',
|
|
94
|
+
pushBodyKey: 'dailyLogin.body',
|
|
95
|
+
inAppMessageKey: 'dailyLogin.body',
|
|
96
|
+
catalogKeys: ['dailyLogin.title', 'dailyLogin.body']
|
|
97
|
+
},
|
|
98
|
+
WELCOME: {
|
|
99
|
+
emitsPush: true,
|
|
100
|
+
deepLink: () => '/recruit',
|
|
101
|
+
pushTitleKey: 'welcome.title',
|
|
102
|
+
pushBodyKey: 'welcome.body',
|
|
103
|
+
richRender: 'WELCOME',
|
|
104
|
+
catalogKeys: ['welcome.title', 'welcome.body']
|
|
105
|
+
},
|
|
106
|
+
// In-app only (no push); the UI keeps a bespoke renderer (amount formatting) and its own `compensation` label.
|
|
107
|
+
COMPENSATION: {
|
|
108
|
+
emitsPush: false,
|
|
109
|
+
richRender: 'COMPENSATION',
|
|
110
|
+
catalogKeys: []
|
|
111
|
+
}
|
|
112
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type NotificationLocale } from './notification-type';
|
|
2
|
+
export interface RenderedPush {
|
|
3
|
+
title: string;
|
|
4
|
+
body: string;
|
|
5
|
+
data: Record<string, string>;
|
|
6
|
+
}
|
|
7
|
+
export declare function interpolate(template: string, params: Record<string, string>): string;
|
|
8
|
+
export declare function translateNotification(locale: NotificationLocale, key: string, params?: Record<string, string>): string;
|
|
9
|
+
export declare function renderPush(type: string, payload: Record<string, unknown>, locale: NotificationLocale): RenderedPush | undefined;
|
|
10
|
+
export declare function notificationPath(type: string, payload: Record<string, unknown>): string;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.interpolate = interpolate;
|
|
4
|
+
exports.translateNotification = translateNotification;
|
|
5
|
+
exports.renderPush = renderPush;
|
|
6
|
+
exports.notificationPath = notificationPath;
|
|
7
|
+
const notification_type_1 = require("./notification-type");
|
|
8
|
+
const registry_1 = require("./registry");
|
|
9
|
+
const catalog_1 = require("./catalog");
|
|
10
|
+
function readNested(node, dotKey) {
|
|
11
|
+
let current = node;
|
|
12
|
+
for (const part of dotKey.split('.')) {
|
|
13
|
+
if (typeof current !== 'object' || current == null)
|
|
14
|
+
return undefined;
|
|
15
|
+
current = current[part];
|
|
16
|
+
}
|
|
17
|
+
return typeof current === 'string' ? current : undefined;
|
|
18
|
+
}
|
|
19
|
+
function lookupCatalog(locale, dotKey) {
|
|
20
|
+
return readNested(catalog_1.NOTIFICATION_CATALOG[locale], dotKey) ??
|
|
21
|
+
readNested(catalog_1.NOTIFICATION_CATALOG[notification_type_1.DEFAULT_NOTIFICATION_LOCALE], dotKey) ??
|
|
22
|
+
dotKey;
|
|
23
|
+
}
|
|
24
|
+
function resolveKey(resolver, payload) {
|
|
25
|
+
if (resolver == null)
|
|
26
|
+
return undefined;
|
|
27
|
+
return typeof resolver === 'function' ? resolver(payload) : resolver;
|
|
28
|
+
}
|
|
29
|
+
function interpolate(template, params) {
|
|
30
|
+
return template.replace(/\{\{(\w+)\}\}/g, (_match, key) => params[key] ?? '');
|
|
31
|
+
}
|
|
32
|
+
// Resolve one catalog key to a localized, interpolated string. Missing key/locale falls back to English,
|
|
33
|
+
// then to the raw key. Exposed so consumers can render notification copy off the shared catalog too.
|
|
34
|
+
function translateNotification(locale, key, params = {}) {
|
|
35
|
+
return interpolate(lookupCatalog(locale, key), params);
|
|
36
|
+
}
|
|
37
|
+
// Build the localized push payload for a notification. Returns undefined for in-app-only types (emitsPush
|
|
38
|
+
// false, e.g. COMPENSATION). Unknown/future types fall back to the generic copy and the root path, matching
|
|
39
|
+
// the previous hardcoded fallback. Callers pass the recipient's locale (normalizeNotificationLocale first).
|
|
40
|
+
function renderPush(type, payload, locale) {
|
|
41
|
+
const buildData = (path) => ({ type, payload: JSON.stringify(payload), path });
|
|
42
|
+
const descriptor = registry_1.NOTIFICATION_REGISTRY[type];
|
|
43
|
+
if (descriptor == null) {
|
|
44
|
+
return {
|
|
45
|
+
title: translateNotification(locale, 'generic.title'),
|
|
46
|
+
body: translateNotification(locale, 'generic.body'),
|
|
47
|
+
data: buildData('/')
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
if (!descriptor.emitsPush)
|
|
51
|
+
return undefined;
|
|
52
|
+
const params = descriptor.params?.(payload) ?? {};
|
|
53
|
+
const titleKey = resolveKey(descriptor.pushTitleKey, payload) ?? 'generic.title';
|
|
54
|
+
const bodyKey = resolveKey(descriptor.pushBodyKey, payload) ?? 'generic.body';
|
|
55
|
+
return {
|
|
56
|
+
title: translateNotification(locale, titleKey, params),
|
|
57
|
+
body: translateNotification(locale, bodyKey, params),
|
|
58
|
+
data: buildData(descriptor.deepLink?.(payload) ?? '/')
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
// Deep-link path for a type/payload, used for both the push data.path and the in-app click target.
|
|
62
|
+
function notificationPath(type, payload) {
|
|
63
|
+
return registry_1.NOTIFICATION_REGISTRY[type]?.deepLink?.(payload) ?? '/';
|
|
64
|
+
}
|
|
@@ -270,6 +270,10 @@ export declare const TacticsInputSchema: z.ZodObject<{
|
|
|
270
270
|
offensivePreferences: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
271
271
|
rotation: z.ZodNumber;
|
|
272
272
|
order: z.ZodArray<z.ZodCustom<Player, Player>>;
|
|
273
|
+
tempos: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodEnum<{
|
|
274
|
+
FAST: "FAST";
|
|
275
|
+
HIGH: "HIGH";
|
|
276
|
+
}>>>;
|
|
273
277
|
}, z.core.$strip>>>;
|
|
274
278
|
systemSets: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
275
279
|
rotationSystem: z.ZodEnum<typeof RotationSystemEnum>;
|
|
@@ -278,6 +282,10 @@ export declare const TacticsInputSchema: z.ZodObject<{
|
|
|
278
282
|
offensivePreferenceSets: z.ZodOptional<z.ZodArray<z.ZodArray<z.ZodObject<{
|
|
279
283
|
rotation: z.ZodNumber;
|
|
280
284
|
order: z.ZodArray<z.ZodCustom<Player, Player>>;
|
|
285
|
+
tempos: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodEnum<{
|
|
286
|
+
FAST: "FAST";
|
|
287
|
+
HIGH: "HIGH";
|
|
288
|
+
}>>>;
|
|
281
289
|
}, z.core.$strip>>>>;
|
|
282
290
|
replaceKnockedImmediately: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodBoolean>>;
|
|
283
291
|
injuryReplacements: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
@@ -48,11 +48,15 @@ const lineupSchema = zod_1.z.object({
|
|
|
48
48
|
});
|
|
49
49
|
}
|
|
50
50
|
});
|
|
51
|
+
// Per-player tempo preference (set-tempo rework): absent key = Auto.
|
|
52
|
+
const tempoPreferenceSchema = zod_1.z.enum(['FAST', 'HIGH']);
|
|
51
53
|
// One rotation's ranked attacker priority. `rotation` is the main setter's rotational slot (1..6); `order` lists
|
|
52
|
-
// the attackers ranked best-first.
|
|
54
|
+
// the attackers ranked best-first. `tempos` optionally forces a per-starter tempo (Auto when absent). Cross-field
|
|
55
|
+
// checks (starters only in both order and tempos, no dupes) live in the superRefine below.
|
|
53
56
|
const offensivePreferenceSchema = zod_1.z.object({
|
|
54
57
|
rotation: zod_1.z.number().int().min(1).max(6),
|
|
55
|
-
order: zod_1.z.array(playerInstanceSchema)
|
|
58
|
+
order: zod_1.z.array(playerInstanceSchema),
|
|
59
|
+
tempos: zod_1.z.record(zod_1.z.string(), tempoPreferenceSchema).optional()
|
|
56
60
|
});
|
|
57
61
|
// One set's rotation-system override (per-set tactics): system + its designated setters travel together.
|
|
58
62
|
const systemSetSchema = zod_1.z.object({
|
|
@@ -158,6 +162,12 @@ exports.TacticsInputSchema = zod_1.z.object({
|
|
|
158
162
|
ctx.addIssue({ code: 'custom', message: 'OFFENSIVE_PREFERENCE_ATTACKER_NOT_A_STARTER', path });
|
|
159
163
|
}
|
|
160
164
|
}
|
|
165
|
+
// Tempo preference keys are starter ids too (same rule as order).
|
|
166
|
+
for (const id of Object.keys(pref.tempos ?? {})) {
|
|
167
|
+
if (!starterIds.has(id)) {
|
|
168
|
+
ctx.addIssue({ code: 'custom', message: 'OFFENSIVE_PREFERENCE_TEMPO_NOT_A_STARTER', path });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
161
171
|
}
|
|
162
172
|
};
|
|
163
173
|
validateSystem(data.rotationSystem, data.designatedSetters, ['designatedSetters']);
|
|
@@ -170,6 +170,30 @@ const test_helpers_1 = require("../../test-helpers");
|
|
|
170
170
|
(0, globals_1.expect)(res.error.issues.some(i => i.message === 'OFFENSIVE_PREFERENCE_ATTACKER_NOT_A_STARTER')).toBe(true);
|
|
171
171
|
}
|
|
172
172
|
});
|
|
173
|
+
(0, globals_1.it)('accepts offensive preferences with a starter-keyed tempos map', () => {
|
|
174
|
+
const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
|
|
175
|
+
offensivePreferences: [{ rotation: 1, order: [p2, p3], tempos: { [p2.id]: 'FAST', [p3.id]: 'HIGH' } }]
|
|
176
|
+
}));
|
|
177
|
+
(0, globals_1.expect)(res.success).toBe(true);
|
|
178
|
+
if (res.success) {
|
|
179
|
+
(0, globals_1.expect)(res.data.offensivePreferences[0].tempos).toEqual({ [p2.id]: 'FAST', [p3.id]: 'HIGH' });
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
(0, globals_1.it)('rejects a tempos map keyed by a non-starter', () => {
|
|
183
|
+
const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
|
|
184
|
+
offensivePreferences: [{ rotation: 1, order: [p2], tempos: { [reserve.id]: 'FAST' } }]
|
|
185
|
+
}));
|
|
186
|
+
(0, globals_1.expect)(res.success).toBe(false);
|
|
187
|
+
if (!res.success) {
|
|
188
|
+
(0, globals_1.expect)(res.error.issues.some(i => i.message === 'OFFENSIVE_PREFERENCE_TEMPO_NOT_A_STARTER')).toBe(true);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
(0, globals_1.it)('rejects an invalid tempo value', () => {
|
|
192
|
+
const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
|
|
193
|
+
offensivePreferences: [{ rotation: 1, order: [p2], tempos: { [p2.id]: 'SLOW' } }]
|
|
194
|
+
}));
|
|
195
|
+
(0, globals_1.expect)(res.success).toBe(false);
|
|
196
|
+
});
|
|
173
197
|
(0, globals_1.it)('still validates the flat fields when per-set overrides are present', () => {
|
|
174
198
|
// Flat 5-1 with no setter is invalid even though every set entry is valid.
|
|
175
199
|
const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
|
|
@@ -278,6 +278,10 @@ export declare const TeamInputSchema: z.ZodObject<{
|
|
|
278
278
|
offensivePreferences: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
279
279
|
rotation: z.ZodNumber;
|
|
280
280
|
order: z.ZodArray<z.ZodCustom<Player, Player>>;
|
|
281
|
+
tempos: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodEnum<{
|
|
282
|
+
FAST: "FAST";
|
|
283
|
+
HIGH: "HIGH";
|
|
284
|
+
}>>>;
|
|
281
285
|
}, z.core.$strip>>>;
|
|
282
286
|
systemSets: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
283
287
|
rotationSystem: z.ZodEnum<typeof import("..").RotationSystemEnum>;
|
|
@@ -286,6 +290,10 @@ export declare const TeamInputSchema: z.ZodObject<{
|
|
|
286
290
|
offensivePreferenceSets: z.ZodOptional<z.ZodArray<z.ZodArray<z.ZodObject<{
|
|
287
291
|
rotation: z.ZodNumber;
|
|
288
292
|
order: z.ZodArray<z.ZodCustom<Player, Player>>;
|
|
293
|
+
tempos: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodEnum<{
|
|
294
|
+
FAST: "FAST";
|
|
295
|
+
HIGH: "HIGH";
|
|
296
|
+
}>>>;
|
|
289
297
|
}, z.core.$strip>>>>;
|
|
290
298
|
replaceKnockedImmediately: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodBoolean>>;
|
|
291
299
|
injuryReplacements: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
@@ -42,6 +42,7 @@ export interface DesignatedSubAttributes {
|
|
|
42
42
|
export interface OffensivePreferenceAttributes {
|
|
43
43
|
rotation: number;
|
|
44
44
|
order: PlayerId[];
|
|
45
|
+
tempos?: Record<PlayerId, 'FAST' | 'HIGH'>;
|
|
45
46
|
}
|
|
46
47
|
export interface SystemSetAttributes {
|
|
47
48
|
rotationSystem: RotationSystemEnum;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { describe, it, expect } from '@jest/globals';
|
|
2
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
3
|
+
import { EventTypeEnum, Set, SetFailureEnum, SetTempoEnum, SetTypeEnum } from '../../service';
|
|
4
|
+
import { transformToCompact, transformToSet } from './rally-event';
|
|
5
|
+
describe('rally-event compact round trip — set tempo marker', () => {
|
|
6
|
+
const playerId = uuidv4();
|
|
7
|
+
const roster = [playerId];
|
|
8
|
+
const playerIndex = new Map(roster.map((id, i) => [id, i]));
|
|
9
|
+
function makeSet(tempo) {
|
|
10
|
+
return Set.create({
|
|
11
|
+
playerId,
|
|
12
|
+
score: 90,
|
|
13
|
+
target: 4,
|
|
14
|
+
failure: SetFailureEnum.NO_FAILURE,
|
|
15
|
+
type: SetTypeEnum.OVERHAND,
|
|
16
|
+
...(tempo !== undefined ? { tempo } : {})
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
it('packs each tempo as m and round-trips it', () => {
|
|
20
|
+
for (const tempo of [SetTempoEnum.FIRST, SetTempoEnum.SECOND, SetTempoEnum.HIGH]) {
|
|
21
|
+
const compact = transformToCompact(makeSet(tempo), playerIndex);
|
|
22
|
+
expect(compact.m).toBe(tempo);
|
|
23
|
+
const decoded = transformToSet(compact, roster);
|
|
24
|
+
expect(decoded.tempo).toBe(tempo);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
it('omits m entirely for a set without a tempo', () => {
|
|
28
|
+
const compact = transformToCompact(makeSet(), playerIndex);
|
|
29
|
+
expect('m' in compact).toBe(false);
|
|
30
|
+
const decoded = transformToSet(compact, roster);
|
|
31
|
+
expect(decoded.tempo).toBeUndefined();
|
|
32
|
+
});
|
|
33
|
+
it('decodes a legacy compact event (no m key) with no tempo', () => {
|
|
34
|
+
const legacy = { p: 0, e: EventTypeEnum.SET, f: 0, t: 0, a: 4, s: 90 };
|
|
35
|
+
const decoded = transformToSet(legacy, roster);
|
|
36
|
+
expect(decoded.tempo).toBeUndefined();
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -12,6 +12,7 @@ export interface CompactEvent {
|
|
|
12
12
|
j?: number;
|
|
13
13
|
i?: number;
|
|
14
14
|
h?: number;
|
|
15
|
+
m?: number;
|
|
15
16
|
}
|
|
16
17
|
export declare function transformToCompact(evt: RallyEvent, playerIndex: Map<string, number>): CompactEvent;
|
|
17
18
|
export declare function transformToBlock(event: CompactEvent, roster: string[]): Block;
|
|
@@ -34,6 +34,9 @@ export function transformToCompact(evt, playerIndex) {
|
|
|
34
34
|
if (evt instanceof Block) {
|
|
35
35
|
out.b = evt.blockers.map(id => playerIndex.get(id));
|
|
36
36
|
}
|
|
37
|
+
if (evt instanceof Set && evt.tempo !== undefined) {
|
|
38
|
+
out.m = evt.tempo;
|
|
39
|
+
}
|
|
37
40
|
}
|
|
38
41
|
else if (evt instanceof LiberoReplacement) {
|
|
39
42
|
out.t = evt.type;
|
|
@@ -91,7 +94,8 @@ export function transformToSet(event, roster) {
|
|
|
91
94
|
playerId: roster[event.p],
|
|
92
95
|
target: event.a,
|
|
93
96
|
score: event.s,
|
|
94
|
-
incident: incidentFromCompact(event.i, event.h, roster)
|
|
97
|
+
incident: incidentFromCompact(event.i, event.h, roster),
|
|
98
|
+
tempo: event.m
|
|
95
99
|
});
|
|
96
100
|
}
|
|
97
101
|
export function transformToSpike(event, roster) {
|