gymmonk-schema 0.46.0 → 0.46.2
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/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/membership-status.d.ts +23 -3
- package/dist/membership-status.d.ts.map +1 -1
- package/dist/membership-status.js +24 -4
- package/dist/membership-status.js.map +1 -1
- package/dist/membership.d.ts +0 -24
- package/dist/membership.d.ts.map +1 -1
- package/dist/membership.js +1 -26
- package/dist/membership.js.map +1 -1
- package/dist/onboarding-form.js.map +1 -1
- package/dist/onboarding.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -28,9 +28,9 @@ export * from './location.js';
|
|
|
28
28
|
export * from './member.js';
|
|
29
29
|
export * from './member-profile.js';
|
|
30
30
|
export * from './membership.js';
|
|
31
|
-
export * from './membership-status.js';
|
|
32
31
|
export * from './membership-plan.js';
|
|
33
32
|
export * from './membership-request.js';
|
|
33
|
+
export * from './membership-status.js';
|
|
34
34
|
export * from './messages.js';
|
|
35
35
|
export * from './notification.js';
|
|
36
36
|
export * from './onboarding.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAElC,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAE5B,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAE9B,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAE/B,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAE9B,cAAc,WAAW,CAAC;AAE1B,cAAc,uBAAuB,CAAC;AACtC,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAE9B,cAAc,eAAe,CAAC;AAE9B,cAAc,aAAa,CAAC;AAC5B,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAElC,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAE5B,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAE9B,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAE/B,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAE9B,cAAc,WAAW,CAAC;AAE1B,cAAc,uBAAuB,CAAC;AACtC,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAE9B,cAAc,eAAe,CAAC;AAE9B,cAAc,aAAa,CAAC;AAC5B,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAEhC,cAAc,sBAAsB,CAAC;AACrC,cAAc,yBAAyB,CAAC;AACxC,cAAc,wBAAwB,CAAC;AAEvC,cAAc,eAAe,CAAC;AAE9B,cAAc,mBAAmB,CAAC;AAClC,cAAc,iBAAiB,CAAC;AAEhC,cAAc,sBAAsB,CAAC;AAErC,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AAEnC,cAAc,qBAAqB,CAAC;AACpC,cAAc,yBAAyB,CAAC;AACxC,cAAc,kBAAkB,CAAC;AACjC,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAE7B,cAAc,wBAAwB,CAAC;AACvC,cAAc,mBAAmB,CAAC;AAClC,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,mBAAmB,CAAC;AAClC,cAAc,WAAW,CAAC;AAC1B,cAAc,mBAAmB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -37,10 +37,10 @@ export * from './location.js';
|
|
|
37
37
|
export * from './member.js';
|
|
38
38
|
export * from './member-profile.js';
|
|
39
39
|
export * from './membership.js';
|
|
40
|
-
export * from './membership-status.js';
|
|
41
40
|
// ─── Membership ──────────────────────────────────────────────────────────────
|
|
42
41
|
export * from './membership-plan.js';
|
|
43
42
|
export * from './membership-request.js';
|
|
43
|
+
export * from './membership-status.js';
|
|
44
44
|
// ─── API result messages + error codes ───────────────────────────────────────
|
|
45
45
|
export * from './messages.js';
|
|
46
46
|
// ─── Engagement & analytics ──────────────────────────────────────────────────
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,gFAAgF;AAChF,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,6EAA6E;AAC7E,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,gFAAgF;AAChF,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAC9B,gFAAgF;AAChF,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAC/B,gFAAgF;AAChF,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,gFAAgF;AAChF,cAAc,WAAW,CAAC;AAC1B,gFAAgF;AAChF,cAAc,uBAAuB,CAAC;AACtC,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAC9B,gFAAgF;AAChF,cAAc,eAAe,CAAC;AAC9B,gFAAgF;AAChF,cAAc,aAAa,CAAC;AAC5B,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAChC,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,gFAAgF;AAChF,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,6EAA6E;AAC7E,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,gFAAgF;AAChF,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAC9B,gFAAgF;AAChF,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAC/B,gFAAgF;AAChF,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,gFAAgF;AAChF,cAAc,WAAW,CAAC;AAC1B,gFAAgF;AAChF,cAAc,uBAAuB,CAAC;AACtC,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAC9B,gFAAgF;AAChF,cAAc,eAAe,CAAC;AAC9B,gFAAgF;AAChF,cAAc,aAAa,CAAC;AAC5B,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAChC,gFAAgF;AAChF,cAAc,sBAAsB,CAAC;AACrC,cAAc,yBAAyB,CAAC;AACxC,cAAc,wBAAwB,CAAC;AACvC,gFAAgF;AAChF,cAAc,eAAe,CAAC;AAC9B,gFAAgF;AAChF,cAAc,mBAAmB,CAAC;AAClC,cAAc,iBAAiB,CAAC;AAChC,gFAAgF;AAChF,cAAc,sBAAsB,CAAC;AACrC,gFAAgF;AAChF,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,gFAAgF;AAChF,cAAc,qBAAqB,CAAC;AACpC,cAAc,yBAAyB,CAAC;AACxC,cAAc,kBAAkB,CAAC;AACjC,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,gFAAgF;AAChF,cAAc,wBAAwB,CAAC;AACvC,cAAc,mBAAmB,CAAC;AAClC,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,mBAAmB,CAAC;AAClC,cAAc,WAAW,CAAC;AAC1B,cAAc,mBAAmB,CAAC","sourcesContent":["/**\n * gymmonk-schema — Public API\n * ===========================\n * Shared Zod schemas, enums and domain types for GymMonk (fitness SaaS).\n * Single source of truth (SSOT) for data shapes, TypeScript types and\n * validation, consumed by both gymmonk-backend and gymmonk-web-client.\n *\n * Every domain module is re-exported through this barrel. Import names are\n * globally unique across modules, so `export *` composes without collisions.\n */\n\n// ─── Owner desk-entry FORM validation (client-side working shapes) ───────────\nexport * from './add-person-form.js';\nexport * from './announcement.js';\n// ─── Location master data (the address picker's lookup API) ───────────────\nexport * from './area.js';\nexport * from './center.js';\n// ─── Check-in QR (poster payload shared by owner, member and backend) ────────\nexport * from './chat.js';\nexport * from './check-in.js';\n// ─── Foundation ──────────────────────────────────────────────────────────────\nexport * from './codes.js';\nexport * from './common.js';\nexport * from './enrolment.js';\nexport * from './equipment.js';\n// ─── Training ────────────────────────────────────────────────────────────────\nexport * from './exercise.js';\nexport * from './feedback.js';\n// ─── Uploads ─────────────────────────────────────────────────────────────────\nexport * from './file.js';\n// ─── \"List your gym\" — a member registers the business they run ──────────────\nexport * from './gym-registration.js';\nexport * from './home.js';\nexport * from './insights.js';\n// ─── Where a place physically is (Maps link, pin, check-in position) ─────────\nexport * from './location.js';\n// ─── People ──────────────────────────────────────────────────────────────────\nexport * from './member.js';\nexport * from './member-profile.js';\nexport * from './membership.js';\n// ─── Membership ──────────────────────────────────────────────────────────────\nexport * from './membership-plan.js';\nexport * from './membership-request.js';\nexport * from './membership-status.js';\n// ─── API result messages + error codes ───────────────────────────────────────\nexport * from './messages.js';\n// ─── Engagement & analytics ──────────────────────────────────────────────────\nexport * from './notification.js';\nexport * from './onboarding.js';\n// ─── Onboarding wizard FORM validation (client-side working shapes) ──────────\nexport * from './onboarding-form.js';\n// ─── Gym structure & onboarding ──────────────────────────────────────────────\nexport * from './organisation.js';\nexport * from './owner-profile.js';\n// ─── Platform console (people + gyms directories, role keys) ─────────────────\nexport * from './plan-abilities.js';\nexport * from './plan-features-data.js';\nexport * from './plan-limits.js';\nexport * from './platform.js';\nexport * from './pricing.js';\n// ─── Member \"edit my profile\" FORM validation ────────────────────────────────\nexport * from './profile-edit-form.js';\nexport * from './saas-catalog.js';\nexport * from './session.js';\nexport * from './shared.js';\nexport * from './social.js';\nexport * from './staff.js';\nexport * from './subscription.js';\nexport * from './user.js';\nexport * from './workout-plan.js';\n"]}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* ============================================================
|
|
4
4
|
* Everything the product knows about "where does this member stand" lives
|
|
5
5
|
* here: the stored lifecycle, the effective one, the words shown on screen,
|
|
6
|
-
* the money reading, and the
|
|
6
|
+
* the money reading, and the three thresholds that decide "soon".
|
|
7
7
|
*
|
|
8
8
|
* ── Why this module exists ──────────────────────────────────────────────────
|
|
9
9
|
* These facts were spread across five files in two repos. The stored enum and
|
|
@@ -47,8 +47,28 @@ import { z } from 'zod';
|
|
|
47
47
|
/**
|
|
48
48
|
* The STORED lifecycle column.
|
|
49
49
|
*
|
|
50
|
-
* `
|
|
51
|
-
*
|
|
50
|
+
* `pending` is where every membership STARTS, whoever created it. Choosing a
|
|
51
|
+
* plan is not the same event as paying for one, and the two used to be welded
|
|
52
|
+
* together: the moment a member tapped a plan it went live, and the moment an
|
|
53
|
+
* owner assigned one at the desk it went live, whether or not any money had
|
|
54
|
+
* changed hands. A gym then had no way to tell a paid member from an optimistic
|
|
55
|
+
* one, and no lever to withhold access from the second.
|
|
56
|
+
*
|
|
57
|
+
* The only transition out of `pending` is the gym marking the payment done.
|
|
58
|
+
* That is deliberately a HUMAN act by the owner or a manager rather than a
|
|
59
|
+
* gateway callback: these gyms take cash at the desk and UPI transfers on a
|
|
60
|
+
* phone, so the person who knows the money arrived is the person at the gym.
|
|
61
|
+
*
|
|
62
|
+
* `rejected` is the other way out, for a screenshot that shows nothing, the
|
|
63
|
+
* wrong amount, or a transfer that never landed. It is a dead end — the member
|
|
64
|
+
* requests again rather than the gym reopening a closed decision.
|
|
65
|
+
*
|
|
66
|
+
* `invited` predates all of this and nothing writes it. Left in the union
|
|
67
|
+
* because removing a value from a stored enum is not worth a migration, and old
|
|
68
|
+
* rows still have to parse.
|
|
69
|
+
*
|
|
70
|
+
* `expiring` is NOT here — it is derived from `endDate` at the edge. See
|
|
71
|
+
* `DisplayMembershipStatus`.
|
|
52
72
|
*/
|
|
53
73
|
export declare const membershipStatusSchema: z.ZodEnum<{
|
|
54
74
|
invited: "invited";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"membership-status.d.ts","sourceRoot":"","sources":["../src/membership-status.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAMxB
|
|
1
|
+
{"version":3,"file":"membership-status.d.ts","sourceRoot":"","sources":["../src/membership-status.ts"],"names":[],"mappings":"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"}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* ============================================================
|
|
4
4
|
* Everything the product knows about "where does this member stand" lives
|
|
5
5
|
* here: the stored lifecycle, the effective one, the words shown on screen,
|
|
6
|
-
* the money reading, and the
|
|
6
|
+
* the money reading, and the three thresholds that decide "soon".
|
|
7
7
|
*
|
|
8
8
|
* ── Why this module exists ──────────────────────────────────────────────────
|
|
9
9
|
* These facts were spread across five files in two repos. The stored enum and
|
|
@@ -50,8 +50,28 @@ import { z } from 'zod';
|
|
|
50
50
|
/**
|
|
51
51
|
* The STORED lifecycle column.
|
|
52
52
|
*
|
|
53
|
-
* `
|
|
54
|
-
*
|
|
53
|
+
* `pending` is where every membership STARTS, whoever created it. Choosing a
|
|
54
|
+
* plan is not the same event as paying for one, and the two used to be welded
|
|
55
|
+
* together: the moment a member tapped a plan it went live, and the moment an
|
|
56
|
+
* owner assigned one at the desk it went live, whether or not any money had
|
|
57
|
+
* changed hands. A gym then had no way to tell a paid member from an optimistic
|
|
58
|
+
* one, and no lever to withhold access from the second.
|
|
59
|
+
*
|
|
60
|
+
* The only transition out of `pending` is the gym marking the payment done.
|
|
61
|
+
* That is deliberately a HUMAN act by the owner or a manager rather than a
|
|
62
|
+
* gateway callback: these gyms take cash at the desk and UPI transfers on a
|
|
63
|
+
* phone, so the person who knows the money arrived is the person at the gym.
|
|
64
|
+
*
|
|
65
|
+
* `rejected` is the other way out, for a screenshot that shows nothing, the
|
|
66
|
+
* wrong amount, or a transfer that never landed. It is a dead end — the member
|
|
67
|
+
* requests again rather than the gym reopening a closed decision.
|
|
68
|
+
*
|
|
69
|
+
* `invited` predates all of this and nothing writes it. Left in the union
|
|
70
|
+
* because removing a value from a stored enum is not worth a migration, and old
|
|
71
|
+
* rows still have to parse.
|
|
72
|
+
*
|
|
73
|
+
* `expiring` is NOT here — it is derived from `endDate` at the edge. See
|
|
74
|
+
* `DisplayMembershipStatus`.
|
|
55
75
|
*/
|
|
56
76
|
export const membershipStatusSchema = z.enum([
|
|
57
77
|
'pending',
|
|
@@ -311,7 +331,7 @@ export const FEE_STATUS_LABEL = {
|
|
|
311
331
|
rejected: () => 'Payment declined',
|
|
312
332
|
pending: () => 'Confirm payment',
|
|
313
333
|
lapsed: () => 'Renewal due',
|
|
314
|
-
'due-soon': (r) =>
|
|
334
|
+
'due-soon': (r) => r.daysLeft === 0 ? 'Due today' : `Due in ${plural(r.daysLeft ?? 0, 'day')}`,
|
|
315
335
|
'no-plan': () => 'No plan yet',
|
|
316
336
|
cancelled: () => 'Cancelled',
|
|
317
337
|
paid: () => 'Paid',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"membership-status.js","sourceRoot":"","sources":["../src/membership-status.ts"],"names":[],"mappings":"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","sourcesContent":["/**\r\n * gymmonk-schema — Membership status, all of it, in one place\r\n * ============================================================\r\n * Everything the product knows about \"where does this member stand\" lives\r\n * here: the stored lifecycle, the effective one, the words shown on screen,\r\n * the money reading, and the two thresholds that decide \"soon\".\r\n *\r\n * ── Why this module exists ──────────────────────────────────────────────────\r\n * These facts were spread across five files in two repos. The stored enum and\r\n * the effective rule were in `membership.ts`; the DISPLAY vocabulary and its\r\n * derivation were in the web client's `shared/lib/format.ts`, out of the\r\n * backend's reach; the labels were inline in a pill component; the fee\r\n * vocabulary was in an owner-console file and took a web VIEW TYPE as its\r\n * input, so nothing else could call it. Two of the web's own feature folders\r\n * had also declared `MembershipStatus` as an alias of the DISPLAY type — the\r\n * same name as the stored one, meaning something different, in the same\r\n * codebase.\r\n *\r\n * ── There are TWO axes, and collapsing them would lose information ──────────\r\n * They are correlated, not identical, and they answer different questions:\r\n *\r\n * LIFECYCLE can this person train today? (the door asks this)\r\n * FEES does this person owe money? (the owner's roster asks this)\r\n *\r\n * A member part-way through a term they have half-paid is `active` on the\r\n * first axis and `unpaid` on the second, and gyms take part payments every\r\n * week. One flat enum would have to pick a side.\r\n *\r\n * The axes relate like this, and `readFeeStatus` encodes exactly this table:\r\n *\r\n * lifecycle money owed → fee status\r\n * active no → paid (or due-soon near the end of the term)\r\n * active yes → unpaid\r\n * expired yes → overdue\r\n * expired no → lapsed\r\n * pending n/a → pending (the GYM owes the next move)\r\n * none n/a → no-plan (the OWNER owes the next move)\r\n *\r\n * ── What is NOT here ────────────────────────────────────────────────────────\r\n * Colours. A tone is a Tailwind class, it belongs to whichever client is\r\n * painting, and there is exactly one consumer of each. The words live here\r\n * because two surfaces must never call the same state different things.\r\n *\r\n * @module gymmonk-schema/membership-status\r\n */\r\n\r\nimport { z } from 'zod';\r\n\r\n// ═══════════════════════════════════════════════════════════════════════════\r\n// AXIS 1 — LIFECYCLE: can this member train?\r\n// ═══════════════════════════════════════════════════════════════════════════\r\n\r\n/**\r\n * The STORED lifecycle column.\r\n *\r\n * `invited` is legacy and no longer written by anything; it is kept in the\r\n * union so old rows still parse.\r\n */\r\nexport const membershipStatusSchema = z.enum([\r\n 'pending',\r\n 'rejected',\r\n 'invited',\r\n 'active',\r\n 'paused',\r\n 'expired',\r\n 'cancelled',\r\n]);\r\nexport type MembershipStatus = z.infer<typeof membershipStatusSchema>;\r\n\r\n/** The fields any status question needs. Deliberately not a full membership. */\r\nexport interface MembershipStatusInput {\r\n status: MembershipStatus;\r\n endDate: string;\r\n pausedFrom?: string | null;\r\n pausedTo?: string | null;\r\n}\r\n\r\n/**\r\n * What a membership IS right now, as against what was last written to its\r\n * `status` column.\r\n *\r\n * ── Why this has to exist ───────────────────────────────────────────────────\r\n * Two facts about this system make the stored status untrustworthy on its own:\r\n *\r\n * 1. NOTHING sweeps memberships to `expired`. There is no job. A member who\r\n * stopped paying in March keeps `status: 'active'` in the database all\r\n * year, so the term's END DATE is the only honest answer.\r\n * 2. NOTHING resumes a pause. `pausedTo` was written and then never read by\r\n * any code path, so a two-week pause locked a member out permanently and\r\n * the only way back was an owner noticing and flipping the roster switch\r\n * by hand.\r\n *\r\n * ── What is NOT re-derived ──────────────────────────────────────────────────\r\n * `pending`, `rejected`, `cancelled` and `expired` are all decisions a PERSON\r\n * made, and are returned untouched. `expired` matters most: an owner\r\n * deactivating a member sets exactly that, and re-deriving it back to `active`\r\n * because the term still has days left would silently overrule them.\r\n *\r\n * `today` is passed in rather than read from the clock, because the answer is\r\n * the GYM's day — see `gymDayOf` on the server and `istDayOf` on the client.\r\n * `YYYY-MM-DD` compares lexicographically, so these are exact calendar-day\r\n * comparisons with no instant arithmetic and no zone hidden inside.\r\n */\r\nexport function effectiveMembershipStatus(\r\n membership: MembershipStatusInput,\r\n today: string,\r\n): MembershipStatus {\r\n const { status, endDate, pausedFrom, pausedTo } = membership;\r\n\r\n // Decided by a person. Never second-guessed.\r\n if (\r\n status === 'pending' ||\r\n status === 'rejected' ||\r\n status === 'cancelled' ||\r\n status === 'expired' ||\r\n status === 'invited'\r\n ) {\r\n return status;\r\n }\r\n\r\n /**\r\n * A pause is a WINDOW, not a switch somebody has to remember to turn off.\r\n *\r\n * Reading it as a window is what makes the resume happen without a scheduled\r\n * job: before it, the plan runs; inside it, the member is out; after it, they\r\n * are simply back, on the end date the pause already pushed out for them.\r\n * A pause with no end recorded is treated as still running, because the safe\r\n * failure for a missing bound is to keep the member out rather than to let\r\n * an unbounded pause silently expire.\r\n */\r\n if (status === 'paused') {\r\n if (!pausedTo) return 'paused';\r\n const startedYet = !pausedFrom || today >= pausedFrom;\r\n if (startedYet && today <= pausedTo) return 'paused';\r\n }\r\n\r\n return endDate >= today ? 'active' : 'expired';\r\n}\r\n\r\n/** Can this member train today? The one question the door asks. */\r\nexport function isMembershipLive(membership: MembershipStatusInput, today: string): boolean {\r\n return effectiveMembershipStatus(membership, today) === 'active';\r\n}\r\n\r\n// ─── Lifecycle, as the UI says it ───────────────────────────────────────────\r\n\r\n/**\r\n * How close to `endDate` a live membership starts reading as \"expiring\".\r\n *\r\n * A PRESENTATION band and nothing more: the door never sees `expiring`, and a\r\n * member inside this window is fully `active` as far as access is concerned.\r\n * Distinct from `FEE_DUE_SOON_DAYS`, which is a much tighter window because it\r\n * gates a payment reminder rather than a colour.\r\n */\r\nexport const MEMBERSHIP_EXPIRING_WINDOW_DAYS = 30;\r\n\r\n/**\r\n * How far ahead the owner's home ALERT looks for memberships about to lapse.\r\n *\r\n * Deliberately much tighter than the pill's band, and the reason all three\r\n * windows are declared together here rather than as a `const` in whichever\r\n * file happened to need one:\r\n *\r\n * MEMBERSHIP_EXPIRING_WINDOW_DAYS 30 paints a pill amber; the roster's\r\n * \"Expiring Soon\" segment and filter\r\n * MEMBERSHIP_EXPIRY_ALERT_DAYS 7 the one-line banner on owner home,\r\n * which says \"expiring this week\"\r\n * FEE_DUE_SOON_DAYS 3 arms a payment reminder\r\n *\r\n * They are different NUMBERS because they drive different actions — a colour,\r\n * a banner and a message to a member should not all fire at once — but they\r\n * are the same QUESTION, so they belong side by side where the next person to\r\n * change one can see the other two.\r\n */\r\nexport const MEMBERSHIP_EXPIRY_ALERT_DAYS = 7;\r\n\r\n/**\r\n * The lifecycle states a SCREEN distinguishes.\r\n *\r\n * `cancelled` and `invited` are absent on purpose — both read as `expired` to\r\n * a viewer, and a pill that said \"Cancelled\" next to one that said \"Expired\"\r\n * would imply a difference the member cannot act on differently.\r\n */\r\nexport type DisplayMembershipStatus =\r\n | 'pending'\r\n | 'rejected'\r\n | 'active'\r\n | 'expiring'\r\n | 'paused'\r\n | 'expired';\r\n\r\n/**\r\n * The lifecycle, narrowed onto what a screen shows.\r\n *\r\n * Adds exactly one thing over `effectiveMembershipStatus`: the `expiring`\r\n * warning band. Everything else is a straight mapping, so a screen and the\r\n * door can never disagree about whether somebody is live — only about how\r\n * loudly to say it.\r\n */\r\nexport function displayMembershipStatus(\r\n membership: MembershipStatusInput,\r\n today: string,\r\n): DisplayMembershipStatus {\r\n const effective = effectiveMembershipStatus(membership, today);\r\n\r\n switch (effective) {\r\n case 'pending':\r\n return 'pending';\r\n case 'rejected':\r\n return 'rejected';\r\n case 'paused':\r\n return 'paused';\r\n case 'expired':\r\n case 'cancelled':\r\n return 'expired';\r\n default: {\r\n // Live. The only question left is how close the end is.\r\n const left = daysBetween(today, membership.endDate);\r\n return left <= MEMBERSHIP_EXPIRING_WINDOW_DAYS ? 'expiring' : 'active';\r\n }\r\n }\r\n}\r\n\r\n/**\r\n * Is this display state one where the member can still train?\r\n *\r\n * `expiring` is LIVE. It is a warning band, not a lifecycle state — the term\r\n * has days left and the door lets them in — so anything filtering or counting\r\n * \"active members\" has to include it. Written down here because that is\r\n * exactly the kind of rule each list would otherwise re-derive slightly\r\n * differently, and one of them would forget `expiring` and quietly hide every\r\n * member in their last month.\r\n */\r\nexport function isLiveDisplayStatus(status: DisplayMembershipStatus): boolean {\r\n return status === 'active' || status === 'expiring';\r\n}\r\n\r\n/**\r\n * The words on the lifecycle pill.\r\n *\r\n * Here rather than in the component because two surfaces calling the same\r\n * state different things is the failure this module exists to prevent.\r\n * \"Awaiting payment\" is deliberately not \"Pending\": pending is what the\r\n * database calls it, and the member wants to know who they are waiting on.\r\n */\r\nexport const MEMBERSHIP_STATUS_LABEL: Record<DisplayMembershipStatus, string> = {\r\n pending: 'Awaiting payment',\r\n rejected: 'Not confirmed',\r\n active: 'Active',\r\n expiring: 'Expiring',\r\n paused: 'Paused',\r\n expired: 'Expired',\r\n};\r\n\r\n// ═══════════════════════════════════════════════════════════════════════════\r\n// AXIS 2 — FEES: does this member owe money?\r\n// ═══════════════════════════════════════════════════════════════════════════\r\n\r\n/** How close to the end of a term a member starts reading as \"due soon\". */\r\nexport const FEE_DUE_SOON_DAYS = 3;\r\n\r\n/**\r\n * Where a member stands on money, as one phrase an owner can scan down a list.\r\n *\r\n * Every state exists because it needs a DIFFERENT action. This began life as\r\n * \"paid or not\", which collapsed five unrelated situations into one green\r\n * chip: a member with no plan, a member whose payment was waiting on the GYM,\r\n * and a member who had genuinely settled all read \"Paid\", so the roster asked\r\n * the owner to do nothing about two people who needed something done.\r\n *\r\n * overdue money owed, term already ended → chase them\r\n * unpaid money owed, term still running → chase them\r\n * rejected the gym turned their payment down → talk to them\r\n * pending they have paid, the GYM must confirm → owner confirms\r\n * lapsed term ended, nothing sold to replace → chase a renewal\r\n * due-soon ends within a few days → chase a renewal\r\n * no-plan never been sold a membership → owner assigns one\r\n * cancelled they ended it and left → nothing to collect\r\n * paid settled, with time left on the term → nothing\r\n */\r\nexport type FeeStatus =\r\n | 'overdue'\r\n | 'unpaid'\r\n | 'rejected'\r\n | 'pending'\r\n | 'lapsed'\r\n | 'due-soon'\r\n | 'no-plan'\r\n | 'cancelled'\r\n | 'paid';\r\n\r\n/**\r\n * What the fee question needs, as plain facts.\r\n *\r\n * Deliberately NOT a view model. This used to take the owner console's roster\r\n * card type, which is why nothing else in the product could ask the question —\r\n * including the backend, which computes two of these fields in the first place.\r\n */\r\nexport interface FeeStatusInput {\r\n /** The stored lifecycle, or null when no membership was ever sold. */\r\n membershipStatus: MembershipStatus | null;\r\n /** The server's own judgement: money outstanding AND the term already ended. */\r\n overdueDays?: number | null;\r\n /** Still owed on the current term, in whole rupees. */\r\n balanceDueInr: number;\r\n /** End of the current term, `YYYY-MM-DD`, or null when there is none. */\r\n expiresOn?: string | null;\r\n}\r\n\r\nexport interface FeeReading {\r\n status: FeeStatus;\r\n /**\r\n * Does this need the owner's eye? Drives the chip's colour.\r\n *\r\n * Deliberately NOT the same question as `chase`. A member waiting on the gym\r\n * to confirm their payment needs attention and must not be sent a reminder —\r\n * they have already paid, and the outstanding job is the owner's.\r\n */\r\n attention: boolean;\r\n /**\r\n * Is there something to remind this member ABOUT?\r\n *\r\n * Gates the reminder action. False wherever the next move belongs to the gym\r\n * rather than the member, which is what stops an owner messaging somebody\r\n * \"your fees are overdue\" while their payment sits in the gym's own\r\n * confirmation queue.\r\n */\r\n chase: boolean;\r\n /** Whole days until the term ends; negative once past, null when no term. */\r\n daysLeft: number | null;\r\n /** Days past the end of an unpaid term, when that is what happened. */\r\n overdueDays: number | null;\r\n /** Still owed, in whole rupees. Zero unless the status says otherwise. */\r\n amountDueInr: number;\r\n}\r\n\r\n/**\r\n * The fee reading for one member.\r\n *\r\n * Ordered by whose move it is and how hard the fact is. `overdueDays` is the\r\n * server's own judgement — money outstanding AND the term already ended — so\r\n * it is trusted rather than recomputed here.\r\n */\r\nexport function readFeeStatus(input: FeeStatusInput, today: string): FeeReading {\r\n const { membershipStatus, balanceDueInr } = input;\r\n const overdueDays = input.overdueDays ?? null;\r\n const base = { daysLeft: null, overdueDays: null, amountDueInr: 0 } as const;\r\n\r\n /**\r\n * Never sold a membership. Checked FIRST, because every money question below\r\n * presumes one exists — and a member with no plan cannot be behind on a fee\r\n * nobody has charged them.\r\n */\r\n if (membershipStatus === null) {\r\n return { ...base, status: 'no-plan', attention: true, chase: false };\r\n }\r\n\r\n /**\r\n * Waiting on the GYM, not on the member.\r\n *\r\n * Ahead of the balance check on purpose: a pending membership carries its\r\n * full price as outstanding, so this member would otherwise read as owing\r\n * money and be sent a payment reminder — for money they have already handed\r\n * over and the gym has simply not confirmed yet.\r\n */\r\n if (membershipStatus === 'pending') {\r\n return { ...base, status: 'pending', attention: true, chase: false };\r\n }\r\n\r\n /** The gym looked at their payment and turned it down. */\r\n if (membershipStatus === 'rejected') {\r\n return { ...base, status: 'rejected', attention: true, chase: true };\r\n }\r\n\r\n /**\r\n * They ended it themselves and left.\r\n *\r\n * Checked before the date logic, which would otherwise see a term end in the\r\n * past and read them as \"Renewal due\" — with a reminder ready to tell\r\n * somebody who deliberately quit that their membership has expired.\r\n */\r\n if (membershipStatus === 'cancelled') {\r\n return { ...base, status: 'cancelled', attention: false, chase: false };\r\n }\r\n\r\n // Late: the server has already established both halves of that.\r\n if (overdueDays && overdueDays > 0) {\r\n return {\r\n ...base,\r\n status: 'overdue',\r\n attention: true,\r\n chase: true,\r\n overdueDays,\r\n amountDueInr: balanceDueInr,\r\n };\r\n }\r\n\r\n /**\r\n * Owes money, but the term is still running.\r\n *\r\n * A real state and NOT the same as overdue: a gym that takes half now and\r\n * half on Friday has members here every week, and calling them late would be\r\n * wrong.\r\n */\r\n if (balanceDueInr > 0) {\r\n return { ...base, status: 'unpaid', attention: true, chase: true, amountDueInr: balanceDueInr };\r\n }\r\n\r\n const daysLeft = input.expiresOn ? daysBetween(today, input.expiresOn) : null;\r\n\r\n /**\r\n * The term ran out and nothing was sold to replace it.\r\n *\r\n * Nothing is OUTSTANDING — a term nobody assigned was never invoiced — so\r\n * this used to fall through and read \"Paid\", in green, for the member most\r\n * worth chasing on the whole roster.\r\n */\r\n if (daysLeft !== null && daysLeft < 0) {\r\n return { ...base, status: 'lapsed', attention: true, chase: true, daysLeft };\r\n }\r\n\r\n if (daysLeft !== null && daysLeft >= 0 && daysLeft <= FEE_DUE_SOON_DAYS) {\r\n return { ...base, status: 'due-soon', attention: true, chase: true, daysLeft };\r\n }\r\n\r\n /**\r\n * Settled, with time left on the term. The only genuinely quiet state, and\r\n * the only one that earns a green chip.\r\n */\r\n return { ...base, status: 'paid', attention: false, chase: false, daysLeft };\r\n}\r\n\r\n/**\r\n * The words on the fee chip.\r\n *\r\n * Functions rather than plain strings because three of these states have to\r\n * name a number, and the AMOUNT is formatted by whoever is rendering — a\r\n * client knows how to write ₹1,200 for its locale and this package does not.\r\n * The wording still lives here, so no surface can invent its own phrasing for\r\n * a state another surface already names.\r\n */\r\nexport const FEE_STATUS_LABEL: Record<FeeStatus, (reading: FeeReading, amount: string) => string> =\r\n {\r\n overdue: (r, amount) => `${amount} overdue ${plural(r.overdueDays ?? 0, 'day')}`,\r\n unpaid: (_r, amount) => `${amount} due`,\r\n rejected: () => 'Payment declined',\r\n pending: () => 'Confirm payment',\r\n lapsed: () => 'Renewal due',\r\n 'due-soon': (r) => (r.daysLeft === 0 ? 'Due today' : `Due in ${plural(r.daysLeft ?? 0, 'day')}`),\r\n 'no-plan': () => 'No plan yet',\r\n cancelled: () => 'Cancelled',\r\n paid: () => 'Paid',\r\n };\r\n\r\n// ═══════════════════════════════════════════════════════════════════════════\r\n// Shared helpers\r\n// ═══════════════════════════════════════════════════════════════════════════\r\n\r\n/**\r\n * Whole days from `from` to `to`, both `YYYY-MM-DD`.\r\n *\r\n * Anchored at UTC midnight on BOTH sides, so the subtraction is whole days and\r\n * carries no zone of its own — the caller has already decided which day\r\n * \"today\" is, in the gym's timezone, and this must not second-guess that.\r\n */\r\nexport function daysBetween(from: string, to: string): number {\r\n const a = Date.parse(`${from}T00:00:00.000Z`);\r\n const b = Date.parse(`${to}T00:00:00.000Z`);\r\n if (Number.isNaN(a) || Number.isNaN(b)) return 0;\r\n return Math.round((b - a) / 86_400_000);\r\n}\r\n\r\n/** `2, 'day'` → `\"2 days\"`. */\r\nfunction plural(n: number, word: string): string {\r\n return n === 1 ? `${n} ${word}` : `${n} ${word}s`;\r\n}\r\n"]}
|
|
1
|
+
{"version":3,"file":"membership-status.js","sourceRoot":"","sources":["../src/membership-status.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * gymmonk-schema — Membership status, all of it, in one place\n * ============================================================\n * Everything the product knows about \"where does this member stand\" lives\n * here: the stored lifecycle, the effective one, the words shown on screen,\n * the money reading, and the three thresholds that decide \"soon\".\n *\n * ── Why this module exists ──────────────────────────────────────────────────\n * These facts were spread across five files in two repos. The stored enum and\n * the effective rule were in `membership.ts`; the DISPLAY vocabulary and its\n * derivation were in the web client's `shared/lib/format.ts`, out of the\n * backend's reach; the labels were inline in a pill component; the fee\n * vocabulary was in an owner-console file and took a web VIEW TYPE as its\n * input, so nothing else could call it. Two of the web's own feature folders\n * had also declared `MembershipStatus` as an alias of the DISPLAY type — the\n * same name as the stored one, meaning something different, in the same\n * codebase.\n *\n * ── There are TWO axes, and collapsing them would lose information ──────────\n * They are correlated, not identical, and they answer different questions:\n *\n * LIFECYCLE can this person train today? (the door asks this)\n * FEES does this person owe money? (the owner's roster asks this)\n *\n * A member part-way through a term they have half-paid is `active` on the\n * first axis and `unpaid` on the second, and gyms take part payments every\n * week. One flat enum would have to pick a side.\n *\n * The axes relate like this, and `readFeeStatus` encodes exactly this table:\n *\n * lifecycle money owed → fee status\n * active no → paid (or due-soon near the end of the term)\n * active yes → unpaid\n * expired yes → overdue\n * expired no → lapsed\n * pending n/a → pending (the GYM owes the next move)\n * none n/a → no-plan (the OWNER owes the next move)\n *\n * ── What is NOT here ────────────────────────────────────────────────────────\n * Colours. A tone is a Tailwind class, it belongs to whichever client is\n * painting, and there is exactly one consumer of each. The words live here\n * because two surfaces must never call the same state different things.\n *\n * @module gymmonk-schema/membership-status\n */\n\nimport { z } from 'zod';\n\n// ═══════════════════════════════════════════════════════════════════════════\n// AXIS 1 — LIFECYCLE: can this member train?\n// ═══════════════════════════════════════════════════════════════════════════\n\n/**\n * The STORED lifecycle column.\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 the two used to be welded\n * together: the moment a member tapped a plan it went live, and the moment an\n * owner assigned one at the desk it went live, whether or not any money had\n * changed hands. A gym then had no way to tell a paid member from an optimistic\n * 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 member\n * requests again rather than the gym reopening a closed decision.\n *\n * `invited` predates all of this and nothing writes it. Left in the union\n * because removing a value from a stored enum is not worth a migration, and old\n * rows still have to parse.\n *\n * `expiring` is NOT here — it is derived from `endDate` at the edge. See\n * `DisplayMembershipStatus`.\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/** The fields any status question needs. Deliberately not a full membership. */\nexport interface MembershipStatusInput {\n status: MembershipStatus;\n endDate: string;\n pausedFrom?: string | null;\n pausedTo?: string | null;\n}\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 * ── 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: MembershipStatusInput,\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(membership: MembershipStatusInput, today: string): boolean {\n return effectiveMembershipStatus(membership, today) === 'active';\n}\n\n// ─── Lifecycle, as the UI says it ───────────────────────────────────────────\n\n/**\n * How close to `endDate` a live membership starts reading as \"expiring\".\n *\n * A PRESENTATION band and nothing more: the door never sees `expiring`, and a\n * member inside this window is fully `active` as far as access is concerned.\n * Distinct from `FEE_DUE_SOON_DAYS`, which is a much tighter window because it\n * gates a payment reminder rather than a colour.\n */\nexport const MEMBERSHIP_EXPIRING_WINDOW_DAYS = 30;\n\n/**\n * How far ahead the owner's home ALERT looks for memberships about to lapse.\n *\n * Deliberately much tighter than the pill's band, and the reason all three\n * windows are declared together here rather than as a `const` in whichever\n * file happened to need one:\n *\n * MEMBERSHIP_EXPIRING_WINDOW_DAYS 30 paints a pill amber; the roster's\n * \"Expiring Soon\" segment and filter\n * MEMBERSHIP_EXPIRY_ALERT_DAYS 7 the one-line banner on owner home,\n * which says \"expiring this week\"\n * FEE_DUE_SOON_DAYS 3 arms a payment reminder\n *\n * They are different NUMBERS because they drive different actions — a colour,\n * a banner and a message to a member should not all fire at once — but they\n * are the same QUESTION, so they belong side by side where the next person to\n * change one can see the other two.\n */\nexport const MEMBERSHIP_EXPIRY_ALERT_DAYS = 7;\n\n/**\n * The lifecycle states a SCREEN distinguishes.\n *\n * `cancelled` and `invited` are absent on purpose — both read as `expired` to\n * a viewer, and a pill that said \"Cancelled\" next to one that said \"Expired\"\n * would imply a difference the member cannot act on differently.\n */\nexport type DisplayMembershipStatus =\n | 'pending'\n | 'rejected'\n | 'active'\n | 'expiring'\n | 'paused'\n | 'expired';\n\n/**\n * The lifecycle, narrowed onto what a screen shows.\n *\n * Adds exactly one thing over `effectiveMembershipStatus`: the `expiring`\n * warning band. Everything else is a straight mapping, so a screen and the\n * door can never disagree about whether somebody is live — only about how\n * loudly to say it.\n */\nexport function displayMembershipStatus(\n membership: MembershipStatusInput,\n today: string,\n): DisplayMembershipStatus {\n const effective = effectiveMembershipStatus(membership, today);\n\n switch (effective) {\n case 'pending':\n return 'pending';\n case 'rejected':\n return 'rejected';\n case 'paused':\n return 'paused';\n case 'expired':\n case 'cancelled':\n return 'expired';\n default: {\n // Live. The only question left is how close the end is.\n const left = daysBetween(today, membership.endDate);\n return left <= MEMBERSHIP_EXPIRING_WINDOW_DAYS ? 'expiring' : 'active';\n }\n }\n}\n\n/**\n * Is this display state one where the member can still train?\n *\n * `expiring` is LIVE. It is a warning band, not a lifecycle state — the term\n * has days left and the door lets them in — so anything filtering or counting\n * \"active members\" has to include it. Written down here because that is\n * exactly the kind of rule each list would otherwise re-derive slightly\n * differently, and one of them would forget `expiring` and quietly hide every\n * member in their last month.\n */\nexport function isLiveDisplayStatus(status: DisplayMembershipStatus): boolean {\n return status === 'active' || status === 'expiring';\n}\n\n/**\n * The words on the lifecycle pill.\n *\n * Here rather than in the component because two surfaces calling the same\n * state different things is the failure this module exists to prevent.\n * \"Awaiting payment\" is deliberately not \"Pending\": pending is what the\n * database calls it, and the member wants to know who they are waiting on.\n */\nexport const MEMBERSHIP_STATUS_LABEL: Record<DisplayMembershipStatus, string> = {\n pending: 'Awaiting payment',\n rejected: 'Not confirmed',\n active: 'Active',\n expiring: 'Expiring',\n paused: 'Paused',\n expired: 'Expired',\n};\n\n// ═══════════════════════════════════════════════════════════════════════════\n// AXIS 2 — FEES: does this member owe money?\n// ═══════════════════════════════════════════════════════════════════════════\n\n/** How close to the end of a term a member starts reading as \"due soon\". */\nexport const FEE_DUE_SOON_DAYS = 3;\n\n/**\n * Where a member stands on money, as one phrase an owner can scan down a list.\n *\n * Every state exists because it needs a DIFFERENT action. This began life as\n * \"paid or not\", which collapsed five unrelated situations into one green\n * chip: a member with no plan, a member whose payment was waiting on the GYM,\n * and a member who had genuinely settled all read \"Paid\", so the roster asked\n * the owner to do nothing about two people who needed something done.\n *\n * overdue money owed, term already ended → chase them\n * unpaid money owed, term still running → chase them\n * rejected the gym turned their payment down → talk to them\n * pending they have paid, the GYM must confirm → owner confirms\n * lapsed term ended, nothing sold to replace → chase a renewal\n * due-soon ends within a few days → chase a renewal\n * no-plan never been sold a membership → owner assigns one\n * cancelled they ended it and left → nothing to collect\n * paid settled, with time left on the term → nothing\n */\nexport type FeeStatus =\n | 'overdue'\n | 'unpaid'\n | 'rejected'\n | 'pending'\n | 'lapsed'\n | 'due-soon'\n | 'no-plan'\n | 'cancelled'\n | 'paid';\n\n/**\n * What the fee question needs, as plain facts.\n *\n * Deliberately NOT a view model. This used to take the owner console's roster\n * card type, which is why nothing else in the product could ask the question —\n * including the backend, which computes two of these fields in the first place.\n */\nexport interface FeeStatusInput {\n /** The stored lifecycle, or null when no membership was ever sold. */\n membershipStatus: MembershipStatus | null;\n /** The server's own judgement: money outstanding AND the term already ended. */\n overdueDays?: number | null;\n /** Still owed on the current term, in whole rupees. */\n balanceDueInr: number;\n /** End of the current term, `YYYY-MM-DD`, or null when there is none. */\n expiresOn?: string | null;\n}\n\nexport interface FeeReading {\n status: FeeStatus;\n /**\n * Does this need the owner's eye? Drives the chip's colour.\n *\n * Deliberately NOT the same question as `chase`. A member waiting on the gym\n * to confirm their payment needs attention and must not be sent a reminder —\n * they have already paid, and the outstanding job is the owner's.\n */\n attention: boolean;\n /**\n * Is there something to remind this member ABOUT?\n *\n * Gates the reminder action. False wherever the next move belongs to the gym\n * rather than the member, which is what stops an owner messaging somebody\n * \"your fees are overdue\" while their payment sits in the gym's own\n * confirmation queue.\n */\n chase: boolean;\n /** Whole days until the term ends; negative once past, null when no term. */\n daysLeft: number | null;\n /** Days past the end of an unpaid term, when that is what happened. */\n overdueDays: number | null;\n /** Still owed, in whole rupees. Zero unless the status says otherwise. */\n amountDueInr: number;\n}\n\n/**\n * The fee reading for one member.\n *\n * Ordered by whose move it is and how hard the fact is. `overdueDays` is the\n * server's own judgement — money outstanding AND the term already ended — so\n * it is trusted rather than recomputed here.\n */\nexport function readFeeStatus(input: FeeStatusInput, today: string): FeeReading {\n const { membershipStatus, balanceDueInr } = input;\n const overdueDays = input.overdueDays ?? null;\n const base = { daysLeft: null, overdueDays: null, amountDueInr: 0 } as const;\n\n /**\n * Never sold a membership. Checked FIRST, because every money question below\n * presumes one exists — and a member with no plan cannot be behind on a fee\n * nobody has charged them.\n */\n if (membershipStatus === null) {\n return { ...base, status: 'no-plan', attention: true, chase: false };\n }\n\n /**\n * Waiting on the GYM, not on the member.\n *\n * Ahead of the balance check on purpose: a pending membership carries its\n * full price as outstanding, so this member would otherwise read as owing\n * money and be sent a payment reminder — for money they have already handed\n * over and the gym has simply not confirmed yet.\n */\n if (membershipStatus === 'pending') {\n return { ...base, status: 'pending', attention: true, chase: false };\n }\n\n /** The gym looked at their payment and turned it down. */\n if (membershipStatus === 'rejected') {\n return { ...base, status: 'rejected', attention: true, chase: true };\n }\n\n /**\n * They ended it themselves and left.\n *\n * Checked before the date logic, which would otherwise see a term end in the\n * past and read them as \"Renewal due\" — with a reminder ready to tell\n * somebody who deliberately quit that their membership has expired.\n */\n if (membershipStatus === 'cancelled') {\n return { ...base, status: 'cancelled', attention: false, chase: false };\n }\n\n // Late: the server has already established both halves of that.\n if (overdueDays && overdueDays > 0) {\n return {\n ...base,\n status: 'overdue',\n attention: true,\n chase: true,\n overdueDays,\n amountDueInr: balanceDueInr,\n };\n }\n\n /**\n * Owes money, but the term is still running.\n *\n * A real state and NOT the same as overdue: a gym that takes half now and\n * half on Friday has members here every week, and calling them late would be\n * wrong.\n */\n if (balanceDueInr > 0) {\n return { ...base, status: 'unpaid', attention: true, chase: true, amountDueInr: balanceDueInr };\n }\n\n const daysLeft = input.expiresOn ? daysBetween(today, input.expiresOn) : null;\n\n /**\n * The term ran out and nothing was sold to replace it.\n *\n * Nothing is OUTSTANDING — a term nobody assigned was never invoiced — so\n * this used to fall through and read \"Paid\", in green, for the member most\n * worth chasing on the whole roster.\n */\n if (daysLeft !== null && daysLeft < 0) {\n return { ...base, status: 'lapsed', attention: true, chase: true, daysLeft };\n }\n\n if (daysLeft !== null && daysLeft >= 0 && daysLeft <= FEE_DUE_SOON_DAYS) {\n return { ...base, status: 'due-soon', attention: true, chase: true, daysLeft };\n }\n\n /**\n * Settled, with time left on the term. The only genuinely quiet state, and\n * the only one that earns a green chip.\n */\n return { ...base, status: 'paid', attention: false, chase: false, daysLeft };\n}\n\n/**\n * The words on the fee chip.\n *\n * Functions rather than plain strings because three of these states have to\n * name a number, and the AMOUNT is formatted by whoever is rendering — a\n * client knows how to write ₹1,200 for its locale and this package does not.\n * The wording still lives here, so no surface can invent its own phrasing for\n * a state another surface already names.\n */\nexport const FEE_STATUS_LABEL: Record<FeeStatus, (reading: FeeReading, amount: string) => string> =\n {\n overdue: (r, amount) => `${amount} overdue ${plural(r.overdueDays ?? 0, 'day')}`,\n unpaid: (_r, amount) => `${amount} due`,\n rejected: () => 'Payment declined',\n pending: () => 'Confirm payment',\n lapsed: () => 'Renewal due',\n 'due-soon': (r) =>\n r.daysLeft === 0 ? 'Due today' : `Due in ${plural(r.daysLeft ?? 0, 'day')}`,\n 'no-plan': () => 'No plan yet',\n cancelled: () => 'Cancelled',\n paid: () => 'Paid',\n };\n\n// ═══════════════════════════════════════════════════════════════════════════\n// Shared helpers\n// ═══════════════════════════════════════════════════════════════════════════\n\n/**\n * Whole days from `from` to `to`, both `YYYY-MM-DD`.\n *\n * Anchored at UTC midnight on BOTH sides, so the subtraction is whole days and\n * carries no zone of its own — the caller has already decided which day\n * \"today\" is, in the gym's timezone, and this must not second-guess that.\n */\nexport function daysBetween(from: string, to: string): number {\n const a = Date.parse(`${from}T00:00:00.000Z`);\n const b = Date.parse(`${to}T00:00:00.000Z`);\n if (Number.isNaN(a) || Number.isNaN(b)) return 0;\n return Math.round((b - a) / 86_400_000);\n}\n\n/** `2, 'day'` → `\"2 days\"`. */\nfunction plural(n: number, word: string): string {\n return n === 1 ? `${n} ${word}` : `${n} ${word}s`;\n}\n"]}
|
package/dist/membership.d.ts
CHANGED
|
@@ -12,30 +12,6 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { z } from 'zod';
|
|
14
14
|
export { effectiveMembershipStatus, isMembershipLive, type MembershipStatus, membershipStatusSchema, } from './membership-status.js';
|
|
15
|
-
/**
|
|
16
|
-
* Membership lifecycle.
|
|
17
|
-
*
|
|
18
|
-
* `pending` is where every membership STARTS, whoever created it. Choosing a
|
|
19
|
-
* plan is not the same event as paying for one, and until this release the two
|
|
20
|
-
* were welded together: the moment a member tapped a plan it went live, and the
|
|
21
|
-
* moment an owner assigned one at the desk it went live, whether or not any
|
|
22
|
-
* money had changed hands. A gym then had no way to tell a paid member from an
|
|
23
|
-
* optimistic one, and no lever to withhold access from the second.
|
|
24
|
-
*
|
|
25
|
-
* The only transition out of `pending` is the gym marking the payment done.
|
|
26
|
-
* That is deliberately a HUMAN act by the owner or a manager rather than a
|
|
27
|
-
* gateway callback: these gyms take cash at the desk and UPI transfers on a
|
|
28
|
-
* phone, so the person who knows the money arrived is the person at the gym.
|
|
29
|
-
*
|
|
30
|
-
* `rejected` is the other way out, for a screenshot that shows nothing, the
|
|
31
|
-
* wrong amount, or a transfer that never landed. It is a dead end — the
|
|
32
|
-
* member requests again rather than the gym reopening a closed decision.
|
|
33
|
-
*
|
|
34
|
-
* `invited` predates all of this and nothing writes it. Left in place because
|
|
35
|
-
* removing a value from a stored enum is not worth a migration.
|
|
36
|
-
*
|
|
37
|
-
* `expiring` is NOT stored — it is derived from `endDate` at the edge.
|
|
38
|
-
*/
|
|
39
15
|
export declare const paymentMethodSchema: z.ZodEnum<{
|
|
40
16
|
upi: "upi";
|
|
41
17
|
cash: "cash";
|
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;
|
|
1
|
+
{"version":3,"file":"membership.d.ts","sourceRoot":"","sources":["../src/membership.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAQxB,OAAO,EACL,yBAAyB,EACzB,gBAAgB,EAChB,KAAK,gBAAgB,EACrB,sBAAsB,GACvB,MAAM,wBAAwB,CAAC;AAahC,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;;;;;;;;;;;;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
|
@@ -18,33 +18,8 @@ import { z } from 'zod';
|
|
|
18
18
|
* there is still exactly one definition behind both spellings.
|
|
19
19
|
*/
|
|
20
20
|
export { effectiveMembershipStatus, isMembershipLive, membershipStatusSchema, } from './membership-status.js';
|
|
21
|
-
import { membershipStatusSchema } from './membership-status.js';
|
|
22
21
|
import { isoDateSchema, isoDateTimeSchema, objectIdSchema, optionalTrimmedString, priceInrSchema, } from './common.js';
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Membership lifecycle.
|
|
26
|
-
*
|
|
27
|
-
* `pending` is where every membership STARTS, whoever created it. Choosing a
|
|
28
|
-
* plan is not the same event as paying for one, and until this release the two
|
|
29
|
-
* were welded together: the moment a member tapped a plan it went live, and the
|
|
30
|
-
* moment an owner assigned one at the desk it went live, whether or not any
|
|
31
|
-
* money had changed hands. A gym then had no way to tell a paid member from an
|
|
32
|
-
* optimistic one, and no lever to withhold access from the second.
|
|
33
|
-
*
|
|
34
|
-
* The only transition out of `pending` is the gym marking the payment done.
|
|
35
|
-
* That is deliberately a HUMAN act by the owner or a manager rather than a
|
|
36
|
-
* gateway callback: these gyms take cash at the desk and UPI transfers on a
|
|
37
|
-
* phone, so the person who knows the money arrived is the person at the gym.
|
|
38
|
-
*
|
|
39
|
-
* `rejected` is the other way out, for a screenshot that shows nothing, the
|
|
40
|
-
* wrong amount, or a transfer that never landed. It is a dead end — the
|
|
41
|
-
* member requests again rather than the gym reopening a closed decision.
|
|
42
|
-
*
|
|
43
|
-
* `invited` predates all of this and nothing writes it. Left in place because
|
|
44
|
-
* removing a value from a stored enum is not worth a migration.
|
|
45
|
-
*
|
|
46
|
-
* `expiring` is NOT stored — it is derived from `endDate` at the edge.
|
|
47
|
-
*/
|
|
22
|
+
import { membershipStatusSchema } from './membership-status.js';
|
|
48
23
|
// ─── Payments ────────────────────────────────────────────────────────────────
|
|
49
24
|
export const paymentMethodSchema = z.enum(['upi', 'cash', 'card']);
|
|
50
25
|
export const paymentStatusSchema = z.enum(['success', 'failed', 'pending']);
|
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":["/**\r\n * gymmonk-schema — Membership & payments\r\n * =======================================\r\n * A member's live membership at a center: which plan/tenure, its lifecycle\r\n * status and dates, pause windows, and the payment ledger. v1 is single-center,\r\n * so one active membership per user.\r\n *\r\n * The \"expiring\" state the UI shows is DERIVED from `endDate` at the edge; the\r\n * stored `status` is the true lifecycle state.\r\n *\r\n * @module gymmonk-schema/membership\r\n */\r\n\r\nimport { z } from 'zod';\r\n/*\r\n * Membership STATUS moved to its own module — see `membership-status.ts` for\r\n * why. Re-exported here so the many call sites that reach for the stored enum\r\n * alongside the membership DTO do not all have to change import paths, and so\r\n * there is still exactly one definition behind both spellings.\r\n */\r\nexport {\r\n effectiveMembershipStatus,\r\n isMembershipLive,\r\n type MembershipStatus,\r\n membershipStatusSchema,\r\n} from './membership-status.js';\r\nimport { membershipStatusSchema } from './membership-status.js';\r\nimport {\r\n isoDateSchema,\r\n isoDateTimeSchema,\r\n objectIdSchema,\r\n optionalTrimmedString,\r\n priceInrSchema,\r\n} from './common.js';\r\n\r\n// ─── Status ──────────────────────────────────────────────────────────────────\r\n\r\n/**\r\n * Membership lifecycle.\r\n *\r\n * `pending` is where every membership STARTS, whoever created it. Choosing a\r\n * plan is not the same event as paying for one, and until this release the two\r\n * were welded together: the moment a member tapped a plan it went live, and the\r\n * moment an owner assigned one at the desk it went live, whether or not any\r\n * money had changed hands. A gym then had no way to tell a paid member from an\r\n * optimistic one, and no lever to withhold access from the second.\r\n *\r\n * The only transition out of `pending` is the gym marking the payment done.\r\n * That is deliberately a HUMAN act by the owner or a manager rather than a\r\n * gateway callback: these gyms take cash at the desk and UPI transfers on a\r\n * phone, so the person who knows the money arrived is the person at the gym.\r\n *\r\n * `rejected` is the other way out, for a screenshot that shows nothing, the\r\n * wrong amount, or a transfer that never landed. It is a dead end — the\r\n * member requests again rather than the gym reopening a closed decision.\r\n *\r\n * `invited` predates all of this and nothing writes it. Left in place because\r\n * removing a value from a stored enum is not worth a migration.\r\n *\r\n * `expiring` is NOT stored — it is derived from `endDate` at the edge.\r\n */\r\n\r\n// ─── Payments ────────────────────────────────────────────────────────────────\r\n\r\nexport const paymentMethodSchema = z.enum(['upi', 'cash', 'card']);\r\nexport type PaymentMethod = z.infer<typeof paymentMethodSchema>;\r\n\r\nexport const paymentStatusSchema = z.enum(['success', 'failed', 'pending']);\r\nexport type PaymentStatus = z.infer<typeof paymentStatusSchema>;\r\n\r\nexport const paymentSchema = z.object({\r\n id: objectIdSchema,\r\n membershipId: objectIdSchema,\r\n amountInr: priceInrSchema,\r\n method: paymentMethodSchema,\r\n status: paymentStatusSchema,\r\n paidAt: isoDateTimeSchema,\r\n note: z.string().nullable(),\r\n});\r\nexport type Payment = z.infer<typeof paymentSchema>;\r\n\r\n// ─── Entity (API response) ───────────────────────────────────────────────────\r\n\r\nexport const membershipSchema = z.object({\r\n id: objectIdSchema,\r\n /**\r\n * The enrolment this plan was sold against.\r\n *\r\n * The link that makes a plan belong to a stint at a gym rather than to a\r\n * person. It replaces a `(centerId, memberCode)` pair that had to be matched\r\n * by hand in three places, and it is what lets a plan be sold to somebody the\r\n * desk has only just written down — an enrolment exists from that moment,\r\n * whereas an account may not exist for weeks.\r\n */\r\n enrolmentId: objectIdSchema,\r\n /**\r\n * The account, denormalised from the enrolment for member-facing queries.\r\n * NULL until they claim, for the same reason it is null on the enrolment.\r\n */\r\n userId: objectIdSchema.nullable(),\r\n centerId: objectIdSchema,\r\n planId: objectIdSchema,\r\n planName: z.string(),\r\n /**\r\n * Which variant of the plan was bought, and how it read at the time.\r\n *\r\n * A plain string, not a fixed tenure enum: variant ids are minted per plan\r\n * now, so a gym can sell a 10-day pass. The LABEL is a snapshot for the same\r\n * reason a payment records what it was for — the owner may rename or delete\r\n * the variant later, and a membership must still be able to say what was\r\n * sold.\r\n */\r\n variantId: z.string(),\r\n variantLabel: z.string(),\r\n priceInr: priceInrSchema,\r\n status: membershipStatusSchema,\r\n /**\r\n * The term, PROVISIONAL while the membership is pending.\r\n *\r\n * Dated from the member's JOINING DATE, not from the day either party\r\n * pressed a button. Terms are a cycle — somebody who joined on the 4th runs\r\n * 4 Jul→4 Aug, 4 Aug→4 Sep — so a gym confirming a week late records the\r\n * month that was owed rather than starting a fresh one from today. Dating\r\n * them from the confirmation, which is what this used to do, walked every\r\n * member's renewal day forward by however long the desk took to look.\r\n *\r\n * Written at request time so every screen that reads them keeps working, and\r\n * recomputed on confirmation in case the cycle moved in between.\r\n */\r\n startDate: isoDateSchema,\r\n endDate: isoDateSchema,\r\n pausedFrom: isoDateSchema.nullable(),\r\n pausedTo: isoDateSchema.nullable(),\r\n /**\r\n * The screenshot the member attached, if they attached one.\r\n *\r\n * Optional on purpose. A member paying cash at the desk has nothing to\r\n * screenshot, and refusing to accept their request until they produce one\r\n * would lock the commonest case out of the flow entirely.\r\n */\r\n paymentProofUrl: z.string().nullable(),\r\n /** When the plan was chosen. Distinct from `createdAt` only in intent. */\r\n requestedAt: isoDateTimeSchema,\r\n /** When the gym marked the payment done or turned it down. */\r\n decidedAt: isoDateTimeSchema.nullable(),\r\n /** Who decided. Null while pending. */\r\n decidedBy: objectIdSchema.nullable(),\r\n /** Why it was turned down, in the gym's words. */\r\n decisionNote: z.string().nullable(),\r\n createdAt: isoDateTimeSchema,\r\n updatedAt: isoDateTimeSchema,\r\n});\r\nexport type Membership = z.infer<typeof membershipSchema>;\r\n\r\n// ─── Assign / renew / upgrade ────────────────────────────────────────────────\r\n\r\n/**\r\n * Assign a plan to a member (new membership, renewal, or upgrade). Records the\r\n * charge in one shot; omit `payment` for an unpaid/invited assignment.\r\n */\r\nexport const assignMembershipBodySchema = z.object({\r\n planId: objectIdSchema,\r\n variantId: z.string().trim().min(1),\r\n /** Explicit start; defaults to today server-side when omitted. */\r\n startDate: isoDateSchema.optional(),\r\n /**\r\n * A screenshot of the transfer, uploaded to storage first and passed as its\r\n * URL. Optional — see `paymentProofUrl` on the entity.\r\n */\r\n paymentProofUrl: z.string().trim().max(2000).optional(),\r\n payment: z\r\n .object({\r\n amountInr: priceInrSchema,\r\n method: paymentMethodSchema,\r\n status: paymentStatusSchema.default('success'),\r\n })\r\n .optional(),\r\n});\r\nexport type AssignMembershipBody = z.infer<typeof assignMembershipBodySchema>;\r\n\r\n// ——— Confirm / reject the payment ————————————————————————\r\n\r\n/**\r\n * The gym marking a pending membership's payment as done, which is the single\r\n * act that activates it.\r\n *\r\n * It carries the LEDGER entry rather than being a bare approval, because those\r\n * are the same event: the owner is asserting that this much money arrived by\r\n * this route. Splitting them would let a membership go live with nothing\r\n * written down, which is exactly the hole this feature exists to close.\r\n *\r\n * The amount is not forced to equal the plan price. A gym that takes half now\r\n * and half on Friday still wants the member training, and the balance is\r\n * already tracked against the term.\r\n */\r\nexport const confirmMembershipPaymentBodySchema = z.object({\r\n amountInr: priceInrSchema,\r\n method: paymentMethodSchema,\r\n note: optionalTrimmedString(240),\r\n /**\r\n * The term the owner BELIEVES they are paying for, echoed back from what the\r\n * screen showed them.\r\n *\r\n * The server derives the term itself and never trusts this as input — it is\r\n * a guard, not a parameter. The owner is told \"this covers 3 Jul to 3 Aug\"\r\n * and then presses a button; if a second manager confirms a term in the\r\n * seconds between, the derived term moves on and this one no longer matches.\r\n * Without the echo that owner silently pays for a DIFFERENT month than the\r\n * one they read, and the gym's ledger disagrees with the conversation that\r\n * produced it. Optional so a client that shows no term still works.\r\n */\r\n expectedTermStart: isoDateSchema.optional(),\r\n});\r\nexport type ConfirmMembershipPaymentBody = z.infer<typeof confirmMembershipPaymentBodySchema>;\r\n\r\n/**\r\n * One billing term, dated from the member's JOINING DATE.\r\n *\r\n * Terms are a derived cycle, not free-form dates the desk types in: a member\r\n * who joined on the 4th is billed 4 Jul→4 Aug, 4 Aug→4 Sep, and so on, each\r\n * one the length of the variant they bought. Nobody picks a start date,\r\n * because every gym already answers \"which month is this for?\" the same way,\r\n * and letting two people type it produces two answers.\r\n *\r\n * `isOverdue` is the term having already ended — the owner marking a month\r\n * they forgot at the time. That is a normal, expected act rather than an\r\n * error, so it is surfaced as a fact about the term rather than a warning.\r\n */\r\nexport const membershipTermSchema = z.object({\r\n startDate: isoDateSchema,\r\n endDate: isoDateSchema,\r\n isOverdue: z.boolean(),\r\n});\r\nexport type MembershipTerm = z.infer<typeof membershipTermSchema>;\r\n\r\n/**\r\n * One row of the gym's confirmation queue.\r\n *\r\n * The membership plus just enough of the person to act on it. The owner is\r\n * deciding about a HUMAN — \"did Ravi's transfer land\" — and a list of plan\r\n * names and amounts with no names attached is not something anybody can\r\n * answer. Kept to four fields rather than embedding the whole roster row,\r\n * because this list is read on every visit to the members screen.\r\n */\r\nexport const pendingMembershipRowSchema = z.object({\r\n membership: membershipSchema,\r\n member: z.object({\r\n enrolmentId: objectIdSchema,\r\n gymMemberCode: z.string(),\r\n name: z.string(),\r\n avatarUrl: z.string().nullable(),\r\n }),\r\n});\r\nexport type PendingMembershipRow = z.infer<typeof pendingMembershipRowSchema>;\r\n\r\n/** Turning a request down. The reason reaches the member. */\r\nexport const rejectMembershipPaymentBodySchema = z.object({\r\n reason: optionalTrimmedString(240),\r\n});\r\nexport type RejectMembershipPaymentBody = z.infer<typeof rejectMembershipPaymentBodySchema>;\r\n\r\n// ─── Pause ───────────────────────────────────────────────────────────────────\r\n\r\nexport const pauseDurationSchema = z.enum(['1w', '2w', '1m', 'custom']);\r\nexport type PauseDuration = z.infer<typeof pauseDurationSchema>;\r\n\r\nexport const pauseMembershipBodySchema = z\r\n .object({\r\n duration: pauseDurationSchema,\r\n customFrom: isoDateSchema.optional(),\r\n customTo: isoDateSchema.optional(),\r\n })\r\n .refine((v) => v.duration !== 'custom' || (v.customFrom && v.customTo), {\r\n message: 'Pick both pause dates',\r\n path: ['customTo'],\r\n });\r\nexport type PauseMembershipBody = z.infer<typeof pauseMembershipBodySchema>;\r\n\r\n// ─── Cancel ──────────────────────────────────────────────────────────────────\r\n\r\n/** Cancellation requires echoing the member code as a safety confirmation. */\r\nexport const cancelMembershipBodySchema = z.object({\r\n confirmationCode: z.string().trim().min(1),\r\n reason: optionalTrimmedString(240),\r\n});\r\nexport type CancelMembershipBody = z.infer<typeof cancelMembershipBodySchema>;\r\n\r\n// ─── Collect an offline payment ──────────────────────────────────────────────\r\n\r\nexport const collectPaymentBodySchema = z.object({\r\n amountInr: priceInrSchema,\r\n method: paymentMethodSchema,\r\n note: optionalTrimmedString(240),\r\n});\r\nexport type CollectPaymentBody = z.infer<typeof collectPaymentBodySchema>;\r\n"]}
|
|
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';\n\n/*\n * Membership STATUS moved to its own module — see `membership-status.ts` for\n * why. Re-exported here so the many call sites that reach for the stored enum\n * alongside the membership DTO do not all have to change import paths, and so\n * there is still exactly one definition behind both spellings.\n */\nexport {\n effectiveMembershipStatus,\n isMembershipLive,\n type MembershipStatus,\n membershipStatusSchema,\n} from './membership-status.js';\n\nimport {\n isoDateSchema,\n isoDateTimeSchema,\n objectIdSchema,\n optionalTrimmedString,\n priceInrSchema,\n} from './common.js';\nimport { membershipStatusSchema } from './membership-status.js';\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 * 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"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"onboarding-form.js","sourceRoot":"","sources":["../src/onboarding-form.ts"],"names":[],"mappings":"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","sourcesContent":["/**\r\n * gymmonk-schema — Onboarding & gym-registration FORM schemas\r\n * ===========================================================\r\n * The client-side validation contract for the member onboarding wizard and the\r\n * \"list your gym\" wizard.\r\n *\r\n * Why these live here and are distinct from the `*OnboardingBody` API schemas:\r\n * a half-filled form legitimately holds `null` for a choice the user has not\r\n * made yet, whereas the API body only ever sees completed values. So these are\r\n * the SAME rules expressed over the form's working shape — every message,\r\n * regex and bound is defined once, here, and both sides agree on it.\r\n *\r\n * Everything reuses the primitives in `shared.ts` / `common.ts`\r\n * (`INDIAN_STATES`, `CENTER_TYPES`, `WEEKDAYS`, phone/pincode rules) rather\r\n * than restating them.\r\n *\r\n * @module gymmonk-schema/onboarding-form\r\n */\r\n\r\nimport { z } from 'zod';\r\nimport { type CenterType, centerTypeSchema } from './center.js';\r\nimport { ageInYears, isRealDate, PHONE_REGEX } from './common.js';\r\nimport type { WEEKDAYS } from './shared.js';\r\nimport { type Address, addressSchema } from './shared.js';\r\n\r\n// ─── Field-level rules (shared by both wizards) ──────────────────────────────\r\n\r\n/** A required 10-digit Indian mobile, as typed into a form field. */\r\nconst formPhone = z.string().regex(PHONE_REGEX, 'Enter a valid 10-digit mobile number');\r\n\r\n/** An optional phone: blank is allowed, anything else must be valid. */\r\nconst optionalFormPhone = z\r\n .string()\r\n .refine((v) => v === '' || PHONE_REGEX.test(v), 'Enter a valid 10-digit mobile number');\r\n\r\n/** An optional email: blank is allowed, anything else must look like an email. */\r\nconst optionalFormEmail = z\r\n .string()\r\n .refine((v) => v === '' || /^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$/.test(v), {\r\n message: 'Enter a valid email',\r\n });\r\n\r\n/** A choice the user has not made yet is `null` until they pick one. */\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// ─── Address (form shape: one atomic value, null until picked) ────────────\r\n\r\n/**\r\n * An address inside an onboarding form is ONE cell, not four.\r\n *\r\n * It used to be `street` + `city` + `state` + `pincode`, each independently\r\n * fillable, which is why this module needed rules for a half-filled address.\r\n * Now it is picked through the location drawer, which either hands back a\r\n * complete `Address` — area chain, ids, names, ancestry, coordinates — or\r\n * nothing at all. So there is no partial state left to validate, and `null`\r\n * simply means \"not chosen yet\".\r\n */\r\nexport const onboardingAddressSchema = requiredChoice(addressSchema, 'Add your address');\r\n\r\n/** Working shape of an address inside an onboarding form. */\r\nexport type OnboardingAddress = Address | null;\r\n\r\n/** An address has been picked. */\r\nexport function isOnboardingAddressComplete(a: OnboardingAddress): boolean {\r\n return a !== null;\r\n}\r\n\r\n// ─── Social links (form shape: every handle is a plain string) ───────────────\r\n\r\nexport const onboardingSocialLinksSchema = z.object({\r\n jansathi: z.string(),\r\n youtube: z.string(),\r\n instagram: z.string(),\r\n facebook: z.string(),\r\n});\r\nexport type OnboardingSocialLinks = z.infer<typeof onboardingSocialLinksSchema>;\r\n\r\n// ─── Opening hours (form shape) ─────────────────────────────────────────────\r\n\r\nexport const onboardingDayHoursSchema = z.object({\r\n open: z.boolean(),\r\n from: z.string(),\r\n to: z.string(),\r\n});\r\nexport type OnboardingDayHours = z.infer<typeof onboardingDayHoursSchema>;\r\nexport type OnboardingWeeklyHours = Record<(typeof WEEKDAYS)[number], OnboardingDayHours>;\r\n\r\n// ─── Member onboarding form ─────────────────────────────────────────────────\r\n\r\n/** Age bounds a gym membership realistically spans. */\r\nexport const MEMBER_MIN_AGE_YEARS = 10;\r\nexport const MEMBER_MAX_AGE_YEARS = 100;\r\n\r\n/**\r\n * The three-cell date-of-birth rule, in ONE place.\r\n *\r\n * Each cell is bounded by its own input, but only the assembled date can catch\r\n * an impossible one (31 Feb) or an implausible age. The issue is attached to\r\n * `dobDay` so it renders under the field group rather than under one cell.\r\n *\r\n * Exported because the member's own profile editor asks for the SAME date with\r\n * the same three cells. When the rule lived inline in the wizard, that editor\r\n * had a free-text box parsed with `new Date()` instead, which accepted \"March\r\n * 18, 1994\", accepted nonsense just as happily, and silently dropped whatever\r\n * it could not read.\r\n */\r\nexport function refineMemberDob(\r\n v: { dobDay: string; dobMonth: string; dobYear: string },\r\n ctx: z.RefinementCtx,\r\n): void {\r\n if (!v.dobDay || !v.dobMonth || v.dobYear.length !== 4) return;\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 < MEMBER_MIN_AGE_YEARS) {\r\n ctx.addIssue({\r\n code: 'custom',\r\n path: ['dobDay'],\r\n message: `You must be at least ${MEMBER_MIN_AGE_YEARS} to join`,\r\n });\r\n } else if (age > MEMBER_MAX_AGE_YEARS) {\r\n ctx.addIssue({ code: 'custom', path: ['dobDay'], message: 'Check the year of birth' });\r\n }\r\n}\r\n\r\nexport const memberOnboardingFormSchema = z\r\n .object({\r\n firstName: z.string().trim().min(1, 'First name is required'),\r\n lastName: z.string().trim(),\r\n dobDay: z.string().min(1, 'Enter the day'),\r\n dobMonth: z.string().min(1, 'Enter the month'),\r\n dobYear: z.string().length(4, 'Enter a 4-digit year'),\r\n gender: requiredChoice(z.enum(['male', 'female', 'other']), 'Select your gender'),\r\n heightCm: requiredChoice(z.number(), 'Add your height'),\r\n weightKg: requiredChoice(z.number(), 'Add your weight'),\r\n neckCm: requiredChoice(z.number(), 'Add your neck'),\r\n waistCm: requiredChoice(z.number(), 'Add your waist'),\r\n hipsCm: z.number().nullable(),\r\n /**\r\n * The member's OWN number, asked on step one beside their name.\r\n *\r\n * It moved here when the emergency step was removed. Emergency contact and\r\n * blood group genuinely belong in the profile — nobody needs them to\r\n * finish signing up — but this one is how the gym reaches the member at\r\n * all, and it drives every chase action on the owner's roster card. Left\r\n * on the removed step it would simply have stopped being collected.\r\n */\r\n phone: formPhone,\r\n /** GCS URL of the uploaded photo. Optional — it can be added later. */\r\n avatarUrl: z.string(),\r\n })\r\n .superRefine(refineMemberDob)\r\n /**\r\n * The API requires the hips measurement for female members (needed for the\r\n * body-fat estimate), so it fails inline on the measurements step instead of\r\n * 422-ing at Finish. Own refine — no early-return coupling with the DOB rule.\r\n */\r\n .superRefine((v, ctx) => {\r\n if (v.gender === 'female' && v.hipsCm == null) {\r\n ctx.addIssue({\r\n code: 'custom',\r\n path: ['hipsCm'],\r\n message: 'Hips measurement is required',\r\n });\r\n }\r\n });\r\n\r\n/**\r\n * The form's working shape. Hand-written (not `z.infer`) so unset numeric /\r\n * choice fields can hold `null` while the schema's `requiredChoice` rules still\r\n * drive the field errors — the resolver is cast to this type at `useForm`.\r\n */\r\nexport type MemberOnboardingForm = {\r\n firstName: string;\r\n lastName: string;\r\n dobDay: string;\r\n dobMonth: string;\r\n dobYear: string;\r\n gender: 'male' | 'female' | 'other' | null;\r\n heightCm: number | null;\r\n weightKg: number | null;\r\n neckCm: number | null;\r\n waistCm: number | null;\r\n hipsCm: number | null;\r\n phone: string;\r\n avatarUrl: string;\r\n};\r\n\r\nexport const EMPTY_MEMBER_ONBOARDING_FORM: MemberOnboardingForm = {\r\n firstName: '',\r\n lastName: '',\r\n dobDay: '',\r\n dobMonth: '',\r\n dobYear: '',\r\n gender: null,\r\n heightCm: null,\r\n weightKg: null,\r\n neckCm: null,\r\n waistCm: null,\r\n hipsCm: null,\r\n phone: '',\r\n avatarUrl: '',\r\n};\r\n\r\n/**\r\n * One onboarding step, described ONCE.\r\n *\r\n * The wizard chrome (screen name, help line, progress denominator) and the\r\n * validation gate used to live in the page as a local `META` array beside a\r\n * separate field map — two lists that had to be kept the same length by hand.\r\n * They are one list here, so a step cannot exist without a title or be left\r\n * without a validation gate.\r\n */\r\nexport interface OnboardingStepConfig<TField extends string> {\r\n /** Stable identifier — safe for analytics and resume, unlike the index. */\r\n key: string;\r\n /** Screen name in the step header. */\r\n title: string;\r\n /** Help line under the screen name. */\r\n helpText: string;\r\n /**\r\n * Fields validated before this step may be left. Naming a parent path (e.g.\r\n * `address`) validates its nested fields too.\r\n */\r\n fields: readonly TField[];\r\n}\r\n\r\nexport const MEMBER_ONBOARDING_STEPS: readonly OnboardingStepConfig<\r\n keyof MemberOnboardingForm & string\r\n>[] = [\r\n {\r\n key: 'basics',\r\n title: 'Basic information',\r\n helpText: \"Enter your details, we'll need these to set things up.\",\r\n fields: ['firstName', 'dobDay', 'dobMonth', 'dobYear', 'gender', 'phone'],\r\n },\r\n {\r\n key: 'body-metrics',\r\n title: 'Personal details',\r\n helpText: 'Your height and weight, to calculate your BMI.',\r\n fields: ['heightCm', 'weightKg'],\r\n },\r\n {\r\n key: 'body-composition',\r\n title: 'Personal details',\r\n helpText: 'A few measurements, to estimate your body fat.',\r\n fields: ['neckCm', 'waistCm', 'hipsCm'],\r\n },\r\n] as const;\r\n\r\nexport const MEMBER_ONBOARDING_TOTAL_STEPS = MEMBER_ONBOARDING_STEPS.length;\r\n\r\n/**\r\n * Fields validated before advancing each member step.\r\n * DERIVED from `MEMBER_ONBOARDING_STEPS` so the two can never disagree.\r\n */\r\nexport const MEMBER_STEP_FIELDS: (keyof MemberOnboardingForm)[][] = MEMBER_ONBOARDING_STEPS.map(\r\n (s) => [...s.fields],\r\n);\r\n\r\n// ─── Gym registration form (\"list your gym\") ────────────────────────────────\r\n\r\n/**\r\n * The gym-registration wizard's working shape.\r\n *\r\n * THREE steps, not five. The two it used to open with — \"your details\" and\r\n * \"contact details\" — asked a signed-in member for their name, gender, date of\r\n * birth and phone, every one of which onboarding already captured. Registering\r\n * a business asks about the BUSINESS; the person is already known.\r\n */\r\n/**\r\n * The gym-registration form's FIELD SHAPE, exported separately from the schema.\r\n *\r\n * Separate because the schema carries a `superRefine` (see\r\n * `refineGymRegistrationForm`) and a refined object cannot be `.extend()`ed —\r\n * zod hands back a ZodEffects, and calling `.extend` on one throws at MODULE\r\n * LOAD, which no build catches and which takes the whole page down on import.\r\n * The web client adds the two pasted Maps links to this form, so it needs the\r\n * shape and the refinement as separate pieces it can recombine.\r\n */\r\nexport const gymRegistrationFormShape = {\r\n // 1 — organisation\r\n orgName: z.string().trim().min(1, 'Organisation name is required'),\r\n orgDescription: z.string(),\r\n hqAddress: onboardingAddressSchema,\r\n orgWebsite: z.string(),\r\n gstNumber: z.string(),\r\n cinNumber: z.string(),\r\n numberOfCenters: requiredChoice(z.number(), 'Add the number of centers'),\r\n orgSocials: onboardingSocialLinksSchema,\r\n // 2 — center\r\n centerName: z.string().trim().min(1, 'Center name is required'),\r\n centerDescription: z.string(),\r\n /**\r\n * The real enum, not `z.array(z.string())`.\r\n *\r\n * It was strings, and `toGymRegistrationBody` then CAST them to the enum on\r\n * the way to the API. A cast is not a check: retiring a centre type left the\r\n * wizard perfectly happy to submit the retired value and the server to\r\n * reject the whole registration, with the owner told only that something was\r\n * wrong. Now a value the API will not accept cannot leave the form.\r\n */\r\n centerTypes: z.array(centerTypeSchema).min(1, 'Pick at least one'),\r\n /**\r\n * \"Same as organisation location\".\r\n *\r\n * Most gyms register one branch at the address they just typed on the\r\n * previous screen, and asking for it twice invites a typo between two things\r\n * that are meant to be the same place — one of which decides whether a\r\n * member standing at the door is close enough to check in.\r\n *\r\n * It is the CHOICE that is stored, not a copy of the answer. The address is\r\n * resolved from the organisation at submit, so an owner who goes back and\r\n * corrects the HQ address does not leave a stale duplicate behind on the\r\n * centre.\r\n */\r\n centerSameAsOrg: z.boolean(),\r\n /**\r\n * Nullable here and required by `refineGymRegistrationForm` instead, because\r\n * \"required\" is now conditional — see that function.\r\n */\r\n centerAddress: addressSchema.nullable(),\r\n floorArea: z.number().nullable(),\r\n centerPhone1: formPhone,\r\n centerPhone2: optionalFormPhone,\r\n centerEmail: optionalFormEmail,\r\n hours: z.object({\r\n mon: onboardingDayHoursSchema,\r\n tue: onboardingDayHoursSchema,\r\n wed: onboardingDayHoursSchema,\r\n thu: onboardingDayHoursSchema,\r\n fri: onboardingDayHoursSchema,\r\n sat: onboardingDayHoursSchema,\r\n sun: onboardingDayHoursSchema,\r\n }),\r\n photos: z.array(z.string()),\r\n // 3 — plan\r\n planId: requiredChoice(z.string(), 'Choose a plan'),\r\n tenureId: z.string().nullable(),\r\n};\r\n\r\n/**\r\n * The centre needs an address unless it is inheriting the organisation's.\r\n *\r\n * A cross-field rule, so it cannot live on `centerAddress` itself. The issue is\r\n * raised AT `centerAddress` deliberately: the wizard validates each step by\r\n * calling `trigger` with that step's field names, and an error parked on the\r\n * object root would match none of them — Continue would refuse to advance with\r\n * nothing on screen explaining why. That exact shape of bug has cost this\r\n * codebase two silent forms already.\r\n *\r\n * Structurally typed rather than tied to `GymRegistrationForm` so the web\r\n * client can apply the same function to its own extended schema.\r\n */\r\nexport function refineGymRegistrationForm(\r\n v: { centerSameAsOrg: boolean; centerAddress: Address | null },\r\n ctx: z.RefinementCtx,\r\n): void {\r\n if (v.centerSameAsOrg) return;\r\n if (v.centerAddress === null) {\r\n ctx.addIssue({ code: 'custom', path: ['centerAddress'], message: 'Add your address' });\r\n }\r\n}\r\n\r\nexport const gymRegistrationFormSchema = z\r\n .object(gymRegistrationFormShape)\r\n .superRefine(refineGymRegistrationForm);\r\n\r\n/** Hand-written working shape (nullable \"unset\" fields); see the member form. */\r\nexport type GymRegistrationForm = {\r\n orgName: string;\r\n orgDescription: string;\r\n hqAddress: OnboardingAddress;\r\n orgWebsite: string;\r\n gstNumber: string;\r\n cinNumber: string;\r\n numberOfCenters: number | null;\r\n orgSocials: OnboardingSocialLinks;\r\n centerName: string;\r\n centerDescription: string;\r\n centerTypes: CenterType[];\r\n /** \"Same as organisation location\" — see `gymRegistrationFormShape`. */\r\n centerSameAsOrg: boolean;\r\n centerAddress: OnboardingAddress;\r\n floorArea: number | null;\r\n centerPhone1: string;\r\n centerPhone2: string;\r\n centerEmail: string;\r\n hours: OnboardingWeeklyHours;\r\n photos: string[];\r\n planId: string | null;\r\n tenureId: string | null;\r\n};\r\n\r\nconst EMPTY_ONBOARDING_SOCIALS: OnboardingSocialLinks = {\r\n jansathi: '',\r\n youtube: '',\r\n instagram: '',\r\n facebook: '',\r\n};\r\n\r\n/** The opening hours a new center starts with. */\r\nexport const DEFAULT_ONBOARDING_HOURS: OnboardingWeeklyHours = {\r\n mon: { open: true, from: '06:00', to: '22:00' },\r\n tue: { open: true, from: '06:00', to: '22:00' },\r\n wed: { open: true, from: '06:00', to: '22:00' },\r\n thu: { open: true, from: '06:00', to: '22:00' },\r\n fri: { open: true, from: '06:00', to: '22:00' },\r\n sat: { open: true, from: '07:00', to: '21:00' },\r\n sun: { open: false, from: '07:00', to: '13:00' },\r\n};\r\n\r\nexport const EMPTY_GYM_REGISTRATION_FORM: GymRegistrationForm = {\r\n orgName: '',\r\n orgDescription: '',\r\n hqAddress: null,\r\n orgWebsite: '',\r\n gstNumber: '',\r\n cinNumber: '',\r\n numberOfCenters: null,\r\n orgSocials: { ...EMPTY_ONBOARDING_SOCIALS },\r\n centerName: '',\r\n centerDescription: '',\r\n centerTypes: [],\r\n // Unticked. A pre-ticked box would put the organisation's address on the\r\n // centre without the owner ever looking at it — and that address is what a\r\n // member's check-in is measured against.\r\n centerSameAsOrg: false,\r\n centerAddress: null,\r\n floorArea: null,\r\n centerPhone1: '',\r\n centerPhone2: '',\r\n centerEmail: '',\r\n hours: structuredClone(DEFAULT_ONBOARDING_HOURS),\r\n photos: [],\r\n planId: null,\r\n tenureId: null,\r\n};\r\n\r\n/**\r\n * The gym-registration wizard, described once.\r\n *\r\n * The last step is the pricing screen, which renders its own header; its title\r\n * and help line are unused by the wizard chrome but kept so every step has one.\r\n */\r\nexport const GYM_REGISTRATION_STEPS: readonly OnboardingStepConfig<\r\n keyof GymRegistrationForm & string\r\n>[] = [\r\n {\r\n key: 'organisation',\r\n title: 'Your organisation',\r\n helpText: 'Tell us about the business that runs the gym.',\r\n fields: ['orgName', 'hqAddress', 'numberOfCenters'],\r\n },\r\n {\r\n key: 'center',\r\n title: 'Your first center',\r\n helpText: 'Set up your primary center. You can add more later.',\r\n fields: [\r\n 'centerName',\r\n 'centerTypes',\r\n 'centerAddress',\r\n 'centerPhone1',\r\n 'centerPhone2',\r\n 'centerEmail',\r\n ],\r\n },\r\n {\r\n key: 'plan',\r\n title: 'Choose your plan',\r\n helpText: 'Pick the plan that fits your gym.',\r\n fields: ['planId', 'tenureId'],\r\n },\r\n] as const;\r\n\r\nexport const GYM_REGISTRATION_TOTAL_STEPS = GYM_REGISTRATION_STEPS.length;\r\n\r\n/**\r\n * Fields validated (via `form.trigger`) before advancing each step.\r\n * DERIVED from `GYM_REGISTRATION_STEPS` so the two can never disagree.\r\n */\r\nexport const GYM_REGISTRATION_STEP_FIELDS: (keyof GymRegistrationForm)[][] =\r\n GYM_REGISTRATION_STEPS.map((s) => [...s.fields]);\r\n\r\n// ─── Retained: the owner onboarding wizard ───────────────────────────────────\r\n// The gym-registration flow above replaces this, but the owner console's repo\r\n// still ships the four-step owner wizard and cannot migrate in the same change\r\n// that publishes this package. Nothing above imports any of it.\r\n//\r\n// TODO(onboarding-migration): delete once the owner wizard moves to\r\n// `GYM_REGISTRATION_STEPS`.\r\n\r\n// ─── Owner onboarding form ──────────────────────────────────────────────────\r\n\r\n/** Age bounds for a gym owner (the DOB itself stays optional). */\r\nexport const OWNER_MIN_AGE_YEARS = 16;\r\nexport const OWNER_MAX_AGE_YEARS = 100;\r\n\r\nexport const ownerOnboardingFormSchema = z\r\n .object({\r\n // 1 — basics\r\n firstName: z.string().trim().min(1, 'First name is required'),\r\n lastName: z.string().trim(),\r\n dobDay: z.string(),\r\n dobMonth: z.string(),\r\n dobYear: z.string(),\r\n gender: requiredChoice(z.enum(['male', 'female', 'other']), 'Select your gender'),\r\n // 2 — contact\r\n phone: formPhone,\r\n altEmail: optionalFormEmail,\r\n website: z.string(),\r\n noWebsite: z.boolean(),\r\n address: onboardingAddressSchema,\r\n socials: onboardingSocialLinksSchema,\r\n // 3 — organisation\r\n orgName: z.string().trim().min(1, 'Organisation name is required'),\r\n orgDescription: z.string(),\r\n hqAddress: onboardingAddressSchema,\r\n gstNumber: z.string(),\r\n cinNumber: z.string(),\r\n numberOfCenters: requiredChoice(z.number(), 'Add the number of centers'),\r\n orgSocials: onboardingSocialLinksSchema,\r\n // 4 — center\r\n centerName: z.string().trim().min(1, 'Center name is required'),\r\n centerDescription: z.string(),\r\n centerTypes: z.array(z.string()).min(1, 'Pick at least one'),\r\n centerAddress: onboardingAddressSchema,\r\n floorArea: z.number().nullable(),\r\n centerPhone1: formPhone,\r\n centerPhone2: optionalFormPhone,\r\n centerEmail: optionalFormEmail,\r\n hours: z.object({\r\n mon: onboardingDayHoursSchema,\r\n tue: onboardingDayHoursSchema,\r\n wed: onboardingDayHoursSchema,\r\n thu: onboardingDayHoursSchema,\r\n fri: onboardingDayHoursSchema,\r\n sat: onboardingDayHoursSchema,\r\n sun: onboardingDayHoursSchema,\r\n }),\r\n photos: z.array(z.string()),\r\n /** GCS URL of the owner's uploaded photo. Optional. */\r\n avatarUrl: z.string(),\r\n // 5 — plan\r\n planId: requiredChoice(z.string(), 'Choose a plan'),\r\n tenureId: z.string().nullable(),\r\n })\r\n /**\r\n * The owner's DOB is optional, so this only engages once any cell is touched:\r\n * then all three are required and must form a real, plausible date.\r\n */\r\n .superRefine((v, ctx) => {\r\n const touched = Boolean(v.dobDay || v.dobMonth || v.dobYear);\r\n if (!touched) return;\r\n\r\n const fail = (message: string) => ctx.addIssue({ code: 'custom', path: ['dobDay'], message });\r\n\r\n if (!v.dobDay || !v.dobMonth || v.dobYear.length !== 4) {\r\n fail('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 fail('That date does not exist');\r\n return;\r\n }\r\n\r\n const age = ageInYears(year, month, day);\r\n if (age < OWNER_MIN_AGE_YEARS) fail(`You must be at least ${OWNER_MIN_AGE_YEARS}`);\r\n else if (age > OWNER_MAX_AGE_YEARS) fail('Check the year of birth');\r\n });\r\n\r\n/** Hand-written working shape (nullable \"unset\" fields); see the member form. */\r\nexport type OwnerOnboardingForm = {\r\n firstName: string;\r\n lastName: string;\r\n dobDay: string;\r\n dobMonth: string;\r\n dobYear: string;\r\n gender: 'male' | 'female' | 'other' | null;\r\n phone: string;\r\n altEmail: string;\r\n website: string;\r\n noWebsite: boolean;\r\n address: OnboardingAddress;\r\n socials: OnboardingSocialLinks;\r\n orgName: string;\r\n orgDescription: string;\r\n hqAddress: OnboardingAddress;\r\n gstNumber: string;\r\n cinNumber: string;\r\n numberOfCenters: number | null;\r\n orgSocials: OnboardingSocialLinks;\r\n centerName: string;\r\n centerDescription: string;\r\n centerTypes: string[];\r\n centerAddress: OnboardingAddress;\r\n floorArea: number | null;\r\n centerPhone1: string;\r\n centerPhone2: string;\r\n centerEmail: string;\r\n hours: OnboardingWeeklyHours;\r\n photos: string[];\r\n avatarUrl: string;\r\n planId: string | null;\r\n tenureId: string | null;\r\n};\r\n\r\nexport const EMPTY_OWNER_ONBOARDING_FORM: OwnerOnboardingForm = {\r\n firstName: '',\r\n lastName: '',\r\n dobDay: '',\r\n dobMonth: '',\r\n dobYear: '',\r\n gender: null,\r\n phone: '',\r\n altEmail: '',\r\n website: '',\r\n noWebsite: false,\r\n address: null,\r\n socials: { ...EMPTY_ONBOARDING_SOCIALS },\r\n orgName: '',\r\n orgDescription: '',\r\n hqAddress: null,\r\n gstNumber: '',\r\n cinNumber: '',\r\n numberOfCenters: null,\r\n orgSocials: { ...EMPTY_ONBOARDING_SOCIALS },\r\n centerName: '',\r\n centerDescription: '',\r\n centerTypes: [],\r\n centerAddress: null,\r\n floorArea: null,\r\n centerPhone1: '',\r\n centerPhone2: '',\r\n centerEmail: '',\r\n hours: structuredClone(DEFAULT_ONBOARDING_HOURS),\r\n photos: [],\r\n avatarUrl: '',\r\n planId: null,\r\n tenureId: null,\r\n};\r\n\r\n/**\r\n * The owner wizard, described once.\r\n *\r\n * The DOB cells belong in step 1's `fields` even though the date is optional:\r\n * the refine above still rejects a half-typed date, and a field missing from\r\n * this map would be unreachable twice over — Continue would not surface it, and\r\n * the final submit (which finds the offending STEP by searching these lists)\r\n * would find no step and leave the plan screen dead.\r\n *\r\n * The last step is the pricing screen, which renders its own header; its title\r\n * and help line are unused by the wizard chrome but kept so every step has one.\r\n */\r\nexport const OWNER_ONBOARDING_STEPS: readonly OnboardingStepConfig<\r\n keyof OwnerOnboardingForm & string\r\n>[] = [\r\n {\r\n key: 'basics',\r\n title: 'Your details',\r\n helpText: 'A few basics about you.',\r\n fields: ['firstName', 'gender', 'dobDay', 'dobMonth', 'dobYear'],\r\n },\r\n {\r\n key: 'contact',\r\n title: 'Contact details',\r\n helpText: 'How members and we can reach you.',\r\n fields: ['phone', 'altEmail', 'address'],\r\n },\r\n {\r\n key: 'organisation',\r\n title: 'Organisation',\r\n helpText: 'Tell us about your business.',\r\n fields: ['orgName', 'hqAddress', 'numberOfCenters'],\r\n },\r\n {\r\n key: 'center',\r\n title: 'Your first center',\r\n helpText: 'Set up your primary center. You can add more later.',\r\n fields: [\r\n 'centerName',\r\n 'centerTypes',\r\n 'centerAddress',\r\n 'centerPhone1',\r\n 'centerPhone2',\r\n 'centerEmail',\r\n ],\r\n },\r\n {\r\n key: 'plan',\r\n title: 'Choose your plan',\r\n helpText: 'Pick the plan that fits your gym.',\r\n fields: ['planId', 'tenureId'],\r\n },\r\n] as const;\r\n\r\nexport const OWNER_ONBOARDING_TOTAL_STEPS = OWNER_ONBOARDING_STEPS.length;\r\n\r\n/**\r\n * Fields validated (via `form.trigger`) before advancing each owner step.\r\n * DERIVED from `OWNER_ONBOARDING_STEPS` so the two can never disagree.\r\n */\r\nexport const OWNER_STEP_FIELDS: (keyof OwnerOnboardingForm)[][] = OWNER_ONBOARDING_STEPS.map(\r\n (s) => [...s.fields],\r\n);\r\n\r\n// ─── Gym selection (member step 0) ──────────────────────────────────────────\r\n\r\nexport const gymSelectionSchema = z.object({\r\n gymId: z.string().trim().min(1, 'Enter your gym id or pick a gym below'),\r\n});\r\nexport type GymSelection = z.infer<typeof gymSelectionSchema>;\r\n"]}
|
|
1
|
+
{"version":3,"file":"onboarding-form.js","sourceRoot":"","sources":["../src/onboarding-form.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * gymmonk-schema — Onboarding & gym-registration FORM schemas\n * ===========================================================\n * The client-side validation contract for the member onboarding wizard and the\n * \"list your gym\" wizard.\n *\n * Why these live here and are distinct from the `*OnboardingBody` API schemas:\n * a half-filled form legitimately holds `null` for a choice the user has not\n * made yet, whereas the API body only ever sees completed values. So these are\n * the SAME rules expressed over the form's working shape — every message,\n * regex and bound is defined once, here, and both sides agree on it.\n *\n * Everything reuses the primitives in `shared.ts` / `common.ts`\n * (`INDIAN_STATES`, `CENTER_TYPES`, `WEEKDAYS`, phone/pincode rules) rather\n * than restating them.\n *\n * @module gymmonk-schema/onboarding-form\n */\n\nimport { z } from 'zod';\nimport { type CenterType, centerTypeSchema } from './center.js';\nimport { ageInYears, isRealDate, PHONE_REGEX } from './common.js';\nimport type { WEEKDAYS } from './shared.js';\nimport { type Address, addressSchema } from './shared.js';\n\n// ─── Field-level rules (shared by both wizards) ──────────────────────────────\n\n/** A required 10-digit Indian mobile, as typed into a form field. */\nconst formPhone = z.string().regex(PHONE_REGEX, 'Enter a valid 10-digit mobile number');\n\n/** An optional phone: blank is allowed, anything else must be valid. */\nconst optionalFormPhone = z\n .string()\n .refine((v) => v === '' || PHONE_REGEX.test(v), 'Enter a valid 10-digit mobile number');\n\n/** An optional email: blank is allowed, anything else must look like an email. */\nconst optionalFormEmail = z\n .string()\n .refine((v) => v === '' || /^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$/.test(v), {\n message: 'Enter a valid email',\n });\n\n/** A choice the user has not made yet is `null` until they pick one. */\nconst requiredChoice = <T extends z.ZodTypeAny>(inner: T, message: string) =>\n inner.nullable().refine((v: unknown) => v !== null, { message });\n\n// ─── Address (form shape: one atomic value, null until picked) ────────────\n\n/**\n * An address inside an onboarding form is ONE cell, not four.\n *\n * It used to be `street` + `city` + `state` + `pincode`, each independently\n * fillable, which is why this module needed rules for a half-filled address.\n * Now it is picked through the location drawer, which either hands back a\n * complete `Address` — area chain, ids, names, ancestry, coordinates — or\n * nothing at all. So there is no partial state left to validate, and `null`\n * simply means \"not chosen yet\".\n */\nexport const onboardingAddressSchema = requiredChoice(addressSchema, 'Add your address');\n\n/** Working shape of an address inside an onboarding form. */\nexport type OnboardingAddress = Address | null;\n\n/** An address has been picked. */\nexport function isOnboardingAddressComplete(a: OnboardingAddress): boolean {\n return a !== null;\n}\n\n// ─── Social links (form shape: every handle is a plain string) ───────────────\n\nexport const onboardingSocialLinksSchema = z.object({\n jansathi: z.string(),\n youtube: z.string(),\n instagram: z.string(),\n facebook: z.string(),\n});\nexport type OnboardingSocialLinks = z.infer<typeof onboardingSocialLinksSchema>;\n\n// ─── Opening hours (form shape) ─────────────────────────────────────────────\n\nexport const onboardingDayHoursSchema = z.object({\n open: z.boolean(),\n from: z.string(),\n to: z.string(),\n});\nexport type OnboardingDayHours = z.infer<typeof onboardingDayHoursSchema>;\nexport type OnboardingWeeklyHours = Record<(typeof WEEKDAYS)[number], OnboardingDayHours>;\n\n// ─── Member onboarding form ─────────────────────────────────────────────────\n\n/** Age bounds a gym membership realistically spans. */\nexport const MEMBER_MIN_AGE_YEARS = 10;\nexport const MEMBER_MAX_AGE_YEARS = 100;\n\n/**\n * The three-cell date-of-birth rule, in ONE place.\n *\n * Each cell is bounded by its own input, but only the assembled date can catch\n * an impossible one (31 Feb) or an implausible age. The issue is attached to\n * `dobDay` so it renders under the field group rather than under one cell.\n *\n * Exported because the member's own profile editor asks for the SAME date with\n * the same three cells. When the rule lived inline in the wizard, that editor\n * had a free-text box parsed with `new Date()` instead, which accepted \"March\n * 18, 1994\", accepted nonsense just as happily, and silently dropped whatever\n * it could not read.\n */\nexport function refineMemberDob(\n v: { dobDay: string; dobMonth: string; dobYear: string },\n ctx: z.RefinementCtx,\n): void {\n if (!v.dobDay || !v.dobMonth || v.dobYear.length !== 4) return;\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 < MEMBER_MIN_AGE_YEARS) {\n ctx.addIssue({\n code: 'custom',\n path: ['dobDay'],\n message: `You must be at least ${MEMBER_MIN_AGE_YEARS} to join`,\n });\n } else if (age > MEMBER_MAX_AGE_YEARS) {\n ctx.addIssue({ code: 'custom', path: ['dobDay'], message: 'Check the year of birth' });\n }\n}\n\nexport const memberOnboardingFormSchema = z\n .object({\n firstName: z.string().trim().min(1, 'First name is required'),\n lastName: z.string().trim(),\n dobDay: z.string().min(1, 'Enter the day'),\n dobMonth: z.string().min(1, 'Enter the month'),\n dobYear: z.string().length(4, 'Enter a 4-digit year'),\n gender: requiredChoice(z.enum(['male', 'female', 'other']), 'Select your gender'),\n heightCm: requiredChoice(z.number(), 'Add your height'),\n weightKg: requiredChoice(z.number(), 'Add your weight'),\n neckCm: requiredChoice(z.number(), 'Add your neck'),\n waistCm: requiredChoice(z.number(), 'Add your waist'),\n hipsCm: z.number().nullable(),\n /**\n * The member's OWN number, asked on step one beside their name.\n *\n * It moved here when the emergency step was removed. Emergency contact and\n * blood group genuinely belong in the profile — nobody needs them to\n * finish signing up — but this one is how the gym reaches the member at\n * all, and it drives every chase action on the owner's roster card. Left\n * on the removed step it would simply have stopped being collected.\n */\n phone: formPhone,\n /** GCS URL of the uploaded photo. Optional — it can be added later. */\n avatarUrl: z.string(),\n })\n .superRefine(refineMemberDob)\n /**\n * The API requires the hips measurement for female members (needed for the\n * body-fat estimate), so it fails inline on the measurements step instead of\n * 422-ing at Finish. Own refine — no early-return coupling with the DOB rule.\n */\n .superRefine((v, ctx) => {\n if (v.gender === 'female' && v.hipsCm == null) {\n ctx.addIssue({\n code: 'custom',\n path: ['hipsCm'],\n message: 'Hips measurement is required',\n });\n }\n });\n\n/**\n * The form's working shape. Hand-written (not `z.infer`) so unset numeric /\n * choice fields can hold `null` while the schema's `requiredChoice` rules still\n * drive the field errors — the resolver is cast to this type at `useForm`.\n */\nexport type MemberOnboardingForm = {\n firstName: string;\n lastName: string;\n dobDay: string;\n dobMonth: string;\n dobYear: string;\n gender: 'male' | 'female' | 'other' | null;\n heightCm: number | null;\n weightKg: number | null;\n neckCm: number | null;\n waistCm: number | null;\n hipsCm: number | null;\n phone: string;\n avatarUrl: string;\n};\n\nexport const EMPTY_MEMBER_ONBOARDING_FORM: MemberOnboardingForm = {\n firstName: '',\n lastName: '',\n dobDay: '',\n dobMonth: '',\n dobYear: '',\n gender: null,\n heightCm: null,\n weightKg: null,\n neckCm: null,\n waistCm: null,\n hipsCm: null,\n phone: '',\n avatarUrl: '',\n};\n\n/**\n * One onboarding step, described ONCE.\n *\n * The wizard chrome (screen name, help line, progress denominator) and the\n * validation gate used to live in the page as a local `META` array beside a\n * separate field map — two lists that had to be kept the same length by hand.\n * They are one list here, so a step cannot exist without a title or be left\n * without a validation gate.\n */\nexport interface OnboardingStepConfig<TField extends string> {\n /** Stable identifier — safe for analytics and resume, unlike the index. */\n key: string;\n /** Screen name in the step header. */\n title: string;\n /** Help line under the screen name. */\n helpText: string;\n /**\n * Fields validated before this step may be left. Naming a parent path (e.g.\n * `address`) validates its nested fields too.\n */\n fields: readonly TField[];\n}\n\nexport const MEMBER_ONBOARDING_STEPS: readonly OnboardingStepConfig<\n keyof MemberOnboardingForm & string\n>[] = [\n {\n key: 'basics',\n title: 'Basic information',\n helpText: \"Enter your details, we'll need these to set things up.\",\n fields: ['firstName', 'dobDay', 'dobMonth', 'dobYear', 'gender', 'phone'],\n },\n {\n key: 'body-metrics',\n title: 'Personal details',\n helpText: 'Your height and weight, to calculate your BMI.',\n fields: ['heightCm', 'weightKg'],\n },\n {\n key: 'body-composition',\n title: 'Personal details',\n helpText: 'A few measurements, to estimate your body fat.',\n fields: ['neckCm', 'waistCm', 'hipsCm'],\n },\n] as const;\n\nexport const MEMBER_ONBOARDING_TOTAL_STEPS = MEMBER_ONBOARDING_STEPS.length;\n\n/**\n * Fields validated before advancing each member step.\n * DERIVED from `MEMBER_ONBOARDING_STEPS` so the two can never disagree.\n */\nexport const MEMBER_STEP_FIELDS: (keyof MemberOnboardingForm)[][] = MEMBER_ONBOARDING_STEPS.map(\n (s) => [...s.fields],\n);\n\n// ─── Gym registration form (\"list your gym\") ────────────────────────────────\n\n/**\n * The gym-registration wizard's working shape.\n *\n * THREE steps, not five. The two it used to open with — \"your details\" and\n * \"contact details\" — asked a signed-in member for their name, gender, date of\n * birth and phone, every one of which onboarding already captured. Registering\n * a business asks about the BUSINESS; the person is already known.\n */\n/**\n * The gym-registration form's FIELD SHAPE, exported separately from the schema.\n *\n * Separate because the schema carries a `superRefine` (see\n * `refineGymRegistrationForm`) and a refined object cannot be `.extend()`ed —\n * zod hands back a ZodEffects, and calling `.extend` on one throws at MODULE\n * LOAD, which no build catches and which takes the whole page down on import.\n * The web client adds the two pasted Maps links to this form, so it needs the\n * shape and the refinement as separate pieces it can recombine.\n */\nexport const gymRegistrationFormShape = {\n // 1 — organisation\n orgName: z.string().trim().min(1, 'Organisation name is required'),\n orgDescription: z.string(),\n hqAddress: onboardingAddressSchema,\n orgWebsite: z.string(),\n gstNumber: z.string(),\n cinNumber: z.string(),\n numberOfCenters: requiredChoice(z.number(), 'Add the number of centers'),\n orgSocials: onboardingSocialLinksSchema,\n // 2 — center\n centerName: z.string().trim().min(1, 'Center name is required'),\n centerDescription: z.string(),\n /**\n * The real enum, not `z.array(z.string())`.\n *\n * It was strings, and `toGymRegistrationBody` then CAST them to the enum on\n * the way to the API. A cast is not a check: retiring a centre type left the\n * wizard perfectly happy to submit the retired value and the server to\n * reject the whole registration, with the owner told only that something was\n * wrong. Now a value the API will not accept cannot leave the form.\n */\n centerTypes: z.array(centerTypeSchema).min(1, 'Pick at least one'),\n /**\n * \"Same as organisation location\".\n *\n * Most gyms register one branch at the address they just typed on the\n * previous screen, and asking for it twice invites a typo between two things\n * that are meant to be the same place — one of which decides whether a\n * member standing at the door is close enough to check in.\n *\n * It is the CHOICE that is stored, not a copy of the answer. The address is\n * resolved from the organisation at submit, so an owner who goes back and\n * corrects the HQ address does not leave a stale duplicate behind on the\n * centre.\n */\n centerSameAsOrg: z.boolean(),\n /**\n * Nullable here and required by `refineGymRegistrationForm` instead, because\n * \"required\" is now conditional — see that function.\n */\n centerAddress: addressSchema.nullable(),\n floorArea: z.number().nullable(),\n centerPhone1: formPhone,\n centerPhone2: optionalFormPhone,\n centerEmail: optionalFormEmail,\n hours: z.object({\n mon: onboardingDayHoursSchema,\n tue: onboardingDayHoursSchema,\n wed: onboardingDayHoursSchema,\n thu: onboardingDayHoursSchema,\n fri: onboardingDayHoursSchema,\n sat: onboardingDayHoursSchema,\n sun: onboardingDayHoursSchema,\n }),\n photos: z.array(z.string()),\n // 3 — plan\n planId: requiredChoice(z.string(), 'Choose a plan'),\n tenureId: z.string().nullable(),\n};\n\n/**\n * The centre needs an address unless it is inheriting the organisation's.\n *\n * A cross-field rule, so it cannot live on `centerAddress` itself. The issue is\n * raised AT `centerAddress` deliberately: the wizard validates each step by\n * calling `trigger` with that step's field names, and an error parked on the\n * object root would match none of them — Continue would refuse to advance with\n * nothing on screen explaining why. That exact shape of bug has cost this\n * codebase two silent forms already.\n *\n * Structurally typed rather than tied to `GymRegistrationForm` so the web\n * client can apply the same function to its own extended schema.\n */\nexport function refineGymRegistrationForm(\n v: { centerSameAsOrg: boolean; centerAddress: Address | null },\n ctx: z.RefinementCtx,\n): void {\n if (v.centerSameAsOrg) return;\n if (v.centerAddress === null) {\n ctx.addIssue({ code: 'custom', path: ['centerAddress'], message: 'Add your address' });\n }\n}\n\nexport const gymRegistrationFormSchema = z\n .object(gymRegistrationFormShape)\n .superRefine(refineGymRegistrationForm);\n\n/** Hand-written working shape (nullable \"unset\" fields); see the member form. */\nexport type GymRegistrationForm = {\n orgName: string;\n orgDescription: string;\n hqAddress: OnboardingAddress;\n orgWebsite: string;\n gstNumber: string;\n cinNumber: string;\n numberOfCenters: number | null;\n orgSocials: OnboardingSocialLinks;\n centerName: string;\n centerDescription: string;\n centerTypes: CenterType[];\n /** \"Same as organisation location\" — see `gymRegistrationFormShape`. */\n centerSameAsOrg: boolean;\n centerAddress: OnboardingAddress;\n floorArea: number | null;\n centerPhone1: string;\n centerPhone2: string;\n centerEmail: string;\n hours: OnboardingWeeklyHours;\n photos: string[];\n planId: string | null;\n tenureId: string | null;\n};\n\nconst EMPTY_ONBOARDING_SOCIALS: OnboardingSocialLinks = {\n jansathi: '',\n youtube: '',\n instagram: '',\n facebook: '',\n};\n\n/** The opening hours a new center starts with. */\nexport const DEFAULT_ONBOARDING_HOURS: OnboardingWeeklyHours = {\n mon: { open: true, from: '06:00', to: '22:00' },\n tue: { open: true, from: '06:00', to: '22:00' },\n wed: { open: true, from: '06:00', to: '22:00' },\n thu: { open: true, from: '06:00', to: '22:00' },\n fri: { open: true, from: '06:00', to: '22:00' },\n sat: { open: true, from: '07:00', to: '21:00' },\n sun: { open: false, from: '07:00', to: '13:00' },\n};\n\nexport const EMPTY_GYM_REGISTRATION_FORM: GymRegistrationForm = {\n orgName: '',\n orgDescription: '',\n hqAddress: null,\n orgWebsite: '',\n gstNumber: '',\n cinNumber: '',\n numberOfCenters: null,\n orgSocials: { ...EMPTY_ONBOARDING_SOCIALS },\n centerName: '',\n centerDescription: '',\n centerTypes: [],\n // Unticked. A pre-ticked box would put the organisation's address on the\n // centre without the owner ever looking at it — and that address is what a\n // member's check-in is measured against.\n centerSameAsOrg: false,\n centerAddress: null,\n floorArea: null,\n centerPhone1: '',\n centerPhone2: '',\n centerEmail: '',\n hours: structuredClone(DEFAULT_ONBOARDING_HOURS),\n photos: [],\n planId: null,\n tenureId: null,\n};\n\n/**\n * The gym-registration wizard, described once.\n *\n * The last step is the pricing screen, which renders its own header; its title\n * and help line are unused by the wizard chrome but kept so every step has one.\n */\nexport const GYM_REGISTRATION_STEPS: readonly OnboardingStepConfig<\n keyof GymRegistrationForm & string\n>[] = [\n {\n key: 'organisation',\n title: 'Your organisation',\n helpText: 'Tell us about the business that runs the gym.',\n fields: ['orgName', 'hqAddress', 'numberOfCenters'],\n },\n {\n key: 'center',\n title: 'Your first center',\n helpText: 'Set up your primary center. You can add more later.',\n fields: [\n 'centerName',\n 'centerTypes',\n 'centerAddress',\n 'centerPhone1',\n 'centerPhone2',\n 'centerEmail',\n ],\n },\n {\n key: 'plan',\n title: 'Choose your plan',\n helpText: 'Pick the plan that fits your gym.',\n fields: ['planId', 'tenureId'],\n },\n] as const;\n\nexport const GYM_REGISTRATION_TOTAL_STEPS = GYM_REGISTRATION_STEPS.length;\n\n/**\n * Fields validated (via `form.trigger`) before advancing each step.\n * DERIVED from `GYM_REGISTRATION_STEPS` so the two can never disagree.\n */\nexport const GYM_REGISTRATION_STEP_FIELDS: (keyof GymRegistrationForm)[][] =\n GYM_REGISTRATION_STEPS.map((s) => [...s.fields]);\n\n// ─── Retained: the owner onboarding wizard ───────────────────────────────────\n// The gym-registration flow above replaces this, but the owner console's repo\n// still ships the four-step owner wizard and cannot migrate in the same change\n// that publishes this package. Nothing above imports any of it.\n//\n// TODO(onboarding-migration): delete once the owner wizard moves to\n// `GYM_REGISTRATION_STEPS`.\n\n// ─── Owner onboarding form ──────────────────────────────────────────────────\n\n/** Age bounds for a gym owner (the DOB itself stays optional). */\nexport const OWNER_MIN_AGE_YEARS = 16;\nexport const OWNER_MAX_AGE_YEARS = 100;\n\nexport const ownerOnboardingFormSchema = z\n .object({\n // 1 — basics\n firstName: z.string().trim().min(1, 'First name is required'),\n lastName: z.string().trim(),\n dobDay: z.string(),\n dobMonth: z.string(),\n dobYear: z.string(),\n gender: requiredChoice(z.enum(['male', 'female', 'other']), 'Select your gender'),\n // 2 — contact\n phone: formPhone,\n altEmail: optionalFormEmail,\n website: z.string(),\n noWebsite: z.boolean(),\n address: onboardingAddressSchema,\n socials: onboardingSocialLinksSchema,\n // 3 — organisation\n orgName: z.string().trim().min(1, 'Organisation name is required'),\n orgDescription: z.string(),\n hqAddress: onboardingAddressSchema,\n gstNumber: z.string(),\n cinNumber: z.string(),\n numberOfCenters: requiredChoice(z.number(), 'Add the number of centers'),\n orgSocials: onboardingSocialLinksSchema,\n // 4 — center\n centerName: z.string().trim().min(1, 'Center name is required'),\n centerDescription: z.string(),\n centerTypes: z.array(z.string()).min(1, 'Pick at least one'),\n centerAddress: onboardingAddressSchema,\n floorArea: z.number().nullable(),\n centerPhone1: formPhone,\n centerPhone2: optionalFormPhone,\n centerEmail: optionalFormEmail,\n hours: z.object({\n mon: onboardingDayHoursSchema,\n tue: onboardingDayHoursSchema,\n wed: onboardingDayHoursSchema,\n thu: onboardingDayHoursSchema,\n fri: onboardingDayHoursSchema,\n sat: onboardingDayHoursSchema,\n sun: onboardingDayHoursSchema,\n }),\n photos: z.array(z.string()),\n /** GCS URL of the owner's uploaded photo. Optional. */\n avatarUrl: z.string(),\n // 5 — plan\n planId: requiredChoice(z.string(), 'Choose a plan'),\n tenureId: z.string().nullable(),\n })\n /**\n * The owner's DOB is optional, so this only engages once any cell is touched:\n * then all three are required and must form a real, plausible date.\n */\n .superRefine((v, ctx) => {\n const touched = Boolean(v.dobDay || v.dobMonth || v.dobYear);\n if (!touched) return;\n\n const fail = (message: string) => ctx.addIssue({ code: 'custom', path: ['dobDay'], message });\n\n if (!v.dobDay || !v.dobMonth || v.dobYear.length !== 4) {\n fail('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 fail('That date does not exist');\n return;\n }\n\n const age = ageInYears(year, month, day);\n if (age < OWNER_MIN_AGE_YEARS) fail(`You must be at least ${OWNER_MIN_AGE_YEARS}`);\n else if (age > OWNER_MAX_AGE_YEARS) fail('Check the year of birth');\n });\n\n/** Hand-written working shape (nullable \"unset\" fields); see the member form. */\nexport type OwnerOnboardingForm = {\n firstName: string;\n lastName: string;\n dobDay: string;\n dobMonth: string;\n dobYear: string;\n gender: 'male' | 'female' | 'other' | null;\n phone: string;\n altEmail: string;\n website: string;\n noWebsite: boolean;\n address: OnboardingAddress;\n socials: OnboardingSocialLinks;\n orgName: string;\n orgDescription: string;\n hqAddress: OnboardingAddress;\n gstNumber: string;\n cinNumber: string;\n numberOfCenters: number | null;\n orgSocials: OnboardingSocialLinks;\n centerName: string;\n centerDescription: string;\n centerTypes: string[];\n centerAddress: OnboardingAddress;\n floorArea: number | null;\n centerPhone1: string;\n centerPhone2: string;\n centerEmail: string;\n hours: OnboardingWeeklyHours;\n photos: string[];\n avatarUrl: string;\n planId: string | null;\n tenureId: string | null;\n};\n\nexport const EMPTY_OWNER_ONBOARDING_FORM: OwnerOnboardingForm = {\n firstName: '',\n lastName: '',\n dobDay: '',\n dobMonth: '',\n dobYear: '',\n gender: null,\n phone: '',\n altEmail: '',\n website: '',\n noWebsite: false,\n address: null,\n socials: { ...EMPTY_ONBOARDING_SOCIALS },\n orgName: '',\n orgDescription: '',\n hqAddress: null,\n gstNumber: '',\n cinNumber: '',\n numberOfCenters: null,\n orgSocials: { ...EMPTY_ONBOARDING_SOCIALS },\n centerName: '',\n centerDescription: '',\n centerTypes: [],\n centerAddress: null,\n floorArea: null,\n centerPhone1: '',\n centerPhone2: '',\n centerEmail: '',\n hours: structuredClone(DEFAULT_ONBOARDING_HOURS),\n photos: [],\n avatarUrl: '',\n planId: null,\n tenureId: null,\n};\n\n/**\n * The owner wizard, described once.\n *\n * The DOB cells belong in step 1's `fields` even though the date is optional:\n * the refine above still rejects a half-typed date, and a field missing from\n * this map would be unreachable twice over — Continue would not surface it, and\n * the final submit (which finds the offending STEP by searching these lists)\n * would find no step and leave the plan screen dead.\n *\n * The last step is the pricing screen, which renders its own header; its title\n * and help line are unused by the wizard chrome but kept so every step has one.\n */\nexport const OWNER_ONBOARDING_STEPS: readonly OnboardingStepConfig<\n keyof OwnerOnboardingForm & string\n>[] = [\n {\n key: 'basics',\n title: 'Your details',\n helpText: 'A few basics about you.',\n fields: ['firstName', 'gender', 'dobDay', 'dobMonth', 'dobYear'],\n },\n {\n key: 'contact',\n title: 'Contact details',\n helpText: 'How members and we can reach you.',\n fields: ['phone', 'altEmail', 'address'],\n },\n {\n key: 'organisation',\n title: 'Organisation',\n helpText: 'Tell us about your business.',\n fields: ['orgName', 'hqAddress', 'numberOfCenters'],\n },\n {\n key: 'center',\n title: 'Your first center',\n helpText: 'Set up your primary center. You can add more later.',\n fields: [\n 'centerName',\n 'centerTypes',\n 'centerAddress',\n 'centerPhone1',\n 'centerPhone2',\n 'centerEmail',\n ],\n },\n {\n key: 'plan',\n title: 'Choose your plan',\n helpText: 'Pick the plan that fits your gym.',\n fields: ['planId', 'tenureId'],\n },\n] as const;\n\nexport const OWNER_ONBOARDING_TOTAL_STEPS = OWNER_ONBOARDING_STEPS.length;\n\n/**\n * Fields validated (via `form.trigger`) before advancing each owner step.\n * DERIVED from `OWNER_ONBOARDING_STEPS` so the two can never disagree.\n */\nexport const OWNER_STEP_FIELDS: (keyof OwnerOnboardingForm)[][] = OWNER_ONBOARDING_STEPS.map(\n (s) => [...s.fields],\n);\n\n// ─── Gym selection (member step 0) ──────────────────────────────────────────\n\nexport const gymSelectionSchema = z.object({\n gymId: z.string().trim().min(1, 'Enter your gym id or pick a gym below'),\n});\nexport type GymSelection = z.infer<typeof gymSelectionSchema>;\n"]}
|
package/dist/onboarding.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"onboarding.js","sourceRoot":"","sources":["../src/onboarding.ts"],"names":[],"mappings":"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","sourcesContent":["/**\r\n * gymmonk-schema — Onboarding submit payload\r\n * ===========================================\r\n * The end-of-flow POST body, plus the resume cursor.\r\n *\r\n * ONE FLOW, ONE ROLE. Everybody who signs up onboards as a MEMBER — there is no\r\n * \"are you an owner or a member?\" fork any more. Running a gym is something a\r\n * member goes on to do (`gym-registration.ts`), not a different kind of account,\r\n * which is why an owner can also be an ordinary member somewhere else and why\r\n * nobody has to guess their role before they have seen the product.\r\n *\r\n * Onboarding also no longer picks a gym. A new member finishes with no gym\r\n * attached and finds one from their Me tab, so choosing a gym is a repeatable\r\n * action (leave, get declined, move city) rather than a one-shot step buried in\r\n * a flow they can never re-enter.\r\n *\r\n * @module gymmonk-schema/onboarding\r\n */\r\n\r\nimport { z } from 'zod';\r\nimport { createCenterBodySchema } from './center.js';\r\nimport { centerCodeSchema } from './codes.js';\r\nimport {\r\n isoDateSchema,\r\n optionalEmailSchema,\r\n optionalTrimmedString,\r\n phoneSchema,\r\n} from './common.js';\r\nimport { bodyMetricsSchema, emergencyContactSchema } from './member-profile.js';\r\nimport { createOrganisationBodySchema } from './organisation.js';\r\nimport { addressSchema, bloodGroupSchema, genderSchema, socialLinksSchema } from './shared.js';\r\n\r\n// ─── Retained: role (hoisted) ────────────────────────────────────────────────\r\n// Declared here, not with the rest of the retained block at the foot of the\r\n// file, because the progress cursor below references it — and a `const` used\r\n// before its initialiser is a module-load ReferenceError that no build catches.\r\nexport const onboardingRoleSchema = z.enum(['owner', 'member']);\r\nexport type OnboardingRole = z.infer<typeof onboardingRoleSchema>;\r\n\r\n// ─── Member onboarding ───────────────────────────────────────────────────────\r\n\r\nexport const memberOnboardingBodySchema = z\r\n .object({\r\n /**\r\n * The gym code picked at the \"find your gym\" step.\r\n *\r\n * RETAINED, and optional: the one-flow refactor finishes onboarding with\r\n * no gym attached, but the two-role flow still in the owner console's\r\n * repo sends it. Optional so neither side has to change.\r\n */\r\n gymCode: centerCodeSchema.optional(),\r\n firstName: z.string().trim().min(1, 'First name is required').max(80),\r\n lastName: optionalTrimmedString(80),\r\n gender: genderSchema,\r\n dob: isoDateSchema,\r\n phone: phoneSchema,\r\n body: bodyMetricsSchema,\r\n /**\r\n * OPTIONAL, both of them, because onboarding no longer asks.\r\n *\r\n * They were collected on a final \"Emergency info\" step that stood between\r\n * a new member and a finished account for information nobody needs to\r\n * create one. The member fills them in from their own profile, where the\r\n * Legal & Emergency editor already writes exactly these two facts.\r\n *\r\n * Blood group being optional is the SAFETY half of this. The client used\r\n * to send `bloodGroup ?? 'O+'`, so a member who skipped the question was\r\n * recorded as O-positive — a real blood group, indistinguishable from an\r\n * answer, sitting on the screen a gym would read in an emergency. Absent\r\n * has to stay absent.\r\n */\r\n emergencyContact: emergencyContactSchema.optional(),\r\n bloodGroup: bloodGroupSchema.optional(),\r\n /** Uploaded profile photo → stored on the shared `User.avatar`. */\r\n avatarUrl: optionalTrimmedString(2000),\r\n })\r\n .refine((v) => v.body.heightCm != null && v.body.weightKg != null, {\r\n message: 'Height and weight are required',\r\n path: ['body'],\r\n })\r\n .refine((v) => v.body.neckCm != null && v.body.waistCm != null, {\r\n message: 'Neck and waist are required',\r\n path: ['body'],\r\n })\r\n .refine((v) => v.gender !== 'female' || v.body.hipsCm != null, {\r\n message: 'Hips measurement is required',\r\n path: ['body', 'hipsCm'],\r\n });\r\nexport type MemberOnboardingBody = z.infer<typeof memberOnboardingBodySchema>;\r\n\r\n// ─── Onboarding progress & resume ─────────────────────────────────────────────\r\n\r\n/**\r\n * In-progress onboarding draft: the UI-shaped, partially-filled form the client\r\n * persists so a returning user resumes with their typed data intact. Opaque to\r\n * the backend by design — the client owns the form shape, and the data is\r\n * re-validated strictly by the member body schema at finalize. Keys are form\r\n * field names; the backend caps the stored size.\r\n */\r\nexport const onboardingDraftSchema = z.record(z.string(), z.unknown());\r\nexport type OnboardingDraft = z.infer<typeof onboardingDraftSchema>;\r\n\r\n/**\r\n * The server-persisted onboarding cursor: how far the user reached (`step`, the\r\n * 1-indexed sub-step of the member form) and their draft. Read on app entry to\r\n * resume to the exact step. `welcomed` records that the one-time welcome\r\n * carousel has been seen.\r\n */\r\nexport const onboardingProgressSchema = z.object({\r\n welcomed: z.boolean(),\r\n step: z.number().int().min(0).max(20),\r\n memberDraft: onboardingDraftSchema.optional(),\r\n // ── Retained for the two-role flow (see the note at the foot of this file) ──\r\n role: onboardingRoleSchema.nullish(),\r\n gymCode: centerCodeSchema.optional(),\r\n ownerDraft: onboardingDraftSchema.optional(),\r\n});\r\nexport type OnboardingProgress = z.infer<typeof onboardingProgressSchema>;\r\n\r\n/** `PUT /onboarding/progress` body — any subset of the cursor, merged server-side. */\r\nexport const onboardingProgressUpdateSchema = z\r\n .object({\r\n welcomed: z.boolean(),\r\n step: z.number().int().min(0).max(20),\r\n memberDraft: onboardingDraftSchema,\r\n role: onboardingRoleSchema.nullable(),\r\n gymCode: centerCodeSchema,\r\n ownerDraft: onboardingDraftSchema,\r\n })\r\n .partial();\r\nexport type OnboardingProgressUpdate = z.infer<typeof onboardingProgressUpdateSchema>;\r\n\r\n/**\r\n * `GET /onboarding/status` response: completion truth (derived from the user's\r\n * member profile) plus the resume cursor.\r\n *\r\n * `organisationId` is here rather than a `role` field. Onboarding no longer has\r\n * a role to report — everyone finishes it as a member — so what the app shells\r\n * actually need to know is whether this person also runs a gym, which is a fact\r\n * about their organisation, not about their onboarding.\r\n */\r\nexport const onboardingStatusSchema = z.object({\r\n onboarded: z.boolean(),\r\n /** The gym business they own, if any. Null for a plain member. */\r\n organisationId: z.string().nullable(),\r\n welcomed: z.boolean(),\r\n step: z.number().int().min(0),\r\n memberDraft: onboardingDraftSchema.optional(),\r\n role: onboardingRoleSchema.nullish(),\r\n gymCode: z.string().optional(),\r\n ownerDraft: onboardingDraftSchema.optional(),\r\n});\r\nexport type OnboardingStatus = z.infer<typeof onboardingStatusSchema>;\r\n\r\n// ─── Retained: the two-role onboarding flow ──────────────────────────────────\r\n//\r\n// The one-flow refactor above removed these. They are kept because the owner\r\n// console's repo still implements the two-role wizard (role fork, owner basics,\r\n// organisation + first centre, SaaS plan) and cannot migrate in the same change\r\n// that publishes this package. Nothing in the one-flow path imports them, so\r\n// they cost its consumers nothing.\r\n//\r\n// TODO(onboarding-migration): delete this block once the owner wizard moves to\r\n// `gym-registration.ts`.\r\n\r\n/** The owner's personal basics captured before the organisation/center. */\r\nexport const ownerBasicsSchema = z.object({\r\n firstName: z.string().trim().min(1, 'First name is required').max(80),\r\n lastName: optionalTrimmedString(80),\r\n gender: genderSchema,\r\n dob: isoDateSchema.optional(),\r\n phone: phoneSchema,\r\n altEmail: optionalEmailSchema,\r\n website: optionalTrimmedString(200),\r\n socials: socialLinksSchema,\r\n /** The owner's personal contact address (step-2 of owner onboarding). */\r\n address: addressSchema.optional(),\r\n /** Uploaded profile photo → stored on the shared `User.avatar`. */\r\n avatarUrl: optionalTrimmedString(2000),\r\n});\r\nexport type OwnerBasics = z.infer<typeof ownerBasicsSchema>;\r\n\r\n/** The SaaS plan an owner selects at the final onboarding step. */\r\nexport const onboardingPlanSelectionSchema = z.object({\r\n planId: z.string().trim().min(1),\r\n tenureId: z.string().trim().min(1).nullable(),\r\n});\r\nexport type OnboardingPlanSelection = z.infer<typeof onboardingPlanSelectionSchema>;\r\n\r\nexport const ownerOnboardingBodySchema = z.object({\r\n owner: ownerBasicsSchema,\r\n organisation: createOrganisationBodySchema,\r\n center: createCenterBodySchema,\r\n subscription: onboardingPlanSelectionSchema,\r\n});\r\nexport type OwnerOnboardingBody = z.infer<typeof ownerOnboardingBodySchema>;\r\n"]}
|
|
1
|
+
{"version":3,"file":"onboarding.js","sourceRoot":"","sources":["../src/onboarding.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * gymmonk-schema — Onboarding submit payload\n * ===========================================\n * The end-of-flow POST body, plus the resume cursor.\n *\n * ONE FLOW, ONE ROLE. Everybody who signs up onboards as a MEMBER — there is no\n * \"are you an owner or a member?\" fork any more. Running a gym is something a\n * member goes on to do (`gym-registration.ts`), not a different kind of account,\n * which is why an owner can also be an ordinary member somewhere else and why\n * nobody has to guess their role before they have seen the product.\n *\n * Onboarding also no longer picks a gym. A new member finishes with no gym\n * attached and finds one from their Me tab, so choosing a gym is a repeatable\n * action (leave, get declined, move city) rather than a one-shot step buried in\n * a flow they can never re-enter.\n *\n * @module gymmonk-schema/onboarding\n */\n\nimport { z } from 'zod';\nimport { createCenterBodySchema } from './center.js';\nimport { centerCodeSchema } from './codes.js';\nimport {\n isoDateSchema,\n optionalEmailSchema,\n optionalTrimmedString,\n phoneSchema,\n} from './common.js';\nimport { bodyMetricsSchema, emergencyContactSchema } from './member-profile.js';\nimport { createOrganisationBodySchema } from './organisation.js';\nimport { addressSchema, bloodGroupSchema, genderSchema, socialLinksSchema } from './shared.js';\n\n// ─── Retained: role (hoisted) ────────────────────────────────────────────────\n// Declared here, not with the rest of the retained block at the foot of the\n// file, because the progress cursor below references it — and a `const` used\n// before its initialiser is a module-load ReferenceError that no build catches.\nexport const onboardingRoleSchema = z.enum(['owner', 'member']);\nexport type OnboardingRole = z.infer<typeof onboardingRoleSchema>;\n\n// ─── Member onboarding ───────────────────────────────────────────────────────\n\nexport const memberOnboardingBodySchema = z\n .object({\n /**\n * The gym code picked at the \"find your gym\" step.\n *\n * RETAINED, and optional: the one-flow refactor finishes onboarding with\n * no gym attached, but the two-role flow still in the owner console's\n * repo sends it. Optional so neither side has to change.\n */\n gymCode: centerCodeSchema.optional(),\n firstName: z.string().trim().min(1, 'First name is required').max(80),\n lastName: optionalTrimmedString(80),\n gender: genderSchema,\n dob: isoDateSchema,\n phone: phoneSchema,\n body: bodyMetricsSchema,\n /**\n * OPTIONAL, both of them, because onboarding no longer asks.\n *\n * They were collected on a final \"Emergency info\" step that stood between\n * a new member and a finished account for information nobody needs to\n * create one. The member fills them in from their own profile, where the\n * Legal & Emergency editor already writes exactly these two facts.\n *\n * Blood group being optional is the SAFETY half of this. The client used\n * to send `bloodGroup ?? 'O+'`, so a member who skipped the question was\n * recorded as O-positive — a real blood group, indistinguishable from an\n * answer, sitting on the screen a gym would read in an emergency. Absent\n * has to stay absent.\n */\n emergencyContact: emergencyContactSchema.optional(),\n bloodGroup: bloodGroupSchema.optional(),\n /** Uploaded profile photo → stored on the shared `User.avatar`. */\n avatarUrl: optionalTrimmedString(2000),\n })\n .refine((v) => v.body.heightCm != null && v.body.weightKg != null, {\n message: 'Height and weight are required',\n path: ['body'],\n })\n .refine((v) => v.body.neckCm != null && v.body.waistCm != null, {\n message: 'Neck and waist are required',\n path: ['body'],\n })\n .refine((v) => v.gender !== 'female' || v.body.hipsCm != null, {\n message: 'Hips measurement is required',\n path: ['body', 'hipsCm'],\n });\nexport type MemberOnboardingBody = z.infer<typeof memberOnboardingBodySchema>;\n\n// ─── Onboarding progress & resume ─────────────────────────────────────────────\n\n/**\n * In-progress onboarding draft: the UI-shaped, partially-filled form the client\n * persists so a returning user resumes with their typed data intact. Opaque to\n * the backend by design — the client owns the form shape, and the data is\n * re-validated strictly by the member body schema at finalize. Keys are form\n * field names; the backend caps the stored size.\n */\nexport const onboardingDraftSchema = z.record(z.string(), z.unknown());\nexport type OnboardingDraft = z.infer<typeof onboardingDraftSchema>;\n\n/**\n * The server-persisted onboarding cursor: how far the user reached (`step`, the\n * 1-indexed sub-step of the member form) and their draft. Read on app entry to\n * resume to the exact step. `welcomed` records that the one-time welcome\n * carousel has been seen.\n */\nexport const onboardingProgressSchema = z.object({\n welcomed: z.boolean(),\n step: z.number().int().min(0).max(20),\n memberDraft: onboardingDraftSchema.optional(),\n // ── Retained for the two-role flow (see the note at the foot of this file) ──\n role: onboardingRoleSchema.nullish(),\n gymCode: centerCodeSchema.optional(),\n ownerDraft: onboardingDraftSchema.optional(),\n});\nexport type OnboardingProgress = z.infer<typeof onboardingProgressSchema>;\n\n/** `PUT /onboarding/progress` body — any subset of the cursor, merged server-side. */\nexport const onboardingProgressUpdateSchema = z\n .object({\n welcomed: z.boolean(),\n step: z.number().int().min(0).max(20),\n memberDraft: onboardingDraftSchema,\n role: onboardingRoleSchema.nullable(),\n gymCode: centerCodeSchema,\n ownerDraft: onboardingDraftSchema,\n })\n .partial();\nexport type OnboardingProgressUpdate = z.infer<typeof onboardingProgressUpdateSchema>;\n\n/**\n * `GET /onboarding/status` response: completion truth (derived from the user's\n * member profile) plus the resume cursor.\n *\n * `organisationId` is here rather than a `role` field. Onboarding no longer has\n * a role to report — everyone finishes it as a member — so what the app shells\n * actually need to know is whether this person also runs a gym, which is a fact\n * about their organisation, not about their onboarding.\n */\nexport const onboardingStatusSchema = z.object({\n onboarded: z.boolean(),\n /** The gym business they own, if any. Null for a plain member. */\n organisationId: z.string().nullable(),\n welcomed: z.boolean(),\n step: z.number().int().min(0),\n memberDraft: onboardingDraftSchema.optional(),\n role: onboardingRoleSchema.nullish(),\n gymCode: z.string().optional(),\n ownerDraft: onboardingDraftSchema.optional(),\n});\nexport type OnboardingStatus = z.infer<typeof onboardingStatusSchema>;\n\n// ─── Retained: the two-role onboarding flow ──────────────────────────────────\n//\n// The one-flow refactor above removed these. They are kept because the owner\n// console's repo still implements the two-role wizard (role fork, owner basics,\n// organisation + first centre, SaaS plan) and cannot migrate in the same change\n// that publishes this package. Nothing in the one-flow path imports them, so\n// they cost its consumers nothing.\n//\n// TODO(onboarding-migration): delete this block once the owner wizard moves to\n// `gym-registration.ts`.\n\n/** The owner's personal basics captured before the organisation/center. */\nexport const ownerBasicsSchema = z.object({\n firstName: z.string().trim().min(1, 'First name is required').max(80),\n lastName: optionalTrimmedString(80),\n gender: genderSchema,\n dob: isoDateSchema.optional(),\n phone: phoneSchema,\n altEmail: optionalEmailSchema,\n website: optionalTrimmedString(200),\n socials: socialLinksSchema,\n /** The owner's personal contact address (step-2 of owner onboarding). */\n address: addressSchema.optional(),\n /** Uploaded profile photo → stored on the shared `User.avatar`. */\n avatarUrl: optionalTrimmedString(2000),\n});\nexport type OwnerBasics = z.infer<typeof ownerBasicsSchema>;\n\n/** The SaaS plan an owner selects at the final onboarding step. */\nexport const onboardingPlanSelectionSchema = z.object({\n planId: z.string().trim().min(1),\n tenureId: z.string().trim().min(1).nullable(),\n});\nexport type OnboardingPlanSelection = z.infer<typeof onboardingPlanSelectionSchema>;\n\nexport const ownerOnboardingBodySchema = z.object({\n owner: ownerBasicsSchema,\n organisation: createOrganisationBodySchema,\n center: createCenterBodySchema,\n subscription: onboardingPlanSelectionSchema,\n});\nexport type OwnerOnboardingBody = z.infer<typeof ownerOnboardingBodySchema>;\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gymmonk-schema",
|
|
3
|
-
"version": "0.46.
|
|
3
|
+
"version": "0.46.2",
|
|
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",
|