volleyballsimtypes 0.0.519 → 0.0.521
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 +14 -1
- package/dist/cjs/src/data/init-models.js +14 -0
- package/dist/cjs/src/data/models/auth-identity.d.ts +1 -1
- package/dist/cjs/src/data/models/auth-identity.js +1 -1
- package/dist/cjs/src/data/models/index.d.ts +3 -0
- package/dist/cjs/src/data/models/index.js +3 -0
- package/dist/cjs/src/data/models/jersey-badge.d.ts +32 -0
- package/dist/cjs/src/data/models/jersey-badge.js +60 -0
- package/dist/cjs/src/data/models/premium-grant.d.ts +39 -0
- package/dist/cjs/src/data/models/premium-grant.js +82 -0
- package/dist/cjs/src/data/models/team-jersey.d.ts +20 -0
- package/dist/cjs/src/data/models/team-jersey.js +38 -0
- package/dist/cjs/src/data/models/team.d.ts +3 -1
- package/dist/cjs/src/data/transformers/team.js +4 -1
- package/dist/cjs/src/service/competition/event-schedule.d.ts +1 -1
- package/dist/cjs/src/service/competition/event-schedule.js +3 -1
- package/dist/cjs/src/service/competition/event-schedule.test.js +4 -2
- package/dist/cjs/src/service/index.d.ts +1 -0
- package/dist/cjs/src/service/index.js +1 -0
- package/dist/cjs/src/service/player/improvement.test.js +5 -5
- package/dist/cjs/src/service/player/stats.js +30 -16
- package/dist/cjs/src/service/player/stats.test.js +6 -6
- package/dist/cjs/src/service/premium/index.d.ts +3 -0
- package/dist/cjs/src/service/premium/index.js +19 -0
- package/dist/cjs/src/service/premium/patreon-tier-map.d.ts +5 -0
- package/dist/cjs/src/service/premium/patreon-tier-map.js +22 -0
- package/dist/cjs/src/service/premium/patreon-tier-map.test.d.ts +1 -0
- package/dist/cjs/src/service/premium/patreon-tier-map.test.js +48 -0
- package/dist/cjs/src/service/premium/premium-grant.d.ts +17 -0
- package/dist/cjs/src/service/premium/premium-grant.js +15 -0
- package/dist/cjs/src/service/premium/premium-tier.d.ts +10 -0
- package/dist/cjs/src/service/premium/premium-tier.js +24 -0
- package/dist/cjs/src/service/premium/premium.test.d.ts +1 -0
- package/dist/cjs/src/service/premium/premium.test.js +93 -0
- package/dist/cjs/src/service/team/index.d.ts +1 -0
- package/dist/cjs/src/service/team/index.js +1 -0
- package/dist/cjs/src/service/team/jersey.d.ts +74 -0
- package/dist/cjs/src/service/team/jersey.js +33 -0
- package/dist/cjs/src/service/team/schemas/team.z.d.ts +69 -0
- package/dist/cjs/src/service/team/schemas/team.z.js +3 -1
- package/dist/cjs/src/service/team/team.d.ts +2 -0
- package/dist/cjs/src/service/team/team.js +2 -1
- package/dist/cjs/src/stat-config/improvement-floors.json +204 -204
- package/dist/esm/src/api/index.d.ts +14 -1
- package/dist/esm/src/data/init-models.js +15 -1
- package/dist/esm/src/data/models/auth-identity.d.ts +1 -1
- package/dist/esm/src/data/models/auth-identity.js +1 -1
- package/dist/esm/src/data/models/index.d.ts +3 -0
- package/dist/esm/src/data/models/index.js +3 -0
- package/dist/esm/src/data/models/jersey-badge.d.ts +32 -0
- package/dist/esm/src/data/models/jersey-badge.js +56 -0
- package/dist/esm/src/data/models/premium-grant.d.ts +39 -0
- package/dist/esm/src/data/models/premium-grant.js +78 -0
- package/dist/esm/src/data/models/team-jersey.d.ts +20 -0
- package/dist/esm/src/data/models/team-jersey.js +34 -0
- package/dist/esm/src/data/models/team.d.ts +3 -1
- package/dist/esm/src/data/transformers/team.js +4 -1
- package/dist/esm/src/service/competition/event-schedule.d.ts +1 -1
- package/dist/esm/src/service/competition/event-schedule.js +3 -1
- package/dist/esm/src/service/competition/event-schedule.test.js +4 -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/player/improvement.test.js +5 -5
- package/dist/esm/src/service/player/stats.js +30 -16
- package/dist/esm/src/service/player/stats.test.js +6 -6
- package/dist/esm/src/service/premium/index.d.ts +3 -0
- package/dist/esm/src/service/premium/index.js +3 -0
- package/dist/esm/src/service/premium/patreon-tier-map.d.ts +5 -0
- package/dist/esm/src/service/premium/patreon-tier-map.js +18 -0
- package/dist/esm/src/service/premium/patreon-tier-map.test.d.ts +1 -0
- package/dist/esm/src/service/premium/patreon-tier-map.test.js +46 -0
- package/dist/esm/src/service/premium/premium-grant.d.ts +17 -0
- package/dist/esm/src/service/premium/premium-grant.js +11 -0
- package/dist/esm/src/service/premium/premium-tier.d.ts +10 -0
- package/dist/esm/src/service/premium/premium-tier.js +19 -0
- package/dist/esm/src/service/premium/premium.test.d.ts +1 -0
- package/dist/esm/src/service/premium/premium.test.js +91 -0
- package/dist/esm/src/service/team/index.d.ts +1 -0
- package/dist/esm/src/service/team/index.js +1 -0
- package/dist/esm/src/service/team/jersey.d.ts +74 -0
- package/dist/esm/src/service/team/jersey.js +30 -0
- package/dist/esm/src/service/team/schemas/team.z.d.ts +69 -0
- package/dist/esm/src/service/team/schemas/team.z.js +3 -1
- package/dist/esm/src/service/team/team.d.ts +2 -0
- package/dist/esm/src/service/team/team.js +2 -1
- package/dist/esm/src/stat-config/improvement-floors.json +204 -204
- package/package.json +1 -1
|
@@ -64,10 +64,10 @@ function makeStats(base = 50, overrides = {}) {
|
|
|
64
64
|
const total = Object.values(m).reduce((s, v) => s + (v ?? 0), 0);
|
|
65
65
|
(0, globals_1.expect)(total).toBeCloseTo(1, 10);
|
|
66
66
|
});
|
|
67
|
-
(0, globals_1.it)('
|
|
67
|
+
(0, globals_1.it)('reflex has the highest weight (reflex gates the reach + lunge-save)', () => {
|
|
68
68
|
const m = (0, stats_1.getMultipliers)(stats_1.StatsEnum.RECEIVE);
|
|
69
69
|
const max = Math.max(...Object.values(m).map(v => v ?? 0));
|
|
70
|
-
(0, globals_1.expect)(m.
|
|
70
|
+
(0, globals_1.expect)(m.reflex).toBe(max);
|
|
71
71
|
});
|
|
72
72
|
});
|
|
73
73
|
(0, globals_1.describe)('SERVE', () => {
|
|
@@ -147,11 +147,11 @@ function makeStats(base = 50, overrides = {}) {
|
|
|
147
147
|
const strong = (0, stats_1.calculateStatScore)(makeStats(80), stats_1.StatsEnum.SERVE);
|
|
148
148
|
(0, globals_1.expect)(strong).toBeGreaterThan(weak);
|
|
149
149
|
});
|
|
150
|
-
(0, globals_1.it)('SERVE: serve=99 dominates, contributing 0.
|
|
151
|
-
// All other serve-relevant stats at 1: awareness=1, attack=1
|
|
152
|
-
// score ≈ 99*0.
|
|
150
|
+
(0, globals_1.it)('SERVE: serve=99 dominates, contributing 0.75 of the total', () => {
|
|
151
|
+
// All other serve-relevant stats at 1: awareness=1, focus=1, attack=1
|
|
152
|
+
// score ≈ 99*0.75 + 1*0.1 + 1*0.1 + 1*0.05 = 74.25 + 0.25 = 74.5 → 74
|
|
153
153
|
const stats = makeStats(1, { serve: 99 });
|
|
154
|
-
(0, globals_1.expect)((0, stats_1.calculateStatScore)(stats, stats_1.StatsEnum.SERVE)).toBe(
|
|
154
|
+
(0, globals_1.expect)((0, stats_1.calculateStatScore)(stats, stats_1.StatsEnum.SERVE)).toBe(74);
|
|
155
155
|
});
|
|
156
156
|
});
|
|
157
157
|
// ─── invariants (every stat, every branch) ────────────────────────────────────
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./premium-tier"), exports);
|
|
18
|
+
__exportStar(require("./premium-grant"), exports);
|
|
19
|
+
__exportStar(require("./patreon-tier-map"), exports);
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const PATREON_TIER_MAP_KEY = "patreon_tier_map";
|
|
3
|
+
export type PatreonTierMap = Record<string, number>;
|
|
4
|
+
export declare const patreonTierMapSchema: z.ZodType<PatreonTierMap>;
|
|
5
|
+
export declare function resolvePremiumTier(entitledTierIds: string[], map: PatreonTierMap): number;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.patreonTierMapSchema = exports.PATREON_TIER_MAP_KEY = void 0;
|
|
4
|
+
exports.resolvePremiumTier = resolvePremiumTier;
|
|
5
|
+
const zod_1 = require("zod");
|
|
6
|
+
const premium_tier_1 = require("./premium-tier");
|
|
7
|
+
// Admin-configured mapping from a Patreon TIER id (string, opaque, per-campaign) to one of our numeric premium tiers.
|
|
8
|
+
// The whole map is stored as a single AppConfig JSONB row under PATREON_TIER_MAP_KEY (the DB read/write lives in the
|
|
9
|
+
// API; this module is pure so it can be shared and unit-tested). The Patreon webhook resolves a member's currently
|
|
10
|
+
// entitled tier ids through this map to decide the premium tier to grant. See plans/premium-users.md section 5.
|
|
11
|
+
exports.PATREON_TIER_MAP_KEY = 'patreon_tier_map';
|
|
12
|
+
// A valid map assigns each Patreon tier id to an existing premium tier (1..MAX_PREMIUM_TIER). An empty map is valid
|
|
13
|
+
// (nothing mapped yet). Unknown/extra Patreon tier ids are simply absent and resolve to NO_PREMIUM.
|
|
14
|
+
exports.patreonTierMapSchema = zod_1.z.record(zod_1.z.string(), zod_1.z.number().int().min(1).max(premium_tier_1.MAX_PREMIUM_TIER));
|
|
15
|
+
// The highest premium tier the given currently-entitled Patreon tier ids map to, else NO_PREMIUM (0). Ids not present
|
|
16
|
+
// in the map (unmapped or deleted tiers) contribute nothing. This is the single source of truth for "Patreon -> tier".
|
|
17
|
+
function resolvePremiumTier(entitledTierIds, map) {
|
|
18
|
+
return entitledTierIds.reduce((max, id) => {
|
|
19
|
+
const tier = map[id];
|
|
20
|
+
return tier != null && tier > max ? tier : max;
|
|
21
|
+
}, premium_tier_1.NO_PREMIUM);
|
|
22
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const globals_1 = require("@jest/globals");
|
|
4
|
+
const premium_tier_1 = require("./premium-tier");
|
|
5
|
+
const patreon_tier_map_1 = require("./patreon-tier-map");
|
|
6
|
+
// A sample admin mapping: two Patreon tier ids -> premium tiers 1 and 2.
|
|
7
|
+
const MAP = { tier_cosmetic: 1, tier_national: 2 };
|
|
8
|
+
// ─── resolvePremiumTier ──────────────────────────────────────────────────────────
|
|
9
|
+
(0, globals_1.describe)('resolvePremiumTier()', () => {
|
|
10
|
+
(0, globals_1.it)('is NO_PREMIUM (0) when the entitled list is empty', () => {
|
|
11
|
+
(0, globals_1.expect)((0, patreon_tier_map_1.resolvePremiumTier)([], MAP)).toBe(premium_tier_1.NO_PREMIUM);
|
|
12
|
+
(0, globals_1.expect)((0, patreon_tier_map_1.resolvePremiumTier)([], MAP)).toBe(0);
|
|
13
|
+
});
|
|
14
|
+
(0, globals_1.it)('resolves a single mapped tier id to its premium tier', () => {
|
|
15
|
+
(0, globals_1.expect)((0, patreon_tier_map_1.resolvePremiumTier)(['tier_cosmetic'], MAP)).toBe(1);
|
|
16
|
+
(0, globals_1.expect)((0, patreon_tier_map_1.resolvePremiumTier)(['tier_national'], MAP)).toBe(2);
|
|
17
|
+
});
|
|
18
|
+
(0, globals_1.it)('returns the highest premium tier when several entitled ids map', () => {
|
|
19
|
+
(0, globals_1.expect)((0, patreon_tier_map_1.resolvePremiumTier)(['tier_cosmetic', 'tier_national'], MAP)).toBe(2);
|
|
20
|
+
});
|
|
21
|
+
(0, globals_1.it)('ignores unmapped ids (unknown or deleted Patreon tiers) and falls back to a mapped one', () => {
|
|
22
|
+
(0, globals_1.expect)((0, patreon_tier_map_1.resolvePremiumTier)(['tier_deleted', 'tier_cosmetic'], MAP)).toBe(1);
|
|
23
|
+
});
|
|
24
|
+
(0, globals_1.it)('is NO_PREMIUM when NO entitled id is in the map', () => {
|
|
25
|
+
(0, globals_1.expect)((0, patreon_tier_map_1.resolvePremiumTier)(['tier_deleted', 'tier_unknown'], MAP)).toBe(premium_tier_1.NO_PREMIUM);
|
|
26
|
+
});
|
|
27
|
+
(0, globals_1.it)('is NO_PREMIUM against an empty map', () => {
|
|
28
|
+
(0, globals_1.expect)((0, patreon_tier_map_1.resolvePremiumTier)(['tier_cosmetic'], {})).toBe(premium_tier_1.NO_PREMIUM);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
// ─── patreonTierMapSchema ──────────────────────────────────────────────────────────
|
|
32
|
+
(0, globals_1.describe)('patreonTierMapSchema', () => {
|
|
33
|
+
(0, globals_1.it)('accepts an empty map', () => {
|
|
34
|
+
(0, globals_1.expect)(patreon_tier_map_1.patreonTierMapSchema.parse({})).toEqual({});
|
|
35
|
+
});
|
|
36
|
+
(0, globals_1.it)('accepts a map of valid premium tiers', () => {
|
|
37
|
+
(0, globals_1.expect)(patreon_tier_map_1.patreonTierMapSchema.parse(MAP)).toEqual(MAP);
|
|
38
|
+
});
|
|
39
|
+
(0, globals_1.it)('rejects a tier value below 1', () => {
|
|
40
|
+
(0, globals_1.expect)(() => patreon_tier_map_1.patreonTierMapSchema.parse({ t: 0 })).toThrow();
|
|
41
|
+
});
|
|
42
|
+
(0, globals_1.it)('rejects a tier value above MAX_PREMIUM_TIER', () => {
|
|
43
|
+
(0, globals_1.expect)(() => patreon_tier_map_1.patreonTierMapSchema.parse({ t: premium_tier_1.MAX_PREMIUM_TIER + 1 })).toThrow();
|
|
44
|
+
});
|
|
45
|
+
(0, globals_1.it)('rejects a non-integer tier value', () => {
|
|
46
|
+
(0, globals_1.expect)(() => patreon_tier_map_1.patreonTierMapSchema.parse({ t: 1.5 })).toThrow();
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type PremiumGrantSource = 'ADMIN' | 'PATREON';
|
|
2
|
+
export interface PremiumGrant {
|
|
3
|
+
grantId: string;
|
|
4
|
+
userId: string;
|
|
5
|
+
tier: number;
|
|
6
|
+
source: PremiumGrantSource;
|
|
7
|
+
grantedBy?: string | null;
|
|
8
|
+
grantedAt: Date;
|
|
9
|
+
expiresAt?: Date | null;
|
|
10
|
+
revokedAt?: Date | null;
|
|
11
|
+
patreonMemberId?: string | null;
|
|
12
|
+
note?: string | null;
|
|
13
|
+
}
|
|
14
|
+
type ActiveFields = Pick<PremiumGrant, 'revokedAt' | 'expiresAt'>;
|
|
15
|
+
export declare function isGrantActive(g: ActiveFields, now?: Date): boolean;
|
|
16
|
+
export declare function effectiveTier(grants: Array<Pick<PremiumGrant, 'tier' | 'revokedAt' | 'expiresAt'>>, now?: Date): number;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isGrantActive = isGrantActive;
|
|
4
|
+
exports.effectiveTier = effectiveTier;
|
|
5
|
+
const premium_tier_1 = require("./premium-tier");
|
|
6
|
+
// A grant is ACTIVE when it is not revoked and not past its expiry (a null expiry = permanent).
|
|
7
|
+
function isGrantActive(g, now = new Date()) {
|
|
8
|
+
if (g.revokedAt != null)
|
|
9
|
+
return false;
|
|
10
|
+
return g.expiresAt == null || g.expiresAt.getTime() > now.getTime();
|
|
11
|
+
}
|
|
12
|
+
// The user's EFFECTIVE tier: the highest tier among their ACTIVE grants, or NO_PREMIUM (0) when none is active.
|
|
13
|
+
function effectiveTier(grants, now = new Date()) {
|
|
14
|
+
return grants.reduce((max, g) => (isGrantActive(g, now) && g.tier > max ? g.tier : max), premium_tier_1.NO_PREMIUM);
|
|
15
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const NO_PREMIUM = 0;
|
|
2
|
+
export interface PremiumTierDef {
|
|
3
|
+
tier: number;
|
|
4
|
+
key: string;
|
|
5
|
+
name: string;
|
|
6
|
+
}
|
|
7
|
+
export declare const PREMIUM_TIERS: PremiumTierDef[];
|
|
8
|
+
export declare const MAX_PREMIUM_TIER: number;
|
|
9
|
+
export declare function premiumTierDef(tier: number): PremiumTierDef | undefined;
|
|
10
|
+
export declare function hasTier(effectiveTier: number, required: number): boolean;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MAX_PREMIUM_TIER = exports.PREMIUM_TIERS = exports.NO_PREMIUM = void 0;
|
|
4
|
+
exports.premiumTierDef = premiumTierDef;
|
|
5
|
+
exports.hasTier = hasTier;
|
|
6
|
+
// Hierarchical premium tiers (owner 2026-08-17): tier N grants everything below it, so a feature gate is just
|
|
7
|
+
// `effectiveTier >= required`. Tiers are NUMERIC and code-defined here (the DB stores only the number), so adding a
|
|
8
|
+
// tier needs no migration. Names/keys are display + gating helpers. Keep PREMIUM_TIERS ascending by tier. See
|
|
9
|
+
// plans/premium-users.md.
|
|
10
|
+
exports.NO_PREMIUM = 0;
|
|
11
|
+
// The known tiers. Extend this list to add a tier (no schema change). tier -> what it unlocks is enforced by the
|
|
12
|
+
// feature gates, not stored; these names are for the admin view / display only.
|
|
13
|
+
exports.PREMIUM_TIERS = [
|
|
14
|
+
{ tier: 1, key: 'COSMETIC', name: 'Cosmetic Customization' },
|
|
15
|
+
{ tier: 2, key: 'NATIONAL', name: 'World Cup + National Teams' }
|
|
16
|
+
];
|
|
17
|
+
exports.MAX_PREMIUM_TIER = exports.PREMIUM_TIERS.reduce((m, t) => Math.max(m, t.tier), exports.NO_PREMIUM);
|
|
18
|
+
function premiumTierDef(tier) {
|
|
19
|
+
return exports.PREMIUM_TIERS.find((t) => t.tier === tier);
|
|
20
|
+
}
|
|
21
|
+
// A user with `effectiveTier` meets a feature's `required` tier when it is at least as high (hierarchical).
|
|
22
|
+
function hasTier(effectiveTier, required) {
|
|
23
|
+
return effectiveTier >= required;
|
|
24
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const globals_1 = require("@jest/globals");
|
|
4
|
+
const premium_tier_1 = require("./premium-tier");
|
|
5
|
+
const premium_grant_1 = require("./premium-grant");
|
|
6
|
+
// ─── fixed clock ───────────────────────────────────────────────────────────────
|
|
7
|
+
// All time-dependent assertions pass an explicit `now` so they never depend on the wall clock.
|
|
8
|
+
const NOW = new Date('2026-08-17T12:00:00.000Z');
|
|
9
|
+
const PAST = new Date('2026-08-01T00:00:00.000Z');
|
|
10
|
+
const FUTURE = new Date('2026-09-01T00:00:00.000Z');
|
|
11
|
+
function grant(tier, opts = {}) {
|
|
12
|
+
return { tier, expiresAt: opts.expiresAt ?? null, revokedAt: opts.revokedAt ?? null };
|
|
13
|
+
}
|
|
14
|
+
// ─── isGrantActive ───────────────────────────────────────────────────────────────
|
|
15
|
+
(0, globals_1.describe)('isGrantActive()', () => {
|
|
16
|
+
(0, globals_1.it)('is active when not revoked and has no expiry (permanent comp)', () => {
|
|
17
|
+
(0, globals_1.expect)((0, premium_grant_1.isGrantActive)(grant(1), NOW)).toBe(true);
|
|
18
|
+
});
|
|
19
|
+
(0, globals_1.it)('is active when the expiry is in the future', () => {
|
|
20
|
+
(0, globals_1.expect)((0, premium_grant_1.isGrantActive)(grant(1, { expiresAt: FUTURE }), NOW)).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
(0, globals_1.it)('is inactive when the expiry is in the past', () => {
|
|
23
|
+
(0, globals_1.expect)((0, premium_grant_1.isGrantActive)(grant(1, { expiresAt: PAST }), NOW)).toBe(false);
|
|
24
|
+
});
|
|
25
|
+
(0, globals_1.it)('is inactive at the exact expiry instant (expiry is exclusive)', () => {
|
|
26
|
+
(0, globals_1.expect)((0, premium_grant_1.isGrantActive)(grant(1, { expiresAt: NOW }), NOW)).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
(0, globals_1.it)('is inactive when revoked, even with a future expiry', () => {
|
|
29
|
+
(0, globals_1.expect)((0, premium_grant_1.isGrantActive)(grant(1, { expiresAt: FUTURE, revokedAt: PAST }), NOW)).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
// ─── effectiveTier ───────────────────────────────────────────────────────────────
|
|
33
|
+
(0, globals_1.describe)('effectiveTier()', () => {
|
|
34
|
+
(0, globals_1.it)('is NO_PREMIUM (0) for an empty grant list', () => {
|
|
35
|
+
(0, globals_1.expect)((0, premium_grant_1.effectiveTier)([], NOW)).toBe(premium_tier_1.NO_PREMIUM);
|
|
36
|
+
(0, globals_1.expect)((0, premium_grant_1.effectiveTier)([], NOW)).toBe(0);
|
|
37
|
+
});
|
|
38
|
+
(0, globals_1.it)('returns the tier of a single active grant', () => {
|
|
39
|
+
(0, globals_1.expect)((0, premium_grant_1.effectiveTier)([grant(2)], NOW)).toBe(2);
|
|
40
|
+
});
|
|
41
|
+
(0, globals_1.it)('returns the highest tier among several active grants', () => {
|
|
42
|
+
(0, globals_1.expect)((0, premium_grant_1.effectiveTier)([grant(1), grant(2), grant(1, { expiresAt: FUTURE })], NOW)).toBe(2);
|
|
43
|
+
});
|
|
44
|
+
(0, globals_1.it)('keeps a permanent lower-tier floor beneath an expired higher-tier grant', () => {
|
|
45
|
+
// Tier 2 expired, tier 1 permanent still active -> effective 1.
|
|
46
|
+
(0, globals_1.expect)((0, premium_grant_1.effectiveTier)([grant(2, { expiresAt: PAST }), grant(1)], NOW)).toBe(1);
|
|
47
|
+
});
|
|
48
|
+
(0, globals_1.it)('ignores a revoked higher-tier grant and falls back to a lower active one', () => {
|
|
49
|
+
(0, globals_1.expect)((0, premium_grant_1.effectiveTier)([grant(2, { revokedAt: PAST }), grant(1)], NOW)).toBe(1);
|
|
50
|
+
});
|
|
51
|
+
(0, globals_1.it)('is NO_PREMIUM when every grant is expired or revoked', () => {
|
|
52
|
+
(0, globals_1.expect)((0, premium_grant_1.effectiveTier)([grant(2, { expiresAt: PAST }), grant(1, { revokedAt: PAST })], NOW)).toBe(premium_tier_1.NO_PREMIUM);
|
|
53
|
+
});
|
|
54
|
+
(0, globals_1.it)('order of grants does not change the result', () => {
|
|
55
|
+
const grants = [grant(1), grant(2, { expiresAt: FUTURE }), grant(1, { revokedAt: PAST })];
|
|
56
|
+
const reversed = [...grants].reverse();
|
|
57
|
+
(0, globals_1.expect)((0, premium_grant_1.effectiveTier)(grants, NOW)).toBe((0, premium_grant_1.effectiveTier)(reversed, NOW));
|
|
58
|
+
(0, globals_1.expect)((0, premium_grant_1.effectiveTier)(reversed, NOW)).toBe(2);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
// ─── hasTier ─────────────────────────────────────────────────────────────────────
|
|
62
|
+
(0, globals_1.describe)('hasTier()', () => {
|
|
63
|
+
(0, globals_1.it)('is true when the effective tier exceeds the requirement (hierarchical)', () => {
|
|
64
|
+
(0, globals_1.expect)((0, premium_tier_1.hasTier)(2, 1)).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
(0, globals_1.it)('is true when the effective tier equals the requirement', () => {
|
|
67
|
+
(0, globals_1.expect)((0, premium_tier_1.hasTier)(2, 2)).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
(0, globals_1.it)('is false when the effective tier is below the requirement', () => {
|
|
70
|
+
(0, globals_1.expect)((0, premium_tier_1.hasTier)(1, 2)).toBe(false);
|
|
71
|
+
});
|
|
72
|
+
(0, globals_1.it)('a non-premium user (0) meets no positive requirement', () => {
|
|
73
|
+
(0, globals_1.expect)((0, premium_tier_1.hasTier)(premium_tier_1.NO_PREMIUM, 1)).toBe(false);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
// ─── tier catalog ────────────────────────────────────────────────────────────────
|
|
77
|
+
(0, globals_1.describe)('PREMIUM_TIERS catalog', () => {
|
|
78
|
+
(0, globals_1.it)('MAX_PREMIUM_TIER is the highest configured tier', () => {
|
|
79
|
+
(0, globals_1.expect)(premium_tier_1.MAX_PREMIUM_TIER).toBe(2);
|
|
80
|
+
});
|
|
81
|
+
(0, globals_1.it)('is ascending by tier', () => {
|
|
82
|
+
const tiers = premium_tier_1.PREMIUM_TIERS.map((t) => t.tier);
|
|
83
|
+
(0, globals_1.expect)(tiers).toEqual([...tiers].sort((a, b) => a - b));
|
|
84
|
+
});
|
|
85
|
+
(0, globals_1.it)('premiumTierDef resolves a known tier to its definition', () => {
|
|
86
|
+
(0, globals_1.expect)((0, premium_tier_1.premiumTierDef)(1)?.key).toBe('COSMETIC');
|
|
87
|
+
(0, globals_1.expect)((0, premium_tier_1.premiumTierDef)(2)?.key).toBe('NATIONAL');
|
|
88
|
+
});
|
|
89
|
+
(0, globals_1.it)('premiumTierDef is undefined for an unknown tier', () => {
|
|
90
|
+
(0, globals_1.expect)((0, premium_tier_1.premiumTierDef)(0)).toBeUndefined();
|
|
91
|
+
(0, globals_1.expect)((0, premium_tier_1.premiumTierDef)(premium_tier_1.MAX_PREMIUM_TIER + 1)).toBeUndefined();
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -15,6 +15,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
__exportStar(require("./team"), exports);
|
|
18
|
+
__exportStar(require("./jersey"), exports);
|
|
18
19
|
__exportStar(require("./team-generator"), exports);
|
|
19
20
|
__exportStar(require("./formation"), exports);
|
|
20
21
|
__exportStar(require("./tactics"), exports);
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const JerseySchema: z.ZodObject<{
|
|
3
|
+
clothing: z.ZodEnum<{
|
|
4
|
+
shirtCrewNeck: "shirtCrewNeck";
|
|
5
|
+
shirtCrewNeckSleeveless: "shirtCrewNeckSleeveless";
|
|
6
|
+
shirtVNeck: "shirtVNeck";
|
|
7
|
+
shirtVNeckSleeveless: "shirtVNeckSleeveless";
|
|
8
|
+
collarAndSweater: "collarAndSweater";
|
|
9
|
+
collarAndSweaterSleeveless: "collarAndSweaterSleeveless";
|
|
10
|
+
}>;
|
|
11
|
+
pattern: z.ZodEnum<{
|
|
12
|
+
solid: "solid";
|
|
13
|
+
verticalStripes: "verticalStripes";
|
|
14
|
+
verticalBars: "verticalBars";
|
|
15
|
+
pinstripes: "pinstripes";
|
|
16
|
+
hoops: "hoops";
|
|
17
|
+
checkered: "checkered";
|
|
18
|
+
diagonalSash: "diagonalSash";
|
|
19
|
+
splitHalves: "splitHalves";
|
|
20
|
+
chevron: "chevron";
|
|
21
|
+
shoulderYoke: "shoulderYoke";
|
|
22
|
+
sidePanels: "sidePanels";
|
|
23
|
+
centerBand: "centerBand";
|
|
24
|
+
gradientFade: "gradientFade";
|
|
25
|
+
twoToneSplit: "twoToneSplit";
|
|
26
|
+
}>;
|
|
27
|
+
colors: z.ZodArray<z.ZodString>;
|
|
28
|
+
font: z.ZodEnum<{
|
|
29
|
+
block: "block";
|
|
30
|
+
sans: "sans";
|
|
31
|
+
wide: "wide";
|
|
32
|
+
mono: "mono";
|
|
33
|
+
serif: "serif";
|
|
34
|
+
permanentMarker: "permanentMarker";
|
|
35
|
+
kaushanScript: "kaushanScript";
|
|
36
|
+
russoOne: "russoOne";
|
|
37
|
+
teko: "teko";
|
|
38
|
+
saira: "saira";
|
|
39
|
+
michroma: "michroma";
|
|
40
|
+
zillaSlab: "zillaSlab";
|
|
41
|
+
changaOne: "changaOne";
|
|
42
|
+
stardosStencil: "stardosStencil";
|
|
43
|
+
wallpoet: "wallpoet";
|
|
44
|
+
orbitron: "orbitron";
|
|
45
|
+
tourney: "tourney";
|
|
46
|
+
bebasNeue: "bebasNeue";
|
|
47
|
+
anton: "anton";
|
|
48
|
+
jersey25: "jersey25";
|
|
49
|
+
}>;
|
|
50
|
+
numberColor: z.ZodString;
|
|
51
|
+
numberOutline: z.ZodString;
|
|
52
|
+
numberOutlineEnabled: z.ZodDefault<z.ZodBoolean>;
|
|
53
|
+
numberSpacing: z.ZodDefault<z.ZodNumber>;
|
|
54
|
+
numberSize: z.ZodDefault<z.ZodNumber>;
|
|
55
|
+
brand: z.ZodDefault<z.ZodEnum<{
|
|
56
|
+
none: "none";
|
|
57
|
+
sigma: "sigma";
|
|
58
|
+
"person-soccer": "person-soccer";
|
|
59
|
+
"feather-pointed": "feather-pointed";
|
|
60
|
+
"paw-claws": "paw-claws";
|
|
61
|
+
narwhal: "narwhal";
|
|
62
|
+
horseshoe: "horseshoe";
|
|
63
|
+
"spider-black-widow": "spider-black-widow";
|
|
64
|
+
"wreath-laurel": "wreath-laurel";
|
|
65
|
+
omega: "omega";
|
|
66
|
+
trillium: "trillium";
|
|
67
|
+
}>>;
|
|
68
|
+
brandColor1: z.ZodDefault<z.ZodString>;
|
|
69
|
+
brandColor2: z.ZodDefault<z.ZodString>;
|
|
70
|
+
}, z.core.$strip>;
|
|
71
|
+
export type JerseyConfig = z.infer<typeof JerseySchema>;
|
|
72
|
+
export type JerseyClothing = JerseyConfig['clothing'];
|
|
73
|
+
export type JerseyPattern = JerseyConfig['pattern'];
|
|
74
|
+
export type JerseyFont = JerseyConfig['font'];
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.JerseySchema = void 0;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
// 6-digit hex (#rrggbb) or 8-digit hex with alpha (#rrggbbaa). The 8-digit form comes from the UI's opacity-enabled
|
|
6
|
+
// colour pickers; SVG fills render both natively.
|
|
7
|
+
const hexColor = zod_1.z.string().regex(/^#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?$/, 'INVALID_HEX_COLOR');
|
|
8
|
+
// The full description of a team's kit: shirt shape, pattern, up to 3 colours, and the chest-number styling.
|
|
9
|
+
// Produced by the Jersey Editor (premium users) and by the id-derived default; stored on the Team and rendered by
|
|
10
|
+
// the UI Picture. The SVG render helpers + option tables live in the UI (view/jersey/jersey-render); only this
|
|
11
|
+
// shape + its validation are shared here so the DB column, the API DTO, and the UI agree on one definition.
|
|
12
|
+
exports.JerseySchema = zod_1.z.object({
|
|
13
|
+
clothing: zod_1.z.enum(['shirtCrewNeck', 'shirtCrewNeckSleeveless', 'shirtVNeck', 'shirtVNeckSleeveless', 'collarAndSweater', 'collarAndSweaterSleeveless']),
|
|
14
|
+
pattern: zod_1.z.enum(['solid', 'verticalStripes', 'verticalBars', 'pinstripes', 'hoops', 'checkered', 'diagonalSash', 'splitHalves', 'chevron', 'shoulderYoke', 'sidePanels', 'centerBand', 'gradientFade', 'twoToneSplit']),
|
|
15
|
+
colors: zod_1.z.array(hexColor).min(1).max(3),
|
|
16
|
+
font: zod_1.z.enum(['sans', 'wide', 'mono', 'serif', 'block', 'permanentMarker', 'kaushanScript', 'russoOne', 'teko', 'saira', 'michroma', 'zillaSlab', 'changaOne', 'stardosStencil', 'wallpoet', 'orbitron', 'tourney', 'bebasNeue', 'anton', 'jersey25']),
|
|
17
|
+
numberColor: hexColor,
|
|
18
|
+
numberOutline: hexColor,
|
|
19
|
+
// Whether the number's thin contrasting outline is drawn. Off = no stroke. Optional/defaulted (true = keep the
|
|
20
|
+
// previous always-on behaviour) so configs saved before it stay valid.
|
|
21
|
+
numberOutlineEnabled: zod_1.z.boolean().default(true),
|
|
22
|
+
// Extra spacing (in the size-46 chest-number's own units) between the two digits, +/- to taste per font. A
|
|
23
|
+
// single-digit number ignores it and stays centred. Optional/defaulted so configs saved before it stay valid.
|
|
24
|
+
numberSpacing: zod_1.z.number().min(-40).max(80).default(0),
|
|
25
|
+
// Chest number height in avatar-svg units (the number sits on a ~412-tall canvas). The editor slider caps it to a
|
|
26
|
+
// readable range; defaulted (46 = the previous fixed size) so configs saved before it stay valid.
|
|
27
|
+
numberSize: zod_1.z.number().min(20).max(80).default(46),
|
|
28
|
+
// Chest brand (premium): a duotone icon on the left chest (or 'none') plus its two colours. Optional/defaulted so
|
|
29
|
+
// configs saved before it stay valid.
|
|
30
|
+
brand: zod_1.z.enum(['none', 'sigma', 'person-soccer', 'feather-pointed', 'paw-claws', 'narwhal', 'horseshoe', 'spider-black-widow', 'wreath-laurel', 'omega', 'trillium']).default('none'),
|
|
31
|
+
brandColor1: hexColor.default('#ffffff'),
|
|
32
|
+
brandColor2: hexColor.default('#1e293b')
|
|
33
|
+
});
|
|
@@ -345,5 +345,74 @@ export declare const TeamInputSchema: z.ZodObject<{
|
|
|
345
345
|
inMatchDesignate: z.ZodOptional<z.ZodCustom<Player, Player>>;
|
|
346
346
|
}, z.core.$strip>>;
|
|
347
347
|
}, z.core.$strip>>;
|
|
348
|
+
jerseyConfig: z.ZodOptional<z.ZodObject<{
|
|
349
|
+
clothing: z.ZodEnum<{
|
|
350
|
+
shirtCrewNeck: "shirtCrewNeck";
|
|
351
|
+
shirtCrewNeckSleeveless: "shirtCrewNeckSleeveless";
|
|
352
|
+
shirtVNeck: "shirtVNeck";
|
|
353
|
+
shirtVNeckSleeveless: "shirtVNeckSleeveless";
|
|
354
|
+
collarAndSweater: "collarAndSweater";
|
|
355
|
+
collarAndSweaterSleeveless: "collarAndSweaterSleeveless";
|
|
356
|
+
}>;
|
|
357
|
+
pattern: z.ZodEnum<{
|
|
358
|
+
solid: "solid";
|
|
359
|
+
verticalStripes: "verticalStripes";
|
|
360
|
+
verticalBars: "verticalBars";
|
|
361
|
+
pinstripes: "pinstripes";
|
|
362
|
+
hoops: "hoops";
|
|
363
|
+
checkered: "checkered";
|
|
364
|
+
diagonalSash: "diagonalSash";
|
|
365
|
+
splitHalves: "splitHalves";
|
|
366
|
+
chevron: "chevron";
|
|
367
|
+
shoulderYoke: "shoulderYoke";
|
|
368
|
+
sidePanels: "sidePanels";
|
|
369
|
+
centerBand: "centerBand";
|
|
370
|
+
gradientFade: "gradientFade";
|
|
371
|
+
twoToneSplit: "twoToneSplit";
|
|
372
|
+
}>;
|
|
373
|
+
colors: z.ZodArray<z.ZodString>;
|
|
374
|
+
font: z.ZodEnum<{
|
|
375
|
+
block: "block";
|
|
376
|
+
sans: "sans";
|
|
377
|
+
wide: "wide";
|
|
378
|
+
mono: "mono";
|
|
379
|
+
serif: "serif";
|
|
380
|
+
permanentMarker: "permanentMarker";
|
|
381
|
+
kaushanScript: "kaushanScript";
|
|
382
|
+
russoOne: "russoOne";
|
|
383
|
+
teko: "teko";
|
|
384
|
+
saira: "saira";
|
|
385
|
+
michroma: "michroma";
|
|
386
|
+
zillaSlab: "zillaSlab";
|
|
387
|
+
changaOne: "changaOne";
|
|
388
|
+
stardosStencil: "stardosStencil";
|
|
389
|
+
wallpoet: "wallpoet";
|
|
390
|
+
orbitron: "orbitron";
|
|
391
|
+
tourney: "tourney";
|
|
392
|
+
bebasNeue: "bebasNeue";
|
|
393
|
+
anton: "anton";
|
|
394
|
+
jersey25: "jersey25";
|
|
395
|
+
}>;
|
|
396
|
+
numberColor: z.ZodString;
|
|
397
|
+
numberOutline: z.ZodString;
|
|
398
|
+
numberOutlineEnabled: z.ZodDefault<z.ZodBoolean>;
|
|
399
|
+
numberSpacing: z.ZodDefault<z.ZodNumber>;
|
|
400
|
+
numberSize: z.ZodDefault<z.ZodNumber>;
|
|
401
|
+
brand: z.ZodDefault<z.ZodEnum<{
|
|
402
|
+
none: "none";
|
|
403
|
+
sigma: "sigma";
|
|
404
|
+
"person-soccer": "person-soccer";
|
|
405
|
+
"feather-pointed": "feather-pointed";
|
|
406
|
+
"paw-claws": "paw-claws";
|
|
407
|
+
narwhal: "narwhal";
|
|
408
|
+
horseshoe: "horseshoe";
|
|
409
|
+
"spider-black-widow": "spider-black-widow";
|
|
410
|
+
"wreath-laurel": "wreath-laurel";
|
|
411
|
+
omega: "omega";
|
|
412
|
+
trillium: "trillium";
|
|
413
|
+
}>>;
|
|
414
|
+
brandColor1: z.ZodDefault<z.ZodString>;
|
|
415
|
+
brandColor2: z.ZodDefault<z.ZodString>;
|
|
416
|
+
}, z.core.$strip>>;
|
|
348
417
|
}, z.core.$strip>;
|
|
349
418
|
export type TeamInput = z.infer<typeof TeamInputSchema>;
|
|
@@ -5,6 +5,7 @@ const zod_1 = require("zod");
|
|
|
5
5
|
const player_1 = require("../../player");
|
|
6
6
|
const country_1 = require("../../country");
|
|
7
7
|
const tactics_z_1 = require("./tactics.z");
|
|
8
|
+
const jersey_1 = require("../jersey");
|
|
8
9
|
const playerInstanceSchema = zod_1.z.custom((v) => v instanceof player_1.Player, {
|
|
9
10
|
message: 'INVALID_PLAYER_INSTANCE'
|
|
10
11
|
});
|
|
@@ -20,7 +21,8 @@ exports.TeamInputSchema = zod_1.z.object({
|
|
|
20
21
|
roster: zod_1.z.array(playerInstanceSchema),
|
|
21
22
|
country: countrySchema,
|
|
22
23
|
legacyFlagCountry: countrySchema.optional(),
|
|
23
|
-
tactics: tactics_z_1.TacticsInputSchema.optional()
|
|
24
|
+
tactics: tactics_z_1.TacticsInputSchema.optional(),
|
|
25
|
+
jerseyConfig: jersey_1.JerseySchema.optional()
|
|
24
26
|
}).superRefine((data, ctx) => {
|
|
25
27
|
const ids = data.roster.map(p => p.id);
|
|
26
28
|
if (new Set(ids).size !== ids.length) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Player } from '../player';
|
|
2
2
|
import { Country } from '../country';
|
|
3
3
|
import { Tactics } from './tactics';
|
|
4
|
+
import type { JerseyConfig } from './jersey';
|
|
4
5
|
export declare class Team {
|
|
5
6
|
readonly id: string;
|
|
6
7
|
readonly roster: Player[];
|
|
@@ -10,6 +11,7 @@ export declare class Team {
|
|
|
10
11
|
readonly tactics?: Tactics;
|
|
11
12
|
readonly country: Country;
|
|
12
13
|
readonly legacyFlagCountry?: Country;
|
|
14
|
+
readonly jerseyConfig?: JerseyConfig;
|
|
13
15
|
private _rating;
|
|
14
16
|
static create(input: unknown): Team;
|
|
15
17
|
private constructor();
|
|
@@ -19,7 +19,7 @@ class Team {
|
|
|
19
19
|
tactics: result.data.tactics != null ? tactics_1.Tactics.create(result.data.tactics) : undefined
|
|
20
20
|
});
|
|
21
21
|
}
|
|
22
|
-
constructor({ id, rating, name, shortName, divisionId, country, legacyFlagCountry, roster, tactics }) {
|
|
22
|
+
constructor({ id, rating, name, shortName, divisionId, country, legacyFlagCountry, roster, tactics, jerseyConfig }) {
|
|
23
23
|
this.id = id;
|
|
24
24
|
this._rating = rating;
|
|
25
25
|
this.roster = roster;
|
|
@@ -29,6 +29,7 @@ class Team {
|
|
|
29
29
|
this.divisionId = divisionId;
|
|
30
30
|
this.country = country;
|
|
31
31
|
this.legacyFlagCountry = legacyFlagCountry;
|
|
32
|
+
this.jerseyConfig = jerseyConfig;
|
|
32
33
|
}
|
|
33
34
|
get rating() {
|
|
34
35
|
return this._rating;
|