gymmonk-schema 0.44.0 → 0.45.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/add-person-form.d.ts +4 -1
- package/dist/add-person-form.d.ts.map +1 -1
- package/dist/add-person-form.js +63 -0
- package/dist/add-person-form.js.map +1 -1
- package/dist/member.d.ts +6 -0
- package/dist/member.d.ts.map +1 -1
- package/dist/member.js +29 -1
- package/dist/member.js.map +1 -1
- package/dist/membership-plan.d.ts +6 -6
- package/dist/membership.d.ts +66 -0
- package/dist/membership.d.ts.map +1 -1
- package/dist/membership.js +106 -5
- package/dist/membership.js.map +1 -1
- package/dist/messages.d.ts +29 -0
- package/dist/messages.d.ts.map +1 -1
- package/dist/messages.js +29 -0
- package/dist/messages.js.map +1 -1
- package/package.json +1 -1
|
@@ -197,6 +197,7 @@ export declare function toCreateStaffBody(values: AddStaffFormValues): CreateSta
|
|
|
197
197
|
*/
|
|
198
198
|
export declare const editMemberFormSchema: z.ZodObject<{
|
|
199
199
|
trainerId: z.ZodString;
|
|
200
|
+
joiningDate: z.ZodString;
|
|
200
201
|
name: z.ZodString;
|
|
201
202
|
email: z.ZodEmail;
|
|
202
203
|
phone: z.ZodString;
|
|
@@ -225,7 +226,9 @@ export declare const editMemberFormSchema: z.ZodObject<{
|
|
|
225
226
|
phone: z.ZodString;
|
|
226
227
|
}, z.core.$strip>>;
|
|
227
228
|
}, z.core.$strip>;
|
|
228
|
-
export type EditMemberFormValues = AddMemberFormValues
|
|
229
|
+
export type EditMemberFormValues = AddMemberFormValues & {
|
|
230
|
+
joiningDate: string;
|
|
231
|
+
};
|
|
229
232
|
/**
|
|
230
233
|
* Validated edit form → `PATCH /members/:id` body.
|
|
231
234
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"add-person-form.d.ts","sourceRoot":"","sources":["../src/add-person-form.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"add-person-form.d.ts","sourceRoot":"","sources":["../src/add-person-form.ts"],"names":[],"mappings":"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"}
|
package/dist/add-person-form.js
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import { z } from 'zod';
|
|
23
23
|
import { ageInYears, isRealDate, PHONE_REGEX } from './common.js';
|
|
24
|
+
import { DEFAULT_TIMEZONE } from './location.js';
|
|
24
25
|
import { emergencyContactSchema, MAX_EMERGENCY_CONTACTS } from './member-profile.js';
|
|
25
26
|
import { bloodGroupSchema, genderSchema } from './shared.js';
|
|
26
27
|
import { staffRoleSchema } from './staff.js';
|
|
@@ -291,6 +292,50 @@ export function toCreateStaffBody(values) {
|
|
|
291
292
|
role: values.role ?? 'trainer',
|
|
292
293
|
};
|
|
293
294
|
}
|
|
295
|
+
/**
|
|
296
|
+
* The joining date must be a real day, and must already have happened.
|
|
297
|
+
*
|
|
298
|
+
* Validated here as well as on the server because this one is not a formatting
|
|
299
|
+
* preference: the date anchors the member's whole billing cycle, so a typo
|
|
300
|
+
* that slips through bills them on the wrong day of every month until somebody
|
|
301
|
+
* notices. The server holds the same rule — this is the copy that can say so
|
|
302
|
+
* before the owner presses save.
|
|
303
|
+
*/
|
|
304
|
+
function refineJoiningDate(v, ctx) {
|
|
305
|
+
const raw = v.joiningDate.trim();
|
|
306
|
+
// Blank is "leave it as it is", which is always allowed.
|
|
307
|
+
if (!raw)
|
|
308
|
+
return;
|
|
309
|
+
const parts = /^(\d{4})-(\d{2})-(\d{2})$/.exec(raw);
|
|
310
|
+
if (!parts) {
|
|
311
|
+
ctx.addIssue({ code: 'custom', path: ['joiningDate'], message: 'Enter a valid date' });
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
const [year, month, day] = [Number(parts[1]), Number(parts[2]), Number(parts[3])];
|
|
315
|
+
if (!isRealDate(year, month, day)) {
|
|
316
|
+
ctx.addIssue({ code: 'custom', path: ['joiningDate'], message: 'That date does not exist' });
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* "Today" means today AT THE GYM, and the comparison is between strings.
|
|
321
|
+
*
|
|
322
|
+
* `en-CA` is the one common locale whose short format is already
|
|
323
|
+
* `YYYY-MM-DD`, so the zone conversion and the formatting happen in one step
|
|
324
|
+
* and the result sorts lexicographically — an exact calendar-day comparison
|
|
325
|
+
* with no instant arithmetic in it. This mirrors `gymDayOf` on the server,
|
|
326
|
+
* deliberately: a UTC day here would be one behind IST between midnight and
|
|
327
|
+
* 5:30am, and would tell an owner opening the app early that TODAY is in the
|
|
328
|
+
* future.
|
|
329
|
+
*/
|
|
330
|
+
const today = new Intl.DateTimeFormat('en-CA', { timeZone: DEFAULT_TIMEZONE }).format(new Date());
|
|
331
|
+
if (raw > today) {
|
|
332
|
+
ctx.addIssue({
|
|
333
|
+
code: 'custom',
|
|
334
|
+
path: ['joiningDate'],
|
|
335
|
+
message: 'A joining date cannot be in the future',
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
}
|
|
294
339
|
// ─── Edit member ─────────────────────────────────────────────────────────────
|
|
295
340
|
/**
|
|
296
341
|
* Editing a member asks for exactly what adding one asks for.
|
|
@@ -305,9 +350,24 @@ export const editMemberFormSchema = z
|
|
|
305
350
|
.object({
|
|
306
351
|
...addPersonBaseShape,
|
|
307
352
|
trainerId: z.string(),
|
|
353
|
+
/**
|
|
354
|
+
* When the GYM says this person started training there, `YYYY-MM-DD`.
|
|
355
|
+
*
|
|
356
|
+
* Asked only on EDIT. At the desk the answer is always "today" and one
|
|
357
|
+
* more field in a queue is one more thing to get wrong; the correction is
|
|
358
|
+
* something the owner makes later, once they realise the app's record and
|
|
359
|
+
* the gym's disagree.
|
|
360
|
+
*
|
|
361
|
+
* Blank means "leave it alone", never "clear it" — unlike the other
|
|
362
|
+
* optional fields on this form. Every enrolment has a joining date because
|
|
363
|
+
* the billing cycle is measured from one, so there is no empty state to
|
|
364
|
+
* write back.
|
|
365
|
+
*/
|
|
366
|
+
joiningDate: z.string(),
|
|
308
367
|
})
|
|
309
368
|
.superRefine((v, ctx) => {
|
|
310
369
|
refineDob(v, ctx);
|
|
370
|
+
refineJoiningDate(v, ctx);
|
|
311
371
|
});
|
|
312
372
|
/**
|
|
313
373
|
* Validated edit form → `PATCH /members/:id` body.
|
|
@@ -324,6 +384,9 @@ export function toUpdateMemberBody(values) {
|
|
|
324
384
|
bloodGroup: shared.bloodGroup ?? null,
|
|
325
385
|
avatarUrl: values.avatarUrl.trim(),
|
|
326
386
|
assignedTrainerId: orUndefined(values.trainerId) ?? null,
|
|
387
|
+
// OMITTED when blank rather than sent as null: every enrolment has a
|
|
388
|
+
// joining date, so "no answer" means leave the stored one alone.
|
|
389
|
+
joinedAt: orUndefined(values.joiningDate.trim()),
|
|
327
390
|
};
|
|
328
391
|
}
|
|
329
392
|
//# sourceMappingURL=add-person-form.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"add-person-form.js","sourceRoot":"","sources":["../src/add-person-form.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAElE,OAAO,EAAE,sBAAsB,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AAErF,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE7D,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAE7C,gFAAgF;AAEhF;;;;GAIG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAC,CAAC;AAC1C,MAAM,CAAC,MAAM,wBAAwB,GAAG,GAAG,CAAC;AAE5C,gFAAgF;AAEhF;;;;;;;;GAQG;AACH,MAAM,UAAU,eAAe,CAAC,GAAW,EAAE,KAAa,EAAE,IAAY;IACtE,IAAI,CAAC,GAAG,IAAI,CAAC,KAAK,IAAI,CAAC,IAAI;QAAE,OAAO,SAAS,CAAC;IAC9C,OAAO,GAAG,IAAI,IAAI,KAAK,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,IAAI,GAAG,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC;AACrE,CAAC;AAED,iFAAiF;AACjF,MAAM,UAAU,eAAe,CAAC,GAA8B;IAK5D,MAAM,KAAK,GAAG,2BAA2B,CAAC,IAAI,CAAC,GAAG,IAAI,EAAE,CAAC,CAAC;IAC1D,IAAI,CAAC,KAAK;QAAE,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;IAC7D,OAAO,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC,CAAW,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC,CAAW,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC,CAAW,EAAE,CAAC;AACnG,CAAC;AAED,gFAAgF;AAEhF,MAAM,cAAc,GAAG,CAAyB,KAAQ,EAAE,OAAe,EAAE,EAAE,CAC3E,KAAK,CAAC,QAAQ,EAAE,CAAC,MAAM,CAAC,CAAC,CAAU,EAAE,EAAE,CAAC,CAAC,KAAK,IAAI,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC;AAEnE,gFAAgF;AAEhF;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC;KACvC,MAAM,CAAC;IACN,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC;IAChC,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC;IACvC,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE;CACzB,CAAC;KACD,WAAW,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,EAAE;IACtB,MAAM,KAAK,GAAG,CAAC,CAAC,IAAI,KAAK,EAAE,IAAI,CAAC,CAAC,YAAY,KAAK,EAAE,IAAI,CAAC,CAAC,KAAK,KAAK,EAAE,CAAC;IACvE,IAAI,KAAK;QAAE,OAAO;IAClB,IAAI,CAAC,CAAC,IAAI,KAAK,EAAE,EAAE,CAAC;QAClB,GAAG,CAAC,QAAQ,CAAC;YACX,IAAI,EAAE,QAAQ;YACd,IAAI,EAAE,CAAC,MAAM,CAAC;YACd,OAAO,EAAE,wBAAwB;SAClC,CAAC,CAAC;IACL,CAAC;IACD,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC;QAC/B,GAAG,CAAC,QAAQ,CAAC;YACX,IAAI,EAAE,QAAQ;YACd,IAAI,EAAE,CAAC,OAAO,CAAC;YACf,OAAO,EAAE,sCAAsC;SAChD,CAAC,CAAC;IACL,CAAC;AACH,CAAC,CAAC,CAAC;AAQL,gFAAgF;AAEhF,MAAM,kBAAkB,GAAG;IACzB,4EAA4E;IAC5E,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,kBAAkB,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC;IAC3D,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,qBAAqB,CAAC;IACrC,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,WAAW,EAAE,sCAAsC,CAAC;IAC5E;;;;;;;;;;;OAWG;IACH,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE;IAClB,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE;IACpB,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE;IACnB,MAAM,EAAE,cAAc,CAAC,YAAY,EAAE,iBAAiB,CAAC;IAEvD,+BAA+B;IAC/B,UAAU,EAAE,gBAAgB,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IAC9C,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE;IAErB,QAAQ,EAAE,CAAC,CAAC,KAAK,CAAC,yBAAyB,CAAC,CAAC,GAAG,CAAC,sBAAsB,CAAC;CACzE,CAAC;AAEF;;;GAGG;AACH,SAAS,SAAS,CAChB,CAAwD,EACxD,GAAoB;IAEpB,MAAM,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC;IAC7D,0DAA0D;IAC1D,IAAI,CAAC,OAAO;QAAE,OAAO;IAErB,6EAA6E;IAC7E,0EAA0E;IAC1E,iEAAiE;IACjE,IAAI,CAAC,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvD,GAAG,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,QAAQ,CAAC,EAAE,OAAO,EAAE,8BAA8B,EAAE,CAAC,CAAC;QAC5F,OAAO;IACT,CAAC;IAED,MAAM,GAAG,GAAG,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;IAC7B,MAAM,KAAK,GAAG,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;IACjC,MAAM,IAAI,GAAG,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;IAE/B,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,KAAK,EAAE,GAAG,CAAC,EAAE,CAAC;QAClC,GAAG,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,QAAQ,CAAC,EAAE,OAAO,EAAE,0BAA0B,EAAE,CAAC,CAAC;QACxF,OAAO;IACT,CAAC;IAED,MAAM,GAAG,GAAG,UAAU,CAAC,IAAI,EAAE,KAAK,EAAE,GAAG,CAAC,CAAC;IACzC,IAAI,GAAG,GAAG,CAAC,EAAE,CAAC;QACZ,GAAG,CAAC,QAAQ,CAAC;YACX,IAAI,EAAE,QAAQ;YACd,IAAI,EAAE,CAAC,QAAQ,CAAC;YAChB,OAAO,EAAE,uCAAuC;SACjD,CAAC,CAAC;IACL,CAAC;SAAM,IAAI,GAAG,GAAG,wBAAwB,IAAI,GAAG,GAAG,wBAAwB,EAAE,CAAC;QAC5E,GAAG,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,QAAQ,CAAC,EAAE,OAAO,EAAE,yBAAyB,EAAE,CAAC,CAAC;IACzF,CAAC;AACH,CAAC;AAED,gFAAgF;AAEhF,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC;KACjC,MAAM,CAAC;IACN,GAAG,kBAAkB;IACrB;;;;OAIG;IACH,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE;CACtB,CAAC;KACD,WAAW,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,EAAE;IACtB,SAAS,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;AACpB,CAAC,CAAC,CAAC;AAyBL,gFAAgF;AAEhF;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC;KAChC,MAAM,CAAC;IACN,GAAG,kBAAkB;IACrB,IAAI,EAAE,cAAc,CAAC,eAAe,EAAE,qBAAqB,CAAC;CAC7D,CAAC;KACD,WAAW,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,EAAE;IACtB,SAAS,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;AACpB,CAAC,CAAC,CAAC;AAML,gFAAgF;AAEhF,MAAM,UAAU,GAAG;IACjB,IAAI,EAAE,EAAE;IACR,KAAK,EAAE,EAAE;IACT,KAAK,EAAE,EAAE;IACT,MAAM,EAAE,EAAE;IACV,QAAQ,EAAE,EAAE;IACZ,OAAO,EAAE,EAAE;IACX,MAAM,EAAE,IAAI;IACZ,UAAU,EAAE,EAAW;IACvB,SAAS,EAAE,EAAE;CACgD,CAAC;AAEhE,mEAAmE;AACnE,MAAM,CAAC,MAAM,uBAAuB,GAAwB;IAC1D,IAAI,EAAE,EAAE;IACR,YAAY,EAAE,EAAE;IAChB,KAAK,EAAE,EAAE;CACV,CAAC;AAEF,MAAM,CAAC,MAAM,qBAAqB,GAAwB;IACxD,GAAG,UAAU;IACb,QAAQ,EAAE,CAAC,EAAE,GAAG,uBAAuB,EAAE,CAAC;IAC1C,SAAS,EAAE,EAAE;CACd,CAAC;AAEF,MAAM,CAAC,MAAM,oBAAoB,GAAuB;IACtD,GAAG,UAAU;IACb,QAAQ,EAAE,CAAC,EAAE,GAAG,uBAAuB,EAAE,CAAC;IAC1C,IAAI,EAAE,IAAI;CACX,CAAC;AAEF,gFAAgF;AAEhF,wEAAwE;AACxE,SAAS,SAAS,CAAC,KAAa;IAC9B,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACxD,MAAM,QAAQ,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC1C,OAAO,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;AAC1E,CAAC;AAED,4EAA4E;AAC5E,SAAS,WAAW,CAAC,KAAa;IAChC,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC7B,OAAO,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC;AAC9C,CAAC;AAED,8EAA8E;AAC9E,SAAS,OAAO,CAAC,KAAa;IAC5B,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC;AAC7C,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,UAAU,CAAC,CAAyC;IAC3D,4EAA4E;IAC5E,yEAAyE;IACzE,qCAAqC;IACrC,MAAM,iBAAiB,GAAG,CAAC,CAAC,QAAQ;SACjC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,EAAE,IAAI,CAAC,CAAC,YAAY,KAAK,EAAE,IAAI,CAAC,CAAC,KAAK,KAAK,EAAE,CAAC;SACvE,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CACT,sBAAsB,CAAC,KAAK,CAAC;QAC3B,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE;QACnB,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC;QACvB,YAAY,EAAE,WAAW,CAAC,CAAC,CAAC,YAAY,CAAC;KAC1C,CAAC,CACH;SACA,KAAK,CAAC,CAAC,EAAE,sBAAsB,CAAC,CAAC;IAEpC,OAAO;QACL,GAAG,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC;QACpB,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE;QACrB,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC;QACvB,mEAAmE;QACnE,MAAM,EAAE,CAAC,CAAC,MAAM,IAAI,OAAO;QAC3B,kEAAkE;QAClE,qBAAqB;QACrB,GAAG,EAAE,eAAe,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC,OAAO,CAAC;QACrD,UAAU,EAAE,CAAC,CAAC,UAAU,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU;QAC1D,iBAAiB;QACjB,SAAS,EAAE,WAAW,CAAC,CAAC,CAAC,SAAS,CAAC;KACpC,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAChC,MAA2B,EAC3B,WAAkC;IAElC,OAAO;QACL,GAAG,UAAU,CAAC,MAAM,CAAC;QACrB,iBAAiB,EAAE,WAAW,CAAC,MAAM,CAAC,SAAS,CAAC;QAChD,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACxC,CAAC;AACJ,CAAC;AAED,iDAAiD;AACjD,MAAM,UAAU,iBAAiB,CAAC,MAA0B;IAC1D,OAAO;QACL,GAAG,UAAU,CAAC,MAAM,CAAC;QACrB,gDAAgD;QAChD,IAAI,EAAE,MAAM,CAAC,IAAI,IAAI,SAAS;KAC/B,CAAC;AACJ,CAAC;AAED,gFAAgF;AAEhF;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC;KAClC,MAAM,CAAC;IACN,GAAG,kBAAkB;IACrB,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE;CACtB,CAAC;KACD,WAAW,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,EAAE;IACtB,SAAS,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;AACpB,CAAC,CAAC,CAAC;AAIL;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB,CAAC,MAA4B;IAC7D,MAAM,MAAM,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC;IAClC,OAAO;QACL,GAAG,MAAM;QACT,GAAG,EAAE,MAAM,CAAC,GAAG,IAAI,IAAI;QACvB,UAAU,EAAE,MAAM,CAAC,UAAU,IAAI,IAAI;QACrC,SAAS,EAAE,MAAM,CAAC,SAAS,CAAC,IAAI,EAAE;QAClC,iBAAiB,EAAE,WAAW,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,IAAI;KACzD,CAAC;AACJ,CAAC","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 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 email: z.email('Enter a valid email'),\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 .superRefine((v, ctx) => {\n refineDob(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 email: 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};\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 role: requiredChoice(staffRoleSchema, 'Select a staff role'),\n })\n .superRefine((v, ctx) => {\n refineDob(v, ctx);\n });\n\nexport type AddStaffFormValues = Omit<AddMemberFormValues, 'trainerId'> & {\n role: z.infer<typeof staffRoleSchema> | null;\n};\n\n// ─── Empty states ────────────────────────────────────────────────────────────\n\nconst EMPTY_BASE = {\n name: '',\n email: '',\n phone: '',\n dobDay: '',\n dobMonth: '',\n dobYear: '',\n gender: null,\n bloodGroup: '' as const,\n avatarUrl: '',\n} satisfies Omit<AddMemberFormValues, 'contacts' | 'trainerId'>;\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};\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'>) {\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 email: v.email.trim(),\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 ...(initialPlan ? { initialPlan } : {}),\n };\n}\n\n/** Validated staff form → `POST /staff` body. */\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// ─── 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 .superRefine((v, ctx) => {\n refineDob(v, ctx);\n });\n\nexport type EditMemberFormValues = AddMemberFormValues;\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 };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"add-person-form.js","sourceRoot":"","sources":["../src/add-person-form.ts"],"names":[],"mappings":"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","sourcesContent":["/**\r\n * gymmonk-schema — \"Add member\" / \"Add staff\" FORM schemas\r\n * ========================================================\r\n * The client-side validation contract for the owner's desk-entry forms, and the\r\n * mappers that turn a filled form into the `createMember` / `createStaff` API\r\n * bodies.\r\n *\r\n * Why these live here rather than in the client, and why they are distinct from\r\n * `createMemberBodySchema`: the form's working shape is not the wire shape. It\r\n * holds ONE \"name\" field the API splits in two, `null` for a choice not yet\r\n * made, and `''` for an optional value left blank. Those are legitimate mid-edit\r\n * states that the API body must never see. Expressing the same rules over the\r\n * form shape here keeps\r\n * every message, bound and regex defined once — the client cannot drift from\r\n * what the server will accept, and the coercion from one shape to the other is\r\n * part of the contract instead of ad-hoc code in a component.\r\n *\r\n * Mirrors `onboarding-form.ts`, which does the same job for the wizards.\r\n *\r\n * @module gymmonk-schema/add-person-form\r\n */\r\n\r\nimport { z } from 'zod';\r\nimport { ageInYears, isRealDate, PHONE_REGEX } from './common.js';\r\nimport { DEFAULT_TIMEZONE } from './location.js';\r\nimport type { CreateMemberBody, UpdateMemberBody } from './member.js';\r\nimport { emergencyContactSchema, MAX_EMERGENCY_CONTACTS } from './member-profile.js';\r\nimport type { AssignMembershipBody } from './membership.js';\r\nimport { bloodGroupSchema, genderSchema } from './shared.js';\r\nimport type { CreateStaffBody } from './staff.js';\r\nimport { staffRoleSchema } from './staff.js';\r\n\r\n// ─── Bounds ──────────────────────────────────────────────────────────────────\r\n\r\n/**\r\n * Age bounds for someone signed up at the desk. Wider than the member wizard's\r\n * because a gym also enrols juniors with a guardian present, and the date is\r\n * typed by staff rather than by the person themselves.\r\n */\r\nexport const ADD_PERSON_MIN_AGE_YEARS = 5;\r\nexport const ADD_PERSON_MAX_AGE_YEARS = 100;\r\n\r\n// ─── Date of birth, as three cells ───────────────────────────────────────────\r\n\r\n/**\r\n * The three cells joined into the `YYYY-MM-DD` the API stores, or `undefined`\r\n * when nothing was entered.\r\n *\r\n * Desk entry does not require a date of birth — a gym adds people with\r\n * whatever it has to hand — so \"blank\" is a real answer and has to survive the\r\n * whole way to the body as an ABSENT field rather than an empty string the\r\n * API would reject.\r\n */\r\nexport function isoFromDobCells(day: string, month: string, year: string): string | undefined {\r\n if (!day || !month || !year) return undefined;\r\n return `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`;\r\n}\r\n\r\n/** A stored `YYYY-MM-DD` split back into the cells, for seeding an edit form. */\r\nexport function dobCellsFromIso(iso: string | null | undefined): {\r\n dobDay: string;\r\n dobMonth: string;\r\n dobYear: string;\r\n} {\r\n const match = /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(iso ?? '');\r\n if (!match) return { dobDay: '', dobMonth: '', dobYear: '' };\r\n return { dobYear: match[1] as string, dobMonth: match[2] as string, dobDay: match[3] as string };\r\n}\r\n\r\n// ─── Field-level rules ───────────────────────────────────────────────────────\r\n\r\nconst requiredChoice = <T extends z.ZodTypeAny>(inner: T, message: string) =>\r\n inner.nullable().refine((v: unknown) => v !== null, { message });\r\n\r\n// ─── One emergency-contact row ───────────────────────────────────────────────\r\n\r\n/**\r\n * One emergency-contact row: WHO to call, on what number, and how they are\r\n * related. A row is either wholly blank (ignored) or names a person.\r\n *\r\n * ── Why `name` exists now ───────────────────────────────────────────────────\r\n * This form used to collect a relationship and a phone with no name at all,\r\n * and the mapper wrote the relationship INTO `name` because\r\n * `emergencyContactSchema` requires one. The member's own surfaces store a real\r\n * name, so the two halves of the product disagreed about what the field held,\r\n * and the disagreement destroyed data: a member onboards with \"Priya\", the\r\n * owner opens Edit Member and sees Relationship prefilled \"Priya\" (the reader\r\n * fell back to `name`), corrects it to \"Sister\", saves — and the contact is now\r\n * called Sister. Whoever has to ring that number in an emergency is left with\r\n * a word instead of a person.\r\n *\r\n * `relationship` stays OPTIONAL, matching `emergencyContactSchema`. It is\r\n * useful context, not something to block a desk entry over.\r\n */\r\nexport const emergencyContactRowSchema = z\r\n .object({\r\n name: z.string().trim().max(120),\r\n relationship: z.string().trim().max(60),\r\n phone: z.string().trim(),\r\n })\r\n .superRefine((v, ctx) => {\r\n const blank = v.name === '' && v.relationship === '' && v.phone === '';\r\n if (blank) return;\r\n if (v.name === '') {\r\n ctx.addIssue({\r\n code: 'custom',\r\n path: ['name'],\r\n message: \"Add the contact's name\",\r\n });\r\n }\r\n if (!PHONE_REGEX.test(v.phone)) {\r\n ctx.addIssue({\r\n code: 'custom',\r\n path: ['phone'],\r\n message: 'Enter a valid 10-digit mobile number',\r\n });\r\n }\r\n });\r\n\r\nexport interface EmergencyContactRow {\r\n name: string;\r\n relationship: string;\r\n phone: string;\r\n}\r\n\r\n// ─── Shared base ─────────────────────────────────────────────────────────────\r\n\r\nconst addPersonBaseShape = {\r\n /** One field on screen; split into `firstName` / `lastName` on the wire. */\r\n name: z.string().trim().min(1, 'Name is required').max(160),\r\n email: z.email('Enter a valid email'),\r\n phone: z.string().regex(PHONE_REGEX, 'Enter a valid 10-digit mobile number'),\r\n /**\r\n * Three cells, not a native date input.\r\n *\r\n * `<input type=\"date\">` renders in the BROWSER'S locale, so the same field\r\n * read \"mm/dd/yyyy\" at an Indian gym's front desk — and a member's own\r\n * onboarding asked for the very same fact through `DobInput`'s DD/MM/YYYY\r\n * cells. Two surfaces collecting one fact in two formats is how a birthday\r\n * gets typed the wrong way round.\r\n *\r\n * All three or none: desk entry does not require a date of birth, so blank\r\n * is a real answer, but half a date is not.\r\n */\r\n dobDay: z.string(),\r\n dobMonth: z.string(),\r\n dobYear: z.string(),\r\n gender: requiredChoice(genderSchema, 'Select a gender'),\r\n\r\n /** Blank when not recorded. */\r\n bloodGroup: bloodGroupSchema.or(z.literal('')),\r\n avatarUrl: z.string(),\r\n\r\n contacts: z.array(emergencyContactRowSchema).max(MAX_EMERGENCY_CONTACTS),\r\n};\r\n\r\n/**\r\n * Date of birth is optional, but a typed one has to be real and plausible —\r\n * a mis-keyed year is the single most common desk-entry slip.\r\n */\r\nfunction refineDob(\r\n v: { dobDay: string; dobMonth: string; dobYear: string },\r\n ctx: z.RefinementCtx,\r\n): void {\r\n const touched = Boolean(v.dobDay || v.dobMonth || v.dobYear);\r\n // Nothing entered is fine — the desk records what it has.\r\n if (!touched) return;\r\n\r\n // Every message lands on `dobDay`, the first cell, because the three are one\r\n // control on screen: an error parked on `dobYear` would sit under a field\r\n // the form renders as a single row and reads back from `dobDay`.\r\n if (!v.dobDay || !v.dobMonth || v.dobYear.length !== 4) {\r\n ctx.addIssue({ code: 'custom', path: ['dobDay'], message: 'Enter the full date of birth' });\r\n return;\r\n }\r\n\r\n const day = Number(v.dobDay);\r\n const month = Number(v.dobMonth);\r\n const year = Number(v.dobYear);\r\n\r\n if (!isRealDate(year, month, day)) {\r\n ctx.addIssue({ code: 'custom', path: ['dobDay'], message: 'That date does not exist' });\r\n return;\r\n }\r\n\r\n const age = ageInYears(year, month, day);\r\n if (age < 0) {\r\n ctx.addIssue({\r\n code: 'custom',\r\n path: ['dobDay'],\r\n message: 'Date of birth cannot be in the future',\r\n });\r\n } else if (age < ADD_PERSON_MIN_AGE_YEARS || age > ADD_PERSON_MAX_AGE_YEARS) {\r\n ctx.addIssue({ code: 'custom', path: ['dobDay'], message: 'Check the year of birth' });\r\n }\r\n}\r\n\r\n// ─── Member form ─────────────────────────────────────────────────────────────\r\n\r\nexport const addMemberFormSchema = z\r\n .object({\r\n ...addPersonBaseShape,\r\n /**\r\n * Central-auth user id of the trainer, or `''` for none. Only trainers who\r\n * have CLAIMED their invite can be assigned — an unclaimed staff record has\r\n * no user to point at (`MemberProfile.assignedTrainerId` refs `User`).\r\n */\r\n trainerId: z.string(),\r\n })\r\n .superRefine((v, ctx) => {\r\n refineDob(v, ctx);\r\n });\r\n\r\n/**\r\n * Adding a member asks for the person, not their paperwork.\r\n *\r\n * No address, no Aadhaar and no ID proof — on this form or on any other. A\r\n * front desk does not have them when somebody walks in, the member's own\r\n * screens never asked for them, and nothing in the product ever read one back.\r\n * Every surface that collected them was filling a drawer nobody opens.\r\n */\r\nexport type AddMemberFormValues = {\r\n name: string;\r\n email: string;\r\n phone: string;\r\n dobDay: string;\r\n dobMonth: string;\r\n dobYear: string;\r\n gender: z.infer<typeof genderSchema> | null;\r\n bloodGroup: z.infer<typeof bloodGroupSchema> | '';\r\n avatarUrl: string;\r\n contacts: EmergencyContactRow[];\r\n /** Optional — a gym with no trainers yet, or none chosen. `''` for none. */\r\n trainerId: string;\r\n};\r\n\r\n// ─── Staff form ──────────────────────────────────────────────────────────────\r\n\r\n/**\r\n * Staff are asked exactly what a member is asked, plus the job they do.\r\n *\r\n * The home address and the ID upload both went for the same reason: nothing in\r\n * the product ever showed either one back. A gym that needs an employee's\r\n * address for a contract has somewhere else to keep it; what it had here was a\r\n * field collected once and never looked at again, which is the worst thing to\r\n * ask anyone for.\r\n */\r\nexport const addStaffFormSchema = z\r\n .object({\r\n ...addPersonBaseShape,\r\n role: requiredChoice(staffRoleSchema, 'Select a staff role'),\r\n })\r\n .superRefine((v, ctx) => {\r\n refineDob(v, ctx);\r\n });\r\n\r\nexport type AddStaffFormValues = Omit<AddMemberFormValues, 'trainerId'> & {\r\n role: z.infer<typeof staffRoleSchema> | null;\r\n};\r\n\r\n// ─── Empty states ────────────────────────────────────────────────────────────\r\n\r\nconst EMPTY_BASE = {\r\n name: '',\r\n email: '',\r\n phone: '',\r\n dobDay: '',\r\n dobMonth: '',\r\n dobYear: '',\r\n gender: null,\r\n bloodGroup: '' as const,\r\n avatarUrl: '',\r\n} satisfies Omit<AddMemberFormValues, 'contacts' | 'trainerId'>;\r\n\r\n/** One blank row so the section renders with a contact to fill. */\r\nexport const EMPTY_EMERGENCY_CONTACT: EmergencyContactRow = {\r\n name: '',\r\n relationship: '',\r\n phone: '',\r\n};\r\n\r\nexport const EMPTY_ADD_MEMBER_FORM: AddMemberFormValues = {\r\n ...EMPTY_BASE,\r\n contacts: [{ ...EMPTY_EMERGENCY_CONTACT }],\r\n trainerId: '',\r\n};\r\n\r\nexport const EMPTY_ADD_STAFF_FORM: AddStaffFormValues = {\r\n ...EMPTY_BASE,\r\n contacts: [{ ...EMPTY_EMERGENCY_CONTACT }],\r\n role: null,\r\n};\r\n\r\n// ─── Form → API body ─────────────────────────────────────────────────────────\r\n\r\n/** One \"Name\" field on screen, `firstName` + `lastName` on the wire. */\r\nfunction splitName(value: string): { firstName: string; lastName?: string } {\r\n const parts = value.trim().split(/\\s+/).filter(Boolean);\r\n const lastName = parts.slice(1).join(' ');\r\n return { firstName: parts[0] ?? '', ...(lastName ? { lastName } : {}) };\r\n}\r\n\r\n/** Drop a blank optional so it reads as \"not provided\" rather than `''`. */\r\nfunction orUndefined(value: string): string | undefined {\r\n const trimmed = value.trim();\r\n return trimmed === '' ? undefined : trimmed;\r\n}\r\n\r\n/** Phones are STORED raw — strip any display formatting back to 10 digits. */\r\nfunction asPhone(value: string): string {\r\n return value.replace(/\\D/g, '').slice(-10);\r\n}\r\n\r\n/**\r\n * The fields EVERY desk form sends.\r\n *\r\n * All three now ask exactly the same things about the PERSON; what differs is\r\n * what the record is FOR, so each mapper adds its own role / trainer / plan on\r\n * top. Address and ID proof used to be the exception here, asked by two of the\r\n * three; they are gone from all of them.\r\n */\r\nfunction sharedBody(v: Omit<AddMemberFormValues, 'trainerId'>) {\r\n // A row with nothing in it is not a contact. Anything else has already been\r\n // through `emergencyContactRowSchema`, which requires a name and a valid\r\n // phone the moment a row is touched.\r\n const emergencyContacts = v.contacts\r\n .filter((c) => c.name !== '' || c.relationship !== '' || c.phone !== '')\r\n .map((c) =>\r\n emergencyContactSchema.parse({\r\n name: c.name.trim(),\r\n phone: asPhone(c.phone),\r\n relationship: orUndefined(c.relationship),\r\n }),\r\n )\r\n .slice(0, MAX_EMERGENCY_CONTACTS);\r\n\r\n return {\r\n ...splitName(v.name),\r\n email: v.email.trim(),\r\n phone: asPhone(v.phone),\r\n // The form schema guarantees a choice was made before we get here.\r\n gender: v.gender ?? 'other',\r\n // Absent rather than empty when the desk did not record one — see\r\n // `isoFromDobCells`.\r\n dob: isoFromDobCells(v.dobDay, v.dobMonth, v.dobYear),\r\n bloodGroup: v.bloodGroup === '' ? undefined : v.bloodGroup,\r\n emergencyContacts,\r\n avatarUrl: orUndefined(v.avatarUrl),\r\n };\r\n}\r\n\r\n/**\r\n * Validated member form → `POST /members` body. `initialPlan` comes from the\r\n * membership step, which the form itself knows nothing about.\r\n */\r\nexport function toCreateMemberBody(\r\n values: AddMemberFormValues,\r\n initialPlan?: AssignMembershipBody,\r\n): CreateMemberBody {\r\n return {\r\n ...sharedBody(values),\r\n assignedTrainerId: orUndefined(values.trainerId),\r\n ...(initialPlan ? { initialPlan } : {}),\r\n };\r\n}\r\n\r\n/** Validated staff form → `POST /staff` body. */\r\nexport function toCreateStaffBody(values: AddStaffFormValues): CreateStaffBody {\r\n return {\r\n ...sharedBody(values),\r\n // The form schema guarantees a role was chosen.\r\n role: values.role ?? 'trainer',\r\n };\r\n}\r\n\r\n/**\r\n * The joining date must be a real day, and must already have happened.\r\n *\r\n * Validated here as well as on the server because this one is not a formatting\r\n * preference: the date anchors the member's whole billing cycle, so a typo\r\n * that slips through bills them on the wrong day of every month until somebody\r\n * notices. The server holds the same rule — this is the copy that can say so\r\n * before the owner presses save.\r\n */\r\nfunction refineJoiningDate(v: { joiningDate: string }, ctx: z.RefinementCtx): void {\r\n const raw = v.joiningDate.trim();\r\n // Blank is \"leave it as it is\", which is always allowed.\r\n if (!raw) return;\r\n\r\n const parts = /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(raw);\r\n if (!parts) {\r\n ctx.addIssue({ code: 'custom', path: ['joiningDate'], message: 'Enter a valid date' });\r\n return;\r\n }\r\n\r\n const [year, month, day] = [Number(parts[1]), Number(parts[2]), Number(parts[3])];\r\n if (!isRealDate(year, month, day)) {\r\n ctx.addIssue({ code: 'custom', path: ['joiningDate'], message: 'That date does not exist' });\r\n return;\r\n }\r\n\r\n /**\r\n * \"Today\" means today AT THE GYM, and the comparison is between strings.\r\n *\r\n * `en-CA` is the one common locale whose short format is already\r\n * `YYYY-MM-DD`, so the zone conversion and the formatting happen in one step\r\n * and the result sorts lexicographically — an exact calendar-day comparison\r\n * with no instant arithmetic in it. This mirrors `gymDayOf` on the server,\r\n * deliberately: a UTC day here would be one behind IST between midnight and\r\n * 5:30am, and would tell an owner opening the app early that TODAY is in the\r\n * future.\r\n */\r\n const today = new Intl.DateTimeFormat('en-CA', { timeZone: DEFAULT_TIMEZONE }).format(new Date());\r\n if (raw > today) {\r\n ctx.addIssue({\r\n code: 'custom',\r\n path: ['joiningDate'],\r\n message: 'A joining date cannot be in the future',\r\n });\r\n }\r\n}\r\n\r\n// ─── Edit member ─────────────────────────────────────────────────────────────\r\n\r\n/**\r\n * Editing a member asks for exactly what adding one asks for.\r\n *\r\n * It stays a separate schema and a separate mapper because the two differ in\r\n * what \"blank\" means. On the add form a blank optional is simply not provided;\r\n * on the edit form it is an instruction to CLEAR a value that is already\r\n * stored, so `toUpdateMemberBody` sends `null` where `toCreateMemberBody`\r\n * omits the field.\r\n */\r\nexport const editMemberFormSchema = z\r\n .object({\r\n ...addPersonBaseShape,\r\n trainerId: z.string(),\r\n /**\r\n * When the GYM says this person started training there, `YYYY-MM-DD`.\r\n *\r\n * Asked only on EDIT. At the desk the answer is always \"today\" and one\r\n * more field in a queue is one more thing to get wrong; the correction is\r\n * something the owner makes later, once they realise the app's record and\r\n * the gym's disagree.\r\n *\r\n * Blank means \"leave it alone\", never \"clear it\" — unlike the other\r\n * optional fields on this form. Every enrolment has a joining date because\r\n * the billing cycle is measured from one, so there is no empty state to\r\n * write back.\r\n */\r\n joiningDate: z.string(),\r\n })\r\n .superRefine((v, ctx) => {\r\n refineDob(v, ctx);\r\n refineJoiningDate(v, ctx);\r\n });\r\n\r\nexport type EditMemberFormValues = AddMemberFormValues & {\r\n joiningDate: string;\r\n};\r\n\r\n/**\r\n * Validated edit form → `PATCH /members/:id` body.\r\n *\r\n * Sends every field, including the cleared ones as `null`, because the owner\r\n * emptying a field is an edit like any other. The identity fields go along too;\r\n * the server drops them once the member has claimed their account.\r\n */\r\nexport function toUpdateMemberBody(values: EditMemberFormValues): UpdateMemberBody {\r\n const shared = sharedBody(values);\r\n return {\r\n ...shared,\r\n dob: shared.dob ?? null,\r\n bloodGroup: shared.bloodGroup ?? null,\r\n avatarUrl: values.avatarUrl.trim(),\r\n assignedTrainerId: orUndefined(values.trainerId) ?? null,\r\n // OMITTED when blank rather than sent as null: every enrolment has a\r\n // joining date, so \"no answer\" means leave the stored one alone.\r\n joinedAt: orUndefined(values.joiningDate.trim()),\r\n };\r\n}\r\n"]}
|
package/dist/member.d.ts
CHANGED
|
@@ -183,6 +183,11 @@ export declare const memberDetailSchema: z.ZodObject<{
|
|
|
183
183
|
updatedAt: z.ZodISODateTime;
|
|
184
184
|
}, z.core.$strip>>;
|
|
185
185
|
balanceDueInr: z.ZodNumber;
|
|
186
|
+
nextTerm: z.ZodNullable<z.ZodObject<{
|
|
187
|
+
startDate: z.ZodString;
|
|
188
|
+
endDate: z.ZodString;
|
|
189
|
+
isOverdue: z.ZodBoolean;
|
|
190
|
+
}, z.core.$strip>>;
|
|
186
191
|
}, z.core.$strip>;
|
|
187
192
|
export type MemberDetail = z.infer<typeof memberDetailSchema>;
|
|
188
193
|
export declare const createMemberBodySchema: z.ZodObject<{
|
|
@@ -280,6 +285,7 @@ export declare const updateMemberBodySchema: z.ZodObject<{
|
|
|
280
285
|
"AB-": "AB-";
|
|
281
286
|
}>>>;
|
|
282
287
|
assignedTrainerId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
288
|
+
joinedAt: z.ZodOptional<z.ZodString>;
|
|
283
289
|
}, z.core.$strip>;
|
|
284
290
|
export type UpdateMemberBody = z.infer<typeof updateMemberBodySchema>;
|
|
285
291
|
/**
|
package/dist/member.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"member.d.ts","sourceRoot":"","sources":["../src/member.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"member.d.ts","sourceRoot":"","sources":["../src/member.ts"],"names":[],"mappings":"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"}
|
package/dist/member.js
CHANGED
|
@@ -12,7 +12,7 @@ import { gymMemberCodeSchema } from './codes.js';
|
|
|
12
12
|
import { isoDateSchema, isoDateTimeSchema, objectIdSchema, optionalTrimmedString, paginationSchema, phoneSchema, priceInrSchema, } from './common.js';
|
|
13
13
|
import { enrolmentStatusSchema } from './enrolment.js';
|
|
14
14
|
import { emergencyContactSchema, MAX_EMERGENCY_CONTACTS } from './member-profile.js';
|
|
15
|
-
import { assignMembershipBodySchema, membershipSchema, membershipStatusSchema, } from './membership.js';
|
|
15
|
+
import { assignMembershipBodySchema, membershipSchema, membershipStatusSchema, membershipTermSchema, } from './membership.js';
|
|
16
16
|
import { bloodGroupSchema, genderSchema } from './shared.js';
|
|
17
17
|
// ─── Segments ────────────────────────────────────────────────────────────────
|
|
18
18
|
/** Attention buckets surfaced on roster cards. */
|
|
@@ -134,6 +134,18 @@ export const memberDetailSchema = z.object({
|
|
|
134
134
|
* the profile and the list can never disagree about who owes money.
|
|
135
135
|
*/
|
|
136
136
|
balanceDueInr: z.number().int().nonnegative(),
|
|
137
|
+
/**
|
|
138
|
+
* The term the NEXT confirmed payment will cover, so the owner reads it
|
|
139
|
+
* before deciding rather than discovering it afterwards.
|
|
140
|
+
*
|
|
141
|
+
* Null when the answer is not yet knowable: no membership has ever been
|
|
142
|
+
* assigned, so there is no variant duration to measure a term with. It is
|
|
143
|
+
* deliberately computed here rather than in the client — the cycle depends
|
|
144
|
+
* on the joining date, the enrolment's creation month and the terms already
|
|
145
|
+
* sold, and a client that recomputed it from a partial view would show a
|
|
146
|
+
* different month than the one the server is about to write.
|
|
147
|
+
*/
|
|
148
|
+
nextTerm: membershipTermSchema.nullable(),
|
|
137
149
|
});
|
|
138
150
|
// ─── Add member (provisioning) ───────────────────────────────────────────────
|
|
139
151
|
export const createMemberBodySchema = z.object({
|
|
@@ -180,6 +192,22 @@ export const updateMemberBodySchema = createMemberBodySchema
|
|
|
180
192
|
dob: isoDateSchema.nullish(),
|
|
181
193
|
bloodGroup: bloodGroupSchema.nullish(),
|
|
182
194
|
assignedTrainerId: objectIdSchema.nullish(),
|
|
195
|
+
/**
|
|
196
|
+
* The date the GYM says this person started training there.
|
|
197
|
+
*
|
|
198
|
+
* Editable because the app's record and the gym's record are different
|
|
199
|
+
* facts and routinely disagree: somebody who has trained since March is
|
|
200
|
+
* added to GymMonk in July, and until now their joining date was silently
|
|
201
|
+
* the day the desk got round to typing them in. That date is not a
|
|
202
|
+
* cosmetic label — it anchors the billing cycle, so a wrong one bills the
|
|
203
|
+
* member on the wrong day of every month for as long as they stay.
|
|
204
|
+
*
|
|
205
|
+
* Deliberately unbounded in the past: it records something that already
|
|
206
|
+
* happened, and a gym may well be older than this app. The BILLING floor
|
|
207
|
+
* is a separate rule and is enforced where terms are derived, so an
|
|
208
|
+
* honest March joining date can never be turned into a March invoice.
|
|
209
|
+
*/
|
|
210
|
+
joinedAt: isoDateSchema.optional(),
|
|
183
211
|
});
|
|
184
212
|
// ─── Invite & claim ──────────────────────────────────────────────────────────
|
|
185
213
|
/**
|
package/dist/member.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"member.js","sourceRoot":"","sources":["../src/member.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,EACL,aAAa,EACb,iBAAiB,EACjB,cAAc,EACd,qBAAqB,EACrB,gBAAgB,EAChB,WAAW,EACX,cAAc,GACf,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AACvD,OAAO,EAAE,sBAAsB,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AACrF,OAAO,EACL,0BAA0B,EAC1B,gBAAgB,EAChB,sBAAsB,GACvB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE7D,gFAAgF;AAEhF,kDAAkD;AAClD,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,UAAU,EAAE,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC;AAG1E,gFAAgF;AAEhF,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC7C,yEAAyE;IACzE,WAAW,EAAE,cAAc;IAC3B,4EAA4E;IAC5E,MAAM,EAAE,cAAc,CAAC,QAAQ,EAAE;IACjC,YAAY,EAAE,qBAAqB;IACnC,YAAY,EAAE,cAAc,CAAC,QAAQ,EAAE;IACvC,aAAa,EAAE,mBAAmB;IAClC,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;IAChB,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAChC,QAAQ,EAAE,CAAC,CAAC,OAAO,EAAE;IACrB;;;;;;;;;;;;OAYG;IACH,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC5B,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,QAAQ,EAAE;IAC3B,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC/B,MAAM,EAAE,sBAAsB,CAAC,QAAQ,EAAE;IACzC,OAAO,EAAE,aAAa,CAAC,QAAQ,EAAE;IACjC,WAAW,EAAE,iBAAiB,CAAC,QAAQ,EAAE;IACzC,aAAa,EAAE,cAAc;IAC7B,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE;IACxC,QAAQ,EAAE,CAAC,CAAC,KAAK,CAAC,mBAAmB,CAAC;IAEtC;;;;OAIG;IACH,QAAQ,EAAE,iBAAiB;IAE3B;;;;;;;OAOG;IACH,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;IAEzC,oFAAoF;IACpF,SAAS,EAAE,cAAc,CAAC,QAAQ,EAAE;IACpC,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CACnC,CAAC,CAAC;AAEH,yFAAyF;AACzF,MAAM,CAAC,MAAM,sBAAsB,GAAG,EAAE,CAAC;AAGzC,oCAAoC;AACpC,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC9C,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;IACrC,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;IACtC,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;IACxC,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;CACzC,CAAC,CAAC;AAGH,gFAAgF;AAEhF,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,UAAU,CAAC,CAAC,CAAC;AAGpF,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC;AAG3E,MAAM,CAAC,MAAM,qBAAqB,GAAG,gBAAgB,CAAC,MAAM,CAAC;IAC3D,QAAQ,EAAE,cAAc,CAAC,QAAQ,EAAE;IACnC,MAAM,EAAE,kBAAkB,CAAC,OAAO,CAAC,KAAK,CAAC;IACzC,IAAI,EAAE,gBAAgB,CAAC,OAAO,CAAC,QAAQ,CAAC;IACxC,CAAC,EAAE,qBAAqB,CAAC,GAAG,CAAC;CAC9B,CAAC,CAAC;AAGH,gFAAgF;AAEhF,kEAAkE;AAClE,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC1C,iFAAiF;IACjF,EAAE,EAAE,cAAc,CAAC,QAAQ,EAAE;IAC7B,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;IAChB,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE;IACjB,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC5B,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAChC,QAAQ,EAAE,CAAC,CAAC,OAAO,EAAE;CACtB,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC;IACzC,WAAW,EAAE,cAAc;IAC3B,YAAY,EAAE,qBAAqB;IACnC;;;;OAIG;IACH,IAAI,EAAE,mBAAmB;IACzB,aAAa,EAAE,mBAAmB;IAClC,MAAM,EAAE,YAAY,CAAC,QAAQ,EAAE;IAC/B,GAAG,EAAE,aAAa,CAAC,QAAQ,EAAE;IAC7B,UAAU,EAAE,gBAAgB,CAAC,QAAQ,EAAE;IACvC,iBAAiB,EAAE,CAAC,CAAC,KAAK,CAAC,sBAAsB,CAAC;IAClD,QAAQ,EAAE,iBAAiB;IAC3B,WAAW,EAAE,iBAAiB,CAAC,QAAQ,EAAE;IACzC,qBAAqB,EAAE,cAAc,CAAC,QAAQ,EAAE;IAChD,iBAAiB,EAAE,cAAc,CAAC,QAAQ,EAAE;IAC5C;;;;;;;;OAQG;IACH,UAAU,EAAE,gBAAgB,CAAC,QAAQ,EAAE;IACvC;;;;;OAKG;IACH,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;CAC9C,CAAC,CAAC;AAGH,gFAAgF;AAEhF,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC7C,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,wBAAwB,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC;IACrE,QAAQ,EAAE,qBAAqB,CAAC,EAAE,CAAC;IACnC,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,2BAA2B,CAAC;IAC3C,KAAK,EAAE,WAAW;IAClB,MAAM,EAAE,YAAY;IACpB,GAAG,EAAE,aAAa,CAAC,QAAQ,EAAE;IAC7B,UAAU,EAAE,gBAAgB,CAAC,QAAQ,EAAE;IACvC,iBAAiB,EAAE,CAAC,CAAC,KAAK,CAAC,sBAAsB,CAAC,CAAC,GAAG,CAAC,sBAAsB,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IAC1F,SAAS,EAAE,qBAAqB,CAAC,IAAI,CAAC;IACtC,iBAAiB,EAAE,cAAc,CAAC,QAAQ,EAAE;IAC5C,iDAAiD;IACjD,WAAW,EAAE,0BAA0B,CAAC,QAAQ,EAAE;CACnD,CAAC,CAAC;AAGH;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,sBAAsB;KACzD,IAAI,CAAC;IACJ,WAAW,EAAE,IAAI;IACjB,GAAG,EAAE,IAAI;IACT,UAAU,EAAE,IAAI;IAChB,iBAAiB,EAAE,IAAI;CACxB,CAAC;KACD,OAAO,EAAE;KACT,MAAM,CAAC;IACN,GAAG,EAAE,aAAa,CAAC,OAAO,EAAE;IAC5B,UAAU,EAAE,gBAAgB,CAAC,OAAO,EAAE;IACtC,iBAAiB,EAAE,cAAc,CAAC,OAAO,EAAE;CAC5C,CAAC,CAAC;AAGL,gFAAgF;AAEhF;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC;IACzC,+EAA+E;IAC/E,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;IAChB,sEAAsE;IACtE,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE;IACf,0EAA0E;IAC1E,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE;IACjB,8DAA8D;IAC9D,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC5B,SAAS,EAAE,iBAAiB;CAC7B,CAAC,CAAC;AAGH,2EAA2E;AAC3E,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC5C,yEAAyE;IACzE,WAAW,EAAE,cAAc;IAC3B,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;IAChB,MAAM,EAAE,kBAAkB;IAC1B;;;;;;;;OAQG;IACH,UAAU,EAAE,gBAAgB,CAAC,QAAQ,EAAE;CACxC,CAAC,CAAC;AAGH;;;;GAIG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC1C,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE;IACnB,aAAa,EAAE,mBAAmB;IAClC,yEAAyE;IACzE,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE;IACjB,SAAS,EAAE,iBAAiB;IAC5B,4EAA4E;IAC5E,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE;IACpB,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE;CACrB,CAAC,CAAC;AAGH;;;;GAIG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC5C,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;CAC/B,CAAC,CAAC;AAGH,gFAAgF;AAEhF,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;AAG3E,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC9C,SAAS,EAAE,cAAc,CAAC,QAAQ,EAAE;CACrC,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,2BAA2B,GAAG,CAAC,CAAC,MAAM,CAAC;IAClD,MAAM,EAAE,cAAc,CAAC,QAAQ,EAAE;CAClC,CAAC,CAAC;AAGH,wEAAwE;AACxE,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC7C,gBAAgB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;CAC3C,CAAC,CAAC","sourcesContent":["/**\n * gymmonk-schema — Member management (owner console)\n * ==================================================\n * Owner-facing roster + detail projections (User × MemberProfile × Membership)\n * and the \"add member\" provisioning payload. Adding a member provisions a real\n * central-auth account, so the result carries the login email + temp password.\n *\n * @module gymmonk-schema/member\n */\n\nimport { z } from 'zod';\nimport { gymMemberCodeSchema } from './codes.js';\nimport {\n isoDateSchema,\n isoDateTimeSchema,\n objectIdSchema,\n optionalTrimmedString,\n paginationSchema,\n phoneSchema,\n priceInrSchema,\n} from './common.js';\nimport { enrolmentStatusSchema } from './enrolment.js';\nimport { emergencyContactSchema, MAX_EMERGENCY_CONTACTS } from './member-profile.js';\nimport {\n assignMembershipBodySchema,\n membershipSchema,\n membershipStatusSchema,\n} from './membership.js';\nimport { bloodGroupSchema, genderSchema } from './shared.js';\n\n// ─── Segments ────────────────────────────────────────────────────────────────\n\n/** Attention buckets surfaced on roster cards. */\nexport const memberSegmentSchema = z.enum(['expiring', 'new', 'at-risk']);\nexport type MemberSegment = z.infer<typeof memberSegmentSchema>;\n\n// ─── Roster item (list projection) ───────────────────────────────────────────\n\nexport const memberRosterItemSchema = z.object({\n /** The gym record id — always present, even before the member claims. */\n enrolmentId: objectIdSchema,\n /** Null until claimed; the roster shows an \"Invited\" pill in that state. */\n userId: objectIdSchema.nullable(),\n recordStatus: enrolmentStatusSchema,\n membershipId: objectIdSchema.nullable(),\n gymMemberCode: gymMemberCodeSchema,\n name: z.string(),\n avatarUrl: z.string().nullable(),\n verified: z.boolean(),\n /**\n * Contact details, on the ROSTER row rather than only the detail record.\n *\n * The card's whole job when someone is overdue is to let the owner chase it\n * without leaving the list — call, WhatsApp, email. Those are `tel:` /\n * `wa.me` / `mailto:` links, so the row cannot build them without the raw\n * values, and fetching the full member just to dial them would be one\n * request per card.\n *\n * Nullable because an invited record may have only one of the two: the gym\n * creates it with whatever it has, and the member fills in the rest on claim.\n * Phone is RAW 10 digits — `+91` is display-only, never stored.\n */\n phone: z.string().nullable(),\n email: z.email().nullable(),\n planName: z.string().nullable(),\n status: membershipStatusSchema.nullable(),\n endDate: isoDateSchema.nullable(),\n lastVisitAt: isoDateTimeSchema.nullable(),\n balanceDueInr: priceInrSchema,\n overdueDays: z.number().int().nullable(),\n segments: z.array(memberSegmentSchema),\n\n /**\n * When the gym record was created — NOT when the person claimed it. The\n * roster's \"Joined Date\" filter groups by how long someone has been a member\n * of the gym, which starts at the desk, not at the login.\n */\n joinedAt: isoDateTimeSchema,\n\n /**\n * Check-ins in the trailing 14 days, used by the roster's attendance filters\n * to separate a no-show from an irregular from a regular.\n *\n * Counted rather than derived from `lastVisitAt`, because the two answer\n * different questions: someone who came once yesterday and someone who comes\n * daily share a last visit but are not the same member to chase.\n */\n visits14d: z.number().int().nonnegative(),\n\n /** Assigned trainer, so the roster can be filtered down to one trainer's people. */\n trainerId: objectIdSchema.nullable(),\n trainerName: z.string().nullable(),\n});\n\n/** Trailing window the roster's attendance filters and `visits14d` are measured over. */\nexport const ATTENDANCE_WINDOW_DAYS = 14;\nexport type MemberRosterItem = z.infer<typeof memberRosterItemSchema>;\n\n/** Header counts for the roster. */\nexport const memberListSummarySchema = z.object({\n total: z.number().int().nonnegative(),\n active: z.number().int().nonnegative(),\n expiring: z.number().int().nonnegative(),\n inactive: z.number().int().nonnegative(),\n});\nexport type MemberListSummary = z.infer<typeof memberListSummarySchema>;\n\n// ─── List query ──────────────────────────────────────────────────────────────\n\nexport const memberFilterSchema = z.enum(['all', 'active', 'inactive', 'verified']);\nexport type MemberFilter = z.infer<typeof memberFilterSchema>;\n\nexport const memberSortSchema = z.enum(['recent', 'az', 'expiry', 'dues']);\nexport type MemberSort = z.infer<typeof memberSortSchema>;\n\nexport const memberListQuerySchema = paginationSchema.extend({\n centerId: objectIdSchema.optional(),\n filter: memberFilterSchema.default('all'),\n sort: memberSortSchema.default('recent'),\n q: optionalTrimmedString(120),\n});\nexport type MemberListQuery = z.infer<typeof memberListQuerySchema>;\n\n// ─── Detail projection ───────────────────────────────────────────────────────\n\n/** The identity slice a projection pulls from the auth `User`. */\nexport const memberUserRefSchema = z.object({\n /** Null while the record is still `invited` (no central-auth account linked). */\n id: objectIdSchema.nullable(),\n name: z.string(),\n email: z.string(),\n phone: z.string().nullable(),\n avatarUrl: z.string().nullable(),\n verified: z.boolean(),\n});\nexport type MemberUserRef = z.infer<typeof memberUserRefSchema>;\n\nexport const memberDetailSchema = z.object({\n enrolmentId: objectIdSchema,\n recordStatus: enrolmentStatusSchema,\n /**\n * Identity from central auth once claimed. Before that the gym still knows the\n * person (name / contact captured at the desk), so this carries the invited\n * details with `id: null`.\n */\n user: memberUserRefSchema,\n gymMemberCode: gymMemberCodeSchema,\n gender: genderSchema.nullable(),\n dob: isoDateSchema.nullable(),\n bloodGroup: bloodGroupSchema.nullable(),\n emergencyContacts: z.array(emergencyContactSchema),\n joinedAt: isoDateTimeSchema,\n lastVisitAt: isoDateTimeSchema.nullable(),\n assignedWorkoutPlanId: objectIdSchema.nullable(),\n assignedTrainerId: objectIdSchema.nullable(),\n /**\n * The member's current membership, in FULL rather than by id.\n *\n * The profile screen shows the plan name, its billing cycle, the lifecycle\n * status pill and the expiry date all at once, and every one of those lives\n * here. Sending only the id forced the owner console to re-query the roster\n * for the same member just to read the plan columns back, and to match the\n * plan by NAME to find its perk icons — both of which this removes.\n */\n membership: membershipSchema.nullable(),\n /**\n * Still owed on the current term: its price less what has been recorded as\n * collected. Zero when settled, when the membership was cancelled, or when\n * there is no membership at all. Same definition as the roster's column, so\n * the profile and the list can never disagree about who owes money.\n */\n balanceDueInr: z.number().int().nonnegative(),\n});\nexport type MemberDetail = z.infer<typeof memberDetailSchema>;\n\n// ─── Add member (provisioning) ───────────────────────────────────────────────\n\nexport const createMemberBodySchema = z.object({\n firstName: z.string().trim().min(1, 'First name is required').max(80),\n lastName: optionalTrimmedString(80),\n email: z.email('A valid email is required'),\n phone: phoneSchema,\n gender: genderSchema,\n dob: isoDateSchema.optional(),\n bloodGroup: bloodGroupSchema.optional(),\n emergencyContacts: z.array(emergencyContactSchema).max(MAX_EMERGENCY_CONTACTS).default([]),\n avatarUrl: optionalTrimmedString(2000),\n assignedTrainerId: objectIdSchema.optional(),\n /** Optionally assign a plan in the same step. */\n initialPlan: assignMembershipBodySchema.optional(),\n});\nexport type CreateMemberBody = z.infer<typeof createMemberBodySchema>;\n\n/**\n * Edit a member's record from the owner console.\n *\n * Every field is optional — the console sends what the form holds, and a field\n * left out is left alone.\n *\n * ## What the gym may and may not change\n * `firstName` / `lastName` / `email` / `phone` are the INVITE details, and the\n * server accepts them only while the record is unclaimed. Once the member has\n * linked their central-auth account, that account is the source of truth for\n * their identity and is shared with the other products — a gym must not be able\n * to rewrite someone's name or email out from under them. The server ignores\n * those four on a claimed record rather than failing the whole request, so an\n * owner correcting a blood group is never blocked by a name they cannot change.\n *\n * Nullable fields (`dob`, `bloodGroup`, `assignedTrainerId`) accept `null` to\n * CLEAR a value, which `undefined` cannot express.\n */\nexport const updateMemberBodySchema = createMemberBodySchema\n .omit({\n initialPlan: true,\n dob: true,\n bloodGroup: true,\n assignedTrainerId: true,\n })\n .partial()\n .extend({\n dob: isoDateSchema.nullish(),\n bloodGroup: bloodGroupSchema.nullish(),\n assignedTrainerId: objectIdSchema.nullish(),\n });\nexport type UpdateMemberBody = z.infer<typeof updateMemberBodySchema>;\n\n// ─── Invite & claim ──────────────────────────────────────────────────────────\n\n/**\n * The invite a gym hands to a new member.\n *\n * GymMonk never mints credentials for anyone: central auth is the SSOT for\n * identity, and an account there is shared across products. So adding a member\n * creates a gym record only, plus an unguessable invite code. The member\n * registers/logs in themselves through central auth (exactly as in jansathi) and\n * then CLAIMS the record. The owner shares `url` over WhatsApp or shows the QR.\n */\nexport const memberInviteSchema = z.object({\n /** Single-use, unguessable. Regenerating an invite invalidates the old one. */\n code: z.string(),\n /** Absolute claim URL the owner shares (also encoded into the QR). */\n url: z.string(),\n /** The address the member MUST register with for the claim to succeed. */\n email: z.string(),\n /** WhatsApp number to share on, when the gym recorded one. */\n phone: z.string().nullable(),\n expiresAt: isoDateTimeSchema,\n});\nexport type MemberInvite = z.infer<typeof memberInviteSchema>;\n\n/** Result of adding a member: the gym record + the invite to hand over. */\nexport const provisionResultSchema = z.object({\n /** The new member record (NOT a central-auth user — none is created). */\n enrolmentId: objectIdSchema,\n code: z.string(),\n invite: memberInviteSchema,\n /**\n * The membership created from `initialPlan`, or `null`.\n *\n * Null means one of two DIFFERENT things, and the caller can tell them apart\n * because it knows whether it sent `initialPlan`: no plan was chosen, or the\n * chosen plan could not be assigned. Adding a member never fails over the\n * second case — the person is on the roster either way — so this is how the\n * owner finds out the plan still needs assigning rather than assuming it took.\n */\n membership: membershipSchema.nullable(),\n});\nexport type ProvisionResult = z.infer<typeof provisionResultSchema>;\n\n/**\n * What the claim screen shows BEFORE the member authenticates, so they know\n * which address to register with. Public (the code is the bearer secret), and\n * deliberately minimal — gym name, member code, and the expected email.\n */\nexport const invitePreviewSchema = z.object({\n gymName: z.string(),\n gymMemberCode: gymMemberCodeSchema,\n /** Shown verbatim: the member must know exactly which address to use. */\n email: z.string(),\n expiresAt: isoDateTimeSchema,\n /** True once claimed / expired, so the screen can explain why it failed. */\n claimed: z.boolean(),\n expired: z.boolean(),\n});\nexport type InvitePreview = z.infer<typeof invitePreviewSchema>;\n\n/**\n * Claim an invite as the authenticated caller. The server links the record only\n * when the code is valid AND the caller's central-auth email matches the address\n * the invite was issued to.\n */\nexport const claimInviteBodySchema = z.object({\n code: z.string().trim().min(1),\n});\nexport type ClaimInviteBody = z.infer<typeof claimInviteBodySchema>;\n\n// ─── Mutations ───────────────────────────────────────────────────────────────\n\nexport const setMemberStatusBodySchema = z.object({ active: z.boolean() });\nexport type SetMemberStatusBody = z.infer<typeof setMemberStatusBodySchema>;\n\nexport const assignTrainerBodySchema = z.object({\n trainerId: objectIdSchema.nullable(),\n});\nexport type AssignTrainerBody = z.infer<typeof assignTrainerBodySchema>;\n\nexport const assignWorkoutPlanBodySchema = z.object({\n planId: objectIdSchema.nullable(),\n});\nexport type AssignWorkoutPlanBody = z.infer<typeof assignWorkoutPlanBodySchema>;\n\n/** Remove a member from the gym — echoes the member code to confirm. */\nexport const deleteMemberBodySchema = z.object({\n confirmationCode: z.string().trim().min(1),\n});\nexport type DeleteMemberBody = z.infer<typeof deleteMemberBodySchema>;\n"]}
|
|
1
|
+
{"version":3,"file":"member.js","sourceRoot":"","sources":["../src/member.ts"],"names":[],"mappings":"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","sourcesContent":["/**\r\n * gymmonk-schema — Member management (owner console)\r\n * ==================================================\r\n * Owner-facing roster + detail projections (User × MemberProfile × Membership)\r\n * and the \"add member\" provisioning payload. Adding a member provisions a real\r\n * central-auth account, so the result carries the login email + temp password.\r\n *\r\n * @module gymmonk-schema/member\r\n */\r\n\r\nimport { z } from 'zod';\r\nimport { gymMemberCodeSchema } from './codes.js';\r\nimport {\r\n isoDateSchema,\r\n isoDateTimeSchema,\r\n objectIdSchema,\r\n optionalTrimmedString,\r\n paginationSchema,\r\n phoneSchema,\r\n priceInrSchema,\r\n} from './common.js';\r\nimport { enrolmentStatusSchema } from './enrolment.js';\r\nimport { emergencyContactSchema, MAX_EMERGENCY_CONTACTS } from './member-profile.js';\r\nimport {\r\n assignMembershipBodySchema,\r\n membershipSchema,\r\n membershipStatusSchema,\r\n membershipTermSchema,\r\n} from './membership.js';\r\nimport { bloodGroupSchema, genderSchema } from './shared.js';\r\n\r\n// ─── Segments ────────────────────────────────────────────────────────────────\r\n\r\n/** Attention buckets surfaced on roster cards. */\r\nexport const memberSegmentSchema = z.enum(['expiring', 'new', 'at-risk']);\r\nexport type MemberSegment = z.infer<typeof memberSegmentSchema>;\r\n\r\n// ─── Roster item (list projection) ───────────────────────────────────────────\r\n\r\nexport const memberRosterItemSchema = z.object({\r\n /** The gym record id — always present, even before the member claims. */\r\n enrolmentId: objectIdSchema,\r\n /** Null until claimed; the roster shows an \"Invited\" pill in that state. */\r\n userId: objectIdSchema.nullable(),\r\n recordStatus: enrolmentStatusSchema,\r\n membershipId: objectIdSchema.nullable(),\r\n gymMemberCode: gymMemberCodeSchema,\r\n name: z.string(),\r\n avatarUrl: z.string().nullable(),\r\n verified: z.boolean(),\r\n /**\r\n * Contact details, on the ROSTER row rather than only the detail record.\r\n *\r\n * The card's whole job when someone is overdue is to let the owner chase it\r\n * without leaving the list — call, WhatsApp, email. Those are `tel:` /\r\n * `wa.me` / `mailto:` links, so the row cannot build them without the raw\r\n * values, and fetching the full member just to dial them would be one\r\n * request per card.\r\n *\r\n * Nullable because an invited record may have only one of the two: the gym\r\n * creates it with whatever it has, and the member fills in the rest on claim.\r\n * Phone is RAW 10 digits — `+91` is display-only, never stored.\r\n */\r\n phone: z.string().nullable(),\r\n email: z.email().nullable(),\r\n planName: z.string().nullable(),\r\n status: membershipStatusSchema.nullable(),\r\n endDate: isoDateSchema.nullable(),\r\n lastVisitAt: isoDateTimeSchema.nullable(),\r\n balanceDueInr: priceInrSchema,\r\n overdueDays: z.number().int().nullable(),\r\n segments: z.array(memberSegmentSchema),\r\n\r\n /**\r\n * When the gym record was created — NOT when the person claimed it. The\r\n * roster's \"Joined Date\" filter groups by how long someone has been a member\r\n * of the gym, which starts at the desk, not at the login.\r\n */\r\n joinedAt: isoDateTimeSchema,\r\n\r\n /**\r\n * Check-ins in the trailing 14 days, used by the roster's attendance filters\r\n * to separate a no-show from an irregular from a regular.\r\n *\r\n * Counted rather than derived from `lastVisitAt`, because the two answer\r\n * different questions: someone who came once yesterday and someone who comes\r\n * daily share a last visit but are not the same member to chase.\r\n */\r\n visits14d: z.number().int().nonnegative(),\r\n\r\n /** Assigned trainer, so the roster can be filtered down to one trainer's people. */\r\n trainerId: objectIdSchema.nullable(),\r\n trainerName: z.string().nullable(),\r\n});\r\n\r\n/** Trailing window the roster's attendance filters and `visits14d` are measured over. */\r\nexport const ATTENDANCE_WINDOW_DAYS = 14;\r\nexport type MemberRosterItem = z.infer<typeof memberRosterItemSchema>;\r\n\r\n/** Header counts for the roster. */\r\nexport const memberListSummarySchema = z.object({\r\n total: z.number().int().nonnegative(),\r\n active: z.number().int().nonnegative(),\r\n expiring: z.number().int().nonnegative(),\r\n inactive: z.number().int().nonnegative(),\r\n});\r\nexport type MemberListSummary = z.infer<typeof memberListSummarySchema>;\r\n\r\n// ─── List query ──────────────────────────────────────────────────────────────\r\n\r\nexport const memberFilterSchema = z.enum(['all', 'active', 'inactive', 'verified']);\r\nexport type MemberFilter = z.infer<typeof memberFilterSchema>;\r\n\r\nexport const memberSortSchema = z.enum(['recent', 'az', 'expiry', 'dues']);\r\nexport type MemberSort = z.infer<typeof memberSortSchema>;\r\n\r\nexport const memberListQuerySchema = paginationSchema.extend({\r\n centerId: objectIdSchema.optional(),\r\n filter: memberFilterSchema.default('all'),\r\n sort: memberSortSchema.default('recent'),\r\n q: optionalTrimmedString(120),\r\n});\r\nexport type MemberListQuery = z.infer<typeof memberListQuerySchema>;\r\n\r\n// ─── Detail projection ───────────────────────────────────────────────────────\r\n\r\n/** The identity slice a projection pulls from the auth `User`. */\r\nexport const memberUserRefSchema = z.object({\r\n /** Null while the record is still `invited` (no central-auth account linked). */\r\n id: objectIdSchema.nullable(),\r\n name: z.string(),\r\n email: z.string(),\r\n phone: z.string().nullable(),\r\n avatarUrl: z.string().nullable(),\r\n verified: z.boolean(),\r\n});\r\nexport type MemberUserRef = z.infer<typeof memberUserRefSchema>;\r\n\r\nexport const memberDetailSchema = z.object({\r\n enrolmentId: objectIdSchema,\r\n recordStatus: enrolmentStatusSchema,\r\n /**\r\n * Identity from central auth once claimed. Before that the gym still knows the\r\n * person (name / contact captured at the desk), so this carries the invited\r\n * details with `id: null`.\r\n */\r\n user: memberUserRefSchema,\r\n gymMemberCode: gymMemberCodeSchema,\r\n gender: genderSchema.nullable(),\r\n dob: isoDateSchema.nullable(),\r\n bloodGroup: bloodGroupSchema.nullable(),\r\n emergencyContacts: z.array(emergencyContactSchema),\r\n joinedAt: isoDateTimeSchema,\r\n lastVisitAt: isoDateTimeSchema.nullable(),\r\n assignedWorkoutPlanId: objectIdSchema.nullable(),\r\n assignedTrainerId: objectIdSchema.nullable(),\r\n /**\r\n * The member's current membership, in FULL rather than by id.\r\n *\r\n * The profile screen shows the plan name, its billing cycle, the lifecycle\r\n * status pill and the expiry date all at once, and every one of those lives\r\n * here. Sending only the id forced the owner console to re-query the roster\r\n * for the same member just to read the plan columns back, and to match the\r\n * plan by NAME to find its perk icons — both of which this removes.\r\n */\r\n membership: membershipSchema.nullable(),\r\n /**\r\n * Still owed on the current term: its price less what has been recorded as\r\n * collected. Zero when settled, when the membership was cancelled, or when\r\n * there is no membership at all. Same definition as the roster's column, so\r\n * the profile and the list can never disagree about who owes money.\r\n */\r\n balanceDueInr: z.number().int().nonnegative(),\r\n /**\r\n * The term the NEXT confirmed payment will cover, so the owner reads it\r\n * before deciding rather than discovering it afterwards.\r\n *\r\n * Null when the answer is not yet knowable: no membership has ever been\r\n * assigned, so there is no variant duration to measure a term with. It is\r\n * deliberately computed here rather than in the client — the cycle depends\r\n * on the joining date, the enrolment's creation month and the terms already\r\n * sold, and a client that recomputed it from a partial view would show a\r\n * different month than the one the server is about to write.\r\n */\r\n nextTerm: membershipTermSchema.nullable(),\r\n});\r\nexport type MemberDetail = z.infer<typeof memberDetailSchema>;\r\n\r\n// ─── Add member (provisioning) ───────────────────────────────────────────────\r\n\r\nexport const createMemberBodySchema = z.object({\r\n firstName: z.string().trim().min(1, 'First name is required').max(80),\r\n lastName: optionalTrimmedString(80),\r\n email: z.email('A valid email is required'),\r\n phone: phoneSchema,\r\n gender: genderSchema,\r\n dob: isoDateSchema.optional(),\r\n bloodGroup: bloodGroupSchema.optional(),\r\n emergencyContacts: z.array(emergencyContactSchema).max(MAX_EMERGENCY_CONTACTS).default([]),\r\n avatarUrl: optionalTrimmedString(2000),\r\n assignedTrainerId: objectIdSchema.optional(),\r\n /** Optionally assign a plan in the same step. */\r\n initialPlan: assignMembershipBodySchema.optional(),\r\n});\r\nexport type CreateMemberBody = z.infer<typeof createMemberBodySchema>;\r\n\r\n/**\r\n * Edit a member's record from the owner console.\r\n *\r\n * Every field is optional — the console sends what the form holds, and a field\r\n * left out is left alone.\r\n *\r\n * ## What the gym may and may not change\r\n * `firstName` / `lastName` / `email` / `phone` are the INVITE details, and the\r\n * server accepts them only while the record is unclaimed. Once the member has\r\n * linked their central-auth account, that account is the source of truth for\r\n * their identity and is shared with the other products — a gym must not be able\r\n * to rewrite someone's name or email out from under them. The server ignores\r\n * those four on a claimed record rather than failing the whole request, so an\r\n * owner correcting a blood group is never blocked by a name they cannot change.\r\n *\r\n * Nullable fields (`dob`, `bloodGroup`, `assignedTrainerId`) accept `null` to\r\n * CLEAR a value, which `undefined` cannot express.\r\n */\r\nexport const updateMemberBodySchema = createMemberBodySchema\r\n .omit({\r\n initialPlan: true,\r\n dob: true,\r\n bloodGroup: true,\r\n assignedTrainerId: true,\r\n })\r\n .partial()\r\n .extend({\r\n dob: isoDateSchema.nullish(),\r\n bloodGroup: bloodGroupSchema.nullish(),\r\n assignedTrainerId: objectIdSchema.nullish(),\r\n /**\r\n * The date the GYM says this person started training there.\r\n *\r\n * Editable because the app's record and the gym's record are different\r\n * facts and routinely disagree: somebody who has trained since March is\r\n * added to GymMonk in July, and until now their joining date was silently\r\n * the day the desk got round to typing them in. That date is not a\r\n * cosmetic label — it anchors the billing cycle, so a wrong one bills the\r\n * member on the wrong day of every month for as long as they stay.\r\n *\r\n * Deliberately unbounded in the past: it records something that already\r\n * happened, and a gym may well be older than this app. The BILLING floor\r\n * is a separate rule and is enforced where terms are derived, so an\r\n * honest March joining date can never be turned into a March invoice.\r\n */\r\n joinedAt: isoDateSchema.optional(),\r\n });\r\nexport type UpdateMemberBody = z.infer<typeof updateMemberBodySchema>;\r\n\r\n// ─── Invite & claim ──────────────────────────────────────────────────────────\r\n\r\n/**\r\n * The invite a gym hands to a new member.\r\n *\r\n * GymMonk never mints credentials for anyone: central auth is the SSOT for\r\n * identity, and an account there is shared across products. So adding a member\r\n * creates a gym record only, plus an unguessable invite code. The member\r\n * registers/logs in themselves through central auth (exactly as in jansathi) and\r\n * then CLAIMS the record. The owner shares `url` over WhatsApp or shows the QR.\r\n */\r\nexport const memberInviteSchema = z.object({\r\n /** Single-use, unguessable. Regenerating an invite invalidates the old one. */\r\n code: z.string(),\r\n /** Absolute claim URL the owner shares (also encoded into the QR). */\r\n url: z.string(),\r\n /** The address the member MUST register with for the claim to succeed. */\r\n email: z.string(),\r\n /** WhatsApp number to share on, when the gym recorded one. */\r\n phone: z.string().nullable(),\r\n expiresAt: isoDateTimeSchema,\r\n});\r\nexport type MemberInvite = z.infer<typeof memberInviteSchema>;\r\n\r\n/** Result of adding a member: the gym record + the invite to hand over. */\r\nexport const provisionResultSchema = z.object({\r\n /** The new member record (NOT a central-auth user — none is created). */\r\n enrolmentId: objectIdSchema,\r\n code: z.string(),\r\n invite: memberInviteSchema,\r\n /**\r\n * The membership created from `initialPlan`, or `null`.\r\n *\r\n * Null means one of two DIFFERENT things, and the caller can tell them apart\r\n * because it knows whether it sent `initialPlan`: no plan was chosen, or the\r\n * chosen plan could not be assigned. Adding a member never fails over the\r\n * second case — the person is on the roster either way — so this is how the\r\n * owner finds out the plan still needs assigning rather than assuming it took.\r\n */\r\n membership: membershipSchema.nullable(),\r\n});\r\nexport type ProvisionResult = z.infer<typeof provisionResultSchema>;\r\n\r\n/**\r\n * What the claim screen shows BEFORE the member authenticates, so they know\r\n * which address to register with. Public (the code is the bearer secret), and\r\n * deliberately minimal — gym name, member code, and the expected email.\r\n */\r\nexport const invitePreviewSchema = z.object({\r\n gymName: z.string(),\r\n gymMemberCode: gymMemberCodeSchema,\r\n /** Shown verbatim: the member must know exactly which address to use. */\r\n email: z.string(),\r\n expiresAt: isoDateTimeSchema,\r\n /** True once claimed / expired, so the screen can explain why it failed. */\r\n claimed: z.boolean(),\r\n expired: z.boolean(),\r\n});\r\nexport type InvitePreview = z.infer<typeof invitePreviewSchema>;\r\n\r\n/**\r\n * Claim an invite as the authenticated caller. The server links the record only\r\n * when the code is valid AND the caller's central-auth email matches the address\r\n * the invite was issued to.\r\n */\r\nexport const claimInviteBodySchema = z.object({\r\n code: z.string().trim().min(1),\r\n});\r\nexport type ClaimInviteBody = z.infer<typeof claimInviteBodySchema>;\r\n\r\n// ─── Mutations ───────────────────────────────────────────────────────────────\r\n\r\nexport const setMemberStatusBodySchema = z.object({ active: z.boolean() });\r\nexport type SetMemberStatusBody = z.infer<typeof setMemberStatusBodySchema>;\r\n\r\nexport const assignTrainerBodySchema = z.object({\r\n trainerId: objectIdSchema.nullable(),\r\n});\r\nexport type AssignTrainerBody = z.infer<typeof assignTrainerBodySchema>;\r\n\r\nexport const assignWorkoutPlanBodySchema = z.object({\r\n planId: objectIdSchema.nullable(),\r\n});\r\nexport type AssignWorkoutPlanBody = z.infer<typeof assignWorkoutPlanBodySchema>;\r\n\r\n/** Remove a member from the gym — echoes the member code to confirm. */\r\nexport const deleteMemberBodySchema = z.object({\r\n confirmationCode: z.string().trim().min(1),\r\n});\r\nexport type DeleteMemberBody = z.infer<typeof deleteMemberBodySchema>;\r\n"]}
|
|
@@ -24,9 +24,9 @@ export type PlanAccent = z.infer<typeof planAccentSchema>;
|
|
|
24
24
|
*/
|
|
25
25
|
export declare const durationUnitSchema: z.ZodEnum<{
|
|
26
26
|
year: "year";
|
|
27
|
+
month: "month";
|
|
27
28
|
day: "day";
|
|
28
29
|
week: "week";
|
|
29
|
-
month: "month";
|
|
30
30
|
}>;
|
|
31
31
|
export type DurationUnit = z.infer<typeof durationUnitSchema>;
|
|
32
32
|
/** Singular / plural label for each unit, so no caller writes `unit + 's'`. */
|
|
@@ -48,9 +48,9 @@ export declare const planVariantSchema: z.ZodObject<{
|
|
|
48
48
|
durationCount: z.ZodNumber;
|
|
49
49
|
durationUnit: z.ZodEnum<{
|
|
50
50
|
year: "year";
|
|
51
|
+
month: "month";
|
|
51
52
|
day: "day";
|
|
52
53
|
week: "week";
|
|
53
|
-
month: "month";
|
|
54
54
|
}>;
|
|
55
55
|
originalPriceInr: z.ZodNumber;
|
|
56
56
|
finalPriceInr: z.ZodNumber;
|
|
@@ -71,9 +71,9 @@ export declare const checkedPlanVariantSchema: z.ZodObject<{
|
|
|
71
71
|
durationCount: z.ZodNumber;
|
|
72
72
|
durationUnit: z.ZodEnum<{
|
|
73
73
|
year: "year";
|
|
74
|
+
month: "month";
|
|
74
75
|
day: "day";
|
|
75
76
|
week: "week";
|
|
76
|
-
month: "month";
|
|
77
77
|
}>;
|
|
78
78
|
originalPriceInr: z.ZodNumber;
|
|
79
79
|
finalPriceInr: z.ZodNumber;
|
|
@@ -618,9 +618,9 @@ export declare const createMembershipPlanBodySchema: z.ZodObject<{
|
|
|
618
618
|
durationCount: z.ZodNumber;
|
|
619
619
|
durationUnit: z.ZodEnum<{
|
|
620
620
|
year: "year";
|
|
621
|
+
month: "month";
|
|
621
622
|
day: "day";
|
|
622
623
|
week: "week";
|
|
623
|
-
month: "month";
|
|
624
624
|
}>;
|
|
625
625
|
originalPriceInr: z.ZodNumber;
|
|
626
626
|
finalPriceInr: z.ZodNumber;
|
|
@@ -715,9 +715,9 @@ export declare const updateMembershipPlanBodySchema: z.ZodObject<{
|
|
|
715
715
|
durationCount: z.ZodNumber;
|
|
716
716
|
durationUnit: z.ZodEnum<{
|
|
717
717
|
year: "year";
|
|
718
|
+
month: "month";
|
|
718
719
|
day: "day";
|
|
719
720
|
week: "week";
|
|
720
|
-
month: "month";
|
|
721
721
|
}>;
|
|
722
722
|
originalPriceInr: z.ZodNumber;
|
|
723
723
|
finalPriceInr: z.ZodNumber;
|
|
@@ -815,9 +815,9 @@ export declare const membershipPlanSchema: z.ZodObject<{
|
|
|
815
815
|
durationCount: z.ZodNumber;
|
|
816
816
|
durationUnit: z.ZodEnum<{
|
|
817
817
|
year: "year";
|
|
818
|
+
month: "month";
|
|
818
819
|
day: "day";
|
|
819
820
|
week: "week";
|
|
820
|
-
month: "month";
|
|
821
821
|
}>;
|
|
822
822
|
originalPriceInr: z.ZodNumber;
|
|
823
823
|
finalPriceInr: z.ZodNumber;
|
package/dist/membership.d.ts
CHANGED
|
@@ -152,8 +152,74 @@ export declare const confirmMembershipPaymentBodySchema: z.ZodObject<{
|
|
|
152
152
|
card: "card";
|
|
153
153
|
}>;
|
|
154
154
|
note: z.ZodPreprocess<z.ZodOptional<z.ZodString>>;
|
|
155
|
+
expectedTermStart: z.ZodOptional<z.ZodString>;
|
|
155
156
|
}, z.core.$strip>;
|
|
156
157
|
export type ConfirmMembershipPaymentBody = z.infer<typeof confirmMembershipPaymentBodySchema>;
|
|
158
|
+
/**
|
|
159
|
+
* What a membership IS right now, as against what was last written to its
|
|
160
|
+
* `status` column.
|
|
161
|
+
*
|
|
162
|
+
* ── Why this has to exist ───────────────────────────────────────────────────
|
|
163
|
+
* Two facts about this system make the stored status untrustworthy on its own:
|
|
164
|
+
*
|
|
165
|
+
* 1. NOTHING sweeps memberships to `expired`. There is no job. A member who
|
|
166
|
+
* stopped paying in March keeps `status: 'active'` in the database all
|
|
167
|
+
* year, so the term's END DATE is the only honest answer.
|
|
168
|
+
* 2. NOTHING resumes a pause. `pausedTo` was written and then never read by
|
|
169
|
+
* any code path, so a two-week pause locked a member out permanently and
|
|
170
|
+
* the only way back was an owner noticing and flipping the roster switch
|
|
171
|
+
* by hand.
|
|
172
|
+
*
|
|
173
|
+
* Before this, three places answered "is this live?" three different ways: the
|
|
174
|
+
* door required `active` AND an unexpired term, `getActiveMembership` accepted
|
|
175
|
+
* `active` or `paused` and ignored the date entirely, and the client derived
|
|
176
|
+
* its own answer from the end date. A member could therefore be refused at the
|
|
177
|
+
* turnstile by one rule while the owner's screen showed them paused and the
|
|
178
|
+
* renewal code treated them as current. This is that rule, once, for everyone.
|
|
179
|
+
*
|
|
180
|
+
* ── What is NOT re-derived ──────────────────────────────────────────────────
|
|
181
|
+
* `pending`, `rejected`, `cancelled` and `expired` are all decisions a PERSON
|
|
182
|
+
* made, and are returned untouched. `expired` matters most: an owner
|
|
183
|
+
* deactivating a member sets exactly that, and re-deriving it back to `active`
|
|
184
|
+
* because the term still has days left would silently overrule them.
|
|
185
|
+
*
|
|
186
|
+
* `today` is passed in rather than read from the clock, because the answer is
|
|
187
|
+
* the GYM's day — see `gymDayOf` on the server and `istDayOf` on the client.
|
|
188
|
+
* `YYYY-MM-DD` compares lexicographically, so these are exact calendar-day
|
|
189
|
+
* comparisons with no instant arithmetic and no zone hidden inside.
|
|
190
|
+
*/
|
|
191
|
+
export declare function effectiveMembershipStatus(membership: {
|
|
192
|
+
status: MembershipStatus;
|
|
193
|
+
endDate: string;
|
|
194
|
+
pausedFrom?: string | null;
|
|
195
|
+
pausedTo?: string | null;
|
|
196
|
+
}, today: string): MembershipStatus;
|
|
197
|
+
/** Can this member train today? The one question the door asks. */
|
|
198
|
+
export declare function isMembershipLive(membership: {
|
|
199
|
+
status: MembershipStatus;
|
|
200
|
+
endDate: string;
|
|
201
|
+
pausedFrom?: string | null;
|
|
202
|
+
pausedTo?: string | null;
|
|
203
|
+
}, today: string): boolean;
|
|
204
|
+
/**
|
|
205
|
+
* One billing term, dated from the member's JOINING DATE.
|
|
206
|
+
*
|
|
207
|
+
* Terms are a derived cycle, not free-form dates the desk types in: a member
|
|
208
|
+
* who joined on the 4th is billed 4 Jul→4 Aug, 4 Aug→4 Sep, and so on, each
|
|
209
|
+
* one the length of the variant they bought. Nobody picks a start date,
|
|
210
|
+
* because every gym already answers "which month is this for?" the same way,
|
|
211
|
+
* and letting two people type it produces two answers.
|
|
212
|
+
*
|
|
213
|
+
* `isOverdue` is the term having already ended — the owner marking a month
|
|
214
|
+
* they forgot at the time. That is a normal, expected act rather than an
|
|
215
|
+
* error, so it is surfaced as a fact about the term rather than a warning.
|
|
216
|
+
*/
|
|
217
|
+
export declare const membershipTermSchema: z.ZodObject<{
|
|
218
|
+
startDate: z.ZodString;
|
|
219
|
+
endDate: z.ZodString;
|
|
220
|
+
isOverdue: z.ZodBoolean;
|
|
221
|
+
}, z.core.$strip>;
|
|
222
|
+
export type MembershipTerm = z.infer<typeof membershipTermSchema>;
|
|
157
223
|
/**
|
|
158
224
|
* One row of the gym's confirmation queue.
|
|
159
225
|
*
|
package/dist/membership.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"membership.d.ts","sourceRoot":"","sources":["../src/membership.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAWxB;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,sBAAsB;;;;;;;;EAQjC,CAAC;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAItE,eAAO,MAAM,mBAAmB;;;;EAAkC,CAAC;AACnE,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEhE,eAAO,MAAM,mBAAmB;;;;EAA2C,CAAC;AAC5E,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEhE,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;iBAQxB,CAAC;AACH,MAAM,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAIpD,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"membership.d.ts","sourceRoot":"","sources":["../src/membership.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAWxB;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,sBAAsB;;;;;;;;EAQjC,CAAC;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAItE,eAAO,MAAM,mBAAmB;;;;EAAkC,CAAC;AACnE,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEhE,eAAO,MAAM,mBAAmB;;;;EAA2C,CAAC;AAC5E,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEhE,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;iBAQxB,CAAC;AACH,MAAM,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAIpD,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAoE3B,CAAC;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAI1D;;;GAGG;AACH,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;;;;;;iBAiBrC,CAAC;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAI9E;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,kCAAkC;;;;;;;;;iBAiB7C,CAAC;AACH,MAAM,MAAM,4BAA4B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kCAAkC,CAAC,CAAC;AAE9F;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAgB,yBAAyB,CACvC,UAAU,EAAE;IACV,MAAM,EAAE,gBAAgB,CAAC;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B,EACD,KAAK,EAAE,MAAM,GACZ,gBAAgB,CA+BlB;AAED,mEAAmE;AACnE,wBAAgB,gBAAgB,CAC9B,UAAU,EAAE;IACV,MAAM,EAAE,gBAAgB,CAAC;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B,EACD,KAAK,EAAE,MAAM,GACZ,OAAO,CAET;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,oBAAoB;;;;iBAI/B,CAAC;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAElE;;;;;;;;GAQG;AACH,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAQrC,CAAC;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAE9E,6DAA6D;AAC7D,eAAO,MAAM,iCAAiC;;iBAE5C,CAAC;AACH,MAAM,MAAM,2BAA2B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iCAAiC,CAAC,CAAC;AAI5F,eAAO,MAAM,mBAAmB;;;;;EAAuC,CAAC;AACxE,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEhE,eAAO,MAAM,yBAAyB;;;;;;;;;iBASlC,CAAC;AACL,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAI5E,8EAA8E;AAC9E,eAAO,MAAM,0BAA0B;;;iBAGrC,CAAC;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAI9E,eAAO,MAAM,wBAAwB;;;;;;;;iBAInC,CAAC;AACH,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC"}
|
package/dist/membership.js
CHANGED
|
@@ -95,11 +95,15 @@ export const membershipSchema = z.object({
|
|
|
95
95
|
/**
|
|
96
96
|
* The term, PROVISIONAL while the membership is pending.
|
|
97
97
|
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
98
|
+
* Dated from the member's JOINING DATE, not from the day either party
|
|
99
|
+
* pressed a button. Terms are a cycle — somebody who joined on the 4th runs
|
|
100
|
+
* 4 Jul→4 Aug, 4 Aug→4 Sep — so a gym confirming a week late records the
|
|
101
|
+
* month that was owed rather than starting a fresh one from today. Dating
|
|
102
|
+
* them from the confirmation, which is what this used to do, walked every
|
|
103
|
+
* member's renewal day forward by however long the desk took to look.
|
|
104
|
+
*
|
|
105
|
+
* Written at request time so every screen that reads them keeps working, and
|
|
106
|
+
* recomputed on confirmation in case the cycle moved in between.
|
|
103
107
|
*/
|
|
104
108
|
startDate: isoDateSchema,
|
|
105
109
|
endDate: isoDateSchema,
|
|
@@ -165,6 +169,103 @@ export const confirmMembershipPaymentBodySchema = z.object({
|
|
|
165
169
|
amountInr: priceInrSchema,
|
|
166
170
|
method: paymentMethodSchema,
|
|
167
171
|
note: optionalTrimmedString(240),
|
|
172
|
+
/**
|
|
173
|
+
* The term the owner BELIEVES they are paying for, echoed back from what the
|
|
174
|
+
* screen showed them.
|
|
175
|
+
*
|
|
176
|
+
* The server derives the term itself and never trusts this as input — it is
|
|
177
|
+
* a guard, not a parameter. The owner is told "this covers 3 Jul to 3 Aug"
|
|
178
|
+
* and then presses a button; if a second manager confirms a term in the
|
|
179
|
+
* seconds between, the derived term moves on and this one no longer matches.
|
|
180
|
+
* Without the echo that owner silently pays for a DIFFERENT month than the
|
|
181
|
+
* one they read, and the gym's ledger disagrees with the conversation that
|
|
182
|
+
* produced it. Optional so a client that shows no term still works.
|
|
183
|
+
*/
|
|
184
|
+
expectedTermStart: isoDateSchema.optional(),
|
|
185
|
+
});
|
|
186
|
+
/**
|
|
187
|
+
* What a membership IS right now, as against what was last written to its
|
|
188
|
+
* `status` column.
|
|
189
|
+
*
|
|
190
|
+
* ── Why this has to exist ───────────────────────────────────────────────────
|
|
191
|
+
* Two facts about this system make the stored status untrustworthy on its own:
|
|
192
|
+
*
|
|
193
|
+
* 1. NOTHING sweeps memberships to `expired`. There is no job. A member who
|
|
194
|
+
* stopped paying in March keeps `status: 'active'` in the database all
|
|
195
|
+
* year, so the term's END DATE is the only honest answer.
|
|
196
|
+
* 2. NOTHING resumes a pause. `pausedTo` was written and then never read by
|
|
197
|
+
* any code path, so a two-week pause locked a member out permanently and
|
|
198
|
+
* the only way back was an owner noticing and flipping the roster switch
|
|
199
|
+
* by hand.
|
|
200
|
+
*
|
|
201
|
+
* Before this, three places answered "is this live?" three different ways: the
|
|
202
|
+
* door required `active` AND an unexpired term, `getActiveMembership` accepted
|
|
203
|
+
* `active` or `paused` and ignored the date entirely, and the client derived
|
|
204
|
+
* its own answer from the end date. A member could therefore be refused at the
|
|
205
|
+
* turnstile by one rule while the owner's screen showed them paused and the
|
|
206
|
+
* renewal code treated them as current. This is that rule, once, for everyone.
|
|
207
|
+
*
|
|
208
|
+
* ── What is NOT re-derived ──────────────────────────────────────────────────
|
|
209
|
+
* `pending`, `rejected`, `cancelled` and `expired` are all decisions a PERSON
|
|
210
|
+
* made, and are returned untouched. `expired` matters most: an owner
|
|
211
|
+
* deactivating a member sets exactly that, and re-deriving it back to `active`
|
|
212
|
+
* because the term still has days left would silently overrule them.
|
|
213
|
+
*
|
|
214
|
+
* `today` is passed in rather than read from the clock, because the answer is
|
|
215
|
+
* the GYM's day — see `gymDayOf` on the server and `istDayOf` on the client.
|
|
216
|
+
* `YYYY-MM-DD` compares lexicographically, so these are exact calendar-day
|
|
217
|
+
* comparisons with no instant arithmetic and no zone hidden inside.
|
|
218
|
+
*/
|
|
219
|
+
export function effectiveMembershipStatus(membership, today) {
|
|
220
|
+
const { status, endDate, pausedFrom, pausedTo } = membership;
|
|
221
|
+
// Decided by a person. Never second-guessed.
|
|
222
|
+
if (status === 'pending' ||
|
|
223
|
+
status === 'rejected' ||
|
|
224
|
+
status === 'cancelled' ||
|
|
225
|
+
status === 'expired' ||
|
|
226
|
+
status === 'invited') {
|
|
227
|
+
return status;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* A pause is a WINDOW, not a switch somebody has to remember to turn off.
|
|
231
|
+
*
|
|
232
|
+
* Reading it as a window is what makes the resume happen without a scheduled
|
|
233
|
+
* job: before it, the plan runs; inside it, the member is out; after it, they
|
|
234
|
+
* are simply back, on the end date the pause already pushed out for them.
|
|
235
|
+
* A pause with no end recorded is treated as still running, because the safe
|
|
236
|
+
* failure for a missing bound is to keep the member out rather than to let
|
|
237
|
+
* an unbounded pause silently expire.
|
|
238
|
+
*/
|
|
239
|
+
if (status === 'paused') {
|
|
240
|
+
if (!pausedTo)
|
|
241
|
+
return 'paused';
|
|
242
|
+
const startedYet = !pausedFrom || today >= pausedFrom;
|
|
243
|
+
if (startedYet && today <= pausedTo)
|
|
244
|
+
return 'paused';
|
|
245
|
+
}
|
|
246
|
+
return endDate >= today ? 'active' : 'expired';
|
|
247
|
+
}
|
|
248
|
+
/** Can this member train today? The one question the door asks. */
|
|
249
|
+
export function isMembershipLive(membership, today) {
|
|
250
|
+
return effectiveMembershipStatus(membership, today) === 'active';
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* One billing term, dated from the member's JOINING DATE.
|
|
254
|
+
*
|
|
255
|
+
* Terms are a derived cycle, not free-form dates the desk types in: a member
|
|
256
|
+
* who joined on the 4th is billed 4 Jul→4 Aug, 4 Aug→4 Sep, and so on, each
|
|
257
|
+
* one the length of the variant they bought. Nobody picks a start date,
|
|
258
|
+
* because every gym already answers "which month is this for?" the same way,
|
|
259
|
+
* and letting two people type it produces two answers.
|
|
260
|
+
*
|
|
261
|
+
* `isOverdue` is the term having already ended — the owner marking a month
|
|
262
|
+
* they forgot at the time. That is a normal, expected act rather than an
|
|
263
|
+
* error, so it is surfaced as a fact about the term rather than a warning.
|
|
264
|
+
*/
|
|
265
|
+
export const membershipTermSchema = z.object({
|
|
266
|
+
startDate: isoDateSchema,
|
|
267
|
+
endDate: isoDateSchema,
|
|
268
|
+
isOverdue: z.boolean(),
|
|
168
269
|
});
|
|
169
270
|
/**
|
|
170
271
|
* One row of the gym's confirmation queue.
|
package/dist/membership.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"membership.js","sourceRoot":"","sources":["../src/membership.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * gymmonk-schema — Membership & payments\n * =======================================\n * A member's live membership at a center: which plan/tenure, its lifecycle\n * status and dates, pause windows, and the payment ledger. v1 is single-center,\n * so one active membership per user.\n *\n * The \"expiring\" state the UI shows is DERIVED from `endDate` at the edge; the\n * stored `status` is the true lifecycle state.\n *\n * @module gymmonk-schema/membership\n */\n\nimport { z } from 'zod';\nimport {\n isoDateSchema,\n isoDateTimeSchema,\n objectIdSchema,\n optionalTrimmedString,\n priceInrSchema,\n} from './common.js';\n\n// ─── Status ──────────────────────────────────────────────────────────────────\n\n/**\n * Membership lifecycle.\n *\n * `pending` is where every membership STARTS, whoever created it. Choosing a\n * plan is not the same event as paying for one, and until this release the two\n * were welded together: the moment a member tapped a plan it went live, and the\n * moment an owner assigned one at the desk it went live, whether or not any\n * money had changed hands. A gym then had no way to tell a paid member from an\n * optimistic one, and no lever to withhold access from the second.\n *\n * The only transition out of `pending` is the gym marking the payment done.\n * That is deliberately a HUMAN act by the owner or a manager rather than a\n * gateway callback: these gyms take cash at the desk and UPI transfers on a\n * phone, so the person who knows the money arrived is the person at the gym.\n *\n * `rejected` is the other way out, for a screenshot that shows nothing, the\n * wrong amount, or a transfer that never landed. It is a dead end — the\n * member requests again rather than the gym reopening a closed decision.\n *\n * `invited` predates all of this and nothing writes it. Left in place because\n * removing a value from a stored enum is not worth a migration.\n *\n * `expiring` is NOT stored — it is derived from `endDate` at the edge.\n */\nexport const membershipStatusSchema = z.enum([\n 'pending',\n 'rejected',\n 'invited',\n 'active',\n 'paused',\n 'expired',\n 'cancelled',\n]);\nexport type MembershipStatus = z.infer<typeof membershipStatusSchema>;\n\n// ─── Payments ────────────────────────────────────────────────────────────────\n\nexport const paymentMethodSchema = z.enum(['upi', 'cash', 'card']);\nexport type PaymentMethod = z.infer<typeof paymentMethodSchema>;\n\nexport const paymentStatusSchema = z.enum(['success', 'failed', 'pending']);\nexport type PaymentStatus = z.infer<typeof paymentStatusSchema>;\n\nexport const paymentSchema = z.object({\n id: objectIdSchema,\n membershipId: objectIdSchema,\n amountInr: priceInrSchema,\n method: paymentMethodSchema,\n status: paymentStatusSchema,\n paidAt: isoDateTimeSchema,\n note: z.string().nullable(),\n});\nexport type Payment = z.infer<typeof paymentSchema>;\n\n// ─── Entity (API response) ───────────────────────────────────────────────────\n\nexport const membershipSchema = z.object({\n id: objectIdSchema,\n /**\n * The enrolment this plan was sold against.\n *\n * The link that makes a plan belong to a stint at a gym rather than to a\n * person. It replaces a `(centerId, memberCode)` pair that had to be matched\n * by hand in three places, and it is what lets a plan be sold to somebody the\n * desk has only just written down — an enrolment exists from that moment,\n * whereas an account may not exist for weeks.\n */\n enrolmentId: objectIdSchema,\n /**\n * The account, denormalised from the enrolment for member-facing queries.\n * NULL until they claim, for the same reason it is null on the enrolment.\n */\n userId: objectIdSchema.nullable(),\n centerId: objectIdSchema,\n planId: objectIdSchema,\n planName: z.string(),\n /**\n * Which variant of the plan was bought, and how it read at the time.\n *\n * A plain string, not a fixed tenure enum: variant ids are minted per plan\n * now, so a gym can sell a 10-day pass. The LABEL is a snapshot for the same\n * reason a payment records what it was for — the owner may rename or delete\n * the variant later, and a membership must still be able to say what was\n * sold.\n */\n variantId: z.string(),\n variantLabel: z.string(),\n priceInr: priceInrSchema,\n status: membershipStatusSchema,\n /**\n * The term, PROVISIONAL while the membership is pending.\n *\n * Written at request time so every screen that already reads them keeps\n * working, and RECOMPUTED when the gym marks the payment done. A member who\n * waits two days for their owner to check a screenshot must not lose two\n * days of the plan they paid for, so the clock starts when the gym says the\n * money arrived, not when the member tapped the button.\n */\n startDate: isoDateSchema,\n endDate: isoDateSchema,\n pausedFrom: isoDateSchema.nullable(),\n pausedTo: isoDateSchema.nullable(),\n /**\n * The screenshot the member attached, if they attached one.\n *\n * Optional on purpose. A member paying cash at the desk has nothing to\n * screenshot, and refusing to accept their request until they produce one\n * would lock the commonest case out of the flow entirely.\n */\n paymentProofUrl: z.string().nullable(),\n /** When the plan was chosen. Distinct from `createdAt` only in intent. */\n requestedAt: isoDateTimeSchema,\n /** When the gym marked the payment done or turned it down. */\n decidedAt: isoDateTimeSchema.nullable(),\n /** Who decided. Null while pending. */\n decidedBy: objectIdSchema.nullable(),\n /** Why it was turned down, in the gym's words. */\n decisionNote: z.string().nullable(),\n createdAt: isoDateTimeSchema,\n updatedAt: isoDateTimeSchema,\n});\nexport type Membership = z.infer<typeof membershipSchema>;\n\n// ─── Assign / renew / upgrade ────────────────────────────────────────────────\n\n/**\n * Assign a plan to a member (new membership, renewal, or upgrade). Records the\n * charge in one shot; omit `payment` for an unpaid/invited assignment.\n */\nexport const assignMembershipBodySchema = z.object({\n planId: objectIdSchema,\n variantId: z.string().trim().min(1),\n /** Explicit start; defaults to today server-side when omitted. */\n startDate: isoDateSchema.optional(),\n /**\n * A screenshot of the transfer, uploaded to storage first and passed as its\n * URL. Optional — see `paymentProofUrl` on the entity.\n */\n paymentProofUrl: z.string().trim().max(2000).optional(),\n payment: z\n .object({\n amountInr: priceInrSchema,\n method: paymentMethodSchema,\n status: paymentStatusSchema.default('success'),\n })\n .optional(),\n});\nexport type AssignMembershipBody = z.infer<typeof assignMembershipBodySchema>;\n\n// ——— Confirm / reject the payment ————————————————————————\n\n/**\n * The gym marking a pending membership's payment as done, which is the single\n * act that activates it.\n *\n * It carries the LEDGER entry rather than being a bare approval, because those\n * are the same event: the owner is asserting that this much money arrived by\n * this route. Splitting them would let a membership go live with nothing\n * written down, which is exactly the hole this feature exists to close.\n *\n * The amount is not forced to equal the plan price. A gym that takes half now\n * and half on Friday still wants the member training, and the balance is\n * already tracked against the term.\n */\nexport const confirmMembershipPaymentBodySchema = z.object({\n amountInr: priceInrSchema,\n method: paymentMethodSchema,\n note: optionalTrimmedString(240),\n});\nexport type ConfirmMembershipPaymentBody = z.infer<typeof confirmMembershipPaymentBodySchema>;\n\n/**\n * One row of the gym's confirmation queue.\n *\n * The membership plus just enough of the person to act on it. The owner is\n * deciding about a HUMAN — \"did Ravi's transfer land\" — and a list of plan\n * names and amounts with no names attached is not something anybody can\n * answer. Kept to four fields rather than embedding the whole roster row,\n * because this list is read on every visit to the members screen.\n */\nexport const pendingMembershipRowSchema = z.object({\n membership: membershipSchema,\n member: z.object({\n enrolmentId: objectIdSchema,\n gymMemberCode: z.string(),\n name: z.string(),\n avatarUrl: z.string().nullable(),\n }),\n});\nexport type PendingMembershipRow = z.infer<typeof pendingMembershipRowSchema>;\n\n/** Turning a request down. The reason reaches the member. */\nexport const rejectMembershipPaymentBodySchema = z.object({\n reason: optionalTrimmedString(240),\n});\nexport type RejectMembershipPaymentBody = z.infer<typeof rejectMembershipPaymentBodySchema>;\n\n// ─── Pause ───────────────────────────────────────────────────────────────────\n\nexport const pauseDurationSchema = z.enum(['1w', '2w', '1m', 'custom']);\nexport type PauseDuration = z.infer<typeof pauseDurationSchema>;\n\nexport const pauseMembershipBodySchema = z\n .object({\n duration: pauseDurationSchema,\n customFrom: isoDateSchema.optional(),\n customTo: isoDateSchema.optional(),\n })\n .refine((v) => v.duration !== 'custom' || (v.customFrom && v.customTo), {\n message: 'Pick both pause dates',\n path: ['customTo'],\n });\nexport type PauseMembershipBody = z.infer<typeof pauseMembershipBodySchema>;\n\n// ─── Cancel ──────────────────────────────────────────────────────────────────\n\n/** Cancellation requires echoing the member code as a safety confirmation. */\nexport const cancelMembershipBodySchema = z.object({\n confirmationCode: z.string().trim().min(1),\n reason: optionalTrimmedString(240),\n});\nexport type CancelMembershipBody = z.infer<typeof cancelMembershipBodySchema>;\n\n// ─── Collect an offline payment ──────────────────────────────────────────────\n\nexport const collectPaymentBodySchema = z.object({\n amountInr: priceInrSchema,\n method: paymentMethodSchema,\n note: optionalTrimmedString(240),\n});\nexport type CollectPaymentBody = z.infer<typeof collectPaymentBodySchema>;\n"]}
|
|
1
|
+
{"version":3,"file":"membership.js","sourceRoot":"","sources":["../src/membership.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EACL,aAAa,EACb,iBAAiB,EACjB,cAAc,EACd,qBAAqB,EACrB,cAAc,GACf,MAAM,aAAa,CAAC;AAErB,gFAAgF;AAEhF;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,CAAC,IAAI,CAAC;IAC3C,SAAS;IACT,UAAU;IACV,SAAS;IACT,QAAQ;IACR,QAAQ;IACR,SAAS;IACT,WAAW;CACZ,CAAC,CAAC;AAGH,gFAAgF;AAEhF,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;AAGnE,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,QAAQ,EAAE,SAAS,CAAC,CAAC,CAAC;AAG5E,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,CAAC,MAAM,CAAC;IACpC,EAAE,EAAE,cAAc;IAClB,YAAY,EAAE,cAAc;IAC5B,SAAS,EAAE,cAAc;IACzB,MAAM,EAAE,mBAAmB;IAC3B,MAAM,EAAE,mBAAmB;IAC3B,MAAM,EAAE,iBAAiB;IACzB,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CAC5B,CAAC,CAAC;AAGH,gFAAgF;AAEhF,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAC,MAAM,CAAC;IACvC,EAAE,EAAE,cAAc;IAClB;;;;;;;;OAQG;IACH,WAAW,EAAE,cAAc;IAC3B;;;OAGG;IACH,MAAM,EAAE,cAAc,CAAC,QAAQ,EAAE;IACjC,QAAQ,EAAE,cAAc;IACxB,MAAM,EAAE,cAAc;IACtB,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE;IACpB;;;;;;;;OAQG;IACH,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE;IACrB,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE;IACxB,QAAQ,EAAE,cAAc;IACxB,MAAM,EAAE,sBAAsB;IAC9B;;;;;;;;;;;;OAYG;IACH,SAAS,EAAE,aAAa;IACxB,OAAO,EAAE,aAAa;IACtB,UAAU,EAAE,aAAa,CAAC,QAAQ,EAAE;IACpC,QAAQ,EAAE,aAAa,CAAC,QAAQ,EAAE;IAClC;;;;;;OAMG;IACH,eAAe,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACtC,0EAA0E;IAC1E,WAAW,EAAE,iBAAiB;IAC9B,8DAA8D;IAC9D,SAAS,EAAE,iBAAiB,CAAC,QAAQ,EAAE;IACvC,uCAAuC;IACvC,SAAS,EAAE,cAAc,CAAC,QAAQ,EAAE;IACpC,kDAAkD;IAClD,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACnC,SAAS,EAAE,iBAAiB;IAC5B,SAAS,EAAE,iBAAiB;CAC7B,CAAC,CAAC;AAGH,gFAAgF;AAEhF;;;GAGG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG,CAAC,CAAC,MAAM,CAAC;IACjD,MAAM,EAAE,cAAc;IACtB,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACnC,kEAAkE;IAClE,SAAS,EAAE,aAAa,CAAC,QAAQ,EAAE;IACnC;;;OAGG;IACH,eAAe,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE;IACvD,OAAO,EAAE,CAAC;SACP,MAAM,CAAC;QACN,SAAS,EAAE,cAAc;QACzB,MAAM,EAAE,mBAAmB;QAC3B,MAAM,EAAE,mBAAmB,CAAC,OAAO,CAAC,SAAS,CAAC;KAC/C,CAAC;SACD,QAAQ,EAAE;CACd,CAAC,CAAC;AAGH,4DAA4D;AAE5D;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,kCAAkC,GAAG,CAAC,CAAC,MAAM,CAAC;IACzD,SAAS,EAAE,cAAc;IACzB,MAAM,EAAE,mBAAmB;IAC3B,IAAI,EAAE,qBAAqB,CAAC,GAAG,CAAC;IAChC;;;;;;;;;;;OAWG;IACH,iBAAiB,EAAE,aAAa,CAAC,QAAQ,EAAE;CAC5C,CAAC,CAAC;AAGH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,MAAM,UAAU,yBAAyB,CACvC,UAKC,EACD,KAAa;IAEb,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,GAAG,UAAU,CAAC;IAE7D,6CAA6C;IAC7C,IACE,MAAM,KAAK,SAAS;QACpB,MAAM,KAAK,UAAU;QACrB,MAAM,KAAK,WAAW;QACtB,MAAM,KAAK,SAAS;QACpB,MAAM,KAAK,SAAS,EACpB,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC;IAED;;;;;;;;;OASG;IACH,IAAI,MAAM,KAAK,QAAQ,EAAE,CAAC;QACxB,IAAI,CAAC,QAAQ;YAAE,OAAO,QAAQ,CAAC;QAC/B,MAAM,UAAU,GAAG,CAAC,UAAU,IAAI,KAAK,IAAI,UAAU,CAAC;QACtD,IAAI,UAAU,IAAI,KAAK,IAAI,QAAQ;YAAE,OAAO,QAAQ,CAAC;IACvD,CAAC;IAED,OAAO,OAAO,IAAI,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC;AACjD,CAAC;AAED,mEAAmE;AACnE,MAAM,UAAU,gBAAgB,CAC9B,UAKC,EACD,KAAa;IAEb,OAAO,yBAAyB,CAAC,UAAU,EAAE,KAAK,CAAC,KAAK,QAAQ,CAAC;AACnE,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC3C,SAAS,EAAE,aAAa;IACxB,OAAO,EAAE,aAAa;IACtB,SAAS,EAAE,CAAC,CAAC,OAAO,EAAE;CACvB,CAAC,CAAC;AAGH;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG,CAAC,CAAC,MAAM,CAAC;IACjD,UAAU,EAAE,gBAAgB;IAC5B,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC;QACf,WAAW,EAAE,cAAc;QAC3B,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE;QACzB,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;QAChB,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;KACjC,CAAC;CACH,CAAC,CAAC;AAGH,6DAA6D;AAC7D,MAAM,CAAC,MAAM,iCAAiC,GAAG,CAAC,CAAC,MAAM,CAAC;IACxD,MAAM,EAAE,qBAAqB,CAAC,GAAG,CAAC;CACnC,CAAC,CAAC;AAGH,gFAAgF;AAEhF,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC;AAGxE,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC;KACvC,MAAM,CAAC;IACN,QAAQ,EAAE,mBAAmB;IAC7B,UAAU,EAAE,aAAa,CAAC,QAAQ,EAAE;IACpC,QAAQ,EAAE,aAAa,CAAC,QAAQ,EAAE;CACnC,CAAC;KACD,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,QAAQ,IAAI,CAAC,CAAC,CAAC,UAAU,IAAI,CAAC,CAAC,QAAQ,CAAC,EAAE;IACtE,OAAO,EAAE,uBAAuB;IAChC,IAAI,EAAE,CAAC,UAAU,CAAC;CACnB,CAAC,CAAC;AAGL,gFAAgF;AAEhF,8EAA8E;AAC9E,MAAM,CAAC,MAAM,0BAA0B,GAAG,CAAC,CAAC,MAAM,CAAC;IACjD,gBAAgB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC1C,MAAM,EAAE,qBAAqB,CAAC,GAAG,CAAC;CACnC,CAAC,CAAC;AAGH,gFAAgF;AAEhF,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC/C,SAAS,EAAE,cAAc;IACzB,MAAM,EAAE,mBAAmB;IAC3B,IAAI,EAAE,qBAAqB,CAAC,GAAG,CAAC;CACjC,CAAC,CAAC","sourcesContent":["/**\n * gymmonk-schema — Membership & payments\n * =======================================\n * A member's live membership at a center: which plan/tenure, its lifecycle\n * status and dates, pause windows, and the payment ledger. v1 is single-center,\n * so one active membership per user.\n *\n * The \"expiring\" state the UI shows is DERIVED from `endDate` at the edge; the\n * stored `status` is the true lifecycle state.\n *\n * @module gymmonk-schema/membership\n */\n\nimport { z } from 'zod';\nimport {\n isoDateSchema,\n isoDateTimeSchema,\n objectIdSchema,\n optionalTrimmedString,\n priceInrSchema,\n} from './common.js';\n\n// ─── Status ──────────────────────────────────────────────────────────────────\n\n/**\n * Membership lifecycle.\n *\n * `pending` is where every membership STARTS, whoever created it. Choosing a\n * plan is not the same event as paying for one, and until this release the two\n * were welded together: the moment a member tapped a plan it went live, and the\n * moment an owner assigned one at the desk it went live, whether or not any\n * money had changed hands. A gym then had no way to tell a paid member from an\n * optimistic one, and no lever to withhold access from the second.\n *\n * The only transition out of `pending` is the gym marking the payment done.\n * That is deliberately a HUMAN act by the owner or a manager rather than a\n * gateway callback: these gyms take cash at the desk and UPI transfers on a\n * phone, so the person who knows the money arrived is the person at the gym.\n *\n * `rejected` is the other way out, for a screenshot that shows nothing, the\n * wrong amount, or a transfer that never landed. It is a dead end — the\n * member requests again rather than the gym reopening a closed decision.\n *\n * `invited` predates all of this and nothing writes it. Left in place because\n * removing a value from a stored enum is not worth a migration.\n *\n * `expiring` is NOT stored — it is derived from `endDate` at the edge.\n */\nexport const membershipStatusSchema = z.enum([\n 'pending',\n 'rejected',\n 'invited',\n 'active',\n 'paused',\n 'expired',\n 'cancelled',\n]);\nexport type MembershipStatus = z.infer<typeof membershipStatusSchema>;\n\n// ─── Payments ────────────────────────────────────────────────────────────────\n\nexport const paymentMethodSchema = z.enum(['upi', 'cash', 'card']);\nexport type PaymentMethod = z.infer<typeof paymentMethodSchema>;\n\nexport const paymentStatusSchema = z.enum(['success', 'failed', 'pending']);\nexport type PaymentStatus = z.infer<typeof paymentStatusSchema>;\n\nexport const paymentSchema = z.object({\n id: objectIdSchema,\n membershipId: objectIdSchema,\n amountInr: priceInrSchema,\n method: paymentMethodSchema,\n status: paymentStatusSchema,\n paidAt: isoDateTimeSchema,\n note: z.string().nullable(),\n});\nexport type Payment = z.infer<typeof paymentSchema>;\n\n// ─── Entity (API response) ───────────────────────────────────────────────────\n\nexport const membershipSchema = z.object({\n id: objectIdSchema,\n /**\n * The enrolment this plan was sold against.\n *\n * The link that makes a plan belong to a stint at a gym rather than to a\n * person. It replaces a `(centerId, memberCode)` pair that had to be matched\n * by hand in three places, and it is what lets a plan be sold to somebody the\n * desk has only just written down — an enrolment exists from that moment,\n * whereas an account may not exist for weeks.\n */\n enrolmentId: objectIdSchema,\n /**\n * The account, denormalised from the enrolment for member-facing queries.\n * NULL until they claim, for the same reason it is null on the enrolment.\n */\n userId: objectIdSchema.nullable(),\n centerId: objectIdSchema,\n planId: objectIdSchema,\n planName: z.string(),\n /**\n * Which variant of the plan was bought, and how it read at the time.\n *\n * A plain string, not a fixed tenure enum: variant ids are minted per plan\n * now, so a gym can sell a 10-day pass. The LABEL is a snapshot for the same\n * reason a payment records what it was for — the owner may rename or delete\n * the variant later, and a membership must still be able to say what was\n * sold.\n */\n variantId: z.string(),\n variantLabel: z.string(),\n priceInr: priceInrSchema,\n status: membershipStatusSchema,\n /**\n * The term, PROVISIONAL while the membership is pending.\n *\n * Dated from the member's JOINING DATE, not from the day either party\n * pressed a button. Terms are a cycle — somebody who joined on the 4th runs\n * 4 Jul→4 Aug, 4 Aug→4 Sep — so a gym confirming a week late records the\n * month that was owed rather than starting a fresh one from today. Dating\n * them from the confirmation, which is what this used to do, walked every\n * member's renewal day forward by however long the desk took to look.\n *\n * Written at request time so every screen that reads them keeps working, and\n * recomputed on confirmation in case the cycle moved in between.\n */\n startDate: isoDateSchema,\n endDate: isoDateSchema,\n pausedFrom: isoDateSchema.nullable(),\n pausedTo: isoDateSchema.nullable(),\n /**\n * The screenshot the member attached, if they attached one.\n *\n * Optional on purpose. A member paying cash at the desk has nothing to\n * screenshot, and refusing to accept their request until they produce one\n * would lock the commonest case out of the flow entirely.\n */\n paymentProofUrl: z.string().nullable(),\n /** When the plan was chosen. Distinct from `createdAt` only in intent. */\n requestedAt: isoDateTimeSchema,\n /** When the gym marked the payment done or turned it down. */\n decidedAt: isoDateTimeSchema.nullable(),\n /** Who decided. Null while pending. */\n decidedBy: objectIdSchema.nullable(),\n /** Why it was turned down, in the gym's words. */\n decisionNote: z.string().nullable(),\n createdAt: isoDateTimeSchema,\n updatedAt: isoDateTimeSchema,\n});\nexport type Membership = z.infer<typeof membershipSchema>;\n\n// ─── Assign / renew / upgrade ────────────────────────────────────────────────\n\n/**\n * Assign a plan to a member (new membership, renewal, or upgrade). Records the\n * charge in one shot; omit `payment` for an unpaid/invited assignment.\n */\nexport const assignMembershipBodySchema = z.object({\n planId: objectIdSchema,\n variantId: z.string().trim().min(1),\n /** Explicit start; defaults to today server-side when omitted. */\n startDate: isoDateSchema.optional(),\n /**\n * A screenshot of the transfer, uploaded to storage first and passed as its\n * URL. Optional — see `paymentProofUrl` on the entity.\n */\n paymentProofUrl: z.string().trim().max(2000).optional(),\n payment: z\n .object({\n amountInr: priceInrSchema,\n method: paymentMethodSchema,\n status: paymentStatusSchema.default('success'),\n })\n .optional(),\n});\nexport type AssignMembershipBody = z.infer<typeof assignMembershipBodySchema>;\n\n// ——— Confirm / reject the payment ————————————————————————\n\n/**\n * The gym marking a pending membership's payment as done, which is the single\n * act that activates it.\n *\n * It carries the LEDGER entry rather than being a bare approval, because those\n * are the same event: the owner is asserting that this much money arrived by\n * this route. Splitting them would let a membership go live with nothing\n * written down, which is exactly the hole this feature exists to close.\n *\n * The amount is not forced to equal the plan price. A gym that takes half now\n * and half on Friday still wants the member training, and the balance is\n * already tracked against the term.\n */\nexport const confirmMembershipPaymentBodySchema = z.object({\n amountInr: priceInrSchema,\n method: paymentMethodSchema,\n note: optionalTrimmedString(240),\n /**\n * The term the owner BELIEVES they are paying for, echoed back from what the\n * screen showed them.\n *\n * The server derives the term itself and never trusts this as input — it is\n * a guard, not a parameter. The owner is told \"this covers 3 Jul to 3 Aug\"\n * and then presses a button; if a second manager confirms a term in the\n * seconds between, the derived term moves on and this one no longer matches.\n * Without the echo that owner silently pays for a DIFFERENT month than the\n * one they read, and the gym's ledger disagrees with the conversation that\n * produced it. Optional so a client that shows no term still works.\n */\n expectedTermStart: isoDateSchema.optional(),\n});\nexport type ConfirmMembershipPaymentBody = z.infer<typeof confirmMembershipPaymentBodySchema>;\n\n/**\n * What a membership IS right now, as against what was last written to its\n * `status` column.\n *\n * ── Why this has to exist ───────────────────────────────────────────────────\n * Two facts about this system make the stored status untrustworthy on its own:\n *\n * 1. NOTHING sweeps memberships to `expired`. There is no job. A member who\n * stopped paying in March keeps `status: 'active'` in the database all\n * year, so the term's END DATE is the only honest answer.\n * 2. NOTHING resumes a pause. `pausedTo` was written and then never read by\n * any code path, so a two-week pause locked a member out permanently and\n * the only way back was an owner noticing and flipping the roster switch\n * by hand.\n *\n * Before this, three places answered \"is this live?\" three different ways: the\n * door required `active` AND an unexpired term, `getActiveMembership` accepted\n * `active` or `paused` and ignored the date entirely, and the client derived\n * its own answer from the end date. A member could therefore be refused at the\n * turnstile by one rule while the owner's screen showed them paused and the\n * renewal code treated them as current. This is that rule, once, for everyone.\n *\n * ── What is NOT re-derived ──────────────────────────────────────────────────\n * `pending`, `rejected`, `cancelled` and `expired` are all decisions a PERSON\n * made, and are returned untouched. `expired` matters most: an owner\n * deactivating a member sets exactly that, and re-deriving it back to `active`\n * because the term still has days left would silently overrule them.\n *\n * `today` is passed in rather than read from the clock, because the answer is\n * the GYM's day — see `gymDayOf` on the server and `istDayOf` on the client.\n * `YYYY-MM-DD` compares lexicographically, so these are exact calendar-day\n * comparisons with no instant arithmetic and no zone hidden inside.\n */\nexport function effectiveMembershipStatus(\n membership: {\n status: MembershipStatus;\n endDate: string;\n pausedFrom?: string | null;\n pausedTo?: string | null;\n },\n today: string,\n): MembershipStatus {\n const { status, endDate, pausedFrom, pausedTo } = membership;\n\n // Decided by a person. Never second-guessed.\n if (\n status === 'pending' ||\n status === 'rejected' ||\n status === 'cancelled' ||\n status === 'expired' ||\n status === 'invited'\n ) {\n return status;\n }\n\n /**\n * A pause is a WINDOW, not a switch somebody has to remember to turn off.\n *\n * Reading it as a window is what makes the resume happen without a scheduled\n * job: before it, the plan runs; inside it, the member is out; after it, they\n * are simply back, on the end date the pause already pushed out for them.\n * A pause with no end recorded is treated as still running, because the safe\n * failure for a missing bound is to keep the member out rather than to let\n * an unbounded pause silently expire.\n */\n if (status === 'paused') {\n if (!pausedTo) return 'paused';\n const startedYet = !pausedFrom || today >= pausedFrom;\n if (startedYet && today <= pausedTo) return 'paused';\n }\n\n return endDate >= today ? 'active' : 'expired';\n}\n\n/** Can this member train today? The one question the door asks. */\nexport function isMembershipLive(\n membership: {\n status: MembershipStatus;\n endDate: string;\n pausedFrom?: string | null;\n pausedTo?: string | null;\n },\n today: string,\n): boolean {\n return effectiveMembershipStatus(membership, today) === 'active';\n}\n\n/**\n * One billing term, dated from the member's JOINING DATE.\n *\n * Terms are a derived cycle, not free-form dates the desk types in: a member\n * who joined on the 4th is billed 4 Jul→4 Aug, 4 Aug→4 Sep, and so on, each\n * one the length of the variant they bought. Nobody picks a start date,\n * because every gym already answers \"which month is this for?\" the same way,\n * and letting two people type it produces two answers.\n *\n * `isOverdue` is the term having already ended — the owner marking a month\n * they forgot at the time. That is a normal, expected act rather than an\n * error, so it is surfaced as a fact about the term rather than a warning.\n */\nexport const membershipTermSchema = z.object({\n startDate: isoDateSchema,\n endDate: isoDateSchema,\n isOverdue: z.boolean(),\n});\nexport type MembershipTerm = z.infer<typeof membershipTermSchema>;\n\n/**\n * One row of the gym's confirmation queue.\n *\n * The membership plus just enough of the person to act on it. The owner is\n * deciding about a HUMAN — \"did Ravi's transfer land\" — and a list of plan\n * names and amounts with no names attached is not something anybody can\n * answer. Kept to four fields rather than embedding the whole roster row,\n * because this list is read on every visit to the members screen.\n */\nexport const pendingMembershipRowSchema = z.object({\n membership: membershipSchema,\n member: z.object({\n enrolmentId: objectIdSchema,\n gymMemberCode: z.string(),\n name: z.string(),\n avatarUrl: z.string().nullable(),\n }),\n});\nexport type PendingMembershipRow = z.infer<typeof pendingMembershipRowSchema>;\n\n/** Turning a request down. The reason reaches the member. */\nexport const rejectMembershipPaymentBodySchema = z.object({\n reason: optionalTrimmedString(240),\n});\nexport type RejectMembershipPaymentBody = z.infer<typeof rejectMembershipPaymentBodySchema>;\n\n// ─── Pause ───────────────────────────────────────────────────────────────────\n\nexport const pauseDurationSchema = z.enum(['1w', '2w', '1m', 'custom']);\nexport type PauseDuration = z.infer<typeof pauseDurationSchema>;\n\nexport const pauseMembershipBodySchema = z\n .object({\n duration: pauseDurationSchema,\n customFrom: isoDateSchema.optional(),\n customTo: isoDateSchema.optional(),\n })\n .refine((v) => v.duration !== 'custom' || (v.customFrom && v.customTo), {\n message: 'Pick both pause dates',\n path: ['customTo'],\n });\nexport type PauseMembershipBody = z.infer<typeof pauseMembershipBodySchema>;\n\n// ─── Cancel ──────────────────────────────────────────────────────────────────\n\n/** Cancellation requires echoing the member code as a safety confirmation. */\nexport const cancelMembershipBodySchema = z.object({\n confirmationCode: z.string().trim().min(1),\n reason: optionalTrimmedString(240),\n});\nexport type CancelMembershipBody = z.infer<typeof cancelMembershipBodySchema>;\n\n// ─── Collect an offline payment ──────────────────────────────────────────────\n\nexport const collectPaymentBodySchema = z.object({\n amountInr: priceInrSchema,\n method: paymentMethodSchema,\n note: optionalTrimmedString(240),\n});\nexport type CollectPaymentBody = z.infer<typeof collectPaymentBodySchema>;\n"]}
|
package/dist/messages.d.ts
CHANGED
|
@@ -389,6 +389,25 @@ export declare const ERROR_MESSAGE: {
|
|
|
389
389
|
*/
|
|
390
390
|
readonly membershipAwaitingPayment: "Your gym is checking your payment. The plan starts as soon as they confirm it.";
|
|
391
391
|
readonly membershipNoPendingRequest: "There is no plan waiting for confirmation.";
|
|
392
|
+
/**
|
|
393
|
+
* Pausing extends the term by the paused days, so pausing something that is
|
|
394
|
+
* not running would push out a date nobody is training against — and pausing
|
|
395
|
+
* an already-paused membership would extend it twice for one absence.
|
|
396
|
+
*/
|
|
397
|
+
readonly membershipNotPausable: "Only a running membership can be paused. This one has already ended or is paused.";
|
|
398
|
+
/**
|
|
399
|
+
* The joining date records something that has already happened, so a future
|
|
400
|
+
* one is not a preference to honour — it would date the member's whole
|
|
401
|
+
* billing cycle from a day that has not arrived.
|
|
402
|
+
*/
|
|
403
|
+
readonly joiningDateInFuture: "A joining date cannot be in the future.";
|
|
404
|
+
/**
|
|
405
|
+
* The term moved between the screen being drawn and the button being
|
|
406
|
+
* pressed, which in practice means a second manager confirmed one first.
|
|
407
|
+
* Says which month it now covers rather than a bare "try again", because the
|
|
408
|
+
* owner's next question is always "then what am I paying for?".
|
|
409
|
+
*/
|
|
410
|
+
readonly membershipTermMoved: "Someone just confirmed a payment for this member, so the period has moved on. Reopen their profile to see the current one.";
|
|
392
411
|
/**
|
|
393
412
|
* The owner reached for the roster toggle on a plan that has not been paid
|
|
394
413
|
* for. Points at the action that WOULD start it rather than just refusing.
|
|
@@ -475,6 +494,16 @@ export type ErrorMessageKey = keyof typeof ERROR_MESSAGE;
|
|
|
475
494
|
/** Error messages that name a limit or value. */
|
|
476
495
|
export declare const ERROR_MESSAGE_FOR: {
|
|
477
496
|
readonly maxPhotos: (max: number) => string;
|
|
497
|
+
/**
|
|
498
|
+
* Refused at the door because the membership is paused and still inside its
|
|
499
|
+
* window.
|
|
500
|
+
*
|
|
501
|
+
* Its own sentence because "renew your plan" is wrong advice here twice
|
|
502
|
+
* over: nothing has lapsed, and paying again would not open the door a day
|
|
503
|
+
* sooner. The resume date is the only thing this member wants to know, and
|
|
504
|
+
* it is the reason this one takes a parameter at all.
|
|
505
|
+
*/
|
|
506
|
+
readonly membershipPausedUntil: (resumesOn: string) => string;
|
|
478
507
|
/** Role-switch PIN lockout, in whole minutes remaining. */
|
|
479
508
|
readonly mpinLockedOut: (minutes: number) => string;
|
|
480
509
|
};
|
package/dist/messages.d.ts.map
CHANGED
|
@@ -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;;;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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IA8CtE;;;;OAIG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkH6C,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;;;;;;;;;;;IAehE,2EAA2E;;;IAI3E;;;OAGG;;;IAIH;;;OAGG;;IAGH,mEAAmE;;;;;IAKnE,8EAA8E;;;;;;;;;;;;;IAgB9E;;;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;;;;IAIlE,uEAAuE;;IAEvE,8EAA8E;;;;;;;;;;;;;;;;;CAyBrC,CAAC;AAE5C,MAAM,MAAM,eAAe,GAAG,MAAM,OAAO,aAAa,CAAC;AAEzD,iDAAiD;AACjD,eAAO,MAAM,iBAAiB;8BACX,MAAM,KAAG,MAAM;IAChC,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;;;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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IA8CtE;;;;OAIG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkH6C,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;;;;;;;;;;;IAehE,2EAA2E;;;IAI3E;;;OAGG;;;IAIH;;;;OAIG;;IAGH;;;;OAIG;;IAEH;;;;;OAKG;;IAGH;;;OAGG;;IAGH,mEAAmE;;;;;IAKnE,8EAA8E;;;;;;;;;;;;;IAgB9E;;;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;;;;IAIlE,uEAAuE;;IAEvE,8EAA8E;;;;;;;;;;;;;;;;;CAyBrC,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
|
@@ -352,6 +352,25 @@ export const ERROR_MESSAGE = {
|
|
|
352
352
|
*/
|
|
353
353
|
membershipAwaitingPayment: 'Your gym is checking your payment. The plan starts as soon as they confirm it.',
|
|
354
354
|
membershipNoPendingRequest: 'There is no plan waiting for confirmation.',
|
|
355
|
+
/**
|
|
356
|
+
* Pausing extends the term by the paused days, so pausing something that is
|
|
357
|
+
* not running would push out a date nobody is training against — and pausing
|
|
358
|
+
* an already-paused membership would extend it twice for one absence.
|
|
359
|
+
*/
|
|
360
|
+
membershipNotPausable: 'Only a running membership can be paused. This one has already ended or is paused.',
|
|
361
|
+
/**
|
|
362
|
+
* The joining date records something that has already happened, so a future
|
|
363
|
+
* one is not a preference to honour — it would date the member's whole
|
|
364
|
+
* billing cycle from a day that has not arrived.
|
|
365
|
+
*/
|
|
366
|
+
joiningDateInFuture: 'A joining date cannot be in the future.',
|
|
367
|
+
/**
|
|
368
|
+
* The term moved between the screen being drawn and the button being
|
|
369
|
+
* pressed, which in practice means a second manager confirmed one first.
|
|
370
|
+
* Says which month it now covers rather than a bare "try again", because the
|
|
371
|
+
* owner's next question is always "then what am I paying for?".
|
|
372
|
+
*/
|
|
373
|
+
membershipTermMoved: 'Someone just confirmed a payment for this member, so the period has moved on. Reopen their profile to see the current one.',
|
|
355
374
|
/**
|
|
356
375
|
* The owner reached for the roster toggle on a plan that has not been paid
|
|
357
376
|
* for. Points at the action that WOULD start it rather than just refusing.
|
|
@@ -445,6 +464,16 @@ export const ERROR_MESSAGE = {
|
|
|
445
464
|
/** Error messages that name a limit or value. */
|
|
446
465
|
export const ERROR_MESSAGE_FOR = {
|
|
447
466
|
maxPhotos: (max) => `You can add up to ${max} photos`,
|
|
467
|
+
/**
|
|
468
|
+
* Refused at the door because the membership is paused and still inside its
|
|
469
|
+
* window.
|
|
470
|
+
*
|
|
471
|
+
* Its own sentence because "renew your plan" is wrong advice here twice
|
|
472
|
+
* over: nothing has lapsed, and paying again would not open the door a day
|
|
473
|
+
* sooner. The resume date is the only thing this member wants to know, and
|
|
474
|
+
* it is the reason this one takes a parameter at all.
|
|
475
|
+
*/
|
|
476
|
+
membershipPausedUntil: (resumesOn) => `Your membership is paused until ${resumesOn}. You can check in again from that day.`,
|
|
448
477
|
/** Role-switch PIN lockout, in whole minutes remaining. */
|
|
449
478
|
mpinLockedOut: (minutes) => `Too many attempts. Try again in ${minutes} minute${minutes > 1 ? 's' : ''}.`,
|
|
450
479
|
};
|
package/dist/messages.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"messages.js","sourceRoot":"","sources":["../src/messages.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * gymmonk-schema — API result messages\n * ====================================\n * Single source of truth for every user-facing SUCCESS and ERROR message the\n * product shows, plus the API error codes both sides key off.\n *\n * Why it lives here: the schema package is already the SSOT for data shapes,\n * types and validation. Result copy is part of that contract — the backend\n * raises a message, the client shows it, and neither should invent its own\n * wording. Both repos import from here, so copy changes land in one place and\n * can never drift between server and client.\n *\n * Usage:\n * backend → `throw new BadRequestError(ERROR_MESSAGE.centerAddressRequired)`\n * frontend → `showSuccess(...toast(SUCCESS_MESSAGE.profileUpdated))`\n *\n * @module gymmonk-schema/messages\n */\n\n// ─── API error codes ─────────────────────────────────────────────────────────\n\n/**\n * Machine-readable error codes returned in the API envelope\n * (`{ success: false, error: { code, message } }`). The client can branch on a\n * code; the message is what a human reads.\n */\nexport const ERROR_CODE = {\n // General\n INTERNAL_ERROR: 'INTERNAL_ERROR',\n VALIDATION_ERROR: 'VALIDATION_ERROR',\n NOT_FOUND: 'NOT_FOUND',\n\n // Auth\n INVALID_TOKEN: 'INVALID_TOKEN',\n TOKEN_EXPIRED: 'TOKEN_EXPIRED',\n UNAUTHORIZED: 'UNAUTHORIZED',\n FORBIDDEN: 'FORBIDDEN',\n SESSION_INVALIDATED: 'SESSION_INVALIDATED',\n\n // User\n USER_NOT_FOUND: 'USER_NOT_FOUND',\n USER_ALREADY_EXISTS: 'USER_ALREADY_EXISTS',\n INVALID_ROLE: 'INVALID_ROLE',\n\n // Rate limiting\n RATE_LIMITED: 'RATE_LIMITED',\n RATE_LIMIT_EXCEEDED: 'RATE_LIMIT_EXCEEDED',\n\n /**\n * The caller MAY do this — their subscription simply does not stretch to it.\n *\n * Deliberately NOT `FORBIDDEN`, and the distinction is load-bearing on the\n * client. A role denial means \"this is not yours\"; the answer is to go away.\n * A plan denial means \"this costs money\"; the answer is an upgrade prompt with\n * the price on it. A client that cannot tell them apart either shows an\n * upgrade sheet to a trainer who will never be allowed to buy one, or shows a\n * bare 403 to an owner who would happily have paid.\n *\n * Always accompanied by `details`: `{ feature, currentPlan, requiredPlan,\n * current?, limit? }` — enough for the prompt to name the feature, the tier\n * that unlocks it, and how full the current allowance is.\n */\n PLAN_LIMIT_EXCEEDED: 'PLAN_LIMIT_EXCEEDED',\n\n // Upstream / dependency unavailable\n SERVICE_UNAVAILABLE: 'SERVICE_UNAVAILABLE',\n} as const;\n\nexport type ErrorCode = (typeof ERROR_CODE)[keyof typeof ERROR_CODE];\n\n// ─── Message shape ───────────────────────────────────────────────────────────\n\n/** A result message: a headline and an optional quieter second line. */\nexport interface ResultMessage {\n title: string;\n description?: string;\n}\n\n// ─── Success messages ────────────────────────────────────────────────────────\n\n/**\n * Confirmation shown after a write succeeds, keyed by action. Interpolating\n * entries are functions so the caller supplies the value without re-wording.\n */\nexport const SUCCESS_MESSAGE = {\n // Onboarding\n memberOnboardingComplete: {\n title: \"You're all set\",\n description: 'Welcome to GymMonk.',\n },\n ownerOnboardingComplete: {\n title: 'Your gym is ready',\n description: 'Welcome to GymMonk.',\n },\n /** Each wizard step is persisted on its own, so each one confirms. */\n onboardingStepSaved: { title: 'Saved' },\n\n // Identity & profile\n profileUpdated: { title: 'Profile updated' },\n organisationUpdated: { title: 'Organisation updated' },\n\n // Gym structure\n centerAdded: { title: 'Center added' },\n centerUpdated: { title: 'Center updated' },\n membershipPlanCreated: { title: 'Membership plan created' },\n membershipPlanUpdated: { title: 'Membership plan updated' },\n equipmentSaved: { title: 'Equipment updated' },\n\n // Membership (member-side)\n membershipUpdated: { title: 'Membership updated' },\n membershipRenewed: { title: 'Membership renewed' },\n membershipCancelled: { title: 'Membership cancelled' },\n membershipPauseRequested: {\n title: 'Pause request sent',\n description: 'Your gym will confirm the pause.',\n },\n\n // Owner SaaS subscription\n subscriptionUpdated: { title: 'Subscription updated' },\n\n // Training\n workoutPlanCreated: { title: 'Workout plan created' },\n workoutPlanUpdated: { title: 'Workout plan saved' },\n workoutPlanDeleted: {\n title: 'Workout plan deleted',\n description: 'Members on this plan no longer have one.',\n },\n workoutCreated: { title: 'Workout created' },\n workoutUpdated: { title: 'Workout saved' },\n workoutDeleted: { title: 'Workout removed' },\n workoutPlanAssigned: { title: 'Workout plan assigned' },\n\n // Roster (owner console)\n attendanceMarked: { title: 'Attendance saved' },\n memberMarkedInactive: {\n title: 'Member marked inactive',\n description: 'They can no longer check in.',\n },\n memberMarkedActive: { title: 'Member reactivated' },\n memberDeleted: { title: 'Member removed' },\n /**\n * Says what actually happened. Assigning a plan no longer starts it —\n * nothing does until the payment is marked done — so a bare \"Plan\n * updated\" would send the owner away believing the member could train.\n */\n memberPlanAssigned: {\n title: 'Plan assigned',\n description: 'It starts once you mark the payment done.',\n },\n memberMembershipPaused: { title: 'Membership paused' },\n memberMembershipCancelled: { title: 'Membership cancelled' },\n paymentCollected: { title: 'Payment recorded' },\n\n // Sessions\n checkedIn: { title: 'Checked in', description: 'Your session has started.' },\n checkedOut: { title: 'Checked out', description: 'Session saved.' },\n checkInQrRotated: {\n title: 'New QR generated',\n description: 'Print it and replace the old poster.',\n },\n\n // Membership join requests\n membershipRequestSent: {\n title: 'Request sent',\n description: 'The gym will review it shortly.',\n },\n membershipRequestApproved: {\n title: 'Member approved',\n description: 'They can now check in at your gym.',\n },\n membershipRequestRejected: {\n title: 'Request declined',\n description: 'The member has been told.',\n },\n\n // Membership payment confirmation\n membershipRequested: {\n title: 'Plan requested',\n description: 'It starts once your gym confirms the payment.',\n },\n membershipRequestWithdrawn: {\n title: 'Request withdrawn',\n description: 'Pick another plan whenever you are ready.',\n },\n membershipPaymentConfirmed: {\n title: 'Payment confirmed',\n description: 'The plan is now active for this member.',\n },\n membershipPaymentRejected: {\n title: 'Payment turned down',\n description: 'The member has been told and can send it again.',\n },\n\n // Gym registration + platform review\n gymRegistered: {\n title: 'Gym submitted',\n description: 'Our team will review it and let you know.',\n },\n gymApproved: {\n title: 'Gym approved',\n description: 'It is now listed for members to find.',\n },\n gymRejected: {\n title: 'Gym rejected',\n description: 'The owner has been told.',\n },\n\n // Engagement\n feedbackSubmitted: {\n title: 'Feedback sent',\n description: 'Thank you for helping us improve.',\n },\n complaintSubmitted: {\n title: 'Complaint submitted',\n description: 'Your gym will look into it.',\n },\n announcementSent: { title: 'Announcement sent' },\n announcementScheduled: { title: 'Announcement scheduled' },\n announcementDraftSaved: { title: 'Draft saved' },\n announcementDeleted: { title: 'Announcement deleted' },\n announcementCancelled: {\n title: 'Announcement cancelled',\n description: 'It will not be sent.',\n },\n\n // Notification feed\n notificationHidden: { title: 'Notification hidden' },\n notificationRestored: { title: 'Notification restored' },\n allNotificationsRead: { title: 'All caught up' },\n\n // Security (role-switch PIN)\n mpinSet: { title: 'PIN set' },\n mpinReset: { title: 'Your PIN has been reset.' },\n mpinResetLinkSent: {\n title: \"We've emailed you a reset link\",\n description: 'Check your inbox.',\n },\n\n // ── Platform console (superadmin / platform manager) ────────────────────────\n platformRolesUpdated: { title: 'Platform roles updated' },\n platformUserSuspended: {\n title: 'Account suspended',\n description: 'They can no longer sign in.',\n },\n platformUserReinstated: { title: 'Account reinstated' },\n platformUserDeleted: {\n title: 'Account deleted',\n description: 'It is hidden everywhere but can still be restored.',\n },\n platformUserRestored: { title: 'Account restored' },\n platformGymDeleted: {\n title: 'Gym deleted',\n description: 'It is hidden everywhere but can still be restored.',\n },\n platformGymRestored: {\n title: 'Gym restored',\n description: 'It is back in the directory and its owner has the console again.',\n },\n} as const satisfies Record<string, ResultMessage>;\n\nexport type SuccessMessageKey = keyof typeof SUCCESS_MESSAGE;\n\n/** Success messages that name the thing they acted on. */\nexport const SUCCESS_MESSAGE_FOR = {\n memberAdded: (name: string): ResultMessage => ({\n title: 'Member added',\n description: `Share the invite with ${name}.`,\n }),\n staffAdded: (name: string): ResultMessage => ({\n title: 'Staff added',\n description: `Share the invite with ${name}.`,\n }),\n inviteClaimed: (gymName: string): ResultMessage => ({\n title: \"You're all set\",\n description: `Your account is linked to ${gymName}.`,\n }),\n roleSwitched: (roleLabel: string): ResultMessage => ({\n title: `Switched to ${roleLabel}`,\n }),\n gymApprovedFor: (gymName: string): ResultMessage => ({\n title: 'Gym approved',\n description: `${gymName} is now live on GymMonk.`,\n }),\n gymRejectedFor: (gymName: string): ResultMessage => ({\n title: 'Gym rejected',\n description: `The owner of ${gymName} has been told why.`,\n }),\n} as const;\n\n// ─── Error messages ──────────────────────────────────────────────────────────\n\n/**\n * Failure copy. Transport entries describe a request that never reached the\n * API; the rest are domain rules the backend enforces and the client can also\n * check up-front — same wording either way.\n */\nexport const ERROR_MESSAGE = {\n // Transport / generic\n generic: 'Something went wrong. Please try again.',\n network: 'Network error. Check your connection and try again.',\n timeout: 'That took too long. Please try again.',\n sessionExpired: 'Your session expired. Please sign in again.',\n\n // Client-side validation\n formInvalid: 'Please check the highlighted fields.',\n\n // Onboarding\n roleRequired: 'Choose a role to continue',\n ownerOnboardingFailed: 'We could not create your gym. Please try again.',\n memberOnboardingFailed: 'We could not save your details. Please try again.',\n\n // ── Gym registration (\"list your gym\") ─────────────────────────────────────\n gymRegistrationFailed: 'We could not register your gym. Please try again.',\n /**\n * The wizard could not read what the owner had already saved.\n *\n * It refuses to open on a blank form rather than risk the next Continue\n * overwriting a real draft, so this is the whole screen, not a toast.\n */\n gymRegistrationDraftLoadFailed:\n 'We could not open your saved gym details. Check your connection and try again.',\n /**\n * The step the wizard tried to save is implausibly large.\n *\n * A guard against a client sending something absurd rather than a limit the\n * real form approaches, but it is raised by the SERVER and shown verbatim in\n * the step-save toast — so the wording lives here with the rest of the copy,\n * not as a hand-written string at the throw site.\n */\n gymRegistrationDraftTooLarge:\n 'Those gym details are too large to save. Please shorten the descriptions and try again.',\n /**\n * \"Near me\" found nothing inside the radius.\n *\n * Deliberately not the same as `gymSearchNoResults`: this member searched\n * nowhere in particular, so telling them to refine their search would be\n * advice they cannot act on. What helps is a different place or a gym id.\n */\n gymSearchNoneNearby:\n 'No gyms within 50 km of you yet. Try a different location, or enter a gym id if you have one.',\n /** One organisation per account — registering a second is not supported. */\n gymAlreadyRegistered: 'You have already registered a gym on this account.',\n /** Registering a gym is something a MEMBER does, so the profile must exist. */\n gymRegistrationNeedsProfile: 'Finish setting up your profile before registering a gym.',\n /** Every owner action other than editing their own details, before approval. */\n gymAwaitingApproval:\n 'Your gym is waiting for approval. You can manage it once our team has reviewed it.',\n /** The reviewer turned the registration down. */\n gymRegistrationRejected: 'Your gym registration was not approved.',\n organisationNotFound: 'That gym could not be found.',\n\n // ── Platform console ────────────────────────────────────────────────────────\n /** The account is gone, or was already erased. */\n platformUserNotFound: 'That account could not be found.',\n /**\n * An admin acting on their OWN row. Suspending or deleting yourself takes away\n * the access needed to undo it, so the console refuses rather than letting one\n * mis-tap lock the platform's last superadmin out of it.\n */\n platformCannotActOnSelf: 'You cannot do that to your own account.',\n /**\n * Removing the last superadmin. `manage GymApproval` is held by nobody else,\n * so the gym review queue would become unreachable with no way back in short\n * of a database edit.\n */\n platformLastSuperadmin: 'The platform must keep at least one superadmin.',\n /** The typed confirmation did not match the record being deleted. */\n platformConfirmationMismatch: 'That did not match. Nothing was changed.',\n /** Two reviewers acting on the same registration; the second one loses. */\n organisationAlreadyDecided: 'Someone has already reviewed this gym.',\n /** A step failed to persist — the user stays put so nothing is lost. */\n onboardingStepSaveFailed: 'We could not save this step. Please try again.',\n\n // Gym structure prerequisites\n centerRequiredForPlan: 'Pick the center this plan belongs to.',\n planVariantRequired: 'Add at least one pricing variant before saving the plan.',\n planNameTaken: 'This center already has a plan with that name. Pick another name.',\n planVariantDurationRequired: 'Enter how long this variant runs for',\n planVariantPriceRequired: 'Enter the price for this variant',\n /** A \"discount\" that raises the price is a typo, every time. */\n planFinalPriceAboveOriginal: 'The final price cannot be more than the original price',\n centerAddressRequired: 'Add the center address before creating it.',\n noCenterForPlans: 'No gym center found. Add a center before creating plans.',\n noCenterForWorkouts: 'No gym center found. Add a center before creating workouts.',\n noCenterForAnnouncements: 'No gym center found. Add a center before sending announcements.',\n announcementAlreadySent: 'That announcement has already gone out, so it cannot be cancelled.',\n\n // Notification feed\n notificationHideFailed: 'Could not hide that notification.',\n notificationRestoreFailed: 'Could not restore that notification.',\n\n // Membership\n planUnavailable: 'That plan is not available right now.',\n membershipPlanRequired: 'Choose a membership plan to continue',\n /** The member was created, but the plan the owner picked did not stick. */\n initialPlanNotAssigned:\n 'Member added, but the plan could not be assigned. Assign it from their profile.',\n noPlansToAssign: 'No membership plans yet. Create one to assign it while adding a member.',\n /**\n * Not an error the member can act on by trying harder — it is the whole\n * point of the flow, so it reads as a status rather than a refusal.\n */\n membershipAwaitingPayment:\n 'Your gym is checking your payment. The plan starts as soon as they confirm it.',\n membershipNoPendingRequest: 'There is no plan waiting for confirmation.',\n /**\n * The owner reached for the roster toggle on a plan that has not been paid\n * for. Points at the action that WOULD start it rather than just refusing.\n */\n membershipPendingUseConfirm:\n 'This plan is waiting for payment confirmation. Mark the payment done to start it.',\n /** Two managers opening the same request; the second one loses. */\n membershipAlreadyDecided: 'Someone has already answered this payment.',\n membershipConfirmFailed: 'Could not confirm that payment. Please try again.',\n membershipRejectFailed: 'Could not turn down that payment. Please try again.',\n membershipWithdrawFailed: 'Could not withdraw that request. Please try again.',\n /** The URL did not come from our own storage — a broken or hostile client. */\n paymentProofInvalid: 'That payment screenshot could not be attached. Upload it again.',\n\n // Training\n workoutPlanRequired: 'Choose a plan to assign',\n // Roster (owner console)\n attendanceMarkFailed: 'Could not save that attendance. Please try again.',\n memberStatusFailed: \"Could not change that member's status. Please try again.\",\n memberDeleteFailed: 'Could not remove that member. Please try again.',\n memberPlanAssignFailed: 'Could not update that plan. Please try again.',\n memberPauseFailed: 'Could not pause that membership. Please try again.',\n memberCancelFailed: 'Could not cancel that membership. Please try again.',\n paymentCollectFailed: 'Could not record that payment. Please try again.',\n paymentAmountRequired: 'Enter the amount collected',\n pauseDatesRequired: 'Pick both pause dates',\n pauseDatesOrder: 'The end date has to be after the start date',\n /**\n * Attendance, workout plans and payments are all keyed on the central-auth\n * user, which an invited record does not have until the person claims it.\n */\n memberNotClaimed: 'This member has not claimed their invite yet.',\n\n // Sessions\n checkInFailed: 'Could not check you in. Please try again.',\n checkOutFailed: 'Could not check you out. Please try again.',\n /** The camera decoded a QR, but it is not a gymmonk check-in poster. */\n qrNotRecognised: 'That is not a gym check-in QR. Point at the poster at the gym.',\n /** A valid gymmonk QR, but for a gym the member does not belong to. */\n qrWrongGym: 'That QR belongs to a different gym.',\n /** The scanned code matches no center — a retired or mistyped poster. */\n qrUnknown: 'This QR is no longer valid. Ask the gym for the current poster.',\n /** Checking out with a poster from somewhere other than where you started. */\n qrWrongGymForCheckOut: 'Scan the QR at the gym where you checked in.',\n /** Manual entry only accepts a full, well-formed code. */\n checkInCodeInvalid: 'Enter the 16-character code printed under the QR.',\n checkInQrRotateFailed: 'Could not generate a new QR. Please try again.',\n /**\n * Ticking an exercise off records it against a live session, so there has to\n * be one. Shown when a member opens an exercise outside a workout and taps\n * Mark As Done.\n */\n noSessionToTrack: 'Check in at your gym to track this workout.',\n\n // ── Membership join requests ───────────────────────────────────────────────\n /** Scanned a valid poster, but the gym has not accepted this member yet. */\n membershipNotApproved:\n 'Your gym has not accepted your join request yet. You can check in once they do.',\n /** Accepted, but with no live plan — check-in needs both. */\n membershipNotActive: 'Your membership plan is not active. Renew it to start checking in again.',\n /** A second request while one is already open. */\n membershipRequestPending: 'You already have a request waiting with this gym.',\n /** Requesting a gym they are already an accepted member of. */\n membershipRequestAlreadyMember: 'You are already a member of this gym.',\n /** v1 memberships are single-center — leave the current gym first. */\n membershipRequestAlreadyAtAnotherGym:\n 'You already belong to a gym. Cancel that membership before joining another.',\n membershipRequestNotFound: 'That join request no longer exists.',\n /** Two staff acting on the same request; the second one loses. */\n membershipRequestAlreadyDecided: 'Someone has already answered this request.',\n membershipRequestFailed: 'Could not send your request. Please try again.',\n gymCodeUnknown: 'No gym found with that id. Check it with your gym.',\n /** Actions that only make sense once a gym has taken the member on. */\n noGymJoined: 'Join a gym first. You can find one from your Me tab.',\n /** Searching by name needs somewhere to look — see `gymSearchQuerySchema`. */\n gymSearchStateRequired: 'Pick a state to search gyms by name.',\n gymSearchNoResults: 'No gyms matched. Try a different area, or enter the gym id.',\n\n // Camera (QR scanning)\n cameraDenied: 'Camera access is blocked. Allow it in your browser settings to scan.',\n cameraUnavailable: 'No camera found on this device. Enter the code printed under the QR.',\n cameraInsecure: 'Scanning needs a secure (https) connection.',\n cameraFailed: 'Could not start the camera. Enter the code printed under the QR.',\n\n // Engagement\n feedbackRatingRequired: 'Choose a rating to submit your feedback',\n complaintIssueRequired: 'Choose the issue you are reporting',\n\n // Security (role-switch PIN)\n mpinIncorrect: 'Incorrect PIN.',\n mpinVerifyFailed: \"Couldn't verify. Try again.\",\n mpinSetFailed: \"Couldn't set your PIN. Try again.\",\n mpinMismatch: \"Those didn't match. Try again.\",\n mpinResetLinkFailed: \"Couldn't send the reset link. Try again.\",\n mpinResetFailed: \"Couldn't reset your PIN. Try again.\",\n\n // Uploads\n imageUploadFailed: 'Could not upload that image. Please try again.',\n imageProcessFailed: 'Could not process that image. Please try again.',\n} as const satisfies Record<string, string>;\n\nexport type ErrorMessageKey = keyof typeof ERROR_MESSAGE;\n\n/** Error messages that name a limit or value. */\nexport const ERROR_MESSAGE_FOR = {\n maxPhotos: (max: number): string => `You can add up to ${max} photos`,\n /** Role-switch PIN lockout, in whole minutes remaining. */\n mpinLockedOut: (minutes: number): string =>\n `Too many attempts. Try again in ${minutes} minute${minutes > 1 ? 's' : ''}.`,\n} as const;\n"]}
|
|
1
|
+
{"version":3,"file":"messages.js","sourceRoot":"","sources":["../src/messages.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,gFAAgF;AAEhF;;;;GAIG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,UAAU;IACV,cAAc,EAAE,gBAAgB;IAChC,gBAAgB,EAAE,kBAAkB;IACpC,SAAS,EAAE,WAAW;IAEtB,OAAO;IACP,aAAa,EAAE,eAAe;IAC9B,aAAa,EAAE,eAAe;IAC9B,YAAY,EAAE,cAAc;IAC5B,SAAS,EAAE,WAAW;IACtB,mBAAmB,EAAE,qBAAqB;IAE1C,OAAO;IACP,cAAc,EAAE,gBAAgB;IAChC,mBAAmB,EAAE,qBAAqB;IAC1C,YAAY,EAAE,cAAc;IAE5B,gBAAgB;IAChB,YAAY,EAAE,cAAc;IAC5B,mBAAmB,EAAE,qBAAqB;IAE1C;;;;;;;;;;;;;OAaG;IACH,mBAAmB,EAAE,qBAAqB;IAE1C,oCAAoC;IACpC,mBAAmB,EAAE,qBAAqB;CAClC,CAAC;AAYX,gFAAgF;AAEhF;;;GAGG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG;IAC7B,aAAa;IACb,wBAAwB,EAAE;QACxB,KAAK,EAAE,gBAAgB;QACvB,WAAW,EAAE,qBAAqB;KACnC;IACD,uBAAuB,EAAE;QACvB,KAAK,EAAE,mBAAmB;QAC1B,WAAW,EAAE,qBAAqB;KACnC;IACD,sEAAsE;IACtE,mBAAmB,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE;IAEvC,qBAAqB;IACrB,cAAc,EAAE,EAAE,KAAK,EAAE,iBAAiB,EAAE;IAC5C,mBAAmB,EAAE,EAAE,KAAK,EAAE,sBAAsB,EAAE;IAEtD,gBAAgB;IAChB,WAAW,EAAE,EAAE,KAAK,EAAE,cAAc,EAAE;IACtC,aAAa,EAAE,EAAE,KAAK,EAAE,gBAAgB,EAAE;IAC1C,qBAAqB,EAAE,EAAE,KAAK,EAAE,yBAAyB,EAAE;IAC3D,qBAAqB,EAAE,EAAE,KAAK,EAAE,yBAAyB,EAAE;IAC3D,cAAc,EAAE,EAAE,KAAK,EAAE,mBAAmB,EAAE;IAE9C,2BAA2B;IAC3B,iBAAiB,EAAE,EAAE,KAAK,EAAE,oBAAoB,EAAE;IAClD,iBAAiB,EAAE,EAAE,KAAK,EAAE,oBAAoB,EAAE;IAClD,mBAAmB,EAAE,EAAE,KAAK,EAAE,sBAAsB,EAAE;IACtD,wBAAwB,EAAE;QACxB,KAAK,EAAE,oBAAoB;QAC3B,WAAW,EAAE,kCAAkC;KAChD;IAED,0BAA0B;IAC1B,mBAAmB,EAAE,EAAE,KAAK,EAAE,sBAAsB,EAAE;IAEtD,WAAW;IACX,kBAAkB,EAAE,EAAE,KAAK,EAAE,sBAAsB,EAAE;IACrD,kBAAkB,EAAE,EAAE,KAAK,EAAE,oBAAoB,EAAE;IACnD,kBAAkB,EAAE;QAClB,KAAK,EAAE,sBAAsB;QAC7B,WAAW,EAAE,0CAA0C;KACxD;IACD,cAAc,EAAE,EAAE,KAAK,EAAE,iBAAiB,EAAE;IAC5C,cAAc,EAAE,EAAE,KAAK,EAAE,eAAe,EAAE;IAC1C,cAAc,EAAE,EAAE,KAAK,EAAE,iBAAiB,EAAE;IAC5C,mBAAmB,EAAE,EAAE,KAAK,EAAE,uBAAuB,EAAE;IAEvD,yBAAyB;IACzB,gBAAgB,EAAE,EAAE,KAAK,EAAE,kBAAkB,EAAE;IAC/C,oBAAoB,EAAE;QACpB,KAAK,EAAE,wBAAwB;QAC/B,WAAW,EAAE,8BAA8B;KAC5C;IACD,kBAAkB,EAAE,EAAE,KAAK,EAAE,oBAAoB,EAAE;IACnD,aAAa,EAAE,EAAE,KAAK,EAAE,gBAAgB,EAAE;IAC1C;;;;OAIG;IACH,kBAAkB,EAAE;QAClB,KAAK,EAAE,eAAe;QACtB,WAAW,EAAE,2CAA2C;KACzD;IACD,sBAAsB,EAAE,EAAE,KAAK,EAAE,mBAAmB,EAAE;IACtD,yBAAyB,EAAE,EAAE,KAAK,EAAE,sBAAsB,EAAE;IAC5D,gBAAgB,EAAE,EAAE,KAAK,EAAE,kBAAkB,EAAE;IAE/C,WAAW;IACX,SAAS,EAAE,EAAE,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,2BAA2B,EAAE;IAC5E,UAAU,EAAE,EAAE,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,gBAAgB,EAAE;IACnE,gBAAgB,EAAE;QAChB,KAAK,EAAE,kBAAkB;QACzB,WAAW,EAAE,sCAAsC;KACpD;IAED,2BAA2B;IAC3B,qBAAqB,EAAE;QACrB,KAAK,EAAE,cAAc;QACrB,WAAW,EAAE,iCAAiC;KAC/C;IACD,yBAAyB,EAAE;QACzB,KAAK,EAAE,iBAAiB;QACxB,WAAW,EAAE,oCAAoC;KAClD;IACD,yBAAyB,EAAE;QACzB,KAAK,EAAE,kBAAkB;QACzB,WAAW,EAAE,2BAA2B;KACzC;IAED,kCAAkC;IAClC,mBAAmB,EAAE;QACnB,KAAK,EAAE,gBAAgB;QACvB,WAAW,EAAE,+CAA+C;KAC7D;IACD,0BAA0B,EAAE;QAC1B,KAAK,EAAE,mBAAmB;QAC1B,WAAW,EAAE,2CAA2C;KACzD;IACD,0BAA0B,EAAE;QAC1B,KAAK,EAAE,mBAAmB;QAC1B,WAAW,EAAE,yCAAyC;KACvD;IACD,yBAAyB,EAAE;QACzB,KAAK,EAAE,qBAAqB;QAC5B,WAAW,EAAE,iDAAiD;KAC/D;IAED,qCAAqC;IACrC,aAAa,EAAE;QACb,KAAK,EAAE,eAAe;QACtB,WAAW,EAAE,2CAA2C;KACzD;IACD,WAAW,EAAE;QACX,KAAK,EAAE,cAAc;QACrB,WAAW,EAAE,uCAAuC;KACrD;IACD,WAAW,EAAE;QACX,KAAK,EAAE,cAAc;QACrB,WAAW,EAAE,0BAA0B;KACxC;IAED,aAAa;IACb,iBAAiB,EAAE;QACjB,KAAK,EAAE,eAAe;QACtB,WAAW,EAAE,mCAAmC;KACjD;IACD,kBAAkB,EAAE;QAClB,KAAK,EAAE,qBAAqB;QAC5B,WAAW,EAAE,6BAA6B;KAC3C;IACD,gBAAgB,EAAE,EAAE,KAAK,EAAE,mBAAmB,EAAE;IAChD,qBAAqB,EAAE,EAAE,KAAK,EAAE,wBAAwB,EAAE;IAC1D,sBAAsB,EAAE,EAAE,KAAK,EAAE,aAAa,EAAE;IAChD,mBAAmB,EAAE,EAAE,KAAK,EAAE,sBAAsB,EAAE;IACtD,qBAAqB,EAAE;QACrB,KAAK,EAAE,wBAAwB;QAC/B,WAAW,EAAE,sBAAsB;KACpC;IAED,oBAAoB;IACpB,kBAAkB,EAAE,EAAE,KAAK,EAAE,qBAAqB,EAAE;IACpD,oBAAoB,EAAE,EAAE,KAAK,EAAE,uBAAuB,EAAE;IACxD,oBAAoB,EAAE,EAAE,KAAK,EAAE,eAAe,EAAE;IAEhD,6BAA6B;IAC7B,OAAO,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE;IAC7B,SAAS,EAAE,EAAE,KAAK,EAAE,0BAA0B,EAAE;IAChD,iBAAiB,EAAE;QACjB,KAAK,EAAE,gCAAgC;QACvC,WAAW,EAAE,mBAAmB;KACjC;IAED,+EAA+E;IAC/E,oBAAoB,EAAE,EAAE,KAAK,EAAE,wBAAwB,EAAE;IACzD,qBAAqB,EAAE;QACrB,KAAK,EAAE,mBAAmB;QAC1B,WAAW,EAAE,6BAA6B;KAC3C;IACD,sBAAsB,EAAE,EAAE,KAAK,EAAE,oBAAoB,EAAE;IACvD,mBAAmB,EAAE;QACnB,KAAK,EAAE,iBAAiB;QACxB,WAAW,EAAE,oDAAoD;KAClE;IACD,oBAAoB,EAAE,EAAE,KAAK,EAAE,kBAAkB,EAAE;IACnD,kBAAkB,EAAE;QAClB,KAAK,EAAE,aAAa;QACpB,WAAW,EAAE,oDAAoD;KAClE;IACD,mBAAmB,EAAE;QACnB,KAAK,EAAE,cAAc;QACrB,WAAW,EAAE,kEAAkE;KAChF;CAC+C,CAAC;AAInD,0DAA0D;AAC1D,MAAM,CAAC,MAAM,mBAAmB,GAAG;IACjC,WAAW,EAAE,CAAC,IAAY,EAAiB,EAAE,CAAC,CAAC;QAC7C,KAAK,EAAE,cAAc;QACrB,WAAW,EAAE,yBAAyB,IAAI,GAAG;KAC9C,CAAC;IACF,UAAU,EAAE,CAAC,IAAY,EAAiB,EAAE,CAAC,CAAC;QAC5C,KAAK,EAAE,aAAa;QACpB,WAAW,EAAE,yBAAyB,IAAI,GAAG;KAC9C,CAAC;IACF,aAAa,EAAE,CAAC,OAAe,EAAiB,EAAE,CAAC,CAAC;QAClD,KAAK,EAAE,gBAAgB;QACvB,WAAW,EAAE,6BAA6B,OAAO,GAAG;KACrD,CAAC;IACF,YAAY,EAAE,CAAC,SAAiB,EAAiB,EAAE,CAAC,CAAC;QACnD,KAAK,EAAE,eAAe,SAAS,EAAE;KAClC,CAAC;IACF,cAAc,EAAE,CAAC,OAAe,EAAiB,EAAE,CAAC,CAAC;QACnD,KAAK,EAAE,cAAc;QACrB,WAAW,EAAE,GAAG,OAAO,0BAA0B;KAClD,CAAC;IACF,cAAc,EAAE,CAAC,OAAe,EAAiB,EAAE,CAAC,CAAC;QACnD,KAAK,EAAE,cAAc;QACrB,WAAW,EAAE,gBAAgB,OAAO,qBAAqB;KAC1D,CAAC;CACM,CAAC;AAEX,gFAAgF;AAEhF;;;;GAIG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG;IAC3B,sBAAsB;IACtB,OAAO,EAAE,yCAAyC;IAClD,OAAO,EAAE,qDAAqD;IAC9D,OAAO,EAAE,uCAAuC;IAChD,cAAc,EAAE,6CAA6C;IAE7D,yBAAyB;IACzB,WAAW,EAAE,sCAAsC;IAEnD,aAAa;IACb,YAAY,EAAE,2BAA2B;IACzC,qBAAqB,EAAE,iDAAiD;IACxE,sBAAsB,EAAE,mDAAmD;IAE3E,8EAA8E;IAC9E,qBAAqB,EAAE,mDAAmD;IAC1E;;;;;OAKG;IACH,8BAA8B,EAC5B,gFAAgF;IAClF;;;;;;;OAOG;IACH,4BAA4B,EAC1B,yFAAyF;IAC3F;;;;;;OAMG;IACH,mBAAmB,EACjB,+FAA+F;IACjG,4EAA4E;IAC5E,oBAAoB,EAAE,oDAAoD;IAC1E,+EAA+E;IAC/E,2BAA2B,EAAE,0DAA0D;IACvF,gFAAgF;IAChF,mBAAmB,EACjB,oFAAoF;IACtF,iDAAiD;IACjD,uBAAuB,EAAE,yCAAyC;IAClE,oBAAoB,EAAE,8BAA8B;IAEpD,+EAA+E;IAC/E,kDAAkD;IAClD,oBAAoB,EAAE,kCAAkC;IACxD;;;;OAIG;IACH,uBAAuB,EAAE,yCAAyC;IAClE;;;;OAIG;IACH,sBAAsB,EAAE,iDAAiD;IACzE,qEAAqE;IACrE,4BAA4B,EAAE,0CAA0C;IACxE,2EAA2E;IAC3E,0BAA0B,EAAE,wCAAwC;IACpE,wEAAwE;IACxE,wBAAwB,EAAE,gDAAgD;IAE1E,8BAA8B;IAC9B,qBAAqB,EAAE,uCAAuC;IAC9D,mBAAmB,EAAE,0DAA0D;IAC/E,aAAa,EAAE,mEAAmE;IAClF,2BAA2B,EAAE,sCAAsC;IACnE,wBAAwB,EAAE,kCAAkC;IAC5D,gEAAgE;IAChE,2BAA2B,EAAE,wDAAwD;IACrF,qBAAqB,EAAE,4CAA4C;IACnE,gBAAgB,EAAE,0DAA0D;IAC5E,mBAAmB,EAAE,6DAA6D;IAClF,wBAAwB,EAAE,iEAAiE;IAC3F,uBAAuB,EAAE,oEAAoE;IAE7F,oBAAoB;IACpB,sBAAsB,EAAE,mCAAmC;IAC3D,yBAAyB,EAAE,sCAAsC;IAEjE,aAAa;IACb,eAAe,EAAE,uCAAuC;IACxD,sBAAsB,EAAE,sCAAsC;IAC9D,2EAA2E;IAC3E,sBAAsB,EACpB,iFAAiF;IACnF,eAAe,EAAE,yEAAyE;IAC1F;;;OAGG;IACH,yBAAyB,EACvB,gFAAgF;IAClF,0BAA0B,EAAE,4CAA4C;IACxE;;;;OAIG;IACH,qBAAqB,EACnB,mFAAmF;IACrF;;;;OAIG;IACH,mBAAmB,EAAE,yCAAyC;IAC9D;;;;;OAKG;IACH,mBAAmB,EACjB,4HAA4H;IAC9H;;;OAGG;IACH,2BAA2B,EACzB,mFAAmF;IACrF,mEAAmE;IACnE,wBAAwB,EAAE,4CAA4C;IACtE,uBAAuB,EAAE,mDAAmD;IAC5E,sBAAsB,EAAE,qDAAqD;IAC7E,wBAAwB,EAAE,oDAAoD;IAC9E,8EAA8E;IAC9E,mBAAmB,EAAE,iEAAiE;IAEtF,WAAW;IACX,mBAAmB,EAAE,yBAAyB;IAC9C,yBAAyB;IACzB,oBAAoB,EAAE,mDAAmD;IACzE,kBAAkB,EAAE,0DAA0D;IAC9E,kBAAkB,EAAE,iDAAiD;IACrE,sBAAsB,EAAE,+CAA+C;IACvE,iBAAiB,EAAE,oDAAoD;IACvE,kBAAkB,EAAE,qDAAqD;IACzE,oBAAoB,EAAE,kDAAkD;IACxE,qBAAqB,EAAE,4BAA4B;IACnD,kBAAkB,EAAE,uBAAuB;IAC3C,eAAe,EAAE,6CAA6C;IAC9D;;;OAGG;IACH,gBAAgB,EAAE,+CAA+C;IAEjE,WAAW;IACX,aAAa,EAAE,2CAA2C;IAC1D,cAAc,EAAE,4CAA4C;IAC5D,wEAAwE;IACxE,eAAe,EAAE,gEAAgE;IACjF,uEAAuE;IACvE,UAAU,EAAE,qCAAqC;IACjD,yEAAyE;IACzE,SAAS,EAAE,iEAAiE;IAC5E,8EAA8E;IAC9E,qBAAqB,EAAE,8CAA8C;IACrE,0DAA0D;IAC1D,kBAAkB,EAAE,mDAAmD;IACvE,qBAAqB,EAAE,gDAAgD;IACvE;;;;OAIG;IACH,gBAAgB,EAAE,6CAA6C;IAE/D,8EAA8E;IAC9E,4EAA4E;IAC5E,qBAAqB,EACnB,iFAAiF;IACnF,6DAA6D;IAC7D,mBAAmB,EAAE,0EAA0E;IAC/F,kDAAkD;IAClD,wBAAwB,EAAE,mDAAmD;IAC7E,+DAA+D;IAC/D,8BAA8B,EAAE,uCAAuC;IACvE,sEAAsE;IACtE,oCAAoC,EAClC,6EAA6E;IAC/E,yBAAyB,EAAE,qCAAqC;IAChE,kEAAkE;IAClE,+BAA+B,EAAE,4CAA4C;IAC7E,uBAAuB,EAAE,gDAAgD;IACzE,cAAc,EAAE,oDAAoD;IACpE,uEAAuE;IACvE,WAAW,EAAE,sDAAsD;IACnE,8EAA8E;IAC9E,sBAAsB,EAAE,sCAAsC;IAC9D,kBAAkB,EAAE,6DAA6D;IAEjF,uBAAuB;IACvB,YAAY,EAAE,sEAAsE;IACpF,iBAAiB,EAAE,sEAAsE;IACzF,cAAc,EAAE,6CAA6C;IAC7D,YAAY,EAAE,kEAAkE;IAEhF,aAAa;IACb,sBAAsB,EAAE,yCAAyC;IACjE,sBAAsB,EAAE,oCAAoC;IAE5D,6BAA6B;IAC7B,aAAa,EAAE,gBAAgB;IAC/B,gBAAgB,EAAE,6BAA6B;IAC/C,aAAa,EAAE,mCAAmC;IAClD,YAAY,EAAE,gCAAgC;IAC9C,mBAAmB,EAAE,0CAA0C;IAC/D,eAAe,EAAE,qCAAqC;IAEtD,UAAU;IACV,iBAAiB,EAAE,gDAAgD;IACnE,kBAAkB,EAAE,iDAAiD;CAC5B,CAAC;AAI5C,iDAAiD;AACjD,MAAM,CAAC,MAAM,iBAAiB,GAAG;IAC/B,SAAS,EAAE,CAAC,GAAW,EAAU,EAAE,CAAC,qBAAqB,GAAG,SAAS;IACrE;;;;;;;;OAQG;IACH,qBAAqB,EAAE,CAAC,SAAiB,EAAU,EAAE,CACnD,mCAAmC,SAAS,yCAAyC;IACvF,2DAA2D;IAC3D,aAAa,EAAE,CAAC,OAAe,EAAU,EAAE,CACzC,mCAAmC,OAAO,UAAU,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG;CACvE,CAAC","sourcesContent":["/**\n * gymmonk-schema — API result messages\n * ====================================\n * Single source of truth for every user-facing SUCCESS and ERROR message the\n * product shows, plus the API error codes both sides key off.\n *\n * Why it lives here: the schema package is already the SSOT for data shapes,\n * types and validation. Result copy is part of that contract — the backend\n * raises a message, the client shows it, and neither should invent its own\n * wording. Both repos import from here, so copy changes land in one place and\n * can never drift between server and client.\n *\n * Usage:\n * backend → `throw new BadRequestError(ERROR_MESSAGE.centerAddressRequired)`\n * frontend → `showSuccess(...toast(SUCCESS_MESSAGE.profileUpdated))`\n *\n * @module gymmonk-schema/messages\n */\n\n// ─── API error codes ─────────────────────────────────────────────────────────\n\n/**\n * Machine-readable error codes returned in the API envelope\n * (`{ success: false, error: { code, message } }`). The client can branch on a\n * code; the message is what a human reads.\n */\nexport const ERROR_CODE = {\n // General\n INTERNAL_ERROR: 'INTERNAL_ERROR',\n VALIDATION_ERROR: 'VALIDATION_ERROR',\n NOT_FOUND: 'NOT_FOUND',\n\n // Auth\n INVALID_TOKEN: 'INVALID_TOKEN',\n TOKEN_EXPIRED: 'TOKEN_EXPIRED',\n UNAUTHORIZED: 'UNAUTHORIZED',\n FORBIDDEN: 'FORBIDDEN',\n SESSION_INVALIDATED: 'SESSION_INVALIDATED',\n\n // User\n USER_NOT_FOUND: 'USER_NOT_FOUND',\n USER_ALREADY_EXISTS: 'USER_ALREADY_EXISTS',\n INVALID_ROLE: 'INVALID_ROLE',\n\n // Rate limiting\n RATE_LIMITED: 'RATE_LIMITED',\n RATE_LIMIT_EXCEEDED: 'RATE_LIMIT_EXCEEDED',\n\n /**\n * The caller MAY do this — their subscription simply does not stretch to it.\n *\n * Deliberately NOT `FORBIDDEN`, and the distinction is load-bearing on the\n * client. A role denial means \"this is not yours\"; the answer is to go away.\n * A plan denial means \"this costs money\"; the answer is an upgrade prompt with\n * the price on it. A client that cannot tell them apart either shows an\n * upgrade sheet to a trainer who will never be allowed to buy one, or shows a\n * bare 403 to an owner who would happily have paid.\n *\n * Always accompanied by `details`: `{ feature, currentPlan, requiredPlan,\n * current?, limit? }` — enough for the prompt to name the feature, the tier\n * that unlocks it, and how full the current allowance is.\n */\n PLAN_LIMIT_EXCEEDED: 'PLAN_LIMIT_EXCEEDED',\n\n // Upstream / dependency unavailable\n SERVICE_UNAVAILABLE: 'SERVICE_UNAVAILABLE',\n} as const;\n\nexport type ErrorCode = (typeof ERROR_CODE)[keyof typeof ERROR_CODE];\n\n// ─── Message shape ───────────────────────────────────────────────────────────\n\n/** A result message: a headline and an optional quieter second line. */\nexport interface ResultMessage {\n title: string;\n description?: string;\n}\n\n// ─── Success messages ────────────────────────────────────────────────────────\n\n/**\n * Confirmation shown after a write succeeds, keyed by action. Interpolating\n * entries are functions so the caller supplies the value without re-wording.\n */\nexport const SUCCESS_MESSAGE = {\n // Onboarding\n memberOnboardingComplete: {\n title: \"You're all set\",\n description: 'Welcome to GymMonk.',\n },\n ownerOnboardingComplete: {\n title: 'Your gym is ready',\n description: 'Welcome to GymMonk.',\n },\n /** Each wizard step is persisted on its own, so each one confirms. */\n onboardingStepSaved: { title: 'Saved' },\n\n // Identity & profile\n profileUpdated: { title: 'Profile updated' },\n organisationUpdated: { title: 'Organisation updated' },\n\n // Gym structure\n centerAdded: { title: 'Center added' },\n centerUpdated: { title: 'Center updated' },\n membershipPlanCreated: { title: 'Membership plan created' },\n membershipPlanUpdated: { title: 'Membership plan updated' },\n equipmentSaved: { title: 'Equipment updated' },\n\n // Membership (member-side)\n membershipUpdated: { title: 'Membership updated' },\n membershipRenewed: { title: 'Membership renewed' },\n membershipCancelled: { title: 'Membership cancelled' },\n membershipPauseRequested: {\n title: 'Pause request sent',\n description: 'Your gym will confirm the pause.',\n },\n\n // Owner SaaS subscription\n subscriptionUpdated: { title: 'Subscription updated' },\n\n // Training\n workoutPlanCreated: { title: 'Workout plan created' },\n workoutPlanUpdated: { title: 'Workout plan saved' },\n workoutPlanDeleted: {\n title: 'Workout plan deleted',\n description: 'Members on this plan no longer have one.',\n },\n workoutCreated: { title: 'Workout created' },\n workoutUpdated: { title: 'Workout saved' },\n workoutDeleted: { title: 'Workout removed' },\n workoutPlanAssigned: { title: 'Workout plan assigned' },\n\n // Roster (owner console)\n attendanceMarked: { title: 'Attendance saved' },\n memberMarkedInactive: {\n title: 'Member marked inactive',\n description: 'They can no longer check in.',\n },\n memberMarkedActive: { title: 'Member reactivated' },\n memberDeleted: { title: 'Member removed' },\n /**\n * Says what actually happened. Assigning a plan no longer starts it —\n * nothing does until the payment is marked done — so a bare \"Plan\n * updated\" would send the owner away believing the member could train.\n */\n memberPlanAssigned: {\n title: 'Plan assigned',\n description: 'It starts once you mark the payment done.',\n },\n memberMembershipPaused: { title: 'Membership paused' },\n memberMembershipCancelled: { title: 'Membership cancelled' },\n paymentCollected: { title: 'Payment recorded' },\n\n // Sessions\n checkedIn: { title: 'Checked in', description: 'Your session has started.' },\n checkedOut: { title: 'Checked out', description: 'Session saved.' },\n checkInQrRotated: {\n title: 'New QR generated',\n description: 'Print it and replace the old poster.',\n },\n\n // Membership join requests\n membershipRequestSent: {\n title: 'Request sent',\n description: 'The gym will review it shortly.',\n },\n membershipRequestApproved: {\n title: 'Member approved',\n description: 'They can now check in at your gym.',\n },\n membershipRequestRejected: {\n title: 'Request declined',\n description: 'The member has been told.',\n },\n\n // Membership payment confirmation\n membershipRequested: {\n title: 'Plan requested',\n description: 'It starts once your gym confirms the payment.',\n },\n membershipRequestWithdrawn: {\n title: 'Request withdrawn',\n description: 'Pick another plan whenever you are ready.',\n },\n membershipPaymentConfirmed: {\n title: 'Payment confirmed',\n description: 'The plan is now active for this member.',\n },\n membershipPaymentRejected: {\n title: 'Payment turned down',\n description: 'The member has been told and can send it again.',\n },\n\n // Gym registration + platform review\n gymRegistered: {\n title: 'Gym submitted',\n description: 'Our team will review it and let you know.',\n },\n gymApproved: {\n title: 'Gym approved',\n description: 'It is now listed for members to find.',\n },\n gymRejected: {\n title: 'Gym rejected',\n description: 'The owner has been told.',\n },\n\n // Engagement\n feedbackSubmitted: {\n title: 'Feedback sent',\n description: 'Thank you for helping us improve.',\n },\n complaintSubmitted: {\n title: 'Complaint submitted',\n description: 'Your gym will look into it.',\n },\n announcementSent: { title: 'Announcement sent' },\n announcementScheduled: { title: 'Announcement scheduled' },\n announcementDraftSaved: { title: 'Draft saved' },\n announcementDeleted: { title: 'Announcement deleted' },\n announcementCancelled: {\n title: 'Announcement cancelled',\n description: 'It will not be sent.',\n },\n\n // Notification feed\n notificationHidden: { title: 'Notification hidden' },\n notificationRestored: { title: 'Notification restored' },\n allNotificationsRead: { title: 'All caught up' },\n\n // Security (role-switch PIN)\n mpinSet: { title: 'PIN set' },\n mpinReset: { title: 'Your PIN has been reset.' },\n mpinResetLinkSent: {\n title: \"We've emailed you a reset link\",\n description: 'Check your inbox.',\n },\n\n // ── Platform console (superadmin / platform manager) ────────────────────────\n platformRolesUpdated: { title: 'Platform roles updated' },\n platformUserSuspended: {\n title: 'Account suspended',\n description: 'They can no longer sign in.',\n },\n platformUserReinstated: { title: 'Account reinstated' },\n platformUserDeleted: {\n title: 'Account deleted',\n description: 'It is hidden everywhere but can still be restored.',\n },\n platformUserRestored: { title: 'Account restored' },\n platformGymDeleted: {\n title: 'Gym deleted',\n description: 'It is hidden everywhere but can still be restored.',\n },\n platformGymRestored: {\n title: 'Gym restored',\n description: 'It is back in the directory and its owner has the console again.',\n },\n} as const satisfies Record<string, ResultMessage>;\n\nexport type SuccessMessageKey = keyof typeof SUCCESS_MESSAGE;\n\n/** Success messages that name the thing they acted on. */\nexport const SUCCESS_MESSAGE_FOR = {\n memberAdded: (name: string): ResultMessage => ({\n title: 'Member added',\n description: `Share the invite with ${name}.`,\n }),\n staffAdded: (name: string): ResultMessage => ({\n title: 'Staff added',\n description: `Share the invite with ${name}.`,\n }),\n inviteClaimed: (gymName: string): ResultMessage => ({\n title: \"You're all set\",\n description: `Your account is linked to ${gymName}.`,\n }),\n roleSwitched: (roleLabel: string): ResultMessage => ({\n title: `Switched to ${roleLabel}`,\n }),\n gymApprovedFor: (gymName: string): ResultMessage => ({\n title: 'Gym approved',\n description: `${gymName} is now live on GymMonk.`,\n }),\n gymRejectedFor: (gymName: string): ResultMessage => ({\n title: 'Gym rejected',\n description: `The owner of ${gymName} has been told why.`,\n }),\n} as const;\n\n// ─── Error messages ──────────────────────────────────────────────────────────\n\n/**\n * Failure copy. Transport entries describe a request that never reached the\n * API; the rest are domain rules the backend enforces and the client can also\n * check up-front — same wording either way.\n */\nexport const ERROR_MESSAGE = {\n // Transport / generic\n generic: 'Something went wrong. Please try again.',\n network: 'Network error. Check your connection and try again.',\n timeout: 'That took too long. Please try again.',\n sessionExpired: 'Your session expired. Please sign in again.',\n\n // Client-side validation\n formInvalid: 'Please check the highlighted fields.',\n\n // Onboarding\n roleRequired: 'Choose a role to continue',\n ownerOnboardingFailed: 'We could not create your gym. Please try again.',\n memberOnboardingFailed: 'We could not save your details. Please try again.',\n\n // ── Gym registration (\"list your gym\") ─────────────────────────────────────\n gymRegistrationFailed: 'We could not register your gym. Please try again.',\n /**\n * The wizard could not read what the owner had already saved.\n *\n * It refuses to open on a blank form rather than risk the next Continue\n * overwriting a real draft, so this is the whole screen, not a toast.\n */\n gymRegistrationDraftLoadFailed:\n 'We could not open your saved gym details. Check your connection and try again.',\n /**\n * The step the wizard tried to save is implausibly large.\n *\n * A guard against a client sending something absurd rather than a limit the\n * real form approaches, but it is raised by the SERVER and shown verbatim in\n * the step-save toast — so the wording lives here with the rest of the copy,\n * not as a hand-written string at the throw site.\n */\n gymRegistrationDraftTooLarge:\n 'Those gym details are too large to save. Please shorten the descriptions and try again.',\n /**\n * \"Near me\" found nothing inside the radius.\n *\n * Deliberately not the same as `gymSearchNoResults`: this member searched\n * nowhere in particular, so telling them to refine their search would be\n * advice they cannot act on. What helps is a different place or a gym id.\n */\n gymSearchNoneNearby:\n 'No gyms within 50 km of you yet. Try a different location, or enter a gym id if you have one.',\n /** One organisation per account — registering a second is not supported. */\n gymAlreadyRegistered: 'You have already registered a gym on this account.',\n /** Registering a gym is something a MEMBER does, so the profile must exist. */\n gymRegistrationNeedsProfile: 'Finish setting up your profile before registering a gym.',\n /** Every owner action other than editing their own details, before approval. */\n gymAwaitingApproval:\n 'Your gym is waiting for approval. You can manage it once our team has reviewed it.',\n /** The reviewer turned the registration down. */\n gymRegistrationRejected: 'Your gym registration was not approved.',\n organisationNotFound: 'That gym could not be found.',\n\n // ── Platform console ────────────────────────────────────────────────────────\n /** The account is gone, or was already erased. */\n platformUserNotFound: 'That account could not be found.',\n /**\n * An admin acting on their OWN row. Suspending or deleting yourself takes away\n * the access needed to undo it, so the console refuses rather than letting one\n * mis-tap lock the platform's last superadmin out of it.\n */\n platformCannotActOnSelf: 'You cannot do that to your own account.',\n /**\n * Removing the last superadmin. `manage GymApproval` is held by nobody else,\n * so the gym review queue would become unreachable with no way back in short\n * of a database edit.\n */\n platformLastSuperadmin: 'The platform must keep at least one superadmin.',\n /** The typed confirmation did not match the record being deleted. */\n platformConfirmationMismatch: 'That did not match. Nothing was changed.',\n /** Two reviewers acting on the same registration; the second one loses. */\n organisationAlreadyDecided: 'Someone has already reviewed this gym.',\n /** A step failed to persist — the user stays put so nothing is lost. */\n onboardingStepSaveFailed: 'We could not save this step. Please try again.',\n\n // Gym structure prerequisites\n centerRequiredForPlan: 'Pick the center this plan belongs to.',\n planVariantRequired: 'Add at least one pricing variant before saving the plan.',\n planNameTaken: 'This center already has a plan with that name. Pick another name.',\n planVariantDurationRequired: 'Enter how long this variant runs for',\n planVariantPriceRequired: 'Enter the price for this variant',\n /** A \"discount\" that raises the price is a typo, every time. */\n planFinalPriceAboveOriginal: 'The final price cannot be more than the original price',\n centerAddressRequired: 'Add the center address before creating it.',\n noCenterForPlans: 'No gym center found. Add a center before creating plans.',\n noCenterForWorkouts: 'No gym center found. Add a center before creating workouts.',\n noCenterForAnnouncements: 'No gym center found. Add a center before sending announcements.',\n announcementAlreadySent: 'That announcement has already gone out, so it cannot be cancelled.',\n\n // Notification feed\n notificationHideFailed: 'Could not hide that notification.',\n notificationRestoreFailed: 'Could not restore that notification.',\n\n // Membership\n planUnavailable: 'That plan is not available right now.',\n membershipPlanRequired: 'Choose a membership plan to continue',\n /** The member was created, but the plan the owner picked did not stick. */\n initialPlanNotAssigned:\n 'Member added, but the plan could not be assigned. Assign it from their profile.',\n noPlansToAssign: 'No membership plans yet. Create one to assign it while adding a member.',\n /**\n * Not an error the member can act on by trying harder — it is the whole\n * point of the flow, so it reads as a status rather than a refusal.\n */\n membershipAwaitingPayment:\n 'Your gym is checking your payment. The plan starts as soon as they confirm it.',\n membershipNoPendingRequest: 'There is no plan waiting for confirmation.',\n /**\n * Pausing extends the term by the paused days, so pausing something that is\n * not running would push out a date nobody is training against — and pausing\n * an already-paused membership would extend it twice for one absence.\n */\n membershipNotPausable:\n 'Only a running membership can be paused. This one has already ended or is paused.',\n /**\n * The joining date records something that has already happened, so a future\n * one is not a preference to honour — it would date the member's whole\n * billing cycle from a day that has not arrived.\n */\n joiningDateInFuture: 'A joining date cannot be in the future.',\n /**\n * The term moved between the screen being drawn and the button being\n * pressed, which in practice means a second manager confirmed one first.\n * Says which month it now covers rather than a bare \"try again\", because the\n * owner's next question is always \"then what am I paying for?\".\n */\n membershipTermMoved:\n 'Someone just confirmed a payment for this member, so the period has moved on. Reopen their profile to see the current one.',\n /**\n * The owner reached for the roster toggle on a plan that has not been paid\n * for. Points at the action that WOULD start it rather than just refusing.\n */\n membershipPendingUseConfirm:\n 'This plan is waiting for payment confirmation. Mark the payment done to start it.',\n /** Two managers opening the same request; the second one loses. */\n membershipAlreadyDecided: 'Someone has already answered this payment.',\n membershipConfirmFailed: 'Could not confirm that payment. Please try again.',\n membershipRejectFailed: 'Could not turn down that payment. Please try again.',\n membershipWithdrawFailed: 'Could not withdraw that request. Please try again.',\n /** The URL did not come from our own storage — a broken or hostile client. */\n paymentProofInvalid: 'That payment screenshot could not be attached. Upload it again.',\n\n // Training\n workoutPlanRequired: 'Choose a plan to assign',\n // Roster (owner console)\n attendanceMarkFailed: 'Could not save that attendance. Please try again.',\n memberStatusFailed: \"Could not change that member's status. Please try again.\",\n memberDeleteFailed: 'Could not remove that member. Please try again.',\n memberPlanAssignFailed: 'Could not update that plan. Please try again.',\n memberPauseFailed: 'Could not pause that membership. Please try again.',\n memberCancelFailed: 'Could not cancel that membership. Please try again.',\n paymentCollectFailed: 'Could not record that payment. Please try again.',\n paymentAmountRequired: 'Enter the amount collected',\n pauseDatesRequired: 'Pick both pause dates',\n pauseDatesOrder: 'The end date has to be after the start date',\n /**\n * Attendance, workout plans and payments are all keyed on the central-auth\n * user, which an invited record does not have until the person claims it.\n */\n memberNotClaimed: 'This member has not claimed their invite yet.',\n\n // Sessions\n checkInFailed: 'Could not check you in. Please try again.',\n checkOutFailed: 'Could not check you out. Please try again.',\n /** The camera decoded a QR, but it is not a gymmonk check-in poster. */\n qrNotRecognised: 'That is not a gym check-in QR. Point at the poster at the gym.',\n /** A valid gymmonk QR, but for a gym the member does not belong to. */\n qrWrongGym: 'That QR belongs to a different gym.',\n /** The scanned code matches no center — a retired or mistyped poster. */\n qrUnknown: 'This QR is no longer valid. Ask the gym for the current poster.',\n /** Checking out with a poster from somewhere other than where you started. */\n qrWrongGymForCheckOut: 'Scan the QR at the gym where you checked in.',\n /** Manual entry only accepts a full, well-formed code. */\n checkInCodeInvalid: 'Enter the 16-character code printed under the QR.',\n checkInQrRotateFailed: 'Could not generate a new QR. Please try again.',\n /**\n * Ticking an exercise off records it against a live session, so there has to\n * be one. Shown when a member opens an exercise outside a workout and taps\n * Mark As Done.\n */\n noSessionToTrack: 'Check in at your gym to track this workout.',\n\n // ── Membership join requests ───────────────────────────────────────────────\n /** Scanned a valid poster, but the gym has not accepted this member yet. */\n membershipNotApproved:\n 'Your gym has not accepted your join request yet. You can check in once they do.',\n /** Accepted, but with no live plan — check-in needs both. */\n membershipNotActive: 'Your membership plan is not active. Renew it to start checking in again.',\n /** A second request while one is already open. */\n membershipRequestPending: 'You already have a request waiting with this gym.',\n /** Requesting a gym they are already an accepted member of. */\n membershipRequestAlreadyMember: 'You are already a member of this gym.',\n /** v1 memberships are single-center — leave the current gym first. */\n membershipRequestAlreadyAtAnotherGym:\n 'You already belong to a gym. Cancel that membership before joining another.',\n membershipRequestNotFound: 'That join request no longer exists.',\n /** Two staff acting on the same request; the second one loses. */\n membershipRequestAlreadyDecided: 'Someone has already answered this request.',\n membershipRequestFailed: 'Could not send your request. Please try again.',\n gymCodeUnknown: 'No gym found with that id. Check it with your gym.',\n /** Actions that only make sense once a gym has taken the member on. */\n noGymJoined: 'Join a gym first. You can find one from your Me tab.',\n /** Searching by name needs somewhere to look — see `gymSearchQuerySchema`. */\n gymSearchStateRequired: 'Pick a state to search gyms by name.',\n gymSearchNoResults: 'No gyms matched. Try a different area, or enter the gym id.',\n\n // Camera (QR scanning)\n cameraDenied: 'Camera access is blocked. Allow it in your browser settings to scan.',\n cameraUnavailable: 'No camera found on this device. Enter the code printed under the QR.',\n cameraInsecure: 'Scanning needs a secure (https) connection.',\n cameraFailed: 'Could not start the camera. Enter the code printed under the QR.',\n\n // Engagement\n feedbackRatingRequired: 'Choose a rating to submit your feedback',\n complaintIssueRequired: 'Choose the issue you are reporting',\n\n // Security (role-switch PIN)\n mpinIncorrect: 'Incorrect PIN.',\n mpinVerifyFailed: \"Couldn't verify. Try again.\",\n mpinSetFailed: \"Couldn't set your PIN. Try again.\",\n mpinMismatch: \"Those didn't match. Try again.\",\n mpinResetLinkFailed: \"Couldn't send the reset link. Try again.\",\n mpinResetFailed: \"Couldn't reset your PIN. Try again.\",\n\n // Uploads\n imageUploadFailed: 'Could not upload that image. Please try again.',\n imageProcessFailed: 'Could not process that image. Please try again.',\n} as const satisfies Record<string, string>;\n\nexport type ErrorMessageKey = keyof typeof ERROR_MESSAGE;\n\n/** Error messages that name a limit or value. */\nexport const ERROR_MESSAGE_FOR = {\n maxPhotos: (max: number): string => `You can add up to ${max} photos`,\n /**\n * Refused at the door because the membership is paused and still inside its\n * window.\n *\n * Its own sentence because \"renew your plan\" is wrong advice here twice\n * over: nothing has lapsed, and paying again would not open the door a day\n * sooner. The resume date is the only thing this member wants to know, and\n * it is the reason this one takes a parameter at all.\n */\n membershipPausedUntil: (resumesOn: string): string =>\n `Your membership is paused until ${resumesOn}. You can check in again from that day.`,\n /** Role-switch PIN lockout, in whole minutes remaining. */\n mpinLockedOut: (minutes: number): string =>\n `Too many attempts. Try again in ${minutes} minute${minutes > 1 ? 's' : ''}.`,\n} as const;\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gymmonk-schema",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.45.0",
|
|
4
4
|
"description": "Shared Zod schemas, enums and domain types for GymMonk (fitness SaaS) — single source of truth (SSOT) consumed by gymmonk-backend and gymmonk-web-client.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|