gymmonk-schema 0.62.0 → 0.63.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.
@@ -139,7 +139,6 @@ export type AddMemberFormValues = {
139
139
  * ask anyone for.
140
140
  */
141
141
  export declare const addStaffFormSchema: z.ZodObject<{
142
- email: z.ZodEmail;
143
142
  role: z.ZodNullable<z.ZodEnum<{
144
143
  trainer: "trainer";
145
144
  manager: "manager";
@@ -174,8 +173,6 @@ export declare const addStaffFormSchema: z.ZodObject<{
174
173
  }, z.core.$strip>>;
175
174
  }, z.core.$strip>;
176
175
  export type AddStaffFormValues = Omit<AddMemberFormValues, 'trainerId' | 'joiningDate'> & {
177
- /** Staff still give one; members no longer do. See `addStaffFormSchema`. */
178
- email: string;
179
176
  role: z.infer<typeof staffRoleSchema> | null;
180
177
  };
181
178
  /** One blank row so the section renders with a contact to fill. */
@@ -187,7 +184,12 @@ export declare const EMPTY_ADD_STAFF_FORM: AddStaffFormValues;
187
184
  * membership step, which the form itself knows nothing about.
188
185
  */
189
186
  export declare function toCreateMemberBody(values: AddMemberFormValues, initialPlan?: AssignMembershipBody): CreateMemberBody;
190
- /** Validated staff form → `POST /staff` body. */
187
+ /**
188
+ * Validated staff form → `POST /staff` body.
189
+ *
190
+ * The staff body is now the shared body plus a role — no `email`. The two
191
+ * variants finally differ by exactly the one thing that actually differs.
192
+ */
191
193
  export declare function toCreateStaffBody(values: AddStaffFormValues): CreateStaffBody;
192
194
  /**
193
195
  * Editing a member asks for exactly what adding one asks for.
@@ -1 +1 @@
1
- {"version":3,"file":"add-person-form.d.ts","sourceRoot":"","sources":["../src/add-person-form.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"add-person-form.d.ts","sourceRoot":"","sources":["../src/add-person-form.ts"],"names":[],"mappings":"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"}
@@ -106,20 +106,22 @@ const addPersonBaseShape = {
106
106
  /** One field on screen; split into `firstName` / `lastName` on the wire. */
107
107
  name: z.string().trim().min(1, 'Name is required').max(160),
108
108
  /**
109
- * ⚠️ `email` USED TO BE HERE, on the shared base, and it has moved to the
110
- * STAFF form only. Do not put it back.
109
+ * ⚠️ `email` USED TO BE HERE, on the shared base. It is now on NEITHER form.
110
+ * Do not put it back on either one.
111
111
  *
112
112
  * It was required of a front desk adding a member, and it was the field the
113
- * claim was matched on — the member had to register with the exact address
113
+ * claim was matched on — the person had to register with the exact address
114
114
  * the desk had typed. The desk does not reliably know it. The phone is the
115
- * join key now, and `createMemberBodySchema` has no `email` at all.
115
+ * join key now, and neither `createMemberBodySchema` nor
116
+ * `createStaffBodySchema` has an `email` at all.
116
117
  *
117
118
  * Leaving it here was not harmless: Zod strips unknown keys, so the desk
118
119
  * typed an address, the form accepted it, the mapper sent it, the API schema
119
120
  * DELETED it and the create succeeded. Silence at every step, for a field
120
121
  * that used to be the join key. A 400 would have been kinder.
121
122
  *
122
- * STAFF still ask for it, and that is deliberate see `addStaffFormSchema`.
123
+ * Staff held onto it one release longer on an employment-versus-membership
124
+ * argument. See `addStaffFormSchema` for why that did not survive.
123
125
  */
124
126
  phone: z.string().regex(PHONE_REGEX, 'Enter a valid 10-digit mobile number'),
125
127
  /**
@@ -220,21 +222,25 @@ export const addStaffFormSchema = z
220
222
  .object({
221
223
  ...addPersonBaseShape,
222
224
  /**
223
- * Staff DO give an email, and members no longer do. The asymmetry is
224
- * deliberate, not an oversight.
225
+ * ⚠️ NO `email`, FOR STAFF EITHER. It was the last asymmetry between the
226
+ * two variants and it is gone (user, 2026-08-27).
225
227
  *
226
- * `createStaffBodySchema` still requires it, so removing it here would send
227
- * a body the API refuses. But the reason it is still required is the point:
228
- * employment is not membership. A gym hiring a trainer has their email —
229
- * there is a contract, a payroll ledger and a person who applied for the
230
- * job whereas a member walking in off the street is exactly the case
231
- * where asking for one was friction the owners told us to remove.
228
+ * The argument for keeping it was that employment is not membership — a
229
+ * gym hiring a trainer has a contract and a payroll ledger, so it knows the
230
+ * address. That argument survived only because the field predated the join
231
+ * key moving to the phone. Once `claimStaffInvite` stopped matching on the
232
+ * address, the field stopped GATING anything and became one more thing to
233
+ * type at a desk, on the same screen where we had just finished arguing
234
+ * that a member should not be asked for one.
232
235
  *
233
- * Note this is no longer a GATE either way: `claimStaffInvite` matches on
234
- * the code alone now, same as members. This is a contact detail the gym
235
- * keeps, not the thing that links two records.
236
+ * What it did still do was back the duplicate-invite guard, and that has
237
+ * moved to `invitePhone` in `staff.service` the same key members use, and
238
+ * a stricter check than the one it replaced.
239
+ *
240
+ * The read side keeps `email` (`staffListItemSchema`, staff detail): a
241
+ * CLAIMED staff member has one from their central-auth account, and the
242
+ * roster card offers it as a contact. Nullable there, which it already was.
236
243
  */
237
- email: z.email('Enter a valid email'),
238
244
  role: requiredChoice(staffRoleSchema, 'Select a staff role'),
239
245
  })
240
246
  .superRefine((v, ctx) => {
@@ -267,7 +273,6 @@ export const EMPTY_ADD_MEMBER_FORM = {
267
273
  export const EMPTY_ADD_STAFF_FORM = {
268
274
  ...EMPTY_BASE,
269
275
  contacts: [{ ...EMPTY_EMERGENCY_CONTACT }],
270
- email: '',
271
276
  role: null,
272
277
  };
273
278
  // ─── Form → API body ─────────────────────────────────────────────────────────
@@ -335,12 +340,15 @@ export function toCreateMemberBody(values, initialPlan) {
335
340
  ...(initialPlan ? { initialPlan } : {}),
336
341
  };
337
342
  }
338
- /** Validated staff form → `POST /staff` body. */
343
+ /**
344
+ * Validated staff form → `POST /staff` body.
345
+ *
346
+ * The staff body is now the shared body plus a role — no `email`. The two
347
+ * variants finally differ by exactly the one thing that actually differs.
348
+ */
339
349
  export function toCreateStaffBody(values) {
340
350
  return {
341
351
  ...sharedBody(values),
342
- // Staff only — `createStaffBodySchema` still requires it. See the schema.
343
- email: values.email.trim(),
344
352
  // The form schema guarantees a role was chosen.
345
353
  role: values.role ?? 'trainer',
346
354
  };
@@ -1 +1 @@
1
- {"version":3,"file":"add-person-form.js","sourceRoot":"","sources":["../src/add-person-form.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * gymmonk-schema — \"Add member\" / \"Add staff\" FORM schemas\n * ========================================================\n * The client-side validation contract for the owner's desk-entry forms, and the\n * mappers that turn a filled form into the `createMember` / `createStaff` API\n * bodies.\n *\n * Why these live here rather than in the client, and why they are distinct from\n * `createMemberBodySchema`: the form's working shape is not the wire shape. It\n * holds ONE \"name\" field the API splits in two, `null` for a choice not yet\n * made, and `''` for an optional value left blank. Those are legitimate mid-edit\n * states that the API body must never see. Expressing the same rules over the\n * form shape here keeps\n * every message, bound and regex defined once — the client cannot drift from\n * what the server will accept, and the coercion from one shape to the other is\n * part of the contract instead of ad-hoc code in a component.\n *\n * Mirrors `onboarding-form.ts`, which does the same job for the wizards.\n *\n * @module gymmonk-schema/add-person-form\n */\n\nimport { z } from 'zod';\nimport { ageInYears, isRealDate, PHONE_REGEX } from './common.js';\nimport { DEFAULT_TIMEZONE } from './location.js';\nimport type { CreateMemberBody, UpdateMemberBody } from './member.js';\nimport { emergencyContactSchema, MAX_EMERGENCY_CONTACTS } from './member-profile.js';\nimport type { AssignMembershipBody } from './membership.js';\nimport { bloodGroupSchema, genderSchema } from './shared.js';\nimport type { CreateStaffBody } from './staff.js';\nimport { staffRoleSchema } from './staff.js';\n\n// ─── Bounds ──────────────────────────────────────────────────────────────────\n\n/**\n * Age bounds for someone signed up at the desk. Wider than the member wizard's\n * because a gym also enrols juniors with a guardian present, and the date is\n * typed by staff rather than by the person themselves.\n */\nexport const ADD_PERSON_MIN_AGE_YEARS = 5;\nexport const ADD_PERSON_MAX_AGE_YEARS = 100;\n\n// ─── Date of birth, as three cells ───────────────────────────────────────────\n\n/**\n * The three cells joined into the `YYYY-MM-DD` the API stores, or `undefined`\n * when nothing was entered.\n *\n * Desk entry does not require a date of birth — a gym adds people with\n * whatever it has to hand — so \"blank\" is a real answer and has to survive the\n * whole way to the body as an ABSENT field rather than an empty string the\n * API would reject.\n */\nexport function isoFromDobCells(day: string, month: string, year: string): string | undefined {\n if (!day || !month || !year) return undefined;\n return `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`;\n}\n\n/** A stored `YYYY-MM-DD` split back into the cells, for seeding an edit form. */\nexport function dobCellsFromIso(iso: string | null | undefined): {\n dobDay: string;\n dobMonth: string;\n dobYear: string;\n} {\n const match = /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(iso ?? '');\n if (!match) return { dobDay: '', dobMonth: '', dobYear: '' };\n return { dobYear: match[1] as string, dobMonth: match[2] as string, dobDay: match[3] as string };\n}\n\n// ─── Field-level rules ───────────────────────────────────────────────────────\n\nconst requiredChoice = <T extends z.ZodTypeAny>(inner: T, message: string) =>\n inner.nullable().refine((v: unknown) => v !== null, { message });\n\n// ─── One emergency-contact row ───────────────────────────────────────────────\n\n/**\n * One emergency-contact row: WHO to call, on what number, and how they are\n * related. A row is either wholly blank (ignored) or names a person.\n *\n * ── Why `name` exists now ───────────────────────────────────────────────────\n * This form used to collect a relationship and a phone with no name at all,\n * and the mapper wrote the relationship INTO `name` because\n * `emergencyContactSchema` requires one. The member's own surfaces store a real\n * name, so the two halves of the product disagreed about what the field held,\n * and the disagreement destroyed data: a member onboards with \"Priya\", the\n * owner opens Edit Member and sees Relationship prefilled \"Priya\" (the reader\n * fell back to `name`), corrects it to \"Sister\", saves — and the contact is now\n * called Sister. Whoever has to ring that number in an emergency is left with\n * a word instead of a person.\n *\n * `relationship` stays OPTIONAL, matching `emergencyContactSchema`. It is\n * useful context, not something to block a desk entry over.\n */\nexport const emergencyContactRowSchema = z\n .object({\n name: z.string().trim().max(120),\n relationship: z.string().trim().max(60),\n phone: z.string().trim(),\n })\n .superRefine((v, ctx) => {\n const blank = v.name === '' && v.relationship === '' && v.phone === '';\n if (blank) return;\n if (v.name === '') {\n ctx.addIssue({\n code: 'custom',\n path: ['name'],\n message: \"Add the contact's name\",\n });\n }\n if (!PHONE_REGEX.test(v.phone)) {\n ctx.addIssue({\n code: 'custom',\n path: ['phone'],\n message: 'Enter a valid 10-digit mobile number',\n });\n }\n });\n\nexport interface EmergencyContactRow {\n name: string;\n relationship: string;\n phone: string;\n}\n\n// ─── Shared base ─────────────────────────────────────────────────────────────\n\nconst addPersonBaseShape = {\n /** One field on screen; split into `firstName` / `lastName` on the wire. */\n name: z.string().trim().min(1, 'Name is required').max(160),\n /**\n * ⚠️ `email` USED TO BE HERE, on the shared base, and it has moved to the\n * STAFF form only. Do not put it back.\n *\n * It was required of a front desk adding a member, and it was the field the\n * claim was matched on — the member had to register with the exact address\n * the desk had typed. The desk does not reliably know it. The phone is the\n * join key now, and `createMemberBodySchema` has no `email` at all.\n *\n * Leaving it here was not harmless: Zod strips unknown keys, so the desk\n * typed an address, the form accepted it, the mapper sent it, the API schema\n * DELETED it and the create succeeded. Silence at every step, for a field\n * that used to be the join key. A 400 would have been kinder.\n *\n * STAFF still ask for it, and that is deliberate — see `addStaffFormSchema`.\n */\n phone: z.string().regex(PHONE_REGEX, 'Enter a valid 10-digit mobile number'),\n /**\n * Three cells, not a native date input.\n *\n * `<input type=\"date\">` renders in the BROWSER'S locale, so the same field\n * read \"mm/dd/yyyy\" at an Indian gym's front desk — and a member's own\n * onboarding asked for the very same fact through `DobInput`'s DD/MM/YYYY\n * cells. Two surfaces collecting one fact in two formats is how a birthday\n * gets typed the wrong way round.\n *\n * All three or none: desk entry does not require a date of birth, so blank\n * is a real answer, but half a date is not.\n */\n dobDay: z.string(),\n dobMonth: z.string(),\n dobYear: z.string(),\n gender: requiredChoice(genderSchema, 'Select a gender'),\n\n /** Blank when not recorded. */\n bloodGroup: bloodGroupSchema.or(z.literal('')),\n avatarUrl: z.string(),\n\n contacts: z.array(emergencyContactRowSchema).max(MAX_EMERGENCY_CONTACTS),\n};\n\n/**\n * Date of birth is optional, but a typed one has to be real and plausible —\n * a mis-keyed year is the single most common desk-entry slip.\n */\nfunction refineDob(\n v: { dobDay: string; dobMonth: string; dobYear: string },\n ctx: z.RefinementCtx,\n): void {\n const touched = Boolean(v.dobDay || v.dobMonth || v.dobYear);\n // Nothing entered is fine — the desk records what it has.\n if (!touched) return;\n\n // Every message lands on `dobDay`, the first cell, because the three are one\n // control on screen: an error parked on `dobYear` would sit under a field\n // the form renders as a single row and reads back from `dobDay`.\n if (!v.dobDay || !v.dobMonth || v.dobYear.length !== 4) {\n ctx.addIssue({ code: 'custom', path: ['dobDay'], message: 'Enter the full date of birth' });\n return;\n }\n\n const day = Number(v.dobDay);\n const month = Number(v.dobMonth);\n const year = Number(v.dobYear);\n\n if (!isRealDate(year, month, day)) {\n ctx.addIssue({ code: 'custom', path: ['dobDay'], message: 'That date does not exist' });\n return;\n }\n\n const age = ageInYears(year, month, day);\n if (age < 0) {\n ctx.addIssue({\n code: 'custom',\n path: ['dobDay'],\n message: 'Date of birth cannot be in the future',\n });\n } else if (age < ADD_PERSON_MIN_AGE_YEARS || age > ADD_PERSON_MAX_AGE_YEARS) {\n ctx.addIssue({ code: 'custom', path: ['dobDay'], message: 'Check the year of birth' });\n }\n}\n\n// ─── Member form ─────────────────────────────────────────────────────────────\n\nexport const addMemberFormSchema = z\n .object({\n ...addPersonBaseShape,\n /**\n * Central-auth user id of the trainer, or `''` for none. Only trainers who\n * have CLAIMED their invite can be assigned — an unclaimed staff record has\n * no user to point at (`MemberProfile.assignedTrainerId` refs `User`).\n */\n trainerId: z.string(),\n /**\n * The day the gym says this person started training there. `''` = today.\n *\n * Askable at CREATION, not only through a later edit. `createMember` used\n * to stamp today unconditionally, so somebody who joined on the 3rd but was\n * typed in on the 20th was recorded as joining on the 20th — and because\n * this date anchors the billing cycle, they were then billed on the 20th of\n * every month for as long as they stayed. The only fix was to notice.\n *\n * Same `refineJoiningDate` the EDIT form uses, so both surfaces reject the\n * same dates with the same words.\n */\n joiningDate: z.string(),\n })\n .superRefine((v, ctx) => {\n refineDob(v, ctx);\n refineJoiningDate(v, ctx);\n });\n\n/**\n * Adding a member asks for the person, not their paperwork.\n *\n * No address, no Aadhaar and no ID proof — on this form or on any other. A\n * front desk does not have them when somebody walks in, the member's own\n * screens never asked for them, and nothing in the product ever read one back.\n * Every surface that collected them was filling a drawer nobody opens.\n */\nexport type AddMemberFormValues = {\n name: string;\n phone: string;\n dobDay: string;\n dobMonth: string;\n dobYear: string;\n gender: z.infer<typeof genderSchema> | null;\n bloodGroup: z.infer<typeof bloodGroupSchema> | '';\n avatarUrl: string;\n contacts: EmergencyContactRow[];\n /** Optional — a gym with no trainers yet, or none chosen. `''` for none. */\n trainerId: string;\n /** `YYYY-MM-DD`, or `''` for \"today\". See the schema field. */\n joiningDate: string;\n};\n\n// ─── Staff form ──────────────────────────────────────────────────────────────\n\n/**\n * Staff are asked exactly what a member is asked, plus the job they do.\n *\n * The home address and the ID upload both went for the same reason: nothing in\n * the product ever showed either one back. A gym that needs an employee's\n * address for a contract has somewhere else to keep it; what it had here was a\n * field collected once and never looked at again, which is the worst thing to\n * ask anyone for.\n */\nexport const addStaffFormSchema = z\n .object({\n ...addPersonBaseShape,\n /**\n * Staff DO give an email, and members no longer do. The asymmetry is\n * deliberate, not an oversight.\n *\n * `createStaffBodySchema` still requires it, so removing it here would send\n * a body the API refuses. But the reason it is still required is the point:\n * employment is not membership. A gym hiring a trainer has their email —\n * there is a contract, a payroll ledger and a person who applied for the\n * job — whereas a member walking in off the street is exactly the case\n * where asking for one was friction the owners told us to remove.\n *\n * Note this is no longer a GATE either way: `claimStaffInvite` matches on\n * the code alone now, same as members. This is a contact detail the gym\n * keeps, not the thing that links two records.\n */\n email: z.email('Enter a valid email'),\n role: requiredChoice(staffRoleSchema, 'Select a staff role'),\n })\n .superRefine((v, ctx) => {\n refineDob(v, ctx);\n });\n\nexport type AddStaffFormValues = Omit<AddMemberFormValues, 'trainerId' | 'joiningDate'> & {\n /** Staff still give one; members no longer do. See `addStaffFormSchema`. */\n email: string;\n role: z.infer<typeof staffRoleSchema> | null;\n};\n\n// ─── Empty states ────────────────────────────────────────────────────────────\n\nconst EMPTY_BASE = {\n name: '',\n phone: '',\n dobDay: '',\n dobMonth: '',\n dobYear: '',\n gender: null,\n bloodGroup: '' as const,\n avatarUrl: '',\n} satisfies Omit<AddMemberFormValues, 'contacts' | 'trainerId' | 'joiningDate'>;\n\n/** One blank row so the section renders with a contact to fill. */\nexport const EMPTY_EMERGENCY_CONTACT: EmergencyContactRow = {\n name: '',\n relationship: '',\n phone: '',\n};\n\nexport const EMPTY_ADD_MEMBER_FORM: AddMemberFormValues = {\n ...EMPTY_BASE,\n contacts: [{ ...EMPTY_EMERGENCY_CONTACT }],\n trainerId: '',\n // Blank means today, which is what most desk additions are.\n joiningDate: '',\n};\n\nexport const EMPTY_ADD_STAFF_FORM: AddStaffFormValues = {\n ...EMPTY_BASE,\n contacts: [{ ...EMPTY_EMERGENCY_CONTACT }],\n email: '',\n role: null,\n};\n\n// ─── Form → API body ─────────────────────────────────────────────────────────\n\n/** One \"Name\" field on screen, `firstName` + `lastName` on the wire. */\nfunction splitName(value: string): { firstName: string; lastName?: string } {\n const parts = value.trim().split(/\\s+/).filter(Boolean);\n const lastName = parts.slice(1).join(' ');\n return { firstName: parts[0] ?? '', ...(lastName ? { lastName } : {}) };\n}\n\n/** Drop a blank optional so it reads as \"not provided\" rather than `''`. */\nfunction orUndefined(value: string): string | undefined {\n const trimmed = value.trim();\n return trimmed === '' ? undefined : trimmed;\n}\n\n/** Phones are STORED raw — strip any display formatting back to 10 digits. */\nfunction asPhone(value: string): string {\n return value.replace(/\\D/g, '').slice(-10);\n}\n\n/**\n * The fields EVERY desk form sends.\n *\n * All three now ask exactly the same things about the PERSON; what differs is\n * what the record is FOR, so each mapper adds its own role / trainer / plan on\n * top. Address and ID proof used to be the exception here, asked by two of the\n * three; they are gone from all of them.\n */\nfunction sharedBody(v: Omit<AddMemberFormValues, 'trainerId' | 'joiningDate'>) {\n // A row with nothing in it is not a contact. Anything else has already been\n // through `emergencyContactRowSchema`, which requires a name and a valid\n // phone the moment a row is touched.\n const emergencyContacts = v.contacts\n .filter((c) => c.name !== '' || c.relationship !== '' || c.phone !== '')\n .map((c) =>\n emergencyContactSchema.parse({\n name: c.name.trim(),\n phone: asPhone(c.phone),\n relationship: orUndefined(c.relationship),\n }),\n )\n .slice(0, MAX_EMERGENCY_CONTACTS);\n\n return {\n ...splitName(v.name),\n // NO `email`. It is on the staff body only — `toCreateStaffBody` adds it.\n // Emitting it here sent a key `createMemberBodySchema` silently deleted.\n phone: asPhone(v.phone),\n // The form schema guarantees a choice was made before we get here.\n gender: v.gender ?? 'other',\n // Absent rather than empty when the desk did not record one — see\n // `isoFromDobCells`.\n dob: isoFromDobCells(v.dobDay, v.dobMonth, v.dobYear),\n bloodGroup: v.bloodGroup === '' ? undefined : v.bloodGroup,\n emergencyContacts,\n avatarUrl: orUndefined(v.avatarUrl),\n };\n}\n\n/**\n * Validated member form → `POST /members` body. `initialPlan` comes from the\n * membership step, which the form itself knows nothing about.\n */\nexport function toCreateMemberBody(\n values: AddMemberFormValues,\n initialPlan?: AssignMembershipBody,\n): CreateMemberBody {\n return {\n ...sharedBody(values),\n assignedTrainerId: orUndefined(values.trainerId),\n // OMITTED when blank rather than sent as null: the server reads \"no answer\"\n // as today, which is what a desk adding somebody who just walked in means.\n joinedAt: orUndefined(values.joiningDate.trim()),\n ...(initialPlan ? { initialPlan } : {}),\n };\n}\n\n/** Validated staff form → `POST /staff` body. */\nexport function toCreateStaffBody(values: AddStaffFormValues): CreateStaffBody {\n return {\n ...sharedBody(values),\n // Staff only — `createStaffBodySchema` still requires it. See the schema.\n email: values.email.trim(),\n // The form schema guarantees a role was chosen.\n role: values.role ?? 'trainer',\n };\n}\n\n/**\n * The joining date must be a real day, and must already have happened.\n *\n * Validated here as well as on the server because this one is not a formatting\n * preference: the date anchors the member's whole billing cycle, so a typo\n * that slips through bills them on the wrong day of every month until somebody\n * notices. The server holds the same rule — this is the copy that can say so\n * before the owner presses save.\n */\nfunction refineJoiningDate(v: { joiningDate: string }, ctx: z.RefinementCtx): void {\n const raw = v.joiningDate.trim();\n // Blank is \"leave it as it is\", which is always allowed.\n if (!raw) return;\n\n const parts = /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(raw);\n if (!parts) {\n ctx.addIssue({ code: 'custom', path: ['joiningDate'], message: 'Enter a valid date' });\n return;\n }\n\n const [year, month, day] = [Number(parts[1]), Number(parts[2]), Number(parts[3])];\n if (!isRealDate(year, month, day)) {\n ctx.addIssue({ code: 'custom', path: ['joiningDate'], message: 'That date does not exist' });\n return;\n }\n\n /**\n * \"Today\" means today AT THE GYM, and the comparison is between strings.\n *\n * `en-CA` is the one common locale whose short format is already\n * `YYYY-MM-DD`, so the zone conversion and the formatting happen in one step\n * and the result sorts lexicographically — an exact calendar-day comparison\n * with no instant arithmetic in it. This mirrors `gymDayOf` on the server,\n * deliberately: a UTC day here would be one behind IST between midnight and\n * 5:30am, and would tell an owner opening the app early that TODAY is in the\n * future.\n */\n const today = new Intl.DateTimeFormat('en-CA', { timeZone: DEFAULT_TIMEZONE }).format(new Date());\n if (raw > today) {\n ctx.addIssue({\n code: 'custom',\n path: ['joiningDate'],\n message: 'A joining date cannot be in the future',\n });\n }\n}\n\n/**\n * The billing day, when one was typed, must be a day a month can have.\n *\n * Blank is \"leave it as it is\". 29, 30 and 31 are all ACCEPTED — a gym that\n * bills on the 31st is making an ordinary statement, and months without one\n * clamp to their last day where the term is dated. Refusing them here would\n * reject a real answer to avoid arithmetic that is already written.\n */\nfunction refineBillingDay(v: { billingDay: string }, ctx: z.RefinementCtx): void {\n const raw = v.billingDay.trim();\n if (!raw) return;\n\n const n = Number(raw);\n if (!Number.isInteger(n) || n < 1 || n > 31) {\n ctx.addIssue({\n code: 'custom',\n path: ['billingDay'],\n message: 'Pick a day between 1 and 31',\n });\n }\n}\n\n// ─── Edit member ─────────────────────────────────────────────────────────────\n\n/**\n * Editing a member asks for exactly what adding one asks for.\n *\n * It stays a separate schema and a separate mapper because the two differ in\n * what \"blank\" means. On the add form a blank optional is simply not provided;\n * on the edit form it is an instruction to CLEAR a value that is already\n * stored, so `toUpdateMemberBody` sends `null` where `toCreateMemberBody`\n * omits the field.\n */\nexport const editMemberFormSchema = z\n .object({\n ...addPersonBaseShape,\n trainerId: z.string(),\n /**\n * When the GYM says this person started training there, `YYYY-MM-DD`.\n *\n * Asked only on EDIT. At the desk the answer is always \"today\" and one\n * more field in a queue is one more thing to get wrong; the correction is\n * something the owner makes later, once they realise the app's record and\n * the gym's disagree.\n *\n * Blank means \"leave it alone\", never \"clear it\" — unlike the other\n * optional fields on this form. Every enrolment has a joining date because\n * the billing cycle is measured from one, so there is no empty state to\n * write back.\n */\n joiningDate: z.string(),\n /**\n * The day of the month this member's fee falls due, as typed.\n *\n * A SEPARATE fact from the joining date, which is why it is a separate\n * field rather than read off it. A member who joined on the 5th is very\n * often on the gym's 26th cycle, and before this the only way to put them\n * there was to falsify the day they started training.\n *\n * A string because it is a form input; blank means \"leave it alone\", the\n * same as the joining date beside it.\n */\n billingDay: z.string(),\n })\n .superRefine((v, ctx) => {\n refineDob(v, ctx);\n refineJoiningDate(v, ctx);\n refineBillingDay(v, ctx);\n });\n\nexport type EditMemberFormValues = AddMemberFormValues & {\n joiningDate: string;\n billingDay: string;\n};\n\n/**\n * Validated edit form → `PATCH /members/:id` body.\n *\n * Sends every field, including the cleared ones as `null`, because the owner\n * emptying a field is an edit like any other. The identity fields go along too;\n * the server drops them once the member has claimed their account.\n */\nexport function toUpdateMemberBody(values: EditMemberFormValues): UpdateMemberBody {\n const shared = sharedBody(values);\n return {\n ...shared,\n dob: shared.dob ?? null,\n bloodGroup: shared.bloodGroup ?? null,\n avatarUrl: values.avatarUrl.trim(),\n assignedTrainerId: orUndefined(values.trainerId) ?? null,\n // OMITTED when blank rather than sent as null: every enrolment has a\n // joining date, so \"no answer\" means leave the stored one alone.\n joinedAt: orUndefined(values.joiningDate.trim()),\n // Omitted when blank for the same reason: no answer means leave the stored\n // cycle where it is, not clear it.\n billingDayOfMonth: values.billingDay.trim() ? Number(values.billingDay.trim()) : undefined,\n };\n}\n"]}
1
+ {"version":3,"file":"add-person-form.js","sourceRoot":"","sources":["../src/add-person-form.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * gymmonk-schema — \"Add member\" / \"Add staff\" FORM schemas\n * ========================================================\n * The client-side validation contract for the owner's desk-entry forms, and the\n * mappers that turn a filled form into the `createMember` / `createStaff` API\n * bodies.\n *\n * Why these live here rather than in the client, and why they are distinct from\n * `createMemberBodySchema`: the form's working shape is not the wire shape. It\n * holds ONE \"name\" field the API splits in two, `null` for a choice not yet\n * made, and `''` for an optional value left blank. Those are legitimate mid-edit\n * states that the API body must never see. Expressing the same rules over the\n * form shape here keeps\n * every message, bound and regex defined once — the client cannot drift from\n * what the server will accept, and the coercion from one shape to the other is\n * part of the contract instead of ad-hoc code in a component.\n *\n * Mirrors `onboarding-form.ts`, which does the same job for the wizards.\n *\n * @module gymmonk-schema/add-person-form\n */\n\nimport { z } from 'zod';\nimport { ageInYears, isRealDate, PHONE_REGEX } from './common.js';\nimport { DEFAULT_TIMEZONE } from './location.js';\nimport type { CreateMemberBody, UpdateMemberBody } from './member.js';\nimport { emergencyContactSchema, MAX_EMERGENCY_CONTACTS } from './member-profile.js';\nimport type { AssignMembershipBody } from './membership.js';\nimport { bloodGroupSchema, genderSchema } from './shared.js';\nimport type { CreateStaffBody } from './staff.js';\nimport { staffRoleSchema } from './staff.js';\n\n// ─── Bounds ──────────────────────────────────────────────────────────────────\n\n/**\n * Age bounds for someone signed up at the desk. Wider than the member wizard's\n * because a gym also enrols juniors with a guardian present, and the date is\n * typed by staff rather than by the person themselves.\n */\nexport const ADD_PERSON_MIN_AGE_YEARS = 5;\nexport const ADD_PERSON_MAX_AGE_YEARS = 100;\n\n// ─── Date of birth, as three cells ───────────────────────────────────────────\n\n/**\n * The three cells joined into the `YYYY-MM-DD` the API stores, or `undefined`\n * when nothing was entered.\n *\n * Desk entry does not require a date of birth — a gym adds people with\n * whatever it has to hand — so \"blank\" is a real answer and has to survive the\n * whole way to the body as an ABSENT field rather than an empty string the\n * API would reject.\n */\nexport function isoFromDobCells(day: string, month: string, year: string): string | undefined {\n if (!day || !month || !year) return undefined;\n return `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`;\n}\n\n/** A stored `YYYY-MM-DD` split back into the cells, for seeding an edit form. */\nexport function dobCellsFromIso(iso: string | null | undefined): {\n dobDay: string;\n dobMonth: string;\n dobYear: string;\n} {\n const match = /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(iso ?? '');\n if (!match) return { dobDay: '', dobMonth: '', dobYear: '' };\n return { dobYear: match[1] as string, dobMonth: match[2] as string, dobDay: match[3] as string };\n}\n\n// ─── Field-level rules ───────────────────────────────────────────────────────\n\nconst requiredChoice = <T extends z.ZodTypeAny>(inner: T, message: string) =>\n inner.nullable().refine((v: unknown) => v !== null, { message });\n\n// ─── One emergency-contact row ───────────────────────────────────────────────\n\n/**\n * One emergency-contact row: WHO to call, on what number, and how they are\n * related. A row is either wholly blank (ignored) or names a person.\n *\n * ── Why `name` exists now ───────────────────────────────────────────────────\n * This form used to collect a relationship and a phone with no name at all,\n * and the mapper wrote the relationship INTO `name` because\n * `emergencyContactSchema` requires one. The member's own surfaces store a real\n * name, so the two halves of the product disagreed about what the field held,\n * and the disagreement destroyed data: a member onboards with \"Priya\", the\n * owner opens Edit Member and sees Relationship prefilled \"Priya\" (the reader\n * fell back to `name`), corrects it to \"Sister\", saves — and the contact is now\n * called Sister. Whoever has to ring that number in an emergency is left with\n * a word instead of a person.\n *\n * `relationship` stays OPTIONAL, matching `emergencyContactSchema`. It is\n * useful context, not something to block a desk entry over.\n */\nexport const emergencyContactRowSchema = z\n .object({\n name: z.string().trim().max(120),\n relationship: z.string().trim().max(60),\n phone: z.string().trim(),\n })\n .superRefine((v, ctx) => {\n const blank = v.name === '' && v.relationship === '' && v.phone === '';\n if (blank) return;\n if (v.name === '') {\n ctx.addIssue({\n code: 'custom',\n path: ['name'],\n message: \"Add the contact's name\",\n });\n }\n if (!PHONE_REGEX.test(v.phone)) {\n ctx.addIssue({\n code: 'custom',\n path: ['phone'],\n message: 'Enter a valid 10-digit mobile number',\n });\n }\n });\n\nexport interface EmergencyContactRow {\n name: string;\n relationship: string;\n phone: string;\n}\n\n// ─── Shared base ─────────────────────────────────────────────────────────────\n\nconst addPersonBaseShape = {\n /** One field on screen; split into `firstName` / `lastName` on the wire. */\n name: z.string().trim().min(1, 'Name is required').max(160),\n /**\n * ⚠️ `email` USED TO BE HERE, on the shared base. It is now on NEITHER form.\n * Do not put it back on either one.\n *\n * It was required of a front desk adding a member, and it was the field the\n * claim was matched on — the person had to register with the exact address\n * the desk had typed. The desk does not reliably know it. The phone is the\n * join key now, and neither `createMemberBodySchema` nor\n * `createStaffBodySchema` has an `email` at all.\n *\n * Leaving it here was not harmless: Zod strips unknown keys, so the desk\n * typed an address, the form accepted it, the mapper sent it, the API schema\n * DELETED it and the create succeeded. Silence at every step, for a field\n * that used to be the join key. A 400 would have been kinder.\n *\n * Staff held onto it one release longer on an employment-versus-membership\n * argument. See `addStaffFormSchema` for why that did not survive.\n */\n phone: z.string().regex(PHONE_REGEX, 'Enter a valid 10-digit mobile number'),\n /**\n * Three cells, not a native date input.\n *\n * `<input type=\"date\">` renders in the BROWSER'S locale, so the same field\n * read \"mm/dd/yyyy\" at an Indian gym's front desk — and a member's own\n * onboarding asked for the very same fact through `DobInput`'s DD/MM/YYYY\n * cells. Two surfaces collecting one fact in two formats is how a birthday\n * gets typed the wrong way round.\n *\n * All three or none: desk entry does not require a date of birth, so blank\n * is a real answer, but half a date is not.\n */\n dobDay: z.string(),\n dobMonth: z.string(),\n dobYear: z.string(),\n gender: requiredChoice(genderSchema, 'Select a gender'),\n\n /** Blank when not recorded. */\n bloodGroup: bloodGroupSchema.or(z.literal('')),\n avatarUrl: z.string(),\n\n contacts: z.array(emergencyContactRowSchema).max(MAX_EMERGENCY_CONTACTS),\n};\n\n/**\n * Date of birth is optional, but a typed one has to be real and plausible —\n * a mis-keyed year is the single most common desk-entry slip.\n */\nfunction refineDob(\n v: { dobDay: string; dobMonth: string; dobYear: string },\n ctx: z.RefinementCtx,\n): void {\n const touched = Boolean(v.dobDay || v.dobMonth || v.dobYear);\n // Nothing entered is fine — the desk records what it has.\n if (!touched) return;\n\n // Every message lands on `dobDay`, the first cell, because the three are one\n // control on screen: an error parked on `dobYear` would sit under a field\n // the form renders as a single row and reads back from `dobDay`.\n if (!v.dobDay || !v.dobMonth || v.dobYear.length !== 4) {\n ctx.addIssue({ code: 'custom', path: ['dobDay'], message: 'Enter the full date of birth' });\n return;\n }\n\n const day = Number(v.dobDay);\n const month = Number(v.dobMonth);\n const year = Number(v.dobYear);\n\n if (!isRealDate(year, month, day)) {\n ctx.addIssue({ code: 'custom', path: ['dobDay'], message: 'That date does not exist' });\n return;\n }\n\n const age = ageInYears(year, month, day);\n if (age < 0) {\n ctx.addIssue({\n code: 'custom',\n path: ['dobDay'],\n message: 'Date of birth cannot be in the future',\n });\n } else if (age < ADD_PERSON_MIN_AGE_YEARS || age > ADD_PERSON_MAX_AGE_YEARS) {\n ctx.addIssue({ code: 'custom', path: ['dobDay'], message: 'Check the year of birth' });\n }\n}\n\n// ─── Member form ─────────────────────────────────────────────────────────────\n\nexport const addMemberFormSchema = z\n .object({\n ...addPersonBaseShape,\n /**\n * Central-auth user id of the trainer, or `''` for none. Only trainers who\n * have CLAIMED their invite can be assigned — an unclaimed staff record has\n * no user to point at (`MemberProfile.assignedTrainerId` refs `User`).\n */\n trainerId: z.string(),\n /**\n * The day the gym says this person started training there. `''` = today.\n *\n * Askable at CREATION, not only through a later edit. `createMember` used\n * to stamp today unconditionally, so somebody who joined on the 3rd but was\n * typed in on the 20th was recorded as joining on the 20th — and because\n * this date anchors the billing cycle, they were then billed on the 20th of\n * every month for as long as they stayed. The only fix was to notice.\n *\n * Same `refineJoiningDate` the EDIT form uses, so both surfaces reject the\n * same dates with the same words.\n */\n joiningDate: z.string(),\n })\n .superRefine((v, ctx) => {\n refineDob(v, ctx);\n refineJoiningDate(v, ctx);\n });\n\n/**\n * Adding a member asks for the person, not their paperwork.\n *\n * No address, no Aadhaar and no ID proof — on this form or on any other. A\n * front desk does not have them when somebody walks in, the member's own\n * screens never asked for them, and nothing in the product ever read one back.\n * Every surface that collected them was filling a drawer nobody opens.\n */\nexport type AddMemberFormValues = {\n name: string;\n phone: string;\n dobDay: string;\n dobMonth: string;\n dobYear: string;\n gender: z.infer<typeof genderSchema> | null;\n bloodGroup: z.infer<typeof bloodGroupSchema> | '';\n avatarUrl: string;\n contacts: EmergencyContactRow[];\n /** Optional — a gym with no trainers yet, or none chosen. `''` for none. */\n trainerId: string;\n /** `YYYY-MM-DD`, or `''` for \"today\". See the schema field. */\n joiningDate: string;\n};\n\n// ─── Staff form ──────────────────────────────────────────────────────────────\n\n/**\n * Staff are asked exactly what a member is asked, plus the job they do.\n *\n * The home address and the ID upload both went for the same reason: nothing in\n * the product ever showed either one back. A gym that needs an employee's\n * address for a contract has somewhere else to keep it; what it had here was a\n * field collected once and never looked at again, which is the worst thing to\n * ask anyone for.\n */\nexport const addStaffFormSchema = z\n .object({\n ...addPersonBaseShape,\n /**\n * ⚠️ NO `email`, FOR STAFF EITHER. It was the last asymmetry between the\n * two variants and it is gone (user, 2026-08-27).\n *\n * The argument for keeping it was that employment is not membership — a\n * gym hiring a trainer has a contract and a payroll ledger, so it knows the\n * address. That argument survived only because the field predated the join\n * key moving to the phone. Once `claimStaffInvite` stopped matching on the\n * address, the field stopped GATING anything and became one more thing to\n * type at a desk, on the same screen where we had just finished arguing\n * that a member should not be asked for one.\n *\n * What it did still do was back the duplicate-invite guard, and that has\n * moved to `invitePhone` in `staff.service` — the same key members use, and\n * a stricter check than the one it replaced.\n *\n * The read side keeps `email` (`staffListItemSchema`, staff detail): a\n * CLAIMED staff member has one from their central-auth account, and the\n * roster card offers it as a contact. Nullable there, which it already was.\n */\n role: requiredChoice(staffRoleSchema, 'Select a staff role'),\n })\n .superRefine((v, ctx) => {\n refineDob(v, ctx);\n });\n\nexport type AddStaffFormValues = Omit<AddMemberFormValues, 'trainerId' | 'joiningDate'> & {\n role: z.infer<typeof staffRoleSchema> | null;\n};\n\n// ─── Empty states ────────────────────────────────────────────────────────────\n\nconst EMPTY_BASE = {\n name: '',\n phone: '',\n dobDay: '',\n dobMonth: '',\n dobYear: '',\n gender: null,\n bloodGroup: '' as const,\n avatarUrl: '',\n} satisfies Omit<AddMemberFormValues, 'contacts' | 'trainerId' | 'joiningDate'>;\n\n/** One blank row so the section renders with a contact to fill. */\nexport const EMPTY_EMERGENCY_CONTACT: EmergencyContactRow = {\n name: '',\n relationship: '',\n phone: '',\n};\n\nexport const EMPTY_ADD_MEMBER_FORM: AddMemberFormValues = {\n ...EMPTY_BASE,\n contacts: [{ ...EMPTY_EMERGENCY_CONTACT }],\n trainerId: '',\n // Blank means today, which is what most desk additions are.\n joiningDate: '',\n};\n\nexport const EMPTY_ADD_STAFF_FORM: AddStaffFormValues = {\n ...EMPTY_BASE,\n contacts: [{ ...EMPTY_EMERGENCY_CONTACT }],\n role: null,\n};\n\n// ─── Form → API body ─────────────────────────────────────────────────────────\n\n/** One \"Name\" field on screen, `firstName` + `lastName` on the wire. */\nfunction splitName(value: string): { firstName: string; lastName?: string } {\n const parts = value.trim().split(/\\s+/).filter(Boolean);\n const lastName = parts.slice(1).join(' ');\n return { firstName: parts[0] ?? '', ...(lastName ? { lastName } : {}) };\n}\n\n/** Drop a blank optional so it reads as \"not provided\" rather than `''`. */\nfunction orUndefined(value: string): string | undefined {\n const trimmed = value.trim();\n return trimmed === '' ? undefined : trimmed;\n}\n\n/** Phones are STORED raw — strip any display formatting back to 10 digits. */\nfunction asPhone(value: string): string {\n return value.replace(/\\D/g, '').slice(-10);\n}\n\n/**\n * The fields EVERY desk form sends.\n *\n * All three now ask exactly the same things about the PERSON; what differs is\n * what the record is FOR, so each mapper adds its own role / trainer / plan on\n * top. Address and ID proof used to be the exception here, asked by two of the\n * three; they are gone from all of them.\n */\nfunction sharedBody(v: Omit<AddMemberFormValues, 'trainerId' | 'joiningDate'>) {\n // A row with nothing in it is not a contact. Anything else has already been\n // through `emergencyContactRowSchema`, which requires a name and a valid\n // phone the moment a row is touched.\n const emergencyContacts = v.contacts\n .filter((c) => c.name !== '' || c.relationship !== '' || c.phone !== '')\n .map((c) =>\n emergencyContactSchema.parse({\n name: c.name.trim(),\n phone: asPhone(c.phone),\n relationship: orUndefined(c.relationship),\n }),\n )\n .slice(0, MAX_EMERGENCY_CONTACTS);\n\n return {\n ...splitName(v.name),\n // NO `email`. It is on the staff body only — `toCreateStaffBody` adds it.\n // Emitting it here sent a key `createMemberBodySchema` silently deleted.\n phone: asPhone(v.phone),\n // The form schema guarantees a choice was made before we get here.\n gender: v.gender ?? 'other',\n // Absent rather than empty when the desk did not record one — see\n // `isoFromDobCells`.\n dob: isoFromDobCells(v.dobDay, v.dobMonth, v.dobYear),\n bloodGroup: v.bloodGroup === '' ? undefined : v.bloodGroup,\n emergencyContacts,\n avatarUrl: orUndefined(v.avatarUrl),\n };\n}\n\n/**\n * Validated member form → `POST /members` body. `initialPlan` comes from the\n * membership step, which the form itself knows nothing about.\n */\nexport function toCreateMemberBody(\n values: AddMemberFormValues,\n initialPlan?: AssignMembershipBody,\n): CreateMemberBody {\n return {\n ...sharedBody(values),\n assignedTrainerId: orUndefined(values.trainerId),\n // OMITTED when blank rather than sent as null: the server reads \"no answer\"\n // as today, which is what a desk adding somebody who just walked in means.\n joinedAt: orUndefined(values.joiningDate.trim()),\n ...(initialPlan ? { initialPlan } : {}),\n };\n}\n\n/**\n * Validated staff form → `POST /staff` body.\n *\n * The staff body is now the shared body plus a role — no `email`. The two\n * variants finally differ by exactly the one thing that actually differs.\n */\nexport function toCreateStaffBody(values: AddStaffFormValues): CreateStaffBody {\n return {\n ...sharedBody(values),\n // The form schema guarantees a role was chosen.\n role: values.role ?? 'trainer',\n };\n}\n\n/**\n * The joining date must be a real day, and must already have happened.\n *\n * Validated here as well as on the server because this one is not a formatting\n * preference: the date anchors the member's whole billing cycle, so a typo\n * that slips through bills them on the wrong day of every month until somebody\n * notices. The server holds the same rule — this is the copy that can say so\n * before the owner presses save.\n */\nfunction refineJoiningDate(v: { joiningDate: string }, ctx: z.RefinementCtx): void {\n const raw = v.joiningDate.trim();\n // Blank is \"leave it as it is\", which is always allowed.\n if (!raw) return;\n\n const parts = /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(raw);\n if (!parts) {\n ctx.addIssue({ code: 'custom', path: ['joiningDate'], message: 'Enter a valid date' });\n return;\n }\n\n const [year, month, day] = [Number(parts[1]), Number(parts[2]), Number(parts[3])];\n if (!isRealDate(year, month, day)) {\n ctx.addIssue({ code: 'custom', path: ['joiningDate'], message: 'That date does not exist' });\n return;\n }\n\n /**\n * \"Today\" means today AT THE GYM, and the comparison is between strings.\n *\n * `en-CA` is the one common locale whose short format is already\n * `YYYY-MM-DD`, so the zone conversion and the formatting happen in one step\n * and the result sorts lexicographically — an exact calendar-day comparison\n * with no instant arithmetic in it. This mirrors `gymDayOf` on the server,\n * deliberately: a UTC day here would be one behind IST between midnight and\n * 5:30am, and would tell an owner opening the app early that TODAY is in the\n * future.\n */\n const today = new Intl.DateTimeFormat('en-CA', { timeZone: DEFAULT_TIMEZONE }).format(new Date());\n if (raw > today) {\n ctx.addIssue({\n code: 'custom',\n path: ['joiningDate'],\n message: 'A joining date cannot be in the future',\n });\n }\n}\n\n/**\n * The billing day, when one was typed, must be a day a month can have.\n *\n * Blank is \"leave it as it is\". 29, 30 and 31 are all ACCEPTED — a gym that\n * bills on the 31st is making an ordinary statement, and months without one\n * clamp to their last day where the term is dated. Refusing them here would\n * reject a real answer to avoid arithmetic that is already written.\n */\nfunction refineBillingDay(v: { billingDay: string }, ctx: z.RefinementCtx): void {\n const raw = v.billingDay.trim();\n if (!raw) return;\n\n const n = Number(raw);\n if (!Number.isInteger(n) || n < 1 || n > 31) {\n ctx.addIssue({\n code: 'custom',\n path: ['billingDay'],\n message: 'Pick a day between 1 and 31',\n });\n }\n}\n\n// ─── Edit member ─────────────────────────────────────────────────────────────\n\n/**\n * Editing a member asks for exactly what adding one asks for.\n *\n * It stays a separate schema and a separate mapper because the two differ in\n * what \"blank\" means. On the add form a blank optional is simply not provided;\n * on the edit form it is an instruction to CLEAR a value that is already\n * stored, so `toUpdateMemberBody` sends `null` where `toCreateMemberBody`\n * omits the field.\n */\nexport const editMemberFormSchema = z\n .object({\n ...addPersonBaseShape,\n trainerId: z.string(),\n /**\n * When the GYM says this person started training there, `YYYY-MM-DD`.\n *\n * Asked only on EDIT. At the desk the answer is always \"today\" and one\n * more field in a queue is one more thing to get wrong; the correction is\n * something the owner makes later, once they realise the app's record and\n * the gym's disagree.\n *\n * Blank means \"leave it alone\", never \"clear it\" — unlike the other\n * optional fields on this form. Every enrolment has a joining date because\n * the billing cycle is measured from one, so there is no empty state to\n * write back.\n */\n joiningDate: z.string(),\n /**\n * The day of the month this member's fee falls due, as typed.\n *\n * A SEPARATE fact from the joining date, which is why it is a separate\n * field rather than read off it. A member who joined on the 5th is very\n * often on the gym's 26th cycle, and before this the only way to put them\n * there was to falsify the day they started training.\n *\n * A string because it is a form input; blank means \"leave it alone\", the\n * same as the joining date beside it.\n */\n billingDay: z.string(),\n })\n .superRefine((v, ctx) => {\n refineDob(v, ctx);\n refineJoiningDate(v, ctx);\n refineBillingDay(v, ctx);\n });\n\nexport type EditMemberFormValues = AddMemberFormValues & {\n joiningDate: string;\n billingDay: string;\n};\n\n/**\n * Validated edit form → `PATCH /members/:id` body.\n *\n * Sends every field, including the cleared ones as `null`, because the owner\n * emptying a field is an edit like any other. The identity fields go along too;\n * the server drops them once the member has claimed their account.\n */\nexport function toUpdateMemberBody(values: EditMemberFormValues): UpdateMemberBody {\n const shared = sharedBody(values);\n return {\n ...shared,\n dob: shared.dob ?? null,\n bloodGroup: shared.bloodGroup ?? null,\n avatarUrl: values.avatarUrl.trim(),\n assignedTrainerId: orUndefined(values.trainerId) ?? null,\n // OMITTED when blank rather than sent as null: every enrolment has a\n // joining date, so \"no answer\" means leave the stored one alone.\n joinedAt: orUndefined(values.joiningDate.trim()),\n // Omitted when blank for the same reason: no answer means leave the stored\n // cycle where it is, not clear it.\n billingDayOfMonth: values.billingDay.trim() ? Number(values.billingDay.trim()) : undefined,\n };\n}\n"]}
@@ -22,10 +22,22 @@
22
22
  * @module gymmonk-schema/gym-registration
23
23
  */
24
24
  import { z } from 'zod';
25
- /** The SaaS plan the owner selects at the final registration step. */
25
+ /**
26
+ * The plan the owner picks on the LAST registration step.
27
+ *
28
+ * The same three fields `subscribeBodySchema` carries, for the same reasons —
29
+ * see the notes there, particularly on why the coupon is a HINT the server
30
+ * re-derives rather than a price the client gets to name.
31
+ *
32
+ * `tenureId` is null for Free, which has no term.
33
+ */
26
34
  export declare const gymPlanSelectionSchema: z.ZodObject<{
27
- planId: z.ZodString;
35
+ planId: z.ZodEnum<{
36
+ free: "free";
37
+ pro: "pro";
38
+ }>;
28
39
  tenureId: z.ZodNullable<z.ZodString>;
40
+ couponCode: z.ZodPreprocess<z.ZodOptional<z.ZodString>>;
29
41
  }, z.core.$strip>;
30
42
  export type GymPlanSelection = z.infer<typeof gymPlanSelectionSchema>;
31
43
  export declare const gymRegistrationBodySchema: z.ZodObject<{
@@ -196,10 +208,14 @@ export declare const gymRegistrationBodySchema: z.ZodObject<{
196
208
  }, z.core.$strip>;
197
209
  photos: z.ZodDefault<z.ZodArray<z.ZodString>>;
198
210
  }, z.core.$strip>;
199
- subscription: z.ZodOptional<z.ZodObject<{
200
- planId: z.ZodString;
211
+ subscription: z.ZodObject<{
212
+ planId: z.ZodEnum<{
213
+ free: "free";
214
+ pro: "pro";
215
+ }>;
201
216
  tenureId: z.ZodNullable<z.ZodString>;
202
- }, z.core.$strip>>;
217
+ couponCode: z.ZodPreprocess<z.ZodOptional<z.ZodString>>;
218
+ }, z.core.$strip>;
203
219
  }, z.core.$strip>;
204
220
  export type GymRegistrationBody = z.infer<typeof gymRegistrationBodySchema>;
205
221
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"gym-registration.d.ts","sourceRoot":"","sources":["../src/gym-registration.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAQxB,sEAAsE;AACtE,eAAO,MAAM,sBAAsB;;;iBAGjC,CAAC;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAEtE,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAmBpC,CAAC;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAI5E;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,0BAA0B,wCAAoC,CAAC;AAC5E,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAE9E;;;;;GAKG;AACH,eAAO,MAAM,6BAA6B;;;iBAGxC,CAAC;AACH,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,6BAA6B,CAAC,CAAC;AAEpF,2EAA2E;AAC3E,eAAO,MAAM,mCAAmC;;;iBAKpC,CAAC;AACb,MAAM,MAAM,6BAA6B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mCAAmC,CAAC,CAAC"}
1
+ {"version":3,"file":"gym-registration.d.ts","sourceRoot":"","sources":["../src/gym-registration.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAWxB;;;;;;;;GAQG;AACH,eAAO,MAAM,sBAAsB;;;;;;;iBAoBlC,CAAC;AACF,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAEtE,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA6BpC,CAAC;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAI5E;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,0BAA0B,wCAAoC,CAAC;AAC5E,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAE9E;;;;;GAKG;AACH,eAAO,MAAM,6BAA6B;;;iBAGxC,CAAC;AACH,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,6BAA6B,CAAC,CAAC;AAEpF,2EAA2E;AAC3E,eAAO,MAAM,mCAAmC;;;iBAKpC,CAAC;AACb,MAAM,MAAM,6BAA6B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mCAAmC,CAAC,CAAC"}
@@ -23,35 +23,70 @@
23
23
  */
24
24
  import { z } from 'zod';
25
25
  import { createCenterBodySchema } from './center.js';
26
+ import { optionalTrimmedString } from './common.js';
27
+ import { ERROR_MESSAGE } from './messages.js';
26
28
  // The wizard's own step count, so the resume cursor below cannot be capped at a
27
29
  // number the wizard has outgrown. `onboarding-form` imports nothing from here,
28
30
  // so this is a one-way edge, not a cycle.
29
31
  import { GYM_REGISTRATION_TOTAL_STEPS } from './onboarding-form.js';
30
32
  import { createOrganisationBodySchema } from './organisation.js';
31
- /** The SaaS plan the owner selects at the final registration step. */
33
+ import { saasPlanIdSchema } from './subscription.js';
34
+ /**
35
+ * The plan the owner picks on the LAST registration step.
36
+ *
37
+ * The same three fields `subscribeBodySchema` carries, for the same reasons —
38
+ * see the notes there, particularly on why the coupon is a HINT the server
39
+ * re-derives rather than a price the client gets to name.
40
+ *
41
+ * `tenureId` is null for Free, which has no term.
42
+ */
32
43
  export const gymPlanSelectionSchema = z.object({
33
- planId: z.string().trim().min(1),
44
+ planId: saasPlanIdSchema,
34
45
  tenureId: z.string().trim().min(1).nullable(),
46
+ /** A discount code the owner redeemed. Re-validated server-side. */
47
+ couponCode: optionalTrimmedString(40),
48
+ }, {
49
+ /*
50
+ * Fires when the key is ABSENT as well as when it is the wrong type —
51
+ * verified against zod 4.4.3, which routes both through this schema's own
52
+ * `invalid_type` issue. Inner fields keep their own messages, so a bad
53
+ * `planId` still says so rather than being flattened into this sentence.
54
+ *
55
+ * Worth writing because the only body that can reach it is one from a
56
+ * client built before the plan step existed, and "Invalid input: expected
57
+ * object, received undefined" names our shape rather than their problem.
58
+ */
59
+ error: ERROR_MESSAGE.gymRegistrationNeedsPlan,
35
60
  });
36
61
  export const gymRegistrationBodySchema = z.object({
37
62
  organisation: createOrganisationBodySchema,
38
63
  center: createCenterBodySchema,
39
64
  /**
40
- * OPTIONAL, and no longer sent.
65
+ * REQUIRED. A registration is not a registration until a plan is chosen.
66
+ *
67
+ * ── IT WAS OPTIONAL, AND THAT IS THE BUG THIS FIXES ─────────────────────
68
+ * The plan step was pulled out of the wizard when its answer was being
69
+ * discarded — every gym started on a Pro trial whatever was picked, because
70
+ * no price existed to charge. Both halves of that have since changed: there
71
+ * is a real two-tier catalog with real terms and coupons, and `registerGym`
72
+ * honours what arrives here.
73
+ *
74
+ * What did NOT change back was the order, and that left a gap nobody could
75
+ * see. A gym was created by step 4 and queued for platform review straight
76
+ * away; the plan screen came afterwards, outside the flow, with a Back button
77
+ * that led out of it. An owner who closed the app there had a gym sitting in
78
+ * the reviewer's queue and had never been asked what it costs.
41
79
  *
42
- * The wizard'''s pricing carousel is gone. It was never honoured anyway
43
- * `registerGym` starts EVERY new gym on a Pro trial whatever was picked,
44
- * because there is no Razorpay plan behind any tier yet, so the screen asked
45
- * a question whose answer was thrown away. The owner chooses and changes a
46
- * plan from the console'''s subscription screen, which is where they would
47
- * go to change one later regardless.
80
+ * Making the field required is what closes it. There is no way left to create
81
+ * an organisation without saying which plan it is on, so "in the queue" and
82
+ * "finished registering" cannot come apart again.
48
83
  *
49
- * Kept on the contract rather than deleted: an older client cached in
50
- * somebody'''s PWA still sends it, and an optional key it may ignore is the
51
- * difference between that client working and 422-ing on submit. When billing
52
- * lands this becomes the selection a completed payment carries.
84
+ * ⚠️ BREAKING for a client cached before this shipped: one that omits the
85
+ * field is refused with `ERROR_MESSAGE.gymRegistrationNeedsPlan`, which tells
86
+ * the owner to reopen the app. Loud and recoverable, which is the trade this
87
+ * takes over silently registering a gym on a plan nobody chose.
53
88
  */
54
- subscription: gymPlanSelectionSchema.optional(),
89
+ subscription: gymPlanSelectionSchema,
55
90
  });
56
91
  // ─── Resume: the per-step draft ──────────────────────────────────────────────
57
92
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"gym-registration.js","sourceRoot":"","sources":["../src/gym-registration.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AACrD,gFAAgF;AAChF,+EAA+E;AAC/E,0CAA0C;AAC1C,OAAO,EAAE,4BAA4B,EAAE,MAAM,sBAAsB,CAAC;AACpE,OAAO,EAAE,4BAA4B,EAAE,MAAM,mBAAmB,CAAC;AAEjE,sEAAsE;AACtE,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC7C,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAChC,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;CAC9C,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,CAAC,MAAM,CAAC;IAChD,YAAY,EAAE,4BAA4B;IAC1C,MAAM,EAAE,sBAAsB;IAC9B;;;;;;;;;;;;;;OAcG;IACH,YAAY,EAAE,sBAAsB,CAAC,QAAQ,EAAE;CAChD,CAAC,CAAC;AAGH,gFAAgF;AAEhF;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;AAG5E;;;;;GAKG;AACH,MAAM,CAAC,MAAM,6BAA6B,GAAG,CAAC,CAAC,MAAM,CAAC;IACpD,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,4BAA4B,CAAC;IAC/D,KAAK,EAAE,0BAA0B,CAAC,QAAQ,EAAE;CAC7C,CAAC,CAAC;AAGH,2EAA2E;AAC3E,MAAM,CAAC,MAAM,mCAAmC,GAAG,CAAC;KACjD,MAAM,CAAC;IACN,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,4BAA4B,CAAC;IAC/D,KAAK,EAAE,0BAA0B;CAClC,CAAC;KACD,OAAO,EAAE,CAAC","sourcesContent":["/**\n * gymmonk-schema — Gym registration\n * =================================\n * \"List your gym on GymMonk\" — the payload a signed-in MEMBER submits to create\n * the business they run.\n *\n * This used to be owner onboarding, a fork taken before the user had seen\n * anything. It is now an ordinary action taken from the Me tab by somebody who\n * already has an account, and that changes what it has to ask for:\n *\n * - NO personal details. Name, gender, date of birth, phone and photo were\n * all captured at onboarding. Asking again would be asking the same person\n * the same questions twice and then having two answers to keep in step.\n * - NO automatic membership of their own gym. Owning a gym and training at\n * one are separate things — an owner keeps the single member identity they\n * registered with, and is free to be a paying member of somebody else's gym.\n *\n * The submitted organisation starts `pending` and is invisible in the member\n * gym directory until a superadmin or platform manager approves it\n * (`organisationApprovalStatusSchema`).\n *\n * @module gymmonk-schema/gym-registration\n */\n\nimport { z } from 'zod';\nimport { createCenterBodySchema } from './center.js';\n// The wizard's own step count, so the resume cursor below cannot be capped at a\n// number the wizard has outgrown. `onboarding-form` imports nothing from here,\n// so this is a one-way edge, not a cycle.\nimport { GYM_REGISTRATION_TOTAL_STEPS } from './onboarding-form.js';\nimport { createOrganisationBodySchema } from './organisation.js';\n\n/** The SaaS plan the owner selects at the final registration step. */\nexport const gymPlanSelectionSchema = z.object({\n planId: z.string().trim().min(1),\n tenureId: z.string().trim().min(1).nullable(),\n});\nexport type GymPlanSelection = z.infer<typeof gymPlanSelectionSchema>;\n\nexport const gymRegistrationBodySchema = z.object({\n organisation: createOrganisationBodySchema,\n center: createCenterBodySchema,\n /**\n * OPTIONAL, and no longer sent.\n *\n * The wizard'''s pricing carousel is gone. It was never honoured anyway —\n * `registerGym` starts EVERY new gym on a Pro trial whatever was picked,\n * because there is no Razorpay plan behind any tier yet, so the screen asked\n * a question whose answer was thrown away. The owner chooses and changes a\n * plan from the console'''s subscription screen, which is where they would\n * go to change one later regardless.\n *\n * Kept on the contract rather than deleted: an older client cached in\n * somebody'''s PWA still sends it, and an optional key it may ignore is the\n * difference between that client working and 422-ing on submit. When billing\n * lands this becomes the selection a completed payment carries.\n */\n subscription: gymPlanSelectionSchema.optional(),\n});\nexport type GymRegistrationBody = z.infer<typeof gymRegistrationBodySchema>;\n\n// ─── Resume: the per-step draft ──────────────────────────────────────────────\n\n/**\n * The wizard's UI-shaped, partially-filled form, persisted a STEP AT A TIME.\n *\n * Opaque to the backend by design — the client owns the form shape, and what is\n * stored here is re-validated strictly by `gymRegistrationBodySchema` at submit.\n * Keys are form field names; the backend caps the stored size.\n *\n * ── Why the wizard needs its own cursor ─────────────────────────────────────\n * Not the onboarding cursor. That one belongs to onboarding, a flow this user\n * finished long ago and can no longer enter, and writing a `step` into it would\n * move the resume target of a different wizard. This is a separate record with\n * its own lifecycle: it starts when somebody begins describing their gym, and\n * it is retired the moment the gym is actually submitted.\n */\nexport const gymRegistrationDraftSchema = z.record(z.string(), z.unknown());\nexport type GymRegistrationDraft = z.infer<typeof gymRegistrationDraftSchema>;\n\n/**\n * `GET /gym-registration/draft` — how far the caller got and what they typed.\n *\n * `step: 0` with no draft is the ordinary \"never started\" answer, so the client\n * has one shape to read rather than a null to branch on.\n */\nexport const gymRegistrationProgressSchema = z.object({\n step: z.number().int().min(0).max(GYM_REGISTRATION_TOTAL_STEPS),\n draft: gymRegistrationDraftSchema.optional(),\n});\nexport type GymRegistrationProgress = z.infer<typeof gymRegistrationProgressSchema>;\n\n/** `PUT /gym-registration/draft` body — any subset, merged server-side. */\nexport const gymRegistrationProgressUpdateSchema = z\n .object({\n step: z.number().int().min(0).max(GYM_REGISTRATION_TOTAL_STEPS),\n draft: gymRegistrationDraftSchema,\n })\n .partial();\nexport type GymRegistrationProgressUpdate = z.infer<typeof gymRegistrationProgressUpdateSchema>;\n"]}
1
+ {"version":3,"file":"gym-registration.js","sourceRoot":"","sources":["../src/gym-registration.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,gFAAgF;AAChF,+EAA+E;AAC/E,0CAA0C;AAC1C,OAAO,EAAE,4BAA4B,EAAE,MAAM,sBAAsB,CAAC;AACpE,OAAO,EAAE,4BAA4B,EAAE,MAAM,mBAAmB,CAAC;AACjE,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAErD;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,CAAC,MAAM,CAC5C;IACE,MAAM,EAAE,gBAAgB;IACxB,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;IAC7C,oEAAoE;IACpE,UAAU,EAAE,qBAAqB,CAAC,EAAE,CAAC;CACtC,EACD;IACE;;;;;;;;;OASG;IACH,KAAK,EAAE,aAAa,CAAC,wBAAwB;CAC9C,CACF,CAAC;AAGF,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,CAAC,MAAM,CAAC;IAChD,YAAY,EAAE,4BAA4B;IAC1C,MAAM,EAAE,sBAAsB;IAC9B;;;;;;;;;;;;;;;;;;;;;;;;OAwBG;IACH,YAAY,EAAE,sBAAsB;CACrC,CAAC,CAAC;AAGH,gFAAgF;AAEhF;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;AAG5E;;;;;GAKG;AACH,MAAM,CAAC,MAAM,6BAA6B,GAAG,CAAC,CAAC,MAAM,CAAC;IACpD,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,4BAA4B,CAAC;IAC/D,KAAK,EAAE,0BAA0B,CAAC,QAAQ,EAAE;CAC7C,CAAC,CAAC;AAGH,2EAA2E;AAC3E,MAAM,CAAC,MAAM,mCAAmC,GAAG,CAAC;KACjD,MAAM,CAAC;IACN,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,4BAA4B,CAAC;IAC/D,KAAK,EAAE,0BAA0B;CAClC,CAAC;KACD,OAAO,EAAE,CAAC","sourcesContent":["/**\n * gymmonk-schema — Gym registration\n * =================================\n * \"List your gym on GymMonk\" — the payload a signed-in MEMBER submits to create\n * the business they run.\n *\n * This used to be owner onboarding, a fork taken before the user had seen\n * anything. It is now an ordinary action taken from the Me tab by somebody who\n * already has an account, and that changes what it has to ask for:\n *\n * - NO personal details. Name, gender, date of birth, phone and photo were\n * all captured at onboarding. Asking again would be asking the same person\n * the same questions twice and then having two answers to keep in step.\n * - NO automatic membership of their own gym. Owning a gym and training at\n * one are separate things — an owner keeps the single member identity they\n * registered with, and is free to be a paying member of somebody else's gym.\n *\n * The submitted organisation starts `pending` and is invisible in the member\n * gym directory until a superadmin or platform manager approves it\n * (`organisationApprovalStatusSchema`).\n *\n * @module gymmonk-schema/gym-registration\n */\n\nimport { z } from 'zod';\nimport { createCenterBodySchema } from './center.js';\nimport { optionalTrimmedString } from './common.js';\nimport { ERROR_MESSAGE } from './messages.js';\n// The wizard's own step count, so the resume cursor below cannot be capped at a\n// number the wizard has outgrown. `onboarding-form` imports nothing from here,\n// so this is a one-way edge, not a cycle.\nimport { GYM_REGISTRATION_TOTAL_STEPS } from './onboarding-form.js';\nimport { createOrganisationBodySchema } from './organisation.js';\nimport { saasPlanIdSchema } from './subscription.js';\n\n/**\n * The plan the owner picks on the LAST registration step.\n *\n * The same three fields `subscribeBodySchema` carries, for the same reasons —\n * see the notes there, particularly on why the coupon is a HINT the server\n * re-derives rather than a price the client gets to name.\n *\n * `tenureId` is null for Free, which has no term.\n */\nexport const gymPlanSelectionSchema = z.object(\n {\n planId: saasPlanIdSchema,\n tenureId: z.string().trim().min(1).nullable(),\n /** A discount code the owner redeemed. Re-validated server-side. */\n couponCode: optionalTrimmedString(40),\n },\n {\n /*\n * Fires when the key is ABSENT as well as when it is the wrong type —\n * verified against zod 4.4.3, which routes both through this schema's own\n * `invalid_type` issue. Inner fields keep their own messages, so a bad\n * `planId` still says so rather than being flattened into this sentence.\n *\n * Worth writing because the only body that can reach it is one from a\n * client built before the plan step existed, and \"Invalid input: expected\n * object, received undefined\" names our shape rather than their problem.\n */\n error: ERROR_MESSAGE.gymRegistrationNeedsPlan,\n },\n);\nexport type GymPlanSelection = z.infer<typeof gymPlanSelectionSchema>;\n\nexport const gymRegistrationBodySchema = z.object({\n organisation: createOrganisationBodySchema,\n center: createCenterBodySchema,\n /**\n * REQUIRED. A registration is not a registration until a plan is chosen.\n *\n * ── IT WAS OPTIONAL, AND THAT IS THE BUG THIS FIXES ─────────────────────\n * The plan step was pulled out of the wizard when its answer was being\n * discarded every gym started on a Pro trial whatever was picked, because\n * no price existed to charge. Both halves of that have since changed: there\n * is a real two-tier catalog with real terms and coupons, and `registerGym`\n * honours what arrives here.\n *\n * What did NOT change back was the order, and that left a gap nobody could\n * see. A gym was created by step 4 and queued for platform review straight\n * away; the plan screen came afterwards, outside the flow, with a Back button\n * that led out of it. An owner who closed the app there had a gym sitting in\n * the reviewer's queue and had never been asked what it costs.\n *\n * Making the field required is what closes it. There is no way left to create\n * an organisation without saying which plan it is on, so \"in the queue\" and\n * \"finished registering\" cannot come apart again.\n *\n * ⚠️ BREAKING for a client cached before this shipped: one that omits the\n * field is refused with `ERROR_MESSAGE.gymRegistrationNeedsPlan`, which tells\n * the owner to reopen the app. Loud and recoverable, which is the trade this\n * takes over silently registering a gym on a plan nobody chose.\n */\n subscription: gymPlanSelectionSchema,\n});\nexport type GymRegistrationBody = z.infer<typeof gymRegistrationBodySchema>;\n\n// ─── Resume: the per-step draft ──────────────────────────────────────────────\n\n/**\n * The wizard's UI-shaped, partially-filled form, persisted a STEP AT A TIME.\n *\n * Opaque to the backend by design — the client owns the form shape, and what is\n * stored here is re-validated strictly by `gymRegistrationBodySchema` at submit.\n * Keys are form field names; the backend caps the stored size.\n *\n * ── Why the wizard needs its own cursor ─────────────────────────────────────\n * Not the onboarding cursor. That one belongs to onboarding, a flow this user\n * finished long ago and can no longer enter, and writing a `step` into it would\n * move the resume target of a different wizard. This is a separate record with\n * its own lifecycle: it starts when somebody begins describing their gym, and\n * it is retired the moment the gym is actually submitted.\n */\nexport const gymRegistrationDraftSchema = z.record(z.string(), z.unknown());\nexport type GymRegistrationDraft = z.infer<typeof gymRegistrationDraftSchema>;\n\n/**\n * `GET /gym-registration/draft` — how far the caller got and what they typed.\n *\n * `step: 0` with no draft is the ordinary \"never started\" answer, so the client\n * has one shape to read rather than a null to branch on.\n */\nexport const gymRegistrationProgressSchema = z.object({\n step: z.number().int().min(0).max(GYM_REGISTRATION_TOTAL_STEPS),\n draft: gymRegistrationDraftSchema.optional(),\n});\nexport type GymRegistrationProgress = z.infer<typeof gymRegistrationProgressSchema>;\n\n/** `PUT /gym-registration/draft` body — any subset, merged server-side. */\nexport const gymRegistrationProgressUpdateSchema = z\n .object({\n step: z.number().int().min(0).max(GYM_REGISTRATION_TOTAL_STEPS),\n draft: gymRegistrationDraftSchema,\n })\n .partial();\nexport type GymRegistrationProgressUpdate = z.infer<typeof gymRegistrationProgressUpdateSchema>;\n"]}
@@ -395,6 +395,15 @@ export declare const ERROR_MESSAGE: {
395
395
  readonly gymAlreadyRegistered: "You have already registered a gym on this account.";
396
396
  /** Registering a gym is something a MEMBER does, so the profile must exist. */
397
397
  readonly gymRegistrationNeedsProfile: "Finish setting up your profile before registering a gym.";
398
+ /**
399
+ * The submitted registration named no plan.
400
+ *
401
+ * Unreachable from the current wizard — its fifth step IS the plan and its
402
+ * two CTAs are the only way to submit — so this is for a client cached from
403
+ * before the plan became part of the registration body. It says "reopen",
404
+ * because that is the fix and nothing on their screen will produce one.
405
+ */
406
+ readonly gymRegistrationNeedsPlan: "Choose a plan to finish registering your gym. Close and reopen the app if you cannot see the plans.";
398
407
  /** Every owner action other than editing their own details, before approval. */
399
408
  readonly gymAwaitingApproval: "Your gym is waiting for approval. You can manage it once our team has reviewed it.";
400
409
  /** The reviewer turned the registration down. */
@@ -1 +1 @@
1
- {"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../src/messages.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAIH;;;;GAIG;AACH,eAAO,MAAM,UAAU;;;;;;;;;;;;;;IAsBrB;;;;;;;;;;;;;OAaG;;IAGH;;;;;;;;;OASG;;IAGH;;;;;;;OAOG;;;CAKK,CAAC;AAEX,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,OAAO,UAAU,CAAC,CAAC;AAIrE,wEAAwE;AACxE,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAID;;;GAGG;AACH,eAAO,MAAM,eAAe;;;;;;;;;IAU1B,sEAAsE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IA8DtE;;;;OAIG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAwCH;;;;;OAKG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+E6C,CAAC;AAEnD,MAAM,MAAM,iBAAiB,GAAG,MAAM,OAAO,eAAe,CAAC;AAE7D,0DAA0D;AAC1D,eAAO,MAAM,mBAAmB;iCACV,MAAM,KAAG,aAAa;gCAIvB,MAAM,KAAG,aAAa;sCAIhB,MAAM,KAAG,aAAa;uCAIrB,MAAM,KAAG,aAAa;uCAGtB,MAAM,KAAG,aAAa;uCAItB,MAAM,KAAG,aAAa;CAIxC,CAAC;AAIX;;;;GAIG;AACH,eAAO,MAAM,aAAa;;;;;;;;;;IAiBxB;;;;;OAKG;;IAGH;;;;;;;OAOG;;IAGH;;;;;;OAMG;;IAGH,4EAA4E;;IAE5E,+EAA+E;;IAE/E,gFAAgF;;IAGhF,iDAAiD;;;IAKjD,kDAAkD;;IAElD;;;;OAIG;;IAEH;;;;OAIG;;IAEH,qEAAqE;;IAErE,2EAA2E;;IAE3E,wEAAwE;;;;;;;IASxE,gEAAgE;;;;;;;;;IAkBhE,8DAA8D;;IAE9D;;;;OAIG;;IAEH;;;;OAIG;;IAEH;;;;OAIG;;IAGH;;;;;OAKG;;;;IAMH,2EAA2E;;;IAI3E;;;OAGG;;;IAIH;;;;OAIG;;IAGH;;;;OAIG;;IAEH;;;;;OAKG;;IAGH;;;OAGG;;IAGH,mEAAmE;;;;;IAKnE,8EAA8E;;IAE9E,uEAAuE;;;;;;;;;;;;;IAgBvE;;;OAGG;;;;IAMH,wEAAwE;;IAExE,uEAAuE;;IAEvE,yEAAyE;;IAEzE,8EAA8E;;IAE9E,0DAA0D;;;IAG1D;;;;OAIG;;IAIH,4EAA4E;;IAG5E,6DAA6D;;IAE7D,kDAAkD;;IAElD,+DAA+D;;IAE/D,sEAAsE;;;IAItE,kEAAkE;;;IAGlE,qEAAqE;;IAErE;;;;;;OAMG;;IAEH,wDAAwD;;;IAGxD,uEAAuE;;IAEvE,8EAA8E;;;IAI9E;;;;;;;;;;;;;OAaG;;;;;IAMH;;;;;;OAMG;;;;;;;;;;;;CAkBsC,CAAC;AAE5C,MAAM,MAAM,eAAe,GAAG,MAAM,OAAO,aAAa,CAAC;AAEzD,iDAAiD;AACjD,eAAO,MAAM,iBAAiB;8BACX,MAAM,KAAG,MAAM;IAChC;;;;;;;;OAQG;gDACgC,MAAM,KAAG,MAAM;IAElD,2DAA2D;sCAClC,MAAM,KAAG,MAAM;CAEhC,CAAC"}
1
+ {"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../src/messages.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAIH;;;;GAIG;AACH,eAAO,MAAM,UAAU;;;;;;;;;;;;;;IAsBrB;;;;;;;;;;;;;OAaG;;IAGH;;;;;;;;;OASG;;IAGH;;;;;;;OAOG;;;CAKK,CAAC;AAEX,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,OAAO,UAAU,CAAC,CAAC;AAIrE,wEAAwE;AACxE,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAID;;;GAGG;AACH,eAAO,MAAM,eAAe;;;;;;;;;IAU1B,sEAAsE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IA8DtE;;;;OAIG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAwCH;;;;;OAKG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+E6C,CAAC;AAEnD,MAAM,MAAM,iBAAiB,GAAG,MAAM,OAAO,eAAe,CAAC;AAE7D,0DAA0D;AAC1D,eAAO,MAAM,mBAAmB;iCACV,MAAM,KAAG,aAAa;gCAIvB,MAAM,KAAG,aAAa;sCAIhB,MAAM,KAAG,aAAa;uCAIrB,MAAM,KAAG,aAAa;uCAGtB,MAAM,KAAG,aAAa;uCAItB,MAAM,KAAG,aAAa;CAIxC,CAAC;AAIX;;;;GAIG;AACH,eAAO,MAAM,aAAa;;;;;;;;;;IAiBxB;;;;;OAKG;;IAGH;;;;;;;OAOG;;IAGH;;;;;;OAMG;;IAGH,4EAA4E;;IAE5E,+EAA+E;;IAE/E;;;;;;;OAOG;;IAGH,gFAAgF;;IAGhF,iDAAiD;;;IAKjD,kDAAkD;;IAElD;;;;OAIG;;IAEH;;;;OAIG;;IAEH,qEAAqE;;IAErE,2EAA2E;;IAE3E,wEAAwE;;;;;;;IASxE,gEAAgE;;;;;;;;;IAkBhE,8DAA8D;;IAE9D;;;;OAIG;;IAEH;;;;OAIG;;IAEH;;;;OAIG;;IAGH;;;;;OAKG;;;;IAMH,2EAA2E;;;IAI3E;;;OAGG;;;IAIH;;;;OAIG;;IAGH;;;;OAIG;;IAEH;;;;;OAKG;;IAGH;;;OAGG;;IAGH,mEAAmE;;;;;IAKnE,8EAA8E;;IAE9E,uEAAuE;;;;;;;;;;;;;IAgBvE;;;OAGG;;;;IAMH,wEAAwE;;IAExE,uEAAuE;;IAEvE,yEAAyE;;IAEzE,8EAA8E;;IAE9E,0DAA0D;;;IAG1D;;;;OAIG;;IAIH,4EAA4E;;IAG5E,6DAA6D;;IAE7D,kDAAkD;;IAElD,+DAA+D;;IAE/D,sEAAsE;;;IAItE,kEAAkE;;;IAGlE,qEAAqE;;IAErE;;;;;;OAMG;;IAEH,wDAAwD;;;IAGxD,uEAAuE;;IAEvE,8EAA8E;;;IAI9E;;;;;;;;;;;;;OAaG;;;;;IAMH;;;;;;OAMG;;;;;;;;;;;;CAkBsC,CAAC;AAE5C,MAAM,MAAM,eAAe,GAAG,MAAM,OAAO,aAAa,CAAC;AAEzD,iDAAiD;AACjD,eAAO,MAAM,iBAAiB;8BACX,MAAM,KAAG,MAAM;IAChC;;;;;;;;OAQG;gDACgC,MAAM,KAAG,MAAM;IAElD,2DAA2D;sCAClC,MAAM,KAAG,MAAM;CAEhC,CAAC"}
package/dist/messages.js CHANGED
@@ -343,6 +343,15 @@ export const ERROR_MESSAGE = {
343
343
  gymAlreadyRegistered: 'You have already registered a gym on this account.',
344
344
  /** Registering a gym is something a MEMBER does, so the profile must exist. */
345
345
  gymRegistrationNeedsProfile: 'Finish setting up your profile before registering a gym.',
346
+ /**
347
+ * The submitted registration named no plan.
348
+ *
349
+ * Unreachable from the current wizard — its fifth step IS the plan and its
350
+ * two CTAs are the only way to submit — so this is for a client cached from
351
+ * before the plan became part of the registration body. It says "reopen",
352
+ * because that is the fix and nothing on their screen will produce one.
353
+ */
354
+ gymRegistrationNeedsPlan: 'Choose a plan to finish registering your gym. Close and reopen the app if you cannot see the plans.',
346
355
  /** Every owner action other than editing their own details, before approval. */
347
356
  gymAwaitingApproval: 'Your gym is waiting for approval. You can manage it once our team has reviewed it.',
348
357
  /** The reviewer turned the registration down. */