gymmonk-schema 0.32.0 → 0.34.0
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/center.d.ts +12 -6
- package/dist/center.d.ts.map +1 -1
- package/dist/center.js +29 -12
- package/dist/center.js.map +1 -1
- package/dist/check-in.js +2 -2
- package/dist/check-in.js.map +1 -1
- package/dist/codes.d.ts +121 -0
- package/dist/codes.d.ts.map +1 -0
- package/dist/codes.js +151 -0
- package/dist/codes.js.map +1 -0
- package/dist/enrolment.d.ts +100 -0
- package/dist/enrolment.d.ts.map +1 -0
- package/dist/enrolment.js +112 -0
- package/dist/enrolment.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/member-profile.d.ts +26 -26
- package/dist/member-profile.d.ts.map +1 -1
- package/dist/member-profile.js +37 -45
- package/dist/member-profile.js.map +1 -1
- package/dist/member.d.ts +14 -10
- package/dist/member.d.ts.map +1 -1
- package/dist/member.js +11 -9
- package/dist/member.js.map +1 -1
- package/dist/membership-request.d.ts +6 -1
- package/dist/membership-request.d.ts.map +1 -1
- package/dist/membership-request.js +8 -2
- package/dist/membership-request.js.map +1 -1
- package/dist/membership.d.ts +2 -2
- package/dist/membership.d.ts.map +1 -1
- package/dist/membership.js +15 -3
- package/dist/membership.js.map +1 -1
- package/dist/onboarding-form.js +2 -2
- package/dist/onboarding-form.js.map +1 -1
- package/dist/onboarding.d.ts.map +1 -1
- package/dist/onboarding.js +5 -4
- package/dist/onboarding.js.map +1 -1
- package/dist/organisation.d.ts +2 -0
- package/dist/organisation.d.ts.map +1 -1
- package/dist/organisation.js +11 -0
- package/dist/organisation.js.map +1 -1
- package/dist/staff.d.ts +2 -2
- package/dist/staff.js +2 -2
- package/dist/staff.js.map +1 -1
- package/package.json +1 -1
package/dist/center.d.ts
CHANGED
|
@@ -41,6 +41,16 @@ import { z } from 'zod';
|
|
|
41
41
|
*
|
|
42
42
|
* Values are stable slugs — the LABELS are the frontend's business, so
|
|
43
43
|
* rewording a chip never rewrites stored data.
|
|
44
|
+
*
|
|
45
|
+
* ── The field is called `types`; the screen says "What this center offers" ──
|
|
46
|
+
* That divergence is deliberate, not an oversight. "Type" reads as one thing a
|
|
47
|
+
* place IS, and a real branch runs several — a weights floor, a yoga room and
|
|
48
|
+
* Zumba on weekends — so a singular-sounding label pushed owners to pick one
|
|
49
|
+
* and misdescribe themselves. (The picker was once a single-select with a
|
|
50
|
+
* "Mixed" option that saved as plain `gym`, which is how a CrossFit-and-boxing
|
|
51
|
+
* branch came back as neither.) The label was changed to match what is stored;
|
|
52
|
+
* the identifiers were not, because the values are a wire contract and
|
|
53
|
+
* renaming them would rewrite data to fix a word.
|
|
44
54
|
*/
|
|
45
55
|
export declare const CENTER_TYPES: readonly ["gym", "strength", "crossfit", "functional", "bodybuilding", "powerlifting", "calisthenics", "cardio", "hiit", "spinning", "bootcamp", "aerobics", "sports-conditioning", "yoga", "pilates", "meditation", "zumba", "dance", "boxing", "kickboxing", "mma", "martial-arts", "swimming", "physiotherapy"];
|
|
46
56
|
export declare const centerTypeSchema: z.ZodEnum<{
|
|
@@ -75,12 +85,7 @@ export declare const centerStatusSchema: z.ZodEnum<{
|
|
|
75
85
|
inactive: "inactive";
|
|
76
86
|
}>;
|
|
77
87
|
export type CenterStatus = z.infer<typeof centerStatusSchema>;
|
|
78
|
-
|
|
79
|
-
* The public gym code a member enters at onboarding to join, `GYM-YYYY-NNN`.
|
|
80
|
-
* Server-generated and unique; never accepted in a create body.
|
|
81
|
-
*/
|
|
82
|
-
export declare const gymCodeSchema: z.ZodString;
|
|
83
|
-
export type GymCode = z.infer<typeof gymCodeSchema>;
|
|
88
|
+
export { centerCodeSchema, type CenterCode } from './codes.js';
|
|
84
89
|
export declare const createCenterBodySchema: z.ZodObject<{
|
|
85
90
|
checkInRadiusM: z.ZodOptional<z.ZodNumber>;
|
|
86
91
|
mapLinkUrl: z.ZodOptional<z.ZodString>;
|
|
@@ -326,6 +331,7 @@ export declare const centerSchema: z.ZodObject<{
|
|
|
326
331
|
id: z.ZodString;
|
|
327
332
|
organisationId: z.ZodString;
|
|
328
333
|
code: z.ZodString;
|
|
334
|
+
sequence: z.ZodNumber;
|
|
329
335
|
name: z.ZodString;
|
|
330
336
|
description: z.ZodNullable<z.ZodString>;
|
|
331
337
|
types: z.ZodArray<z.ZodEnum<{
|
package/dist/center.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"center.d.ts","sourceRoot":"","sources":["../src/center.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"center.d.ts","sourceRoot":"","sources":["../src/center.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAexB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,eAAO,MAAM,YAAY,oTAgCf,CAAC;AACX,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;EAAuB,CAAC;AACrD,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAI1D,eAAO,MAAM,kBAAkB;;;EAAiC,CAAC;AACjE,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAM9D,OAAO,EAAE,gBAAgB,EAAE,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAI/D,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAqCjC,CAAC;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAEtE,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAAmC,CAAC;AACvE,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAItE,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAqDvB,CAAC;AACH,MAAM,MAAM,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAIlD;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB;;;;iBAI5B,CAAC;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAI5D,yEAAyE;AACzE,eAAO,MAAM,eAAe;;;;;;;;;iBAkB1B,CAAC;AACH,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAExD;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,sBAAsB,KAAK,CAAC;AAEzC,eAAO,MAAM,oBAAoB;;;;;;;;iBAwB7B,CAAC;AACL,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAElE;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;iBAIhC,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAKpE,eAAO,MAAM,qBAAqB;;;;iBAIhC,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC"}
|
package/dist/center.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* @module gymmonk-schema/center
|
|
9
9
|
*/
|
|
10
10
|
import { z } from 'zod';
|
|
11
|
+
import { centerCodeSchema } from './codes.js';
|
|
11
12
|
import { isoDateTimeSchema, objectIdSchema, optionalEmailSchema, optionalPhoneSchema, optionalTrimmedString, phoneSchema, } from './common.js';
|
|
12
13
|
import { mapLinkInputSchema, placePinSchema, timezoneSchema } from './location.js';
|
|
13
14
|
import { addressSchema, weeklyHoursSchema } from './shared.js';
|
|
@@ -45,6 +46,16 @@ import { addressSchema, weeklyHoursSchema } from './shared.js';
|
|
|
45
46
|
*
|
|
46
47
|
* Values are stable slugs — the LABELS are the frontend's business, so
|
|
47
48
|
* rewording a chip never rewrites stored data.
|
|
49
|
+
*
|
|
50
|
+
* ── The field is called `types`; the screen says "What this center offers" ──
|
|
51
|
+
* That divergence is deliberate, not an oversight. "Type" reads as one thing a
|
|
52
|
+
* place IS, and a real branch runs several — a weights floor, a yoga room and
|
|
53
|
+
* Zumba on weekends — so a singular-sounding label pushed owners to pick one
|
|
54
|
+
* and misdescribe themselves. (The picker was once a single-select with a
|
|
55
|
+
* "Mixed" option that saved as plain `gym`, which is how a CrossFit-and-boxing
|
|
56
|
+
* branch came back as neither.) The label was changed to match what is stored;
|
|
57
|
+
* the identifiers were not, because the values are a wire contract and
|
|
58
|
+
* renaming them would rewrite data to fix a word.
|
|
48
59
|
*/
|
|
49
60
|
export const CENTER_TYPES = [
|
|
50
61
|
// General
|
|
@@ -82,19 +93,16 @@ export const CENTER_TYPES = [
|
|
|
82
93
|
export const centerTypeSchema = z.enum(CENTER_TYPES);
|
|
83
94
|
// ─── Status ──────────────────────────────────────────────────────────────────
|
|
84
95
|
export const centerStatusSchema = z.enum(['active', 'inactive']);
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
export
|
|
90
|
-
.string()
|
|
91
|
-
.trim()
|
|
92
|
-
.regex(/^GYM-\d{4}-\d{3,}$/, 'Invalid gym code');
|
|
96
|
+
// The public gym code (`IRN-1-7K2M`) is defined in `codes.ts` alongside the
|
|
97
|
+
// member ids built on the same stem, so the four identifier shapes this product
|
|
98
|
+
// issues can be read — and kept from colliding — in one place. Re-exported here
|
|
99
|
+
// because a centre's code is a fact about a centre.
|
|
100
|
+
export { centerCodeSchema } from './codes.js';
|
|
93
101
|
// ─── Create / update bodies ──────────────────────────────────────────────────
|
|
94
102
|
export const createCenterBodySchema = z.object({
|
|
95
103
|
name: z.string().trim().min(1, 'Center name is required').max(160),
|
|
96
104
|
description: optionalTrimmedString(2000),
|
|
97
|
-
types: z.array(centerTypeSchema).min(1, 'Pick at least one
|
|
105
|
+
types: z.array(centerTypeSchema).min(1, 'Pick at least one'),
|
|
98
106
|
address: addressSchema,
|
|
99
107
|
/** Usable floor area in square feet. */
|
|
100
108
|
floorAreaSqft: z.number().int().min(0).max(1_000_000).optional(),
|
|
@@ -134,8 +142,17 @@ export const updateCenterBodySchema = createCenterBodySchema.partial();
|
|
|
134
142
|
export const centerSchema = z.object({
|
|
135
143
|
id: objectIdSchema,
|
|
136
144
|
organisationId: objectIdSchema,
|
|
137
|
-
/** Public join code, `
|
|
138
|
-
code:
|
|
145
|
+
/** Public join code, `IRN-1-7K2M`. */
|
|
146
|
+
code: centerCodeSchema,
|
|
147
|
+
/**
|
|
148
|
+
* Which centre this is within its organisation — 1 for the first branch the
|
|
149
|
+
* owner opened, 2 for the next.
|
|
150
|
+
*
|
|
151
|
+
* Stored rather than counted on read, because it is baked into this centre's
|
|
152
|
+
* code AND into every member id the centre issues. Deleting branch 2 must not
|
|
153
|
+
* renumber branch 3 into codes that are already printed.
|
|
154
|
+
*/
|
|
155
|
+
sequence: z.number().int().positive(),
|
|
139
156
|
name: z.string(),
|
|
140
157
|
description: z.string().nullable(),
|
|
141
158
|
types: z.array(centerTypeSchema),
|
|
@@ -194,7 +211,7 @@ export const centerStatsSchema = z.object({
|
|
|
194
211
|
/** A lightweight public directory result for the "find a gym" screen. */
|
|
195
212
|
export const nearbyGymSchema = z.object({
|
|
196
213
|
id: objectIdSchema,
|
|
197
|
-
code:
|
|
214
|
+
code: centerCodeSchema,
|
|
198
215
|
name: z.string(),
|
|
199
216
|
/** The narrowest place name we hold (locality → district → state). */
|
|
200
217
|
area: z.string(),
|
package/dist/center.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"center.js","sourceRoot":"","sources":["../src/center.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * gymmonk-schema — Center (branch)\n * ================================\n * A physical gym location belonging to an organisation. Carries the public gym\n * code members use to join, opening hours, equipment scope, and contact info.\n * v1 memberships are single-center, so a Center is the unit a member joins.\n *\n * @module gymmonk-schema/center\n */\n\nimport { z } from 'zod';\nimport {\n isoDateTimeSchema,\n objectIdSchema,\n optionalEmailSchema,\n optionalPhoneSchema,\n optionalTrimmedString,\n phoneSchema,\n} from './common.js';\nimport { mapLinkInputSchema, placePinSchema, timezoneSchema } from './location.js';\nimport { addressSchema, weeklyHoursSchema } from './shared.js';\n\n// ─── Center type ─────────────────────────────────────────────────────────────\n\n/**\n * What a centre OFFERS, multi-select.\n *\n * ── The axis, and why `personal-training` is gone ───────────────────────────\n * Every value here answers one question: what does somebody DO at this place.\n * That makes them disciplines and training modalities, so an owner ticking\n * three of them describes a real branch and a member reading them knows what\n * they are walking into.\n *\n * `personal-training` answered a different question — HOW coaching is\n * delivered, one-to-one instead of in a class — and nearly every gym in the\n * list offers it. A value almost everyone can tick separates nobody from\n * anybody, which is the opposite of what a type is for. Business model\n * (big-box, boutique, 24-hour) is a third axis again, and one this product\n * already answers better elsewhere: floor area and opening hours are recorded\n * fields, not a guess from a chip.\n *\n * ── Where the list comes from ───────────────────────────────────────────────\n * Cross-referenced rather than invented: Google Business Profile's fitness\n * categories (what gyms actually file themselves under, and therefore what\n * members search), ClassPass and Mindbody's activity taxonomies, and the\n * formats the Indian market actually runs — cult.fit's S&C, HRX, boxing, dance\n * fitness and yoga, plus the bodybuilding and martial-arts traditions that a\n * list drawn only from US sources would miss entirely.\n *\n * Deliberately NOT included, so the next person does not re-litigate it:\n * rock climbing, gymnastics and aerial/pole are real facility categories but\n * not gyms in the sense this product manages; women-only and kids' fitness\n * describe an AUDIENCE, not an activity; 24-hour and big-box describe hours\n * and size, both of which are already their own fields.\n *\n * Values are stable slugs — the LABELS are the frontend's business, so\n * rewording a chip never rewrites stored data.\n */\nexport const CENTER_TYPES = [\n // General\n 'gym',\n // Strength\n 'strength',\n 'crossfit',\n 'functional',\n 'bodybuilding',\n 'powerlifting',\n 'calisthenics',\n // Cardio & group training\n 'cardio',\n 'hiit',\n 'spinning',\n 'bootcamp',\n 'aerobics',\n 'sports-conditioning',\n // Mind & body\n 'yoga',\n 'pilates',\n 'meditation',\n // Dance\n 'zumba',\n 'dance',\n // Combat\n 'boxing',\n 'kickboxing',\n 'mma',\n 'martial-arts',\n // Facilities\n 'swimming',\n 'physiotherapy',\n] as const;\nexport const centerTypeSchema = z.enum(CENTER_TYPES);\nexport type CenterType = z.infer<typeof centerTypeSchema>;\n\n// ─── Status ──────────────────────────────────────────────────────────────────\n\nexport const centerStatusSchema = z.enum(['active', 'inactive']);\nexport type CenterStatus = z.infer<typeof centerStatusSchema>;\n\n/**\n * The public gym code a member enters at onboarding to join, `GYM-YYYY-NNN`.\n * Server-generated and unique; never accepted in a create body.\n */\nexport const gymCodeSchema = z\n .string()\n .trim()\n .regex(/^GYM-\\d{4}-\\d{3,}$/, 'Invalid gym code');\nexport type GymCode = z.infer<typeof gymCodeSchema>;\n\n// ─── Create / update bodies ──────────────────────────────────────────────────\n\nexport const createCenterBodySchema = z.object({\n name: z.string().trim().min(1, 'Center name is required').max(160),\n description: optionalTrimmedString(2000),\n types: z.array(centerTypeSchema).min(1, 'Pick at least one type'),\n address: addressSchema,\n /** Usable floor area in square feet. */\n floorAreaSqft: z.number().int().min(0).max(1_000_000).optional(),\n /**\n * How many people the branch holds at once.\n *\n * The denominator behind \"how full are we\" — the centre detail screen shows\n * it beside the active-member count, and crowd-load has no meaning without\n * it. Optional: a gym that has never measured it should not be blocked from\n * saving the rest of its details.\n */\n capacity: z.number().int().min(1).max(100_000).optional(),\n primaryPhone: phoneSchema,\n secondaryPhone: optionalPhoneSchema,\n email: optionalEmailSchema,\n website: optionalTrimmedString(200),\n hours: weeklyHoursSchema,\n /** Uploaded gallery image URLs. */\n photos: z.array(z.string().trim().max(2000)).max(20).default([]),\n /**\n * A pasted Google Maps link. The server reads the coordinates out of it; the\n * client never sends them.\n *\n * This is the pin attendance is measured against, and it is NOT the same as\n * `address.coords`: that is an area centroid resolved from master data, which\n * can be a whole district's midpoint and is useless for proximity.\n */\n ...mapLinkInputSchema.shape,\n /**\n * How close a member must be for a check-in to count, metres. Optional — the\n * server applies its own default when a gym has not chosen one.\n */\n checkInRadiusM: z.number().int().min(50).max(5000).optional(),\n});\nexport type CreateCenterBody = z.infer<typeof createCenterBodySchema>;\n\nexport const updateCenterBodySchema = createCenterBodySchema.partial();\nexport type UpdateCenterBody = z.infer<typeof updateCenterBodySchema>;\n\n// ─── Entity (API response) ───────────────────────────────────────────────────\n\nexport const centerSchema = z.object({\n id: objectIdSchema,\n organisationId: objectIdSchema,\n /** Public join code, `GYM-YYYY-NNN`. */\n code: gymCodeSchema,\n name: z.string(),\n description: z.string().nullable(),\n types: z.array(centerTypeSchema),\n address: addressSchema,\n floorAreaSqft: z.number().int().nullable(),\n capacity: z.number().int().nullable(),\n primaryPhone: z.string(),\n secondaryPhone: z.string().nullable(),\n email: z.string().nullable(),\n website: z.string().nullable(),\n hours: weeklyHoursSchema,\n photos: z.array(z.string()),\n status: centerStatusSchema,\n /** Denormalised active-member count for roster headers. */\n memberCount: z.number().int().nonnegative(),\n // The resolved pin. Absent until an owner pastes a Maps link, which is why\n // attendance is unenforceable rather than failing for a gym without one.\n ...placePinSchema.shape,\n checkInRadiusM: z.number().int().nullable().optional(),\n /**\n * The centre's own timezone, as an IANA identifier (\"Asia/Kolkata\").\n *\n * WHY THE CENTRE'S AND NOT THE VIEWER'S. Attendance is filed under a calendar\n * DAY, and a day is not timezone-free — whichever zone derives it defines when\n * it starts. Deriving it from whoever is looking would put one visit on\n * different days for the member and the owner, and would make an owner's daily\n * roll-up change depending on where they opened the app. A visit belongs to the\n * gym's operating day; only the display of times follows the reader.\n *\n * An IANA NAME, never a UTC offset. Offsets are a property of an instant, not\n * of a place: they move with DST, so a stored \"+05:30\" is already wrong for\n * half the year anywhere that observes it.\n *\n * Server-derived from the centre's pin, never accepted from a client — the\n * same rule the coordinates follow, and for the same reason.\n */\n timezone: timezoneSchema,\n createdAt: isoDateTimeSchema,\n updatedAt: isoDateTimeSchema,\n});\nexport type Center = z.infer<typeof centerSchema>;\n\n// ─── Center stats (owner detail header) ──────────────────────────────────────\n\n/**\n * The three-up header on a centre's detail screen.\n *\n * Computed, never stored — `activeMembers` counts live memberships and\n * `monthlyRevenueInr` sums this month's successful payments, so the card can\n * never drift from the ledger the way a denormalised total would. `capacity`\n * is the one stored value, and is null until the owner fills it in.\n */\nexport const centerStatsSchema = z.object({\n capacity: z.number().int().nullable(),\n activeMembers: z.number().int().nonnegative(),\n monthlyRevenueInr: z.number().nonnegative(),\n});\nexport type CenterStats = z.infer<typeof centerStatsSchema>;\n\n// ─── Gym directory (member \"find a gym\") ─────────────────────────────────────\n\n/** A lightweight public directory result for the \"find a gym\" screen. */\nexport const nearbyGymSchema = z.object({\n id: objectIdSchema,\n code: gymCodeSchema,\n name: z.string(),\n /** The narrowest place name we hold (locality → district → state). */\n area: z.string(),\n district: z.string().nullable(),\n state: z.string().nullable(),\n /**\n * Kilometres from the coordinates the caller supplied.\n *\n * NULL, not 0, when it cannot be computed — the caller sent no position, or\n * the gym's address carries no coordinates. Zero is a real distance (\"you are\n * standing in it\") and using it as the unknown value put every un-geocoded\n * gym at the top of a nearest-first list.\n */\n distanceKm: z.number().nonnegative().nullable(),\n memberCount: z.number().int().nonnegative().optional(),\n});\nexport type NearbyGym = z.infer<typeof nearbyGymSchema>;\n\n/**\n * The gym directory has THREE ways in, and they differ in how far they reach.\n *\n * `code` exact public gym id — matches anywhere in the country, because\n * a member holding the id already knows which gym they mean and\n * may well be joining one in another state.\n * `q` name search — deliberately narrowed to ONE STATE. Gym names\n * repeat heavily (\"Fitness First\"), so a nationwide name search\n * returns a list nobody can pick from.\n * `districtId` browse — the gyms around them, nearest first when they share\n * their position.\n *\n * `stateId` / `districtId` are master-area ids (`area.ts`), matched against the\n * address `ancestry` array every center stores.\n */\nexport const GYM_SEARCH_MAX_RESULTS = 20;\n\nexport const gymSearchQuerySchema = z\n .object({\n /** Exact public gym id, e.g. `GYM-2025-001`. Unscoped by geography. */\n code: optionalTrimmedString(40),\n /** Name fragment. Requires `stateId`. */\n q: optionalTrimmedString(120),\n stateId: optionalTrimmedString(40),\n districtId: optionalTrimmedString(40),\n lat: z.coerce.number().min(-90).max(90).optional(),\n lng: z.coerce.number().min(-180).max(180).optional(),\n limit: z.coerce\n .number()\n .int()\n .min(1)\n .max(GYM_SEARCH_MAX_RESULTS)\n .default(GYM_SEARCH_MAX_RESULTS),\n })\n .refine((v) => !v.q || Boolean(v.stateId), {\n message: 'Pick a state to search gyms by name',\n path: ['stateId'],\n })\n .refine((v) => Boolean(v.code || v.q || v.stateId || v.districtId), {\n message: 'Enter a gym id, or pick where to look',\n path: ['stateId'],\n });\nexport type GymSearchQuery = z.infer<typeof gymSearchQuerySchema>;\n\n/**\n * The directory response.\n *\n * `truncated` says the district holds more gyms than were returned, which is\n * what the screen turns into \"showing the 20 nearest — enter a gym id if yours\n * is not here\" rather than letting the member assume their gym is absent.\n */\nexport const gymSearchResultSchema = z.object({\n items: z.array(nearbyGymSchema),\n total: z.number().int().nonnegative(),\n truncated: z.boolean(),\n});\nexport type GymSearchResult = z.infer<typeof gymSearchResultSchema>;\n\n// ─── Retained: nearby-gyms query ─────────────────────────────────────────────\n// Kept for the gym-search route the owner console's repo still serves. See the\n// note in `onboarding.ts`.\nexport const nearbyGymsQuerySchema = z.object({\n lat: z.coerce.number().min(-90).max(90).optional(),\n lng: z.coerce.number().min(-180).max(180).optional(),\n q: optionalTrimmedString(120),\n});\nexport type NearbyGymsQuery = z.infer<typeof nearbyGymsQuerySchema>;\n"]}
|
|
1
|
+
{"version":3,"file":"center.js","sourceRoot":"","sources":["../src/center.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * gymmonk-schema — Center (branch)\n * ================================\n * A physical gym location belonging to an organisation. Carries the public gym\n * code members use to join, opening hours, equipment scope, and contact info.\n * v1 memberships are single-center, so a Center is the unit a member joins.\n *\n * @module gymmonk-schema/center\n */\n\nimport { z } from 'zod';\nimport { centerCodeSchema } from './codes.js';\nimport {\n isoDateTimeSchema,\n objectIdSchema,\n optionalEmailSchema,\n optionalPhoneSchema,\n optionalTrimmedString,\n phoneSchema,\n} from './common.js';\nimport { mapLinkInputSchema, placePinSchema, timezoneSchema } from './location.js';\nimport { addressSchema, weeklyHoursSchema } from './shared.js';\n\n// ─── Center type ─────────────────────────────────────────────────────────────\n\n/**\n * What a centre OFFERS, multi-select.\n *\n * ── The axis, and why `personal-training` is gone ───────────────────────────\n * Every value here answers one question: what does somebody DO at this place.\n * That makes them disciplines and training modalities, so an owner ticking\n * three of them describes a real branch and a member reading them knows what\n * they are walking into.\n *\n * `personal-training` answered a different question — HOW coaching is\n * delivered, one-to-one instead of in a class — and nearly every gym in the\n * list offers it. A value almost everyone can tick separates nobody from\n * anybody, which is the opposite of what a type is for. Business model\n * (big-box, boutique, 24-hour) is a third axis again, and one this product\n * already answers better elsewhere: floor area and opening hours are recorded\n * fields, not a guess from a chip.\n *\n * ── Where the list comes from ───────────────────────────────────────────────\n * Cross-referenced rather than invented: Google Business Profile's fitness\n * categories (what gyms actually file themselves under, and therefore what\n * members search), ClassPass and Mindbody's activity taxonomies, and the\n * formats the Indian market actually runs — cult.fit's S&C, HRX, boxing, dance\n * fitness and yoga, plus the bodybuilding and martial-arts traditions that a\n * list drawn only from US sources would miss entirely.\n *\n * Deliberately NOT included, so the next person does not re-litigate it:\n * rock climbing, gymnastics and aerial/pole are real facility categories but\n * not gyms in the sense this product manages; women-only and kids' fitness\n * describe an AUDIENCE, not an activity; 24-hour and big-box describe hours\n * and size, both of which are already their own fields.\n *\n * Values are stable slugs — the LABELS are the frontend's business, so\n * rewording a chip never rewrites stored data.\n *\n * ── The field is called `types`; the screen says \"What this center offers\" ──\n * That divergence is deliberate, not an oversight. \"Type\" reads as one thing a\n * place IS, and a real branch runs several — a weights floor, a yoga room and\n * Zumba on weekends — so a singular-sounding label pushed owners to pick one\n * and misdescribe themselves. (The picker was once a single-select with a\n * \"Mixed\" option that saved as plain `gym`, which is how a CrossFit-and-boxing\n * branch came back as neither.) The label was changed to match what is stored;\n * the identifiers were not, because the values are a wire contract and\n * renaming them would rewrite data to fix a word.\n */\nexport const CENTER_TYPES = [\n // General\n 'gym',\n // Strength\n 'strength',\n 'crossfit',\n 'functional',\n 'bodybuilding',\n 'powerlifting',\n 'calisthenics',\n // Cardio & group training\n 'cardio',\n 'hiit',\n 'spinning',\n 'bootcamp',\n 'aerobics',\n 'sports-conditioning',\n // Mind & body\n 'yoga',\n 'pilates',\n 'meditation',\n // Dance\n 'zumba',\n 'dance',\n // Combat\n 'boxing',\n 'kickboxing',\n 'mma',\n 'martial-arts',\n // Facilities\n 'swimming',\n 'physiotherapy',\n] as const;\nexport const centerTypeSchema = z.enum(CENTER_TYPES);\nexport type CenterType = z.infer<typeof centerTypeSchema>;\n\n// ─── Status ──────────────────────────────────────────────────────────────────\n\nexport const centerStatusSchema = z.enum(['active', 'inactive']);\nexport type CenterStatus = z.infer<typeof centerStatusSchema>;\n\n// The public gym code (`IRN-1-7K2M`) is defined in `codes.ts` alongside the\n// member ids built on the same stem, so the four identifier shapes this product\n// issues can be read — and kept from colliding — in one place. Re-exported here\n// because a centre's code is a fact about a centre.\nexport { centerCodeSchema, type CenterCode } from './codes.js';\n\n// ─── Create / update bodies ──────────────────────────────────────────────────\n\nexport const createCenterBodySchema = z.object({\n name: z.string().trim().min(1, 'Center name is required').max(160),\n description: optionalTrimmedString(2000),\n types: z.array(centerTypeSchema).min(1, 'Pick at least one'),\n address: addressSchema,\n /** Usable floor area in square feet. */\n floorAreaSqft: z.number().int().min(0).max(1_000_000).optional(),\n /**\n * How many people the branch holds at once.\n *\n * The denominator behind \"how full are we\" — the centre detail screen shows\n * it beside the active-member count, and crowd-load has no meaning without\n * it. Optional: a gym that has never measured it should not be blocked from\n * saving the rest of its details.\n */\n capacity: z.number().int().min(1).max(100_000).optional(),\n primaryPhone: phoneSchema,\n secondaryPhone: optionalPhoneSchema,\n email: optionalEmailSchema,\n website: optionalTrimmedString(200),\n hours: weeklyHoursSchema,\n /** Uploaded gallery image URLs. */\n photos: z.array(z.string().trim().max(2000)).max(20).default([]),\n /**\n * A pasted Google Maps link. The server reads the coordinates out of it; the\n * client never sends them.\n *\n * This is the pin attendance is measured against, and it is NOT the same as\n * `address.coords`: that is an area centroid resolved from master data, which\n * can be a whole district's midpoint and is useless for proximity.\n */\n ...mapLinkInputSchema.shape,\n /**\n * How close a member must be for a check-in to count, metres. Optional — the\n * server applies its own default when a gym has not chosen one.\n */\n checkInRadiusM: z.number().int().min(50).max(5000).optional(),\n});\nexport type CreateCenterBody = z.infer<typeof createCenterBodySchema>;\n\nexport const updateCenterBodySchema = createCenterBodySchema.partial();\nexport type UpdateCenterBody = z.infer<typeof updateCenterBodySchema>;\n\n// ─── Entity (API response) ───────────────────────────────────────────────────\n\nexport const centerSchema = z.object({\n id: objectIdSchema,\n organisationId: objectIdSchema,\n /** Public join code, `IRN-1-7K2M`. */\n code: centerCodeSchema,\n /**\n * Which centre this is within its organisation — 1 for the first branch the\n * owner opened, 2 for the next.\n *\n * Stored rather than counted on read, because it is baked into this centre's\n * code AND into every member id the centre issues. Deleting branch 2 must not\n * renumber branch 3 into codes that are already printed.\n */\n sequence: z.number().int().positive(),\n name: z.string(),\n description: z.string().nullable(),\n types: z.array(centerTypeSchema),\n address: addressSchema,\n floorAreaSqft: z.number().int().nullable(),\n capacity: z.number().int().nullable(),\n primaryPhone: z.string(),\n secondaryPhone: z.string().nullable(),\n email: z.string().nullable(),\n website: z.string().nullable(),\n hours: weeklyHoursSchema,\n photos: z.array(z.string()),\n status: centerStatusSchema,\n /** Denormalised active-member count for roster headers. */\n memberCount: z.number().int().nonnegative(),\n // The resolved pin. Absent until an owner pastes a Maps link, which is why\n // attendance is unenforceable rather than failing for a gym without one.\n ...placePinSchema.shape,\n checkInRadiusM: z.number().int().nullable().optional(),\n /**\n * The centre's own timezone, as an IANA identifier (\"Asia/Kolkata\").\n *\n * WHY THE CENTRE'S AND NOT THE VIEWER'S. Attendance is filed under a calendar\n * DAY, and a day is not timezone-free — whichever zone derives it defines when\n * it starts. Deriving it from whoever is looking would put one visit on\n * different days for the member and the owner, and would make an owner's daily\n * roll-up change depending on where they opened the app. A visit belongs to the\n * gym's operating day; only the display of times follows the reader.\n *\n * An IANA NAME, never a UTC offset. Offsets are a property of an instant, not\n * of a place: they move with DST, so a stored \"+05:30\" is already wrong for\n * half the year anywhere that observes it.\n *\n * Server-derived from the centre's pin, never accepted from a client — the\n * same rule the coordinates follow, and for the same reason.\n */\n timezone: timezoneSchema,\n createdAt: isoDateTimeSchema,\n updatedAt: isoDateTimeSchema,\n});\nexport type Center = z.infer<typeof centerSchema>;\n\n// ─── Center stats (owner detail header) ──────────────────────────────────────\n\n/**\n * The three-up header on a centre's detail screen.\n *\n * Computed, never stored — `activeMembers` counts live memberships and\n * `monthlyRevenueInr` sums this month's successful payments, so the card can\n * never drift from the ledger the way a denormalised total would. `capacity`\n * is the one stored value, and is null until the owner fills it in.\n */\nexport const centerStatsSchema = z.object({\n capacity: z.number().int().nullable(),\n activeMembers: z.number().int().nonnegative(),\n monthlyRevenueInr: z.number().nonnegative(),\n});\nexport type CenterStats = z.infer<typeof centerStatsSchema>;\n\n// ─── Gym directory (member \"find a gym\") ─────────────────────────────────────\n\n/** A lightweight public directory result for the \"find a gym\" screen. */\nexport const nearbyGymSchema = z.object({\n id: objectIdSchema,\n code: centerCodeSchema,\n name: z.string(),\n /** The narrowest place name we hold (locality → district → state). */\n area: z.string(),\n district: z.string().nullable(),\n state: z.string().nullable(),\n /**\n * Kilometres from the coordinates the caller supplied.\n *\n * NULL, not 0, when it cannot be computed — the caller sent no position, or\n * the gym's address carries no coordinates. Zero is a real distance (\"you are\n * standing in it\") and using it as the unknown value put every un-geocoded\n * gym at the top of a nearest-first list.\n */\n distanceKm: z.number().nonnegative().nullable(),\n memberCount: z.number().int().nonnegative().optional(),\n});\nexport type NearbyGym = z.infer<typeof nearbyGymSchema>;\n\n/**\n * The gym directory has THREE ways in, and they differ in how far they reach.\n *\n * `code` exact public gym id — matches anywhere in the country, because\n * a member holding the id already knows which gym they mean and\n * may well be joining one in another state.\n * `q` name search — deliberately narrowed to ONE STATE. Gym names\n * repeat heavily (\"Fitness First\"), so a nationwide name search\n * returns a list nobody can pick from.\n * `districtId` browse — the gyms around them, nearest first when they share\n * their position.\n *\n * `stateId` / `districtId` are master-area ids (`area.ts`), matched against the\n * address `ancestry` array every center stores.\n */\nexport const GYM_SEARCH_MAX_RESULTS = 20;\n\nexport const gymSearchQuerySchema = z\n .object({\n /** Exact public gym id, e.g. `GYM-2025-001`. Unscoped by geography. */\n code: optionalTrimmedString(40),\n /** Name fragment. Requires `stateId`. */\n q: optionalTrimmedString(120),\n stateId: optionalTrimmedString(40),\n districtId: optionalTrimmedString(40),\n lat: z.coerce.number().min(-90).max(90).optional(),\n lng: z.coerce.number().min(-180).max(180).optional(),\n limit: z.coerce\n .number()\n .int()\n .min(1)\n .max(GYM_SEARCH_MAX_RESULTS)\n .default(GYM_SEARCH_MAX_RESULTS),\n })\n .refine((v) => !v.q || Boolean(v.stateId), {\n message: 'Pick a state to search gyms by name',\n path: ['stateId'],\n })\n .refine((v) => Boolean(v.code || v.q || v.stateId || v.districtId), {\n message: 'Enter a gym id, or pick where to look',\n path: ['stateId'],\n });\nexport type GymSearchQuery = z.infer<typeof gymSearchQuerySchema>;\n\n/**\n * The directory response.\n *\n * `truncated` says the district holds more gyms than were returned, which is\n * what the screen turns into \"showing the 20 nearest — enter a gym id if yours\n * is not here\" rather than letting the member assume their gym is absent.\n */\nexport const gymSearchResultSchema = z.object({\n items: z.array(nearbyGymSchema),\n total: z.number().int().nonnegative(),\n truncated: z.boolean(),\n});\nexport type GymSearchResult = z.infer<typeof gymSearchResultSchema>;\n\n// ─── Retained: nearby-gyms query ─────────────────────────────────────────────\n// Kept for the gym-search route the owner console's repo still serves. See the\n// note in `onboarding.ts`.\nexport const nearbyGymsQuerySchema = z.object({\n lat: z.coerce.number().min(-90).max(90).optional(),\n lng: z.coerce.number().min(-180).max(180).optional(),\n q: optionalTrimmedString(120),\n});\nexport type NearbyGymsQuery = z.infer<typeof nearbyGymsQuerySchema>;\n"]}
|
package/dist/check-in.js
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* @module gymmonk-schema/check-in
|
|
26
26
|
*/
|
|
27
27
|
import { z } from 'zod';
|
|
28
|
-
import {
|
|
28
|
+
import { centerCodeSchema } from './codes.js';
|
|
29
29
|
import { isoDateTimeSchema, objectIdSchema } from './common.js';
|
|
30
30
|
// ─── The code ────────────────────────────────────────────────────────────────
|
|
31
31
|
/** Length of a center's check-in code. 16 × 5 bits = 80 bits of entropy. */
|
|
@@ -107,7 +107,7 @@ export const centerCheckInQrSchema = z.object({
|
|
|
107
107
|
centerId: objectIdSchema,
|
|
108
108
|
centerName: z.string(),
|
|
109
109
|
/** The center's PUBLIC join code — shown as the human-readable caption. */
|
|
110
|
-
gymCode:
|
|
110
|
+
gymCode: centerCodeSchema,
|
|
111
111
|
/** The secret handle the QR encodes. */
|
|
112
112
|
code: checkInCodeSchema,
|
|
113
113
|
/** When the code was last rotated (null → never rotated since minting). */
|
package/dist/check-in.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"check-in.js","sourceRoot":"","sources":["../src/check-in.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"check-in.js","sourceRoot":"","sources":["../src/check-in.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * gymmonk-schema — Check-in QR (the scanned code)\n * ===============================================\n * A member starts and ends a session by pointing their camera at the gym's QR\n * poster. This module is the SINGLE definition of what that QR contains, so the\n * poster (owner web), the scanner (member web) and the verifier (backend) can\n * never drift apart.\n *\n * ── The code ────────────────────────────────────────────────────────────────\n * Every center owns one opaque `checkInCode`: 16 characters of Crockford\n * base32, minted server-side and stored on the Center. It is a SECRET-ish\n * handle, not a public identifier — the gym's public join code (`GYM-2026-001`)\n * is a separate, deliberately guessable thing and must never be usable to check\n * in. Crockford's alphabet omits I, L, O and U, so a code read aloud or typed\n * from a poster cannot be confused with 1/0, and no code can spell a word.\n *\n * ── The payload ─────────────────────────────────────────────────────────────\n * The QR encodes a URL — `https://gymmonk.in/checkin?c=<code>` — rather than\n * the bare code, so that a phone's built-in camera app (which is how most\n * people will scan a poster) opens the app instead of showing meaningless text.\n * `parseCheckInPayload` accepts the URL form, a bare code, and a legacy\n * `gymmonk:checkin:<code>` form, so a poster printed against one origin keeps\n * working if the domain ever changes.\n *\n * @module gymmonk-schema/check-in\n */\n\nimport { z } from 'zod';\nimport { centerCodeSchema } from './codes.js';\nimport { isoDateTimeSchema, objectIdSchema } from './common.js';\n\n// ─── The code ────────────────────────────────────────────────────────────────\n\n/** Length of a center's check-in code. 16 × 5 bits = 80 bits of entropy. */\nexport const CHECK_IN_CODE_LENGTH = 16;\n\n/**\n * Crockford base32 — the digits plus A-Z minus I, L, O and U. Kept here rather\n * than in the backend's code generator so the validator and the generator can\n * never disagree about which characters are legal.\n */\nexport const CHECK_IN_CODE_ALPHABET = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';\n\nconst CHECK_IN_CODE_PATTERN = /^[0-9A-HJKMNP-TV-Z]{16}$/;\n\nexport const checkInCodeSchema = z\n .string()\n .trim()\n .toUpperCase()\n .regex(CHECK_IN_CODE_PATTERN, 'Invalid check-in code');\nexport type CheckInCode = z.infer<typeof checkInCodeSchema>;\n\n// ─── The QR payload ──────────────────────────────────────────────────────────\n\n/** In-app route a scanned poster URL lands on. */\nexport const CHECK_IN_QR_PATH = '/checkin';\n/** Query parameter carrying the code in the poster URL. */\nexport const CHECK_IN_QR_PARAM = 'c';\n/** Legacy / non-URL payload prefix, still accepted by the parser. */\nconst CHECK_IN_URI_PREFIX = 'gymmonk:checkin:';\n\n/**\n * Build the string a center's QR poster encodes.\n *\n * `origin` is the app's public origin (`env.APP_URL`). A trailing slash is\n * tolerated because that is how the value is often pasted into an env file.\n */\nexport function buildCheckInPayload(origin: string, code: string): string {\n const base = origin.replace(/\\/+$/, '');\n return `${base}${CHECK_IN_QR_PATH}?${CHECK_IN_QR_PARAM}=${encodeURIComponent(code)}`;\n}\n\n/**\n * Pull the check-in code out of whatever the camera decoded.\n *\n * Returns the normalised (upper-case) code, or `null` when the QR is simply not\n * one of ours — the scanner uses that null to keep the camera running and say\n * \"that is not a gym QR\" rather than firing a doomed request. Accepts, in order:\n * 1. any URL carrying `?c=<code>` (the current poster format)\n * 2. `gymmonk:checkin:<code>` (legacy custom scheme)\n * 3. a bare code (manual entry, or a code-only QR)\n */\nexport function parseCheckInPayload(raw: string): string | null {\n const text = raw.trim();\n if (!text) return null;\n\n // 1. URL form. Hand-parse the query rather than using `new URL`, so a poster\n // encoded without a scheme (\"gymmonk.in/checkin?c=…\") still resolves.\n const queryAt = text.indexOf('?');\n if (queryAt !== -1) {\n const params = new URLSearchParams(text.slice(queryAt + 1));\n const fromQuery = params.get(CHECK_IN_QR_PARAM);\n if (fromQuery) return normaliseCode(fromQuery);\n return null;\n }\n\n // 2. Custom scheme.\n const lower = text.toLowerCase();\n if (lower.startsWith(CHECK_IN_URI_PREFIX)) {\n return normaliseCode(text.slice(CHECK_IN_URI_PREFIX.length));\n }\n\n // 3. Bare code.\n return normaliseCode(text);\n}\n\n/** Upper-case and validate a candidate code; `null` when it is not one. */\nfunction normaliseCode(candidate: string): string | null {\n const code = candidate.trim().toUpperCase();\n return CHECK_IN_CODE_PATTERN.test(code) ? code : null;\n}\n\n// ─── Owner-facing poster payload ─────────────────────────────────────────────\n\n/**\n * What the owner's \"Check-in QR\" screen renders. The backend returns the code\n * and the client builds the poster URL from its own `APP_URL`, so the same\n * center produces a dev-origin QR on dev and a prod-origin QR on prod without\n * the backend needing to know the frontend's domain.\n */\nexport const centerCheckInQrSchema = z.object({\n centerId: objectIdSchema,\n centerName: z.string(),\n /** The center's PUBLIC join code — shown as the human-readable caption. */\n gymCode: centerCodeSchema,\n /** The secret handle the QR encodes. */\n code: checkInCodeSchema,\n /** When the code was last rotated (null → never rotated since minting). */\n rotatedAt: isoDateTimeSchema.nullable(),\n});\nexport type CenterCheckInQr = z.infer<typeof centerCheckInQrSchema>;\n"]}
|
package/dist/codes.d.ts
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gymmonk-schema — Public identifier codes
|
|
3
|
+
* ========================================
|
|
4
|
+
* Every human-facing identifier the product issues, in one place: the shapes,
|
|
5
|
+
* the alphabet they draw from, and the composers that build them. The backend's
|
|
6
|
+
* generators import from here, which is what stops a generator from emitting a
|
|
7
|
+
* code its own validator rejects.
|
|
8
|
+
*
|
|
9
|
+
* THREE CODES, THREE JOBS. They are deliberately different shapes because they
|
|
10
|
+
* are read by different people in different places:
|
|
11
|
+
*
|
|
12
|
+
* ORG TAG `IRN` three letters, frozen at registration.
|
|
13
|
+
* Never shown alone; it is the stem the two
|
|
14
|
+
* codes below are built on.
|
|
15
|
+
*
|
|
16
|
+
* CENTRE CODE `IRN-1-7K2M` the id a stranger TYPES to find the gym.
|
|
17
|
+
* Optimised for reading off a WhatsApp
|
|
18
|
+
* message or a poster and typing correctly.
|
|
19
|
+
*
|
|
20
|
+
* GYM MEMBER ID `IRN1-0042` the roster number a gym issues to a person
|
|
21
|
+
* who trains there. Sequential within the
|
|
22
|
+
* centre, because "member 42" is a fact the
|
|
23
|
+
* gym uses and a random string is not.
|
|
24
|
+
*
|
|
25
|
+
* GYMMONK ID `GM-2026-00042` the person themself, one per account, for
|
|
26
|
+
* as long as the account exists.
|
|
27
|
+
*
|
|
28
|
+
* WHY THE TAG IS FROZEN AND NOT DERIVED ON READ. It is seeded from the
|
|
29
|
+
* organisation's name at registration and then never recomputed. A gym that
|
|
30
|
+
* rebrands keeps its codes, because those codes are already on signage, on
|
|
31
|
+
* member cards and in the gym's own paperwork — regenerating them would
|
|
32
|
+
* invalidate every one of them to fix a cosmetic mismatch nobody is harmed by.
|
|
33
|
+
*
|
|
34
|
+
* @module gymmonk-schema/codes
|
|
35
|
+
*/
|
|
36
|
+
import { z } from 'zod';
|
|
37
|
+
/**
|
|
38
|
+
* Crockford base32 — the digits plus A-Z minus I, L, O and U.
|
|
39
|
+
*
|
|
40
|
+
* I/1, L/1 and O/0 are the pairs people mistype when copying a code off a
|
|
41
|
+
* screen, and U is dropped so a random draw cannot spell something unfortunate.
|
|
42
|
+
* Shared with the check-in code, which draws from the same alphabet for the
|
|
43
|
+
* same reason.
|
|
44
|
+
*/
|
|
45
|
+
export declare const CODE_ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
46
|
+
/** Length of the organisation tag that stems every code a gym issues. */
|
|
47
|
+
export declare const ORG_TAG_LENGTH = 3;
|
|
48
|
+
/**
|
|
49
|
+
* Three letters identifying the gym business, unique across the platform.
|
|
50
|
+
*
|
|
51
|
+
* LETTERS ONLY, and not the code alphabet: the tag is the part a person is
|
|
52
|
+
* meant to RECOGNISE ("that's us"), so it is seeded from the organisation's
|
|
53
|
+
* name and reads as an abbreviation rather than as a random string. The
|
|
54
|
+
* digit-bearing parts of a code carry the uniqueness.
|
|
55
|
+
*/
|
|
56
|
+
export declare const orgTagSchema: z.ZodString;
|
|
57
|
+
export type OrgTag = z.infer<typeof orgTagSchema>;
|
|
58
|
+
/** Characters of random suffix on a centre code. 32^4 ≈ 1.05 million. */
|
|
59
|
+
export declare const CENTER_CODE_SUFFIX_LENGTH = 4;
|
|
60
|
+
/**
|
|
61
|
+
* A centre's public code, `IRN-1-7K2M` — organisation tag, the centre's number
|
|
62
|
+
* within that organisation, and a random suffix.
|
|
63
|
+
*
|
|
64
|
+
* WHY A RANDOM SUFFIX WHEN TAG + NUMBER IS ALREADY UNIQUE. Typo detection. The
|
|
65
|
+
* tag is unique per organisation and the number is unique within it, so
|
|
66
|
+
* `IRN-1` would identify the centre on its own — but then a member who typed
|
|
67
|
+
* `IRN-2` when they meant `IRN-1` would land silently on a real, wrong gym.
|
|
68
|
+
* With the suffix, a mistyped code finds nothing, which is the outcome that
|
|
69
|
+
* lets them notice.
|
|
70
|
+
*
|
|
71
|
+
* WHY NOT THE CREATION DATE. It was considered and dropped: eight more
|
|
72
|
+
* characters, in the one code whose entire job is to be typed correctly by a
|
|
73
|
+
* stranger, for a fact already on the record and shown beside it on screen.
|
|
74
|
+
*/
|
|
75
|
+
export declare const centerCodeSchema: z.ZodString;
|
|
76
|
+
export type CenterCode = z.infer<typeof centerCodeSchema>;
|
|
77
|
+
/** Build a centre code from its parts. The backend mints `suffix`. */
|
|
78
|
+
export declare function composeCenterCode(tag: string, sequence: number, suffix: string): string;
|
|
79
|
+
/** Minimum digits in the member half of a gym member id. Grows past 9,999. */
|
|
80
|
+
export declare const GYM_MEMBER_SEQ_DIGITS = 4;
|
|
81
|
+
/**
|
|
82
|
+
* A member's id AT ONE GYM, `IRN1-0042` — the centre's stem, then that
|
|
83
|
+
* centre's own running count of the people it has enrolled.
|
|
84
|
+
*
|
|
85
|
+
* SHAPED SO IT CANNOT BE MISTAKEN FOR A CENTRE CODE. Both start with the same
|
|
86
|
+
* stem, so they are separated by structure instead: a centre code always has
|
|
87
|
+
* two dashes and ends in the mixed code alphabet, a member id has one dash and
|
|
88
|
+
* ends in digits. Neither pattern matches the other's strings.
|
|
89
|
+
*
|
|
90
|
+
* ISSUED BY THE GYM, NOT THE PLATFORM. It exists from the moment the desk
|
|
91
|
+
* creates the record — before the person has an account, and whether or not
|
|
92
|
+
* they ever claim one. See `enrolment.ts` for the lifecycle.
|
|
93
|
+
*/
|
|
94
|
+
export declare const gymMemberCodeSchema: z.ZodString;
|
|
95
|
+
export type GymMemberCode = z.infer<typeof gymMemberCodeSchema>;
|
|
96
|
+
/** The stem a centre's member ids are built on — `IRN1` for `IRN-1-7K2M`. */
|
|
97
|
+
export declare function memberCodeStem(tag: string, centerSequence: number): string;
|
|
98
|
+
/** Build a gym member id from its parts. */
|
|
99
|
+
export declare function composeGymMemberCode(tag: string, centerSequence: number, sequence: number): string;
|
|
100
|
+
/**
|
|
101
|
+
* The person's own id, `GM-2026-00042`. One per account, for life.
|
|
102
|
+
*
|
|
103
|
+
* DELIBERATELY CARRIES NOTHING ABOUT A GYM, and nothing about the person
|
|
104
|
+
* either — no initials, no date of birth, no joining date. A name can be
|
|
105
|
+
* corrected and a person can change gyms; an identifier that encodes either
|
|
106
|
+
* becomes a lie the first time they do, on a card already in someone's wallet.
|
|
107
|
+
* What the gym needs a code to SAY is said by the gym member id instead.
|
|
108
|
+
*/
|
|
109
|
+
export declare const gymmonkIdSchema: z.ZodString;
|
|
110
|
+
export type GymmonkId = z.infer<typeof gymmonkIdSchema>;
|
|
111
|
+
/**
|
|
112
|
+
* What a SEARCH BOX accepts, as opposed to what the server stores.
|
|
113
|
+
*
|
|
114
|
+
* Kept separate from `centerCodeSchema` on purpose. A member typing their gym's
|
|
115
|
+
* code should be told "no gym found with that id" by the directory, which has
|
|
116
|
+
* actually looked — not "invalid gym code" by a regex, which reads as though
|
|
117
|
+
* they mistyped when they may simply be at a gym that has not registered.
|
|
118
|
+
* Normalisation (trim + upper-case) still happens here so the lookup matches.
|
|
119
|
+
*/
|
|
120
|
+
export declare const codeLookupSchema: z.ZodString;
|
|
121
|
+
//# sourceMappingURL=codes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codes.d.ts","sourceRoot":"","sources":["../src/codes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa,qCAAqC,CAAC;AAOhE,yEAAyE;AACzE,eAAO,MAAM,cAAc,IAAI,CAAC;AAEhC;;;;;;;GAOG;AACH,eAAO,MAAM,YAAY,aAIyB,CAAC;AACnD,MAAM,MAAM,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAIlD,yEAAyE;AACzE,eAAO,MAAM,yBAAyB,IAAI,CAAC;AAI3C;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,gBAAgB,aAIoB,CAAC;AAClD,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAE1D,sEAAsE;AACtE,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAEvF;AAID,8EAA8E;AAC9E,eAAO,MAAM,qBAAqB,IAAI,CAAC;AAIvC;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,mBAAmB,aAIsB,CAAC;AACvD,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEhE,6EAA6E;AAC7E,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,MAAM,CAE1E;AAED,4CAA4C;AAC5C,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,MAAM,EACX,cAAc,EAAE,MAAM,EACtB,QAAQ,EAAE,MAAM,GACf,MAAM,CAGR;AAMD;;;;;;;;GAQG;AACH,eAAO,MAAM,eAAe,aAIsB,CAAC;AACnD,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAIxD;;;;;;;;GAQG;AACH,eAAO,MAAM,gBAAgB,aAAiD,CAAC"}
|
package/dist/codes.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gymmonk-schema — Public identifier codes
|
|
3
|
+
* ========================================
|
|
4
|
+
* Every human-facing identifier the product issues, in one place: the shapes,
|
|
5
|
+
* the alphabet they draw from, and the composers that build them. The backend's
|
|
6
|
+
* generators import from here, which is what stops a generator from emitting a
|
|
7
|
+
* code its own validator rejects.
|
|
8
|
+
*
|
|
9
|
+
* THREE CODES, THREE JOBS. They are deliberately different shapes because they
|
|
10
|
+
* are read by different people in different places:
|
|
11
|
+
*
|
|
12
|
+
* ORG TAG `IRN` three letters, frozen at registration.
|
|
13
|
+
* Never shown alone; it is the stem the two
|
|
14
|
+
* codes below are built on.
|
|
15
|
+
*
|
|
16
|
+
* CENTRE CODE `IRN-1-7K2M` the id a stranger TYPES to find the gym.
|
|
17
|
+
* Optimised for reading off a WhatsApp
|
|
18
|
+
* message or a poster and typing correctly.
|
|
19
|
+
*
|
|
20
|
+
* GYM MEMBER ID `IRN1-0042` the roster number a gym issues to a person
|
|
21
|
+
* who trains there. Sequential within the
|
|
22
|
+
* centre, because "member 42" is a fact the
|
|
23
|
+
* gym uses and a random string is not.
|
|
24
|
+
*
|
|
25
|
+
* GYMMONK ID `GM-2026-00042` the person themself, one per account, for
|
|
26
|
+
* as long as the account exists.
|
|
27
|
+
*
|
|
28
|
+
* WHY THE TAG IS FROZEN AND NOT DERIVED ON READ. It is seeded from the
|
|
29
|
+
* organisation's name at registration and then never recomputed. A gym that
|
|
30
|
+
* rebrands keeps its codes, because those codes are already on signage, on
|
|
31
|
+
* member cards and in the gym's own paperwork — regenerating them would
|
|
32
|
+
* invalidate every one of them to fix a cosmetic mismatch nobody is harmed by.
|
|
33
|
+
*
|
|
34
|
+
* @module gymmonk-schema/codes
|
|
35
|
+
*/
|
|
36
|
+
import { z } from 'zod';
|
|
37
|
+
// ─── Alphabet ────────────────────────────────────────────────────────────────
|
|
38
|
+
/**
|
|
39
|
+
* Crockford base32 — the digits plus A-Z minus I, L, O and U.
|
|
40
|
+
*
|
|
41
|
+
* I/1, L/1 and O/0 are the pairs people mistype when copying a code off a
|
|
42
|
+
* screen, and U is dropped so a random draw cannot spell something unfortunate.
|
|
43
|
+
* Shared with the check-in code, which draws from the same alphabet for the
|
|
44
|
+
* same reason.
|
|
45
|
+
*/
|
|
46
|
+
export const CODE_ALPHABET = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
|
|
47
|
+
/** The character class matching {@link CODE_ALPHABET}, for use inside patterns. */
|
|
48
|
+
const CODE_CHAR = '[0-9A-HJKMNP-TV-Z]';
|
|
49
|
+
// ─── Organisation tag ────────────────────────────────────────────────────────
|
|
50
|
+
/** Length of the organisation tag that stems every code a gym issues. */
|
|
51
|
+
export const ORG_TAG_LENGTH = 3;
|
|
52
|
+
/**
|
|
53
|
+
* Three letters identifying the gym business, unique across the platform.
|
|
54
|
+
*
|
|
55
|
+
* LETTERS ONLY, and not the code alphabet: the tag is the part a person is
|
|
56
|
+
* meant to RECOGNISE ("that's us"), so it is seeded from the organisation's
|
|
57
|
+
* name and reads as an abbreviation rather than as a random string. The
|
|
58
|
+
* digit-bearing parts of a code carry the uniqueness.
|
|
59
|
+
*/
|
|
60
|
+
export const orgTagSchema = z
|
|
61
|
+
.string()
|
|
62
|
+
.trim()
|
|
63
|
+
.toUpperCase()
|
|
64
|
+
.regex(/^[A-Z]{3}$/, 'Invalid organisation tag');
|
|
65
|
+
// ─── Centre code ─────────────────────────────────────────────────────────────
|
|
66
|
+
/** Characters of random suffix on a centre code. 32^4 ≈ 1.05 million. */
|
|
67
|
+
export const CENTER_CODE_SUFFIX_LENGTH = 4;
|
|
68
|
+
const CENTER_CODE_PATTERN = new RegExp(`^[A-Z]{3}-\\d{1,3}-${CODE_CHAR}{4}$`);
|
|
69
|
+
/**
|
|
70
|
+
* A centre's public code, `IRN-1-7K2M` — organisation tag, the centre's number
|
|
71
|
+
* within that organisation, and a random suffix.
|
|
72
|
+
*
|
|
73
|
+
* WHY A RANDOM SUFFIX WHEN TAG + NUMBER IS ALREADY UNIQUE. Typo detection. The
|
|
74
|
+
* tag is unique per organisation and the number is unique within it, so
|
|
75
|
+
* `IRN-1` would identify the centre on its own — but then a member who typed
|
|
76
|
+
* `IRN-2` when they meant `IRN-1` would land silently on a real, wrong gym.
|
|
77
|
+
* With the suffix, a mistyped code finds nothing, which is the outcome that
|
|
78
|
+
* lets them notice.
|
|
79
|
+
*
|
|
80
|
+
* WHY NOT THE CREATION DATE. It was considered and dropped: eight more
|
|
81
|
+
* characters, in the one code whose entire job is to be typed correctly by a
|
|
82
|
+
* stranger, for a fact already on the record and shown beside it on screen.
|
|
83
|
+
*/
|
|
84
|
+
export const centerCodeSchema = z
|
|
85
|
+
.string()
|
|
86
|
+
.trim()
|
|
87
|
+
.toUpperCase()
|
|
88
|
+
.regex(CENTER_CODE_PATTERN, 'Invalid gym code');
|
|
89
|
+
/** Build a centre code from its parts. The backend mints `suffix`. */
|
|
90
|
+
export function composeCenterCode(tag, sequence, suffix) {
|
|
91
|
+
return `${tag.toUpperCase()}-${sequence}-${suffix.toUpperCase()}`;
|
|
92
|
+
}
|
|
93
|
+
// ─── Gym member id ───────────────────────────────────────────────────────────
|
|
94
|
+
/** Minimum digits in the member half of a gym member id. Grows past 9,999. */
|
|
95
|
+
export const GYM_MEMBER_SEQ_DIGITS = 4;
|
|
96
|
+
const GYM_MEMBER_CODE_PATTERN = /^[A-Z]{3}\d{1,3}-\d{4,}$/;
|
|
97
|
+
/**
|
|
98
|
+
* A member's id AT ONE GYM, `IRN1-0042` — the centre's stem, then that
|
|
99
|
+
* centre's own running count of the people it has enrolled.
|
|
100
|
+
*
|
|
101
|
+
* SHAPED SO IT CANNOT BE MISTAKEN FOR A CENTRE CODE. Both start with the same
|
|
102
|
+
* stem, so they are separated by structure instead: a centre code always has
|
|
103
|
+
* two dashes and ends in the mixed code alphabet, a member id has one dash and
|
|
104
|
+
* ends in digits. Neither pattern matches the other's strings.
|
|
105
|
+
*
|
|
106
|
+
* ISSUED BY THE GYM, NOT THE PLATFORM. It exists from the moment the desk
|
|
107
|
+
* creates the record — before the person has an account, and whether or not
|
|
108
|
+
* they ever claim one. See `enrolment.ts` for the lifecycle.
|
|
109
|
+
*/
|
|
110
|
+
export const gymMemberCodeSchema = z
|
|
111
|
+
.string()
|
|
112
|
+
.trim()
|
|
113
|
+
.toUpperCase()
|
|
114
|
+
.regex(GYM_MEMBER_CODE_PATTERN, 'Invalid member id');
|
|
115
|
+
/** The stem a centre's member ids are built on — `IRN1` for `IRN-1-7K2M`. */
|
|
116
|
+
export function memberCodeStem(tag, centerSequence) {
|
|
117
|
+
return `${tag.toUpperCase()}${centerSequence}`;
|
|
118
|
+
}
|
|
119
|
+
/** Build a gym member id from its parts. */
|
|
120
|
+
export function composeGymMemberCode(tag, centerSequence, sequence) {
|
|
121
|
+
const stem = memberCodeStem(tag, centerSequence);
|
|
122
|
+
return `${stem}-${String(sequence).padStart(GYM_MEMBER_SEQ_DIGITS, '0')}`;
|
|
123
|
+
}
|
|
124
|
+
// ─── GymMonk id (the person) ─────────────────────────────────────────────────
|
|
125
|
+
const GYMMONK_ID_PATTERN = /^GM-\d{4}-\d{5,}$/;
|
|
126
|
+
/**
|
|
127
|
+
* The person's own id, `GM-2026-00042`. One per account, for life.
|
|
128
|
+
*
|
|
129
|
+
* DELIBERATELY CARRIES NOTHING ABOUT A GYM, and nothing about the person
|
|
130
|
+
* either — no initials, no date of birth, no joining date. A name can be
|
|
131
|
+
* corrected and a person can change gyms; an identifier that encodes either
|
|
132
|
+
* becomes a lie the first time they do, on a card already in someone's wallet.
|
|
133
|
+
* What the gym needs a code to SAY is said by the gym member id instead.
|
|
134
|
+
*/
|
|
135
|
+
export const gymmonkIdSchema = z
|
|
136
|
+
.string()
|
|
137
|
+
.trim()
|
|
138
|
+
.toUpperCase()
|
|
139
|
+
.regex(GYMMONK_ID_PATTERN, 'Invalid GymMonk id');
|
|
140
|
+
// ─── Lenient input ───────────────────────────────────────────────────────────
|
|
141
|
+
/**
|
|
142
|
+
* What a SEARCH BOX accepts, as opposed to what the server stores.
|
|
143
|
+
*
|
|
144
|
+
* Kept separate from `centerCodeSchema` on purpose. A member typing their gym's
|
|
145
|
+
* code should be told "no gym found with that id" by the directory, which has
|
|
146
|
+
* actually looked — not "invalid gym code" by a regex, which reads as though
|
|
147
|
+
* they mistyped when they may simply be at a gym that has not registered.
|
|
148
|
+
* Normalisation (trim + upper-case) still happens here so the lookup matches.
|
|
149
|
+
*/
|
|
150
|
+
export const codeLookupSchema = z.string().trim().toUpperCase().min(1).max(40);
|
|
151
|
+
//# sourceMappingURL=codes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codes.js","sourceRoot":"","sources":["../src/codes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,gFAAgF;AAEhF;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,kCAAkC,CAAC;AAEhE,mFAAmF;AACnF,MAAM,SAAS,GAAG,oBAAoB,CAAC;AAEvC,gFAAgF;AAEhF,yEAAyE;AACzE,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,CAAC;AAEhC;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC;KAC1B,MAAM,EAAE;KACR,IAAI,EAAE;KACN,WAAW,EAAE;KACb,KAAK,CAAC,YAAY,EAAE,0BAA0B,CAAC,CAAC;AAGnD,gFAAgF;AAEhF,yEAAyE;AACzE,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,CAAC;AAE3C,MAAM,mBAAmB,GAAG,IAAI,MAAM,CAAC,sBAAsB,SAAS,MAAM,CAAC,CAAC;AAE9E;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC;KAC9B,MAAM,EAAE;KACR,IAAI,EAAE;KACN,WAAW,EAAE;KACb,KAAK,CAAC,mBAAmB,EAAE,kBAAkB,CAAC,CAAC;AAGlD,sEAAsE;AACtE,MAAM,UAAU,iBAAiB,CAAC,GAAW,EAAE,QAAgB,EAAE,MAAc;IAC7E,OAAO,GAAG,GAAG,CAAC,WAAW,EAAE,IAAI,QAAQ,IAAI,MAAM,CAAC,WAAW,EAAE,EAAE,CAAC;AACpE,CAAC;AAED,gFAAgF;AAEhF,8EAA8E;AAC9E,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,CAAC;AAEvC,MAAM,uBAAuB,GAAG,0BAA0B,CAAC;AAE3D;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC;KACjC,MAAM,EAAE;KACR,IAAI,EAAE;KACN,WAAW,EAAE;KACb,KAAK,CAAC,uBAAuB,EAAE,mBAAmB,CAAC,CAAC;AAGvD,6EAA6E;AAC7E,MAAM,UAAU,cAAc,CAAC,GAAW,EAAE,cAAsB;IAChE,OAAO,GAAG,GAAG,CAAC,WAAW,EAAE,GAAG,cAAc,EAAE,CAAC;AACjD,CAAC;AAED,4CAA4C;AAC5C,MAAM,UAAU,oBAAoB,CAClC,GAAW,EACX,cAAsB,EACtB,QAAgB;IAEhB,MAAM,IAAI,GAAG,cAAc,CAAC,GAAG,EAAE,cAAc,CAAC,CAAC;IACjD,OAAO,GAAG,IAAI,IAAI,MAAM,CAAC,QAAQ,CAAC,CAAC,QAAQ,CAAC,qBAAqB,EAAE,GAAG,CAAC,EAAE,CAAC;AAC5E,CAAC;AAED,gFAAgF;AAEhF,MAAM,kBAAkB,GAAG,mBAAmB,CAAC;AAE/C;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC;KAC7B,MAAM,EAAE;KACR,IAAI,EAAE;KACN,WAAW,EAAE;KACb,KAAK,CAAC,kBAAkB,EAAE,oBAAoB,CAAC,CAAC;AAGnD,gFAAgF;AAEhF;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC","sourcesContent":["/**\n * gymmonk-schema — Public identifier codes\n * ========================================\n * Every human-facing identifier the product issues, in one place: the shapes,\n * the alphabet they draw from, and the composers that build them. The backend's\n * generators import from here, which is what stops a generator from emitting a\n * code its own validator rejects.\n *\n * THREE CODES, THREE JOBS. They are deliberately different shapes because they\n * are read by different people in different places:\n *\n * ORG TAG `IRN` three letters, frozen at registration.\n * Never shown alone; it is the stem the two\n * codes below are built on.\n *\n * CENTRE CODE `IRN-1-7K2M` the id a stranger TYPES to find the gym.\n * Optimised for reading off a WhatsApp\n * message or a poster and typing correctly.\n *\n * GYM MEMBER ID `IRN1-0042` the roster number a gym issues to a person\n * who trains there. Sequential within the\n * centre, because \"member 42\" is a fact the\n * gym uses and a random string is not.\n *\n * GYMMONK ID `GM-2026-00042` the person themself, one per account, for\n * as long as the account exists.\n *\n * WHY THE TAG IS FROZEN AND NOT DERIVED ON READ. It is seeded from the\n * organisation's name at registration and then never recomputed. A gym that\n * rebrands keeps its codes, because those codes are already on signage, on\n * member cards and in the gym's own paperwork — regenerating them would\n * invalidate every one of them to fix a cosmetic mismatch nobody is harmed by.\n *\n * @module gymmonk-schema/codes\n */\n\nimport { z } from 'zod';\n\n// ─── Alphabet ────────────────────────────────────────────────────────────────\n\n/**\n * Crockford base32 — the digits plus A-Z minus I, L, O and U.\n *\n * I/1, L/1 and O/0 are the pairs people mistype when copying a code off a\n * screen, and U is dropped so a random draw cannot spell something unfortunate.\n * Shared with the check-in code, which draws from the same alphabet for the\n * same reason.\n */\nexport const CODE_ALPHABET = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';\n\n/** The character class matching {@link CODE_ALPHABET}, for use inside patterns. */\nconst CODE_CHAR = '[0-9A-HJKMNP-TV-Z]';\n\n// ─── Organisation tag ────────────────────────────────────────────────────────\n\n/** Length of the organisation tag that stems every code a gym issues. */\nexport const ORG_TAG_LENGTH = 3;\n\n/**\n * Three letters identifying the gym business, unique across the platform.\n *\n * LETTERS ONLY, and not the code alphabet: the tag is the part a person is\n * meant to RECOGNISE (\"that's us\"), so it is seeded from the organisation's\n * name and reads as an abbreviation rather than as a random string. The\n * digit-bearing parts of a code carry the uniqueness.\n */\nexport const orgTagSchema = z\n .string()\n .trim()\n .toUpperCase()\n .regex(/^[A-Z]{3}$/, 'Invalid organisation tag');\nexport type OrgTag = z.infer<typeof orgTagSchema>;\n\n// ─── Centre code ─────────────────────────────────────────────────────────────\n\n/** Characters of random suffix on a centre code. 32^4 ≈ 1.05 million. */\nexport const CENTER_CODE_SUFFIX_LENGTH = 4;\n\nconst CENTER_CODE_PATTERN = new RegExp(`^[A-Z]{3}-\\\\d{1,3}-${CODE_CHAR}{4}$`);\n\n/**\n * A centre's public code, `IRN-1-7K2M` — organisation tag, the centre's number\n * within that organisation, and a random suffix.\n *\n * WHY A RANDOM SUFFIX WHEN TAG + NUMBER IS ALREADY UNIQUE. Typo detection. The\n * tag is unique per organisation and the number is unique within it, so\n * `IRN-1` would identify the centre on its own — but then a member who typed\n * `IRN-2` when they meant `IRN-1` would land silently on a real, wrong gym.\n * With the suffix, a mistyped code finds nothing, which is the outcome that\n * lets them notice.\n *\n * WHY NOT THE CREATION DATE. It was considered and dropped: eight more\n * characters, in the one code whose entire job is to be typed correctly by a\n * stranger, for a fact already on the record and shown beside it on screen.\n */\nexport const centerCodeSchema = z\n .string()\n .trim()\n .toUpperCase()\n .regex(CENTER_CODE_PATTERN, 'Invalid gym code');\nexport type CenterCode = z.infer<typeof centerCodeSchema>;\n\n/** Build a centre code from its parts. The backend mints `suffix`. */\nexport function composeCenterCode(tag: string, sequence: number, suffix: string): string {\n return `${tag.toUpperCase()}-${sequence}-${suffix.toUpperCase()}`;\n}\n\n// ─── Gym member id ───────────────────────────────────────────────────────────\n\n/** Minimum digits in the member half of a gym member id. Grows past 9,999. */\nexport const GYM_MEMBER_SEQ_DIGITS = 4;\n\nconst GYM_MEMBER_CODE_PATTERN = /^[A-Z]{3}\\d{1,3}-\\d{4,}$/;\n\n/**\n * A member's id AT ONE GYM, `IRN1-0042` — the centre's stem, then that\n * centre's own running count of the people it has enrolled.\n *\n * SHAPED SO IT CANNOT BE MISTAKEN FOR A CENTRE CODE. Both start with the same\n * stem, so they are separated by structure instead: a centre code always has\n * two dashes and ends in the mixed code alphabet, a member id has one dash and\n * ends in digits. Neither pattern matches the other's strings.\n *\n * ISSUED BY THE GYM, NOT THE PLATFORM. It exists from the moment the desk\n * creates the record — before the person has an account, and whether or not\n * they ever claim one. See `enrolment.ts` for the lifecycle.\n */\nexport const gymMemberCodeSchema = z\n .string()\n .trim()\n .toUpperCase()\n .regex(GYM_MEMBER_CODE_PATTERN, 'Invalid member id');\nexport type GymMemberCode = z.infer<typeof gymMemberCodeSchema>;\n\n/** The stem a centre's member ids are built on — `IRN1` for `IRN-1-7K2M`. */\nexport function memberCodeStem(tag: string, centerSequence: number): string {\n return `${tag.toUpperCase()}${centerSequence}`;\n}\n\n/** Build a gym member id from its parts. */\nexport function composeGymMemberCode(\n tag: string,\n centerSequence: number,\n sequence: number,\n): string {\n const stem = memberCodeStem(tag, centerSequence);\n return `${stem}-${String(sequence).padStart(GYM_MEMBER_SEQ_DIGITS, '0')}`;\n}\n\n// ─── GymMonk id (the person) ─────────────────────────────────────────────────\n\nconst GYMMONK_ID_PATTERN = /^GM-\\d{4}-\\d{5,}$/;\n\n/**\n * The person's own id, `GM-2026-00042`. One per account, for life.\n *\n * DELIBERATELY CARRIES NOTHING ABOUT A GYM, and nothing about the person\n * either — no initials, no date of birth, no joining date. A name can be\n * corrected and a person can change gyms; an identifier that encodes either\n * becomes a lie the first time they do, on a card already in someone's wallet.\n * What the gym needs a code to SAY is said by the gym member id instead.\n */\nexport const gymmonkIdSchema = z\n .string()\n .trim()\n .toUpperCase()\n .regex(GYMMONK_ID_PATTERN, 'Invalid GymMonk id');\nexport type GymmonkId = z.infer<typeof gymmonkIdSchema>;\n\n// ─── Lenient input ───────────────────────────────────────────────────────────\n\n/**\n * What a SEARCH BOX accepts, as opposed to what the server stores.\n *\n * Kept separate from `centerCodeSchema` on purpose. A member typing their gym's\n * code should be told \"no gym found with that id\" by the directory, which has\n * actually looked — not \"invalid gym code\" by a regex, which reads as though\n * they mistyped when they may simply be at a gym that has not registered.\n * Normalisation (trim + upper-case) still happens here so the lookup matches.\n */\nexport const codeLookupSchema = z.string().trim().toUpperCase().min(1).max(40);\n"]}
|