gymmonk-schema 0.65.0 → 0.67.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -27,6 +27,7 @@ export * from './food.js';
27
27
  export * from './gym-registration.js';
28
28
  export * from './home.js';
29
29
  export * from './insights.js';
30
+ export * from './late-fee-reminder.js';
30
31
  export * from './location.js';
31
32
  export * from './member.js';
32
33
  export * from './member-profile.js';
@@ -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,mBAAmB,CAAC;AAClC,cAAc,aAAa,CAAC;AAE5B,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAE9B,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAE/B,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAE9B,cAAc,WAAW,CAAC;AAE1B,cAAc,WAAW,CAAC;AAC1B,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;AAC9B,cAAc,mBAAmB,CAAC;AAElC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,iBAAiB,CAAC;AAEhC,cAAc,sBAAsB,CAAC;AAErC,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AAEnC,cAAc,cAAc,CAAC;AAE7B,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;AAElC,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC;AAC1B,cAAc,mBAAmB,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,mBAAmB,CAAC;AAClC,cAAc,aAAa,CAAC;AAE5B,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAE9B,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAE/B,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAE9B,cAAc,WAAW,CAAC;AAE1B,cAAc,WAAW,CAAC;AAC1B,cAAc,uBAAuB,CAAC;AACtC,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAC9B,cAAc,wBAAwB,CAAC;AAEvC,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;AAC9B,cAAc,mBAAmB,CAAC;AAElC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,iBAAiB,CAAC;AAEhC,cAAc,sBAAsB,CAAC;AAErC,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AAEnC,cAAc,cAAc,CAAC;AAE7B,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;AAElC,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC;AAC1B,cAAc,mBAAmB,CAAC"}
package/dist/index.js CHANGED
@@ -34,6 +34,7 @@ export * from './food.js';
34
34
  export * from './gym-registration.js';
35
35
  export * from './home.js';
36
36
  export * from './insights.js';
37
+ export * from './late-fee-reminder.js';
37
38
  // ─── Where a place physically is (Maps link, pin, check-in position) ─────────
38
39
  export * from './location.js';
39
40
  // ─── People ──────────────────────────────────────────────────────────────────
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,mBAAmB,CAAC;AAClC,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,iBAAiB,CAAC;AAChC,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,WAAW,CAAC;AAC1B,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,cAAc,mBAAmB,CAAC;AAClC,gFAAgF;AAChF,cAAc,gBAAgB,CAAC;AAC/B,cAAc,iBAAiB,CAAC;AAChC,gFAAgF;AAChF,cAAc,sBAAsB,CAAC;AACrC,gFAAgF;AAChF,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,gFAAgF;AAChF,cAAc,cAAc,CAAC;AAC7B,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,iFAAiF;AACjF,cAAc,UAAU,CAAC;AACzB,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 './body-metrics.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 './diet-chart.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 './food.js';\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';\nexport * from './notification.js';\n// ─── Engagement & analytics ──────────────────────────────────────────────────\nexport * from './nutrition.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// ─── Razorpay checkout wire shapes ───────────────────────────────────────────\nexport * from './payment.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';\n// ─── Member fee payment by UPI QR (the gym's own VPA, no gateway) ─────────────\nexport * from './upi.js';\nexport * from './user.js';\nexport * from './workout-plan.js';\n"]}
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,mBAAmB,CAAC;AAClC,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,iBAAiB,CAAC;AAChC,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,WAAW,CAAC;AAC1B,cAAc,uBAAuB,CAAC;AACtC,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAC9B,cAAc,wBAAwB,CAAC;AACvC,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,cAAc,mBAAmB,CAAC;AAClC,gFAAgF;AAChF,cAAc,gBAAgB,CAAC;AAC/B,cAAc,iBAAiB,CAAC;AAChC,gFAAgF;AAChF,cAAc,sBAAsB,CAAC;AACrC,gFAAgF;AAChF,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,gFAAgF;AAChF,cAAc,cAAc,CAAC;AAC7B,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,iFAAiF;AACjF,cAAc,UAAU,CAAC;AACzB,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 './body-metrics.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 './diet-chart.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 './food.js';\nexport * from './gym-registration.js';\nexport * from './home.js';\nexport * from './insights.js';\nexport * from './late-fee-reminder.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';\nexport * from './notification.js';\n// ─── Engagement & analytics ──────────────────────────────────────────────────\nexport * from './nutrition.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// ─── Razorpay checkout wire shapes ───────────────────────────────────────────\nexport * from './payment.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';\n// ─── Member fee payment by UPI QR (the gym's own VPA, no gateway) ─────────────\nexport * from './upi.js';\nexport * from './user.js';\nexport * from './workout-plan.js';\n"]}
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Late-fee reminder contracts
3
+ * ===========================
4
+ * What the owner's WhatsApp reminder screen reads. One shape, defined once,
5
+ * because the backend builds it and the web client renders it and a drift
6
+ * between the two shows up as an empty card rather than as an error.
7
+ *
8
+ * ── ⚠️ THREE ZEROES THAT MEAN DIFFERENT THINGS ──────────────────────────────
9
+ * `configured: false` is the PLATFORM having no WhatsApp credentials — nobody
10
+ * on GymMonk is being messaged, and no gym can fix it.
11
+ * `cap: 0` is THIS GYM on a plan without the allowance. An upgrade fixes it.
12
+ * `used: 0` is a gym with the allowance and nobody overdue, which is healthy.
13
+ *
14
+ * They are separate fields rather than one number because a screen that renders
15
+ * "0 reminders" for all three tells an owner nothing about which one to act on,
16
+ * and two of the three are not problems at all.
17
+ *
18
+ * @module late-fee-reminder
19
+ */
20
+ import { z } from 'zod';
21
+ /**
22
+ * Our lifecycle for one reminder, which is NOT Meta's.
23
+ *
24
+ * `sending` is a reservation written before the gateway is called, so a row can
25
+ * sit here briefly during a sweep. `undeliverable` is terminal and, importantly,
26
+ * does not consume the gym's allowance.
27
+ */
28
+ export declare const lateFeeNoticeStatusSchema: z.ZodEnum<{
29
+ sent: "sent";
30
+ sending: "sending";
31
+ undeliverable: "undeliverable";
32
+ }>;
33
+ export type LateFeeNoticeStatus = z.infer<typeof lateFeeNoticeStatusSchema>;
34
+ /**
35
+ * META's outcome, which arrives on a webhook minutes after the send.
36
+ *
37
+ * ⚠️ `sent` here is not the status above. Ours means "Meta accepted it"; this
38
+ * one means Meta says it left. Null means no webhook has arrived yet, which is
39
+ * ordinary for a reminder sent seconds ago and suspicious for one sent a day
40
+ * ago — the difference is worth showing rather than flattening to "sent".
41
+ */
42
+ export declare const lateFeeDeliveryStatusSchema: z.ZodEnum<{
43
+ failed: "failed";
44
+ sent: "sent";
45
+ delivered: "delivered";
46
+ read: "read";
47
+ }>;
48
+ export type LateFeeDeliveryStatus = z.infer<typeof lateFeeDeliveryStatusSchema>;
49
+ /**
50
+ * One row of the owner's history.
51
+ *
52
+ * `memberName` is nullable because the stored notice deliberately holds no
53
+ * personal data — the name is resolved at read time from the enrolment, and a
54
+ * member who has since been erased correctly resolves to nothing.
55
+ */
56
+ export declare const lateFeeReminderEntrySchema: z.ZodObject<{
57
+ id: z.ZodString;
58
+ memberName: z.ZodNullable<z.ZodString>;
59
+ status: z.ZodEnum<{
60
+ sent: "sent";
61
+ sending: "sending";
62
+ undeliverable: "undeliverable";
63
+ }>;
64
+ deliveryStatus: z.ZodNullable<z.ZodEnum<{
65
+ failed: "failed";
66
+ sent: "sent";
67
+ delivered: "delivered";
68
+ read: "read";
69
+ }>>;
70
+ sentAt: z.ZodNullable<z.ZodString>;
71
+ }, z.core.$strip>;
72
+ export type LateFeeReminderEntry = z.infer<typeof lateFeeReminderEntrySchema>;
73
+ export declare const lateFeeReminderUsageSchema: z.ZodObject<{
74
+ configured: z.ZodBoolean;
75
+ cap: z.ZodNumber;
76
+ used: z.ZodNumber;
77
+ remaining: z.ZodNumber;
78
+ recent: z.ZodArray<z.ZodObject<{
79
+ id: z.ZodString;
80
+ memberName: z.ZodNullable<z.ZodString>;
81
+ status: z.ZodEnum<{
82
+ sent: "sent";
83
+ sending: "sending";
84
+ undeliverable: "undeliverable";
85
+ }>;
86
+ deliveryStatus: z.ZodNullable<z.ZodEnum<{
87
+ failed: "failed";
88
+ sent: "sent";
89
+ delivered: "delivered";
90
+ read: "read";
91
+ }>>;
92
+ sentAt: z.ZodNullable<z.ZodString>;
93
+ }, z.core.$strip>>;
94
+ }, z.core.$strip>;
95
+ export type LateFeeReminderUsage = z.infer<typeof lateFeeReminderUsageSchema>;
96
+ //# sourceMappingURL=late-fee-reminder.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"late-fee-reminder.d.ts","sourceRoot":"","sources":["../src/late-fee-reminder.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;;;GAMG;AACH,eAAO,MAAM,yBAAyB;;;;EAA+C,CAAC;AACtF,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAE5E;;;;;;;GAOG;AACH,eAAO,MAAM,2BAA2B;;;;;EAAkD,CAAC;AAC3F,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,2BAA2B,CAAC,CAAC;AAEhF;;;;;;GAMG;AACH,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;;;iBAMrC,CAAC;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAE9E,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;;;;;;;;;iBAQrC,CAAC;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,CAAC"}
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Late-fee reminder contracts
3
+ * ===========================
4
+ * What the owner's WhatsApp reminder screen reads. One shape, defined once,
5
+ * because the backend builds it and the web client renders it and a drift
6
+ * between the two shows up as an empty card rather than as an error.
7
+ *
8
+ * ── ⚠️ THREE ZEROES THAT MEAN DIFFERENT THINGS ──────────────────────────────
9
+ * `configured: false` is the PLATFORM having no WhatsApp credentials — nobody
10
+ * on GymMonk is being messaged, and no gym can fix it.
11
+ * `cap: 0` is THIS GYM on a plan without the allowance. An upgrade fixes it.
12
+ * `used: 0` is a gym with the allowance and nobody overdue, which is healthy.
13
+ *
14
+ * They are separate fields rather than one number because a screen that renders
15
+ * "0 reminders" for all three tells an owner nothing about which one to act on,
16
+ * and two of the three are not problems at all.
17
+ *
18
+ * @module late-fee-reminder
19
+ */
20
+ import { z } from 'zod';
21
+ /**
22
+ * Our lifecycle for one reminder, which is NOT Meta's.
23
+ *
24
+ * `sending` is a reservation written before the gateway is called, so a row can
25
+ * sit here briefly during a sweep. `undeliverable` is terminal and, importantly,
26
+ * does not consume the gym's allowance.
27
+ */
28
+ export const lateFeeNoticeStatusSchema = z.enum(['sending', 'sent', 'undeliverable']);
29
+ /**
30
+ * META's outcome, which arrives on a webhook minutes after the send.
31
+ *
32
+ * ⚠️ `sent` here is not the status above. Ours means "Meta accepted it"; this
33
+ * one means Meta says it left. Null means no webhook has arrived yet, which is
34
+ * ordinary for a reminder sent seconds ago and suspicious for one sent a day
35
+ * ago — the difference is worth showing rather than flattening to "sent".
36
+ */
37
+ export const lateFeeDeliveryStatusSchema = z.enum(['sent', 'delivered', 'read', 'failed']);
38
+ /**
39
+ * One row of the owner's history.
40
+ *
41
+ * `memberName` is nullable because the stored notice deliberately holds no
42
+ * personal data — the name is resolved at read time from the enrolment, and a
43
+ * member who has since been erased correctly resolves to nothing.
44
+ */
45
+ export const lateFeeReminderEntrySchema = z.object({
46
+ id: z.string(),
47
+ memberName: z.string().nullable(),
48
+ status: lateFeeNoticeStatusSchema,
49
+ deliveryStatus: lateFeeDeliveryStatusSchema.nullable(),
50
+ sentAt: z.string().nullable(),
51
+ });
52
+ export const lateFeeReminderUsageSchema = z.object({
53
+ /** Platform-wide: are there WhatsApp credentials at all. */
54
+ configured: z.boolean(),
55
+ /** This gym's monthly allowance. 0 means the plan does not include it. */
56
+ cap: z.number(),
57
+ used: z.number(),
58
+ remaining: z.number(),
59
+ recent: z.array(lateFeeReminderEntrySchema),
60
+ });
61
+ //# sourceMappingURL=late-fee-reminder.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"late-fee-reminder.js","sourceRoot":"","sources":["../src/late-fee-reminder.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,MAAM,EAAE,eAAe,CAAC,CAAC,CAAC;AAGtF;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,2BAA2B,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC;AAG3F;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG,CAAC,CAAC,MAAM,CAAC;IACjD,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE;IACd,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACjC,MAAM,EAAE,yBAAyB;IACjC,cAAc,EAAE,2BAA2B,CAAC,QAAQ,EAAE;IACtD,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CAC9B,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,0BAA0B,GAAG,CAAC,CAAC,MAAM,CAAC;IACjD,4DAA4D;IAC5D,UAAU,EAAE,CAAC,CAAC,OAAO,EAAE;IACvB,0EAA0E;IAC1E,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE;IACf,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;IAChB,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE;IACrB,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,0BAA0B,CAAC;CAC5C,CAAC,CAAC","sourcesContent":["/**\n * Late-fee reminder contracts\n * ===========================\n * What the owner's WhatsApp reminder screen reads. One shape, defined once,\n * because the backend builds it and the web client renders it and a drift\n * between the two shows up as an empty card rather than as an error.\n *\n * ── ⚠️ THREE ZEROES THAT MEAN DIFFERENT THINGS ──────────────────────────────\n * `configured: false` is the PLATFORM having no WhatsApp credentials — nobody\n * on GymMonk is being messaged, and no gym can fix it.\n * `cap: 0` is THIS GYM on a plan without the allowance. An upgrade fixes it.\n * `used: 0` is a gym with the allowance and nobody overdue, which is healthy.\n *\n * They are separate fields rather than one number because a screen that renders\n * \"0 reminders\" for all three tells an owner nothing about which one to act on,\n * and two of the three are not problems at all.\n *\n * @module late-fee-reminder\n */\n\nimport { z } from 'zod';\n\n/**\n * Our lifecycle for one reminder, which is NOT Meta's.\n *\n * `sending` is a reservation written before the gateway is called, so a row can\n * sit here briefly during a sweep. `undeliverable` is terminal and, importantly,\n * does not consume the gym's allowance.\n */\nexport const lateFeeNoticeStatusSchema = z.enum(['sending', 'sent', 'undeliverable']);\nexport type LateFeeNoticeStatus = z.infer<typeof lateFeeNoticeStatusSchema>;\n\n/**\n * META's outcome, which arrives on a webhook minutes after the send.\n *\n * ⚠️ `sent` here is not the status above. Ours means \"Meta accepted it\"; this\n * one means Meta says it left. Null means no webhook has arrived yet, which is\n * ordinary for a reminder sent seconds ago and suspicious for one sent a day\n * ago — the difference is worth showing rather than flattening to \"sent\".\n */\nexport const lateFeeDeliveryStatusSchema = z.enum(['sent', 'delivered', 'read', 'failed']);\nexport type LateFeeDeliveryStatus = z.infer<typeof lateFeeDeliveryStatusSchema>;\n\n/**\n * One row of the owner's history.\n *\n * `memberName` is nullable because the stored notice deliberately holds no\n * personal data — the name is resolved at read time from the enrolment, and a\n * member who has since been erased correctly resolves to nothing.\n */\nexport const lateFeeReminderEntrySchema = z.object({\n id: z.string(),\n memberName: z.string().nullable(),\n status: lateFeeNoticeStatusSchema,\n deliveryStatus: lateFeeDeliveryStatusSchema.nullable(),\n sentAt: z.string().nullable(),\n});\nexport type LateFeeReminderEntry = z.infer<typeof lateFeeReminderEntrySchema>;\n\nexport const lateFeeReminderUsageSchema = z.object({\n /** Platform-wide: are there WhatsApp credentials at all. */\n configured: z.boolean(),\n /** This gym's monthly allowance. 0 means the plan does not include it. */\n cap: z.number(),\n used: z.number(),\n remaining: z.number(),\n recent: z.array(lateFeeReminderEntrySchema),\n});\nexport type LateFeeReminderUsage = z.infer<typeof lateFeeReminderUsageSchema>;\n"]}
@@ -76,8 +76,8 @@ export type AppNotification = z.infer<typeof notificationSchema>;
76
76
  */
77
77
  export declare const notificationFilterSchema: z.ZodEnum<{
78
78
  all: "all";
79
- unread: "unread";
80
79
  read: "read";
80
+ unread: "unread";
81
81
  hidden: "hidden";
82
82
  }>;
83
83
  export type NotificationFilter = z.infer<typeof notificationFilterSchema>;
@@ -86,8 +86,8 @@ export declare const notificationListQuerySchema: z.ZodObject<{
86
86
  limit: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
87
87
  filter: z.ZodDefault<z.ZodEnum<{
88
88
  all: "all";
89
- unread: "unread";
90
89
  read: "read";
90
+ unread: "unread";
91
91
  hidden: "hidden";
92
92
  }>>;
93
93
  }, z.core.$strip>;
@@ -168,6 +168,97 @@ export declare const platformPeopleSummarySchema: z.ZodObject<{
168
168
  owners: z.ZodNumber;
169
169
  }, z.core.$strip>;
170
170
  export type PlatformPeopleSummary = z.infer<typeof platformPeopleSummarySchema>;
171
+ /**
172
+ * One member who owes a gym money, seen from the platform.
173
+ *
174
+ * ── WHY THIS IS NOT A `PlatformUser` ────────────────────────────────────────
175
+ * Two reasons, and the second is the load-bearing one.
176
+ *
177
+ * `PlatformUser` carries no money at all — no plan, no balance, no due date —
178
+ * and bolting those onto every row of a directory that is overwhelmingly NOT
179
+ * members would be null for almost everybody and cost a join per page.
180
+ *
181
+ * ⚠️ And `PlatformUser.organisation` is populated ONLY FOR OWNERS, by design.
182
+ * A member's gym is not on that row. This screen exists to say "chase this
183
+ * person about that gym", so the gym is mandatory here, not nullable.
184
+ *
185
+ * ── KEYED ON THE ENROLMENT, NOT THE USER ────────────────────────────────────
186
+ * `userId` is nullable and frequently null: a gym can sell a plan at the desk
187
+ * to somebody who never installs the app, and those are exactly the members
188
+ * most likely to be late. The enrolment is the thing that always exists, so it
189
+ * is the id. Anything keyed on the user would silently drop the population this
190
+ * screen is most useful for.
191
+ */
192
+ export declare const platformOverdueMemberSchema: z.ZodObject<{
193
+ id: z.ZodString;
194
+ membershipId: z.ZodString;
195
+ userId: z.ZodNullable<z.ZodString>;
196
+ name: z.ZodString;
197
+ email: z.ZodNullable<z.ZodString>;
198
+ phone: z.ZodNullable<z.ZodString>;
199
+ avatar: z.ZodNullable<z.ZodString>;
200
+ roles: z.ZodArray<z.ZodEnum<{
201
+ member: "member";
202
+ trainer: "trainer";
203
+ owner: "owner";
204
+ support_agent: "support_agent";
205
+ moderator: "moderator";
206
+ superadmin: "superadmin";
207
+ platform_manager: "platform_manager";
208
+ gym_manager: "gym_manager";
209
+ frontdesk: "frontdesk";
210
+ }>>;
211
+ status: z.ZodNullable<z.ZodEnum<{
212
+ active: "active";
213
+ inactive: "inactive";
214
+ deleted: "deleted";
215
+ suspended: "suspended";
216
+ }>>;
217
+ isDeleted: z.ZodBoolean;
218
+ gym: z.ZodObject<{
219
+ id: z.ZodString;
220
+ name: z.ZodString;
221
+ approvalStatus: z.ZodEnum<{
222
+ pending: "pending";
223
+ rejected: "rejected";
224
+ approved: "approved";
225
+ }>;
226
+ }, z.core.$strip>;
227
+ planName: z.ZodString;
228
+ dueDate: z.ZodString;
229
+ overdueDays: z.ZodNumber;
230
+ balanceInr: z.ZodNumber;
231
+ priceInr: z.ZodNumber;
232
+ }, z.core.$strip>;
233
+ export type PlatformOverdueMember = z.infer<typeof platformOverdueMemberSchema>;
234
+ /**
235
+ * The overdue-fee list filter.
236
+ *
237
+ * ⚠️ NO UPPER BOUND ON HOW LATE. The automated sweep caps its lookback so its
238
+ * first production run cannot message everybody who has ever been late; this
239
+ * screen sends nothing by itself, so it inherits no such risk. Capping here
240
+ * would silently hide the oldest debts — which are the ones actually worth a
241
+ * phone call.
242
+ */
243
+ export declare const platformOverdueQuerySchema: z.ZodObject<{
244
+ page: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
245
+ limit: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
246
+ q: z.ZodPreprocess<z.ZodOptional<z.ZodString>>;
247
+ organisationId: z.ZodOptional<z.ZodString>;
248
+ minOverdueDays: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
249
+ sort: z.ZodDefault<z.ZodEnum<{
250
+ overdue: "overdue";
251
+ amount: "amount";
252
+ }>>;
253
+ }, z.core.$strip>;
254
+ export type PlatformOverdueQuery = z.infer<typeof platformOverdueQuerySchema>;
255
+ /** Header counts for the overdue list. */
256
+ export declare const platformOverdueSummarySchema: z.ZodObject<{
257
+ members: z.ZodNumber;
258
+ gyms: z.ZodNumber;
259
+ totalDueInr: z.ZodNumber;
260
+ }, z.core.$strip>;
261
+ export type PlatformOverdueSummary = z.infer<typeof platformOverdueSummarySchema>;
171
262
  /**
172
263
  * Suspend or reinstate an account.
173
264
  *
@@ -1 +1 @@
1
- {"version":3,"file":"platform.d.ts","sourceRoot":"","sources":["../src/platform.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAYxB;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,aAAa;;;;;;;;;;EAUxB,CAAC;AACH,MAAM,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAIpD;;;;;;;GAOG;AACH,eAAO,MAAM,qBAAqB;;;;;;;;iBAIhC,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAqB7B,CAAC;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAE9D;;;;;;;GAOG;AACH,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;iBASlC,CAAC;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAExE,8CAA8C;AAC9C,eAAO,MAAM,2BAA2B;;;;;;iBAOtC,CAAC;AACH,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,2BAA2B,CAAC,CAAC;AAIhF;;;;;;;;GAQG;AACH,eAAO,MAAM,uBAAuB;;;;;iBAElC,CAAC;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAExE;;;;;;;GAOG;AACH,eAAO,MAAM,qBAAqB;;;;;;;iBAEhC,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE;;;;;;;GAOG;AACH,eAAO,MAAM,4BAA4B;;iBAEvC,CAAC;AACH,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,4BAA4B,CAAC,CAAC;AAIlF;;;;;;;;;GASG;AACH,eAAO,MAAM,wBAAwB;;;;iBAOnC,CAAC;AACH,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAE1E,oDAAoD;AACpD,eAAO,MAAM,oBAAoB;;;;;;;;;;iBAM/B,CAAC;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAElE;;;;;;;GAOG;AACH,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAgCjC,CAAC;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAItE;;;;;;GAMG;AACH,eAAO,MAAM,wBAAwB;;;;;;iBAOnC,CAAC;AACH,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAE1E;;;;;;GAMG;AACH,eAAO,MAAM,2BAA2B;;iBAEtC,CAAC;AACH,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,2BAA2B,CAAC,CAAC"}
1
+ {"version":3,"file":"platform.d.ts","sourceRoot":"","sources":["../src/platform.ts"],"names":[],"mappings":"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"}
package/dist/platform.js CHANGED
@@ -128,6 +128,101 @@ export const platformPeopleSummarySchema = z.object({
128
128
  staff: z.number().int(),
129
129
  owners: z.number().int(),
130
130
  });
131
+ // ─── Overdue fees ────────────────────────────────────────────────────────────
132
+ /**
133
+ * One member who owes a gym money, seen from the platform.
134
+ *
135
+ * ── WHY THIS IS NOT A `PlatformUser` ────────────────────────────────────────
136
+ * Two reasons, and the second is the load-bearing one.
137
+ *
138
+ * `PlatformUser` carries no money at all — no plan, no balance, no due date —
139
+ * and bolting those onto every row of a directory that is overwhelmingly NOT
140
+ * members would be null for almost everybody and cost a join per page.
141
+ *
142
+ * ⚠️ And `PlatformUser.organisation` is populated ONLY FOR OWNERS, by design.
143
+ * A member's gym is not on that row. This screen exists to say "chase this
144
+ * person about that gym", so the gym is mandatory here, not nullable.
145
+ *
146
+ * ── KEYED ON THE ENROLMENT, NOT THE USER ────────────────────────────────────
147
+ * `userId` is nullable and frequently null: a gym can sell a plan at the desk
148
+ * to somebody who never installs the app, and those are exactly the members
149
+ * most likely to be late. The enrolment is the thing that always exists, so it
150
+ * is the id. Anything keyed on the user would silently drop the population this
151
+ * screen is most useful for.
152
+ */
153
+ export const platformOverdueMemberSchema = z.object({
154
+ /** The enrolment — the only id every overdue member is guaranteed to have. */
155
+ id: objectIdSchema,
156
+ /** The term being chased. Actions and dedupe key on this, never on the person. */
157
+ membershipId: objectIdSchema,
158
+ /** Null when the desk added this member and nobody ever claimed the record. */
159
+ userId: objectIdSchema.nullable(),
160
+ /**
161
+ * Resolved by the backend: the account's name, else the name typed at intake.
162
+ *
163
+ * ⚠️ MAY BE EMPTY, and consumers must handle that rather than assume a
164
+ * placeholder. It is empty only when the gym recorded no name at all, and it
165
+ * is deliberately NOT filled with something like "Unclaimed member" —
166
+ * this string is read aloud in the WhatsApp reminder, so a placeholder here
167
+ * greets a real person as "Hi Unclaimed,".
168
+ */
169
+ name: z.string(),
170
+ email: z.string().nullable(),
171
+ /** Raw 10 digits. Either the account's or the `invitePhone` the desk wrote down. */
172
+ phone: z.string().nullable(),
173
+ avatar: z.string().nullable(),
174
+ /**
175
+ * Every hat, so the operator can tell a plain member from a gym's own owner
176
+ * who is behind on their personal membership. Empty for an unclaimed record —
177
+ * there is no account to hold a role.
178
+ */
179
+ roles: z.array(roleKeySchema),
180
+ /** Account standing. Null for an unclaimed record: there is no account. */
181
+ status: userStatusSchema.nullable(),
182
+ /** Soft-deleted account. An unclaimed record is never deleted, so false. */
183
+ isDeleted: z.boolean(),
184
+ /** MANDATORY here — see the note above on why it is nullable on PlatformUser. */
185
+ gym: platformUserGymSchema,
186
+ planName: z.string(),
187
+ /**
188
+ * The gym day the term ended, as stored: `YYYY-MM-DD`.
189
+ *
190
+ * ⚠️ A calendar label, NOT an instant. Never hand it to `new Date()` for
191
+ * display — `new Date('2026-08-15')` is midnight UTC and renders as the 14th
192
+ * anywhere west of Greenwich.
193
+ */
194
+ dueDate: z.string(),
195
+ overdueDays: z.number().int(),
196
+ balanceInr: z.number(),
197
+ priceInr: z.number(),
198
+ });
199
+ /**
200
+ * The overdue-fee list filter.
201
+ *
202
+ * ⚠️ NO UPPER BOUND ON HOW LATE. The automated sweep caps its lookback so its
203
+ * first production run cannot message everybody who has ever been late; this
204
+ * screen sends nothing by itself, so it inherits no such risk. Capping here
205
+ * would silently hide the oldest debts — which are the ones actually worth a
206
+ * phone call.
207
+ */
208
+ export const platformOverdueQuerySchema = z.object({
209
+ page: z.coerce.number().int().min(1).default(1),
210
+ limit: z.coerce.number().int().min(1).max(100).default(20),
211
+ /** Member name, email or phone. */
212
+ q: optionalTrimmedString(120),
213
+ /** Narrow to one gym, for a support conversation about that gym. */
214
+ organisationId: objectIdSchema.optional(),
215
+ /** At least this many days late. 1 means a full day must have passed. */
216
+ minOverdueDays: z.coerce.number().int().min(1).max(3650).default(1),
217
+ /** Worst debt first, or longest overdue first. */
218
+ sort: z.enum(['overdue', 'amount']).default('overdue'),
219
+ });
220
+ /** Header counts for the overdue list. */
221
+ export const platformOverdueSummarySchema = z.object({
222
+ members: z.number().int(),
223
+ gyms: z.number().int(),
224
+ totalDueInr: z.number(),
225
+ });
131
226
  // ─── People mutations ────────────────────────────────────────────────────────
132
227
  /**
133
228
  * Suspend or reinstate an account.
@@ -1 +1 @@
1
- {"version":3,"file":"platform.js","sourceRoot":"","sources":["../src/platform.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * gymmonk-schema — Platform console (superadmin / platform manager)\n * ================================================================\n * The two directories the platform operates GymMonk from: every person with an\n * account, and every gym on the platform.\n *\n * ONE CONSOLE, TWO ROLES. Superadmin and platform manager see the same screens\n * and call the same endpoints. The single difference is DESTRUCTIVE ACTS:\n * `casl-ability#grantPlatformManager` states `cannot('delete', 'User')` and\n * `cannot('delete', 'Organisation')`, while superadmin holds `manage all`. That\n * is deliberately expressed as an ABILITY rather than as two contracts — a\n * second set of types for \"the manager's version of the people list\" would drift\n * from the first the moment either changed, and the client would be deciding\n * what it is allowed to do instead of being told.\n *\n * SCOPE. These are PLATFORM views: unscoped by gym, unscoped by center. No gym\n * role can reach them (see the `GymApproval` subject note in `casl-ability`), so\n * nothing here is filtered by organisation.\n *\n * @module gymmonk-schema/platform\n */\n\nimport { z } from 'zod';\nimport {\n deletedFilterSchema,\n isoDateTimeSchema,\n objectIdSchema,\n optionalTrimmedString,\n} from './common.js';\nimport { organisationApprovalStatusSchema } from './organisation.js';\nimport { appRoleSchema, userStatusSchema } from './user.js';\n\n// ─── Role keys ───────────────────────────────────────────────────────────────\n\n/**\n * Every role the tab-bar switcher understands — the wire contract for \"what\n * hats does this person wear\".\n *\n * The SSOT for a set that was previously written out three times: as a TypeScript\n * union in the backend's `casl-ability.ts`, again in the web client's\n * `shared/config/roles.ts`, and again as the values of the backend's role-snapshot\n * maps. Three copies of a closed set is three chances for one of them to miss a\n * role, and the people directory filters on exactly this set — a filter offering\n * a role the deriver never emits returns a permanently empty list with no error.\n *\n * Note `platform_manager` vs the `manager` APP ROLE (`appRoleSchema`): the app\n * role is what is stored on the user, the role key is what the product calls the\n * hat. `gym_manager` is a different job entirely, which is why neither can be\n * called plain \"manager\" on the wire.\n */\nexport const roleKeySchema = z.enum([\n 'member',\n 'owner',\n 'superadmin',\n 'platform_manager',\n 'moderator',\n 'support_agent',\n 'gym_manager',\n 'trainer',\n 'frontdesk',\n]);\nexport type RoleKey = z.infer<typeof roleKeySchema>;\n\n// ─── People directory ────────────────────────────────────────────────────────\n\n/**\n * The gym a person owns, summarised for a directory row.\n *\n * Present only for owners. It is on the row rather than behind a second request\n * because \"which gym is this?\" is the first question asked about any owner in a\n * support or vetting context, and a directory that makes you open each row to\n * find out is not a directory.\n */\nexport const platformUserGymSchema = z.object({\n id: objectIdSchema,\n name: z.string(),\n approvalStatus: organisationApprovalStatusSchema,\n});\nexport type PlatformUserGym = z.infer<typeof platformUserGymSchema>;\n\n/**\n * A row in the platform's people directory.\n *\n * `name` arrives RESOLVED — the backend has already applied the\n * `firstName lastName` → email fallback chain, so every client renders the same\n * thing and no screen invents its own version of \"what do we call this person\".\n *\n * `appRoles` and `roles` are BOTH here and they are not the same list.\n * `appRoles` is what is stored on the account and is what an admin can grant or\n * revoke. `roles` is the full derived set including the gym hats implied by\n * domain records — owning an organisation, holding a staff record, having a\n * member profile. An admin who is shown only `appRoles` cannot tell a plain\n * member from the owner of the platform's largest gym.\n */\nexport const platformUserSchema = z.object({\n id: objectIdSchema,\n name: z.string(),\n email: z.string(),\n /** Raw 10 digits, no `+91` — the country code is display-only. */\n phone: z.string().nullable(),\n avatar: z.string().nullable(),\n /** Granted platform roles. Empty for the overwhelming majority of accounts. */\n appRoles: z.array(appRoleSchema),\n /** Every hat, including the gym roles derived from domain records. */\n roles: z.array(roleKeySchema),\n status: userStatusSchema,\n organisation: platformUserGymSchema.nullable(),\n /**\n * Soft-deleted. The account is retained (deleting an owner would cascade into\n * an organisation, its centres, every member and the payment ledger) but is\n * excluded from every ordinary query. Only a superadmin can set this.\n */\n isDeleted: z.boolean(),\n lastActiveAt: isoDateTimeSchema.nullable(),\n createdAt: isoDateTimeSchema,\n});\nexport type PlatformUser = z.infer<typeof platformUserSchema>;\n\n/**\n * The people-list filter.\n *\n * `role` filters on the DERIVED set, not on `appRoles`, so \"show me every gym\n * owner\" works — owner is never an app role. The derivation is denormalised onto\n * the user (`User.roleSnapshot.roles`), which is what makes this an indexed\n * query rather than a fan-out per candidate row.\n */\nexport const platformUserQuerySchema = z.object({\n page: z.coerce.number().int().min(1).default(1),\n limit: z.coerce.number().int().min(1).max(100).default(20),\n /** Name, email or phone. */\n q: optionalTrimmedString(120),\n role: roleKeySchema.optional(),\n status: userStatusSchema.optional(),\n /** Soft-delete visibility. `only` is the superadmin's restore queue. */\n deleted: deletedFilterSchema.default('exclude'),\n});\nexport type PlatformUserQuery = z.infer<typeof platformUserQuerySchema>;\n\n/** Header counts for the people directory. */\nexport const platformPeopleSummarySchema = z.object({\n total: z.number().int(),\n active: z.number().int(),\n suspended: z.number().int(),\n /** Accounts holding at least one platform app-role. */\n staff: z.number().int(),\n owners: z.number().int(),\n});\nexport type PlatformPeopleSummary = z.infer<typeof platformPeopleSummarySchema>;\n\n// ─── People mutations ────────────────────────────────────────────────────────\n\n/**\n * Suspend or reinstate an account.\n *\n * Only these two values. `deleted` is reached through the delete endpoint (which\n * needs a stronger ability and a typed confirmation), and `inactive` is a\n * lifecycle state the product sets for itself — letting an admin write either\n * one here would put the account into a state no screen explains and no action\n * reverses.\n */\nexport const setUserStatusBodySchema = z.object({\n status: z.enum(['active', 'suspended']),\n});\nexport type SetUserStatusBody = z.infer<typeof setUserStatusBodySchema>;\n\n/**\n * Replace the account's platform roles.\n *\n * A whole-array PUT rather than add/remove deltas: the console shows all four\n * checkboxes at once, so the admin is always looking at the complete intended\n * state, and a delta API would let two admins editing the same person silently\n * merge into a set neither chose.\n */\nexport const setAppRolesBodySchema = z.object({\n appRoles: z.array(appRoleSchema).max(4),\n});\nexport type SetAppRolesBody = z.infer<typeof setAppRolesBodySchema>;\n\n/**\n * Soft-delete an account. Superadmin only.\n *\n * The caller must echo the account's EMAIL back. The same echo-to-confirm guard\n * the member and membership deletions use — it is the one pattern that survives\n * a mis-tap on a phone, where a destructive button sits a thumb's width from a\n * harmless one.\n */\nexport const deletePlatformUserBodySchema = z.object({\n confirmationEmail: z.string().trim().min(1),\n});\nexport type DeletePlatformUserBody = z.infer<typeof deletePlatformUserBodySchema>;\n\n// ─── Platform overview (the console's Dashboard tab) ─────────────────────────\n\n/**\n * One month of platform growth.\n *\n * `label` is rendered SERVER-SIDE, already in IST. Month bucketing is done with\n * an explicit `Asia/Kolkata` timezone in the aggregation, and the label is built\n * from the same bucket — so the client never derives a date. Letting it do so\n * would put a UTC container and an IST phone in different months for five and a\n * half hours every night, which is a hydration mismatch that only reproduces\n * after 18:30 IST.\n */\nexport const platformTrendPointSchema = z.object({\n /** Short month, e.g. `Mar`. Includes the year when the window crosses one. */\n label: z.string(),\n /** Accounts created that month. */\n people: z.number().int(),\n /** Gyms registered that month. */\n gyms: z.number().int(),\n});\nexport type PlatformTrendPoint = z.infer<typeof platformTrendPointSchema>;\n\n/** A gym in the \"biggest on the platform\" table. */\nexport const platformTopGymSchema = z.object({\n id: objectIdSchema,\n name: z.string(),\n memberCount: z.number().int(),\n centerCount: z.number().int(),\n approvalStatus: organisationApprovalStatusSchema,\n});\nexport type PlatformTopGym = z.infer<typeof platformTopGymSchema>;\n\n/**\n * Everything the Dashboard tab renders, in one request.\n *\n * One endpoint rather than six. This screen is the console's landing tab, so its\n * cost is paid on every visit — and six round trips would each carry their own\n * loading state, producing a dashboard that reflows four times before settling.\n * The aggregations run concurrently server-side, where they are cheap.\n */\nexport const platformOverviewSchema = z.object({\n totals: z.object({\n /** Registered gyms, whatever their approval state. */\n gyms: z.number().int(),\n centers: z.number().int(),\n /** People with a member record at a gym — NOT the same as `people`. */\n members: z.number().int(),\n /** Memberships currently live (active or paused, not expired). */\n activeMemberships: z.number().int(),\n /** Every account on GymMonk, members and staff alike. */\n people: z.number().int(),\n /** All money successfully collected across every gym, in whole rupees. */\n revenueInr: z.number().int(),\n }),\n /** Growth, oldest month first. */\n trend: z.array(platformTrendPointSchema),\n gymsByStatus: z.object({\n pending: z.number().int(),\n approved: z.number().int(),\n rejected: z.number().int(),\n }),\n topGyms: z.array(platformTopGymSchema),\n /**\n * What is waiting for a human. Every number here is a link on the dashboard,\n * so a count of zero is a good outcome rather than an empty section.\n */\n attention: z.object({\n pendingGyms: z.number().int(),\n suspendedAccounts: z.number().int(),\n /** Approved gyms that never created a centre, so members cannot join them. */\n gymsWithoutCenters: z.number().int(),\n }),\n});\nexport type PlatformOverview = z.infer<typeof platformOverviewSchema>;\n\n// ─── Gym directory ───────────────────────────────────────────────────────────\n\n/**\n * Header counts for the gyms directory, and the review-queue badge.\n *\n * `pending` was previously the only count returned, which was enough for a badge\n * and nothing else: the Gyms tab shows the whole estate, and a screen that can\n * say \"12 pending\" but not \"of how many\" tells an operator nothing about scale.\n */\nexport const platformGymSummarySchema = z.object({\n total: z.number().int(),\n pending: z.number().int(),\n approved: z.number().int(),\n rejected: z.number().int(),\n /** Centres across every approved gym. */\n centers: z.number().int(),\n});\nexport type PlatformGymSummary = z.infer<typeof platformGymSummarySchema>;\n\n/**\n * Soft-delete a gym. Superadmin only.\n *\n * Echoes the gym's NAME rather than an id — an operator reading a confirmation\n * dialog can verify a name against the row they meant to act on; an id they\n * would paste without reading.\n */\nexport const deletePlatformGymBodySchema = z.object({\n confirmationName: z.string().trim().min(1),\n});\nexport type DeletePlatformGymBody = z.infer<typeof deletePlatformGymBodySchema>;\n"]}
1
+ {"version":3,"file":"platform.js","sourceRoot":"","sources":["../src/platform.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EACL,mBAAmB,EACnB,iBAAiB,EACjB,cAAc,EACd,qBAAqB,GACtB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,gCAAgC,EAAE,MAAM,mBAAmB,CAAC;AACrE,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAE5D,gFAAgF;AAEhF;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,CAAC,IAAI,CAAC;IAClC,QAAQ;IACR,OAAO;IACP,YAAY;IACZ,kBAAkB;IAClB,WAAW;IACX,eAAe;IACf,aAAa;IACb,SAAS;IACT,WAAW;CACZ,CAAC,CAAC;AAGH,gFAAgF;AAEhF;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC5C,EAAE,EAAE,cAAc;IAClB,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;IAChB,cAAc,EAAE,gCAAgC;CACjD,CAAC,CAAC;AAGH;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC;IACzC,EAAE,EAAE,cAAc;IAClB,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;IAChB,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE;IACjB,kEAAkE;IAClE,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC5B,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC7B,+EAA+E;IAC/E,QAAQ,EAAE,CAAC,CAAC,KAAK,CAAC,aAAa,CAAC;IAChC,sEAAsE;IACtE,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,aAAa,CAAC;IAC7B,MAAM,EAAE,gBAAgB;IACxB,YAAY,EAAE,qBAAqB,CAAC,QAAQ,EAAE;IAC9C;;;;OAIG;IACH,SAAS,EAAE,CAAC,CAAC,OAAO,EAAE;IACtB,YAAY,EAAE,iBAAiB,CAAC,QAAQ,EAAE;IAC1C,SAAS,EAAE,iBAAiB;CAC7B,CAAC,CAAC;AAGH;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC9C,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;IAC/C,KAAK,EAAE,CAAC,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IAC1D,4BAA4B;IAC5B,CAAC,EAAE,qBAAqB,CAAC,GAAG,CAAC;IAC7B,IAAI,EAAE,aAAa,CAAC,QAAQ,EAAE;IAC9B,MAAM,EAAE,gBAAgB,CAAC,QAAQ,EAAE;IACnC,wEAAwE;IACxE,OAAO,EAAE,mBAAmB,CAAC,OAAO,CAAC,SAAS,CAAC;CAChD,CAAC,CAAC;AAGH,8CAA8C;AAC9C,MAAM,CAAC,MAAM,2BAA2B,GAAG,CAAC,CAAC,MAAM,CAAC;IAClD,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE;IACvB,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE;IACxB,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE;IAC3B,uDAAuD;IACvD,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE;IACvB,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE;CACzB,CAAC,CAAC;AAGH,gFAAgF;AAEhF;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,CAAC,MAAM,2BAA2B,GAAG,CAAC,CAAC,MAAM,CAAC;IAClD,8EAA8E;IAC9E,EAAE,EAAE,cAAc;IAClB,kFAAkF;IAClF,YAAY,EAAE,cAAc;IAC5B,+EAA+E;IAC/E,MAAM,EAAE,cAAc,CAAC,QAAQ,EAAE;IACjC;;;;;;;;OAQG;IACH,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;IAChB,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC5B,oFAAoF;IACpF,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC5B,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC7B;;;;OAIG;IACH,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,aAAa,CAAC;IAC7B,2EAA2E;IAC3E,MAAM,EAAE,gBAAgB,CAAC,QAAQ,EAAE;IACnC,4EAA4E;IAC5E,SAAS,EAAE,CAAC,CAAC,OAAO,EAAE;IACtB,iFAAiF;IACjF,GAAG,EAAE,qBAAqB;IAC1B,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE;IACpB;;;;;;OAMG;IACH,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE;IACnB,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE;IAC7B,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE;IACtB,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE;CACrB,CAAC,CAAC;AAGH;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG,CAAC,CAAC,MAAM,CAAC;IACjD,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;IAC/C,KAAK,EAAE,CAAC,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IAC1D,mCAAmC;IACnC,CAAC,EAAE,qBAAqB,CAAC,GAAG,CAAC;IAC7B,oEAAoE;IACpE,cAAc,EAAE,cAAc,CAAC,QAAQ,EAAE;IACzC,yEAAyE;IACzE,cAAc,EAAE,CAAC,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;IACnE,kDAAkD;IAClD,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC;CACvD,CAAC,CAAC;AAGH,0CAA0C;AAC1C,MAAM,CAAC,MAAM,4BAA4B,GAAG,CAAC,CAAC,MAAM,CAAC;IACnD,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE;IACzB,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE;IACtB,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE;CACxB,CAAC,CAAC;AAGH,gFAAgF;AAEhF;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC9C,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC;CACxC,CAAC,CAAC;AAGH;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC5C,QAAQ,EAAE,CAAC,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;CACxC,CAAC,CAAC;AAGH;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,4BAA4B,GAAG,CAAC,CAAC,MAAM,CAAC;IACnD,iBAAiB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;CAC5C,CAAC,CAAC;AAGH,gFAAgF;AAEhF;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC/C,8EAA8E;IAC9E,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE;IACjB,mCAAmC;IACnC,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE;IACxB,kCAAkC;IAClC,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE;CACvB,CAAC,CAAC;AAGH,oDAAoD;AACpD,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC3C,EAAE,EAAE,cAAc;IAClB,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;IAChB,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE;IAC7B,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE;IAC7B,cAAc,EAAE,gCAAgC;CACjD,CAAC,CAAC;AAGH;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC7C,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC;QACf,sDAAsD;QACtD,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE;QACtB,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE;QACzB,uEAAuE;QACvE,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE;QACzB,kEAAkE;QAClE,iBAAiB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE;QACnC,yDAAyD;QACzD,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE;QACxB,0EAA0E;QAC1E,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE;KAC7B,CAAC;IACF,kCAAkC;IAClC,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,wBAAwB,CAAC;IACxC,YAAY,EAAE,CAAC,CAAC,MAAM,CAAC;QACrB,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE;QACzB,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE;QAC1B,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE;KAC3B,CAAC;IACF,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC,oBAAoB,CAAC;IACtC;;;OAGG;IACH,SAAS,EAAE,CAAC,CAAC,MAAM,CAAC;QAClB,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE;QAC7B,iBAAiB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE;QACnC,8EAA8E;QAC9E,kBAAkB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE;KACrC,CAAC;CACH,CAAC,CAAC;AAGH,gFAAgF;AAEhF;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC/C,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE;IACvB,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE;IACzB,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE;IAC1B,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE;IAC1B,yCAAyC;IACzC,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE;CAC1B,CAAC,CAAC;AAGH;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,2BAA2B,GAAG,CAAC,CAAC,MAAM,CAAC;IAClD,gBAAgB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;CAC3C,CAAC,CAAC","sourcesContent":["/**\n * gymmonk-schema — Platform console (superadmin / platform manager)\n * ================================================================\n * The two directories the platform operates GymMonk from: every person with an\n * account, and every gym on the platform.\n *\n * ONE CONSOLE, TWO ROLES. Superadmin and platform manager see the same screens\n * and call the same endpoints. The single difference is DESTRUCTIVE ACTS:\n * `casl-ability#grantPlatformManager` states `cannot('delete', 'User')` and\n * `cannot('delete', 'Organisation')`, while superadmin holds `manage all`. That\n * is deliberately expressed as an ABILITY rather than as two contracts — a\n * second set of types for \"the manager's version of the people list\" would drift\n * from the first the moment either changed, and the client would be deciding\n * what it is allowed to do instead of being told.\n *\n * SCOPE. These are PLATFORM views: unscoped by gym, unscoped by center. No gym\n * role can reach them (see the `GymApproval` subject note in `casl-ability`), so\n * nothing here is filtered by organisation.\n *\n * @module gymmonk-schema/platform\n */\n\nimport { z } from 'zod';\nimport {\n deletedFilterSchema,\n isoDateTimeSchema,\n objectIdSchema,\n optionalTrimmedString,\n} from './common.js';\nimport { organisationApprovalStatusSchema } from './organisation.js';\nimport { appRoleSchema, userStatusSchema } from './user.js';\n\n// ─── Role keys ───────────────────────────────────────────────────────────────\n\n/**\n * Every role the tab-bar switcher understands — the wire contract for \"what\n * hats does this person wear\".\n *\n * The SSOT for a set that was previously written out three times: as a TypeScript\n * union in the backend's `casl-ability.ts`, again in the web client's\n * `shared/config/roles.ts`, and again as the values of the backend's role-snapshot\n * maps. Three copies of a closed set is three chances for one of them to miss a\n * role, and the people directory filters on exactly this set — a filter offering\n * a role the deriver never emits returns a permanently empty list with no error.\n *\n * Note `platform_manager` vs the `manager` APP ROLE (`appRoleSchema`): the app\n * role is what is stored on the user, the role key is what the product calls the\n * hat. `gym_manager` is a different job entirely, which is why neither can be\n * called plain \"manager\" on the wire.\n */\nexport const roleKeySchema = z.enum([\n 'member',\n 'owner',\n 'superadmin',\n 'platform_manager',\n 'moderator',\n 'support_agent',\n 'gym_manager',\n 'trainer',\n 'frontdesk',\n]);\nexport type RoleKey = z.infer<typeof roleKeySchema>;\n\n// ─── People directory ────────────────────────────────────────────────────────\n\n/**\n * The gym a person owns, summarised for a directory row.\n *\n * Present only for owners. It is on the row rather than behind a second request\n * because \"which gym is this?\" is the first question asked about any owner in a\n * support or vetting context, and a directory that makes you open each row to\n * find out is not a directory.\n */\nexport const platformUserGymSchema = z.object({\n id: objectIdSchema,\n name: z.string(),\n approvalStatus: organisationApprovalStatusSchema,\n});\nexport type PlatformUserGym = z.infer<typeof platformUserGymSchema>;\n\n/**\n * A row in the platform's people directory.\n *\n * `name` arrives RESOLVED — the backend has already applied the\n * `firstName lastName` → email fallback chain, so every client renders the same\n * thing and no screen invents its own version of \"what do we call this person\".\n *\n * `appRoles` and `roles` are BOTH here and they are not the same list.\n * `appRoles` is what is stored on the account and is what an admin can grant or\n * revoke. `roles` is the full derived set including the gym hats implied by\n * domain records — owning an organisation, holding a staff record, having a\n * member profile. An admin who is shown only `appRoles` cannot tell a plain\n * member from the owner of the platform's largest gym.\n */\nexport const platformUserSchema = z.object({\n id: objectIdSchema,\n name: z.string(),\n email: z.string(),\n /** Raw 10 digits, no `+91` — the country code is display-only. */\n phone: z.string().nullable(),\n avatar: z.string().nullable(),\n /** Granted platform roles. Empty for the overwhelming majority of accounts. */\n appRoles: z.array(appRoleSchema),\n /** Every hat, including the gym roles derived from domain records. */\n roles: z.array(roleKeySchema),\n status: userStatusSchema,\n organisation: platformUserGymSchema.nullable(),\n /**\n * Soft-deleted. The account is retained (deleting an owner would cascade into\n * an organisation, its centres, every member and the payment ledger) but is\n * excluded from every ordinary query. Only a superadmin can set this.\n */\n isDeleted: z.boolean(),\n lastActiveAt: isoDateTimeSchema.nullable(),\n createdAt: isoDateTimeSchema,\n});\nexport type PlatformUser = z.infer<typeof platformUserSchema>;\n\n/**\n * The people-list filter.\n *\n * `role` filters on the DERIVED set, not on `appRoles`, so \"show me every gym\n * owner\" works — owner is never an app role. The derivation is denormalised onto\n * the user (`User.roleSnapshot.roles`), which is what makes this an indexed\n * query rather than a fan-out per candidate row.\n */\nexport const platformUserQuerySchema = z.object({\n page: z.coerce.number().int().min(1).default(1),\n limit: z.coerce.number().int().min(1).max(100).default(20),\n /** Name, email or phone. */\n q: optionalTrimmedString(120),\n role: roleKeySchema.optional(),\n status: userStatusSchema.optional(),\n /** Soft-delete visibility. `only` is the superadmin's restore queue. */\n deleted: deletedFilterSchema.default('exclude'),\n});\nexport type PlatformUserQuery = z.infer<typeof platformUserQuerySchema>;\n\n/** Header counts for the people directory. */\nexport const platformPeopleSummarySchema = z.object({\n total: z.number().int(),\n active: z.number().int(),\n suspended: z.number().int(),\n /** Accounts holding at least one platform app-role. */\n staff: z.number().int(),\n owners: z.number().int(),\n});\nexport type PlatformPeopleSummary = z.infer<typeof platformPeopleSummarySchema>;\n\n// ─── Overdue fees ────────────────────────────────────────────────────────────\n\n/**\n * One member who owes a gym money, seen from the platform.\n *\n * ── WHY THIS IS NOT A `PlatformUser` ────────────────────────────────────────\n * Two reasons, and the second is the load-bearing one.\n *\n * `PlatformUser` carries no money at all — no plan, no balance, no due date —\n * and bolting those onto every row of a directory that is overwhelmingly NOT\n * members would be null for almost everybody and cost a join per page.\n *\n * ⚠️ And `PlatformUser.organisation` is populated ONLY FOR OWNERS, by design.\n * A member's gym is not on that row. This screen exists to say \"chase this\n * person about that gym\", so the gym is mandatory here, not nullable.\n *\n * ── KEYED ON THE ENROLMENT, NOT THE USER ────────────────────────────────────\n * `userId` is nullable and frequently null: a gym can sell a plan at the desk\n * to somebody who never installs the app, and those are exactly the members\n * most likely to be late. The enrolment is the thing that always exists, so it\n * is the id. Anything keyed on the user would silently drop the population this\n * screen is most useful for.\n */\nexport const platformOverdueMemberSchema = z.object({\n /** The enrolment — the only id every overdue member is guaranteed to have. */\n id: objectIdSchema,\n /** The term being chased. Actions and dedupe key on this, never on the person. */\n membershipId: objectIdSchema,\n /** Null when the desk added this member and nobody ever claimed the record. */\n userId: objectIdSchema.nullable(),\n /**\n * Resolved by the backend: the account's name, else the name typed at intake.\n *\n * ⚠️ MAY BE EMPTY, and consumers must handle that rather than assume a\n * placeholder. It is empty only when the gym recorded no name at all, and it\n * is deliberately NOT filled with something like \"Unclaimed member\" —\n * this string is read aloud in the WhatsApp reminder, so a placeholder here\n * greets a real person as \"Hi Unclaimed,\".\n */\n name: z.string(),\n email: z.string().nullable(),\n /** Raw 10 digits. Either the account's or the `invitePhone` the desk wrote down. */\n phone: z.string().nullable(),\n avatar: z.string().nullable(),\n /**\n * Every hat, so the operator can tell a plain member from a gym's own owner\n * who is behind on their personal membership. Empty for an unclaimed record —\n * there is no account to hold a role.\n */\n roles: z.array(roleKeySchema),\n /** Account standing. Null for an unclaimed record: there is no account. */\n status: userStatusSchema.nullable(),\n /** Soft-deleted account. An unclaimed record is never deleted, so false. */\n isDeleted: z.boolean(),\n /** MANDATORY here — see the note above on why it is nullable on PlatformUser. */\n gym: platformUserGymSchema,\n planName: z.string(),\n /**\n * The gym day the term ended, as stored: `YYYY-MM-DD`.\n *\n * ⚠️ A calendar label, NOT an instant. Never hand it to `new Date()` for\n * display — `new Date('2026-08-15')` is midnight UTC and renders as the 14th\n * anywhere west of Greenwich.\n */\n dueDate: z.string(),\n overdueDays: z.number().int(),\n balanceInr: z.number(),\n priceInr: z.number(),\n});\nexport type PlatformOverdueMember = z.infer<typeof platformOverdueMemberSchema>;\n\n/**\n * The overdue-fee list filter.\n *\n * ⚠️ NO UPPER BOUND ON HOW LATE. The automated sweep caps its lookback so its\n * first production run cannot message everybody who has ever been late; this\n * screen sends nothing by itself, so it inherits no such risk. Capping here\n * would silently hide the oldest debts — which are the ones actually worth a\n * phone call.\n */\nexport const platformOverdueQuerySchema = z.object({\n page: z.coerce.number().int().min(1).default(1),\n limit: z.coerce.number().int().min(1).max(100).default(20),\n /** Member name, email or phone. */\n q: optionalTrimmedString(120),\n /** Narrow to one gym, for a support conversation about that gym. */\n organisationId: objectIdSchema.optional(),\n /** At least this many days late. 1 means a full day must have passed. */\n minOverdueDays: z.coerce.number().int().min(1).max(3650).default(1),\n /** Worst debt first, or longest overdue first. */\n sort: z.enum(['overdue', 'amount']).default('overdue'),\n});\nexport type PlatformOverdueQuery = z.infer<typeof platformOverdueQuerySchema>;\n\n/** Header counts for the overdue list. */\nexport const platformOverdueSummarySchema = z.object({\n members: z.number().int(),\n gyms: z.number().int(),\n totalDueInr: z.number(),\n});\nexport type PlatformOverdueSummary = z.infer<typeof platformOverdueSummarySchema>;\n\n// ─── People mutations ────────────────────────────────────────────────────────\n\n/**\n * Suspend or reinstate an account.\n *\n * Only these two values. `deleted` is reached through the delete endpoint (which\n * needs a stronger ability and a typed confirmation), and `inactive` is a\n * lifecycle state the product sets for itself — letting an admin write either\n * one here would put the account into a state no screen explains and no action\n * reverses.\n */\nexport const setUserStatusBodySchema = z.object({\n status: z.enum(['active', 'suspended']),\n});\nexport type SetUserStatusBody = z.infer<typeof setUserStatusBodySchema>;\n\n/**\n * Replace the account's platform roles.\n *\n * A whole-array PUT rather than add/remove deltas: the console shows all four\n * checkboxes at once, so the admin is always looking at the complete intended\n * state, and a delta API would let two admins editing the same person silently\n * merge into a set neither chose.\n */\nexport const setAppRolesBodySchema = z.object({\n appRoles: z.array(appRoleSchema).max(4),\n});\nexport type SetAppRolesBody = z.infer<typeof setAppRolesBodySchema>;\n\n/**\n * Soft-delete an account. Superadmin only.\n *\n * The caller must echo the account's EMAIL back. The same echo-to-confirm guard\n * the member and membership deletions use — it is the one pattern that survives\n * a mis-tap on a phone, where a destructive button sits a thumb's width from a\n * harmless one.\n */\nexport const deletePlatformUserBodySchema = z.object({\n confirmationEmail: z.string().trim().min(1),\n});\nexport type DeletePlatformUserBody = z.infer<typeof deletePlatformUserBodySchema>;\n\n// ─── Platform overview (the console's Dashboard tab) ─────────────────────────\n\n/**\n * One month of platform growth.\n *\n * `label` is rendered SERVER-SIDE, already in IST. Month bucketing is done with\n * an explicit `Asia/Kolkata` timezone in the aggregation, and the label is built\n * from the same bucket — so the client never derives a date. Letting it do so\n * would put a UTC container and an IST phone in different months for five and a\n * half hours every night, which is a hydration mismatch that only reproduces\n * after 18:30 IST.\n */\nexport const platformTrendPointSchema = z.object({\n /** Short month, e.g. `Mar`. Includes the year when the window crosses one. */\n label: z.string(),\n /** Accounts created that month. */\n people: z.number().int(),\n /** Gyms registered that month. */\n gyms: z.number().int(),\n});\nexport type PlatformTrendPoint = z.infer<typeof platformTrendPointSchema>;\n\n/** A gym in the \"biggest on the platform\" table. */\nexport const platformTopGymSchema = z.object({\n id: objectIdSchema,\n name: z.string(),\n memberCount: z.number().int(),\n centerCount: z.number().int(),\n approvalStatus: organisationApprovalStatusSchema,\n});\nexport type PlatformTopGym = z.infer<typeof platformTopGymSchema>;\n\n/**\n * Everything the Dashboard tab renders, in one request.\n *\n * One endpoint rather than six. This screen is the console's landing tab, so its\n * cost is paid on every visit — and six round trips would each carry their own\n * loading state, producing a dashboard that reflows four times before settling.\n * The aggregations run concurrently server-side, where they are cheap.\n */\nexport const platformOverviewSchema = z.object({\n totals: z.object({\n /** Registered gyms, whatever their approval state. */\n gyms: z.number().int(),\n centers: z.number().int(),\n /** People with a member record at a gym — NOT the same as `people`. */\n members: z.number().int(),\n /** Memberships currently live (active or paused, not expired). */\n activeMemberships: z.number().int(),\n /** Every account on GymMonk, members and staff alike. */\n people: z.number().int(),\n /** All money successfully collected across every gym, in whole rupees. */\n revenueInr: z.number().int(),\n }),\n /** Growth, oldest month first. */\n trend: z.array(platformTrendPointSchema),\n gymsByStatus: z.object({\n pending: z.number().int(),\n approved: z.number().int(),\n rejected: z.number().int(),\n }),\n topGyms: z.array(platformTopGymSchema),\n /**\n * What is waiting for a human. Every number here is a link on the dashboard,\n * so a count of zero is a good outcome rather than an empty section.\n */\n attention: z.object({\n pendingGyms: z.number().int(),\n suspendedAccounts: z.number().int(),\n /** Approved gyms that never created a centre, so members cannot join them. */\n gymsWithoutCenters: z.number().int(),\n }),\n});\nexport type PlatformOverview = z.infer<typeof platformOverviewSchema>;\n\n// ─── Gym directory ───────────────────────────────────────────────────────────\n\n/**\n * Header counts for the gyms directory, and the review-queue badge.\n *\n * `pending` was previously the only count returned, which was enough for a badge\n * and nothing else: the Gyms tab shows the whole estate, and a screen that can\n * say \"12 pending\" but not \"of how many\" tells an operator nothing about scale.\n */\nexport const platformGymSummarySchema = z.object({\n total: z.number().int(),\n pending: z.number().int(),\n approved: z.number().int(),\n rejected: z.number().int(),\n /** Centres across every approved gym. */\n centers: z.number().int(),\n});\nexport type PlatformGymSummary = z.infer<typeof platformGymSummarySchema>;\n\n/**\n * Soft-delete a gym. Superadmin only.\n *\n * Echoes the gym's NAME rather than an id — an operator reading a confirmation\n * dialog can verify a name against the row they meant to act on; an id they\n * would paste without reading.\n */\nexport const deletePlatformGymBodySchema = z.object({\n confirmationName: z.string().trim().min(1),\n});\nexport type DeletePlatformGymBody = z.infer<typeof deletePlatformGymBodySchema>;\n"]}
package/dist/user.d.ts CHANGED
@@ -85,9 +85,21 @@ export declare const notificationPreferencesSchema: z.ZodObject<{
85
85
  dailyWorkout: z.ZodBoolean;
86
86
  calorieBurn: z.ZodBoolean;
87
87
  membershipRenewal: z.ZodBoolean;
88
+ whatsappLateFee: z.ZodBoolean;
88
89
  }, z.core.$strip>;
89
90
  export type NotificationPreferences = z.infer<typeof notificationPreferencesSchema>;
90
- /** Everything on, nothing paused — what a new account gets. */
91
+ /**
92
+ * Everything on, nothing paused — what a new account gets, and what an account
93
+ * with no stored block resolves to.
94
+ *
95
+ * ⚠️ `whatsappLateFee: true` is doing real work here, not padding the object.
96
+ * Every account created before this field existed has NO stored preferences at
97
+ * all, so they all read through to this default — which is the intended
98
+ * outcome under the opt-out ruling, but it means the value below is the actual
99
+ * consent position of the entire existing member base. Changing it to `false`
100
+ * silently switches the feature off for everyone who has never opened the
101
+ * preference screen.
102
+ */
91
103
  export declare const DEFAULT_NOTIFICATION_PREFERENCES: NotificationPreferences;
92
104
  export declare const updateNotificationPreferencesBodySchema: z.ZodObject<{
93
105
  pauseAll: z.ZodOptional<z.ZodBoolean>;
@@ -95,6 +107,7 @@ export declare const updateNotificationPreferencesBodySchema: z.ZodObject<{
95
107
  dailyWorkout: z.ZodOptional<z.ZodBoolean>;
96
108
  calorieBurn: z.ZodOptional<z.ZodBoolean>;
97
109
  membershipRenewal: z.ZodOptional<z.ZodBoolean>;
110
+ whatsappLateFee: z.ZodOptional<z.ZodBoolean>;
98
111
  }, z.core.$strip>;
99
112
  export type UpdateNotificationPreferencesBody = z.infer<typeof updateNotificationPreferencesBodySchema>;
100
113
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"user.d.ts","sourceRoot":"","sources":["../src/user.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAKxB,kFAAkF;AAClF,eAAO,MAAM,aAAa;;;;;EAAkE,CAAC;AAC7F,MAAM,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAEpD,eAAO,MAAM,gBAAgB;;;;;EAAyD,CAAC;AACvF,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAI1D,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;iBAiCnB,CAAC;AACH,MAAM,MAAM,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,QAAQ,CAAC,CAAC;AAI1C;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,oBAAoB;;;;;iBAQ/B,CAAC;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAIlE;;;;;;;;GAQG;AACH,eAAO,MAAM,6BAA6B;;;;;;iBAUxC,CAAC;AACH,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,6BAA6B,CAAC,CAAC;AAEpF,+DAA+D;AAC/D,eAAO,MAAM,gCAAgC,EAAE,uBAM9C,CAAC;AAEF,eAAO,MAAM,uCAAuC;;;;;;iBAA0C,CAAC;AAC/F,MAAM,MAAM,iCAAiC,GAAG,CAAC,CAAC,KAAK,CACrD,OAAO,uCAAuC,CAC/C,CAAC;AAIF;;;;;;;;GAQG;AACH,eAAO,MAAM,4BAA4B;;iBAEvC,CAAC;AACH,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,4BAA4B,CAAC,CAAC"}
1
+ {"version":3,"file":"user.d.ts","sourceRoot":"","sources":["../src/user.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAKxB,kFAAkF;AAClF,eAAO,MAAM,aAAa;;;;;EAAkE,CAAC;AAC7F,MAAM,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAEpD,eAAO,MAAM,gBAAgB;;;;;EAAyD,CAAC;AACvF,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAI1D,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;iBAiCnB,CAAC;AACH,MAAM,MAAM,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,QAAQ,CAAC,CAAC;AAI1C;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,oBAAoB;;;;;iBAQ/B,CAAC;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAIlE;;;;;;;;GAQG;AACH,eAAO,MAAM,6BAA6B;;;;;;;iBAmCxC,CAAC;AACH,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,6BAA6B,CAAC,CAAC;AAEpF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,gCAAgC,EAAE,uBAO9C,CAAC;AAEF,eAAO,MAAM,uCAAuC;;;;;;;iBAA0C,CAAC;AAC/F,MAAM,MAAM,iCAAiC,GAAG,CAAC,CAAC,KAAK,CACrD,OAAO,uCAAuC,CAC/C,CAAC;AAIF;;;;;;;;GAQG;AACH,eAAO,MAAM,4BAA4B;;iBAEvC,CAAC;AACH,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,4BAA4B,CAAC,CAAC"}
package/dist/user.js CHANGED
@@ -91,14 +91,51 @@ export const notificationPreferencesSchema = z.object({
91
91
  calorieBurn: z.boolean(),
92
92
  /** "Your membership expires in N days". */
93
93
  membershipRenewal: z.boolean(),
94
+ /**
95
+ * The WhatsApp late-fee reminder, and the ONLY switch a member has for it.
96
+ *
97
+ * ── WHY IT IS ON BY DEFAULT, WHICH NONE OF THE OTHERS HAD TO JUSTIFY ──────
98
+ * Product ruling: enrolling at a gym is consent to be told over WhatsApp that
99
+ * a fee is overdue. So this is opt-OUT, unlike anything else here, and that
100
+ * asymmetry is deliberate rather than an oversight — a reminder nobody
101
+ * receives is a debt nobody is told about.
102
+ *
103
+ * ⚠️ IT IS ALSO THE ONLY PREFERENCE THAT COSTS MONEY TO IGNORE. Meta bills
104
+ * per conversation, so a send to somebody who has said no is a charge against
105
+ * the gym's allowance AND a mark against the quality rating of the one
106
+ * business number every gym on the platform shares.
107
+ *
108
+ * ⚠️ `pauseAll` SILENCES THIS TOO. That follows this object's own stated
109
+ * contract — a paused account must not be woken by a kind added later — and
110
+ * it is the safe direction: under-sending is recoverable, an unwanted paid
111
+ * message is not.
112
+ *
113
+ * ⚠️ This flag is NOT the whole answer, because it can only speak for people
114
+ * who have an account. A member the gym wrote down but who never claimed
115
+ * theirs still receives reminders and can still reply STOP, which is recorded
116
+ * against their number instead. Both are checked before a send.
117
+ */
118
+ whatsappLateFee: z.boolean(),
94
119
  });
95
- /** Everything on, nothing paused — what a new account gets. */
120
+ /**
121
+ * Everything on, nothing paused — what a new account gets, and what an account
122
+ * with no stored block resolves to.
123
+ *
124
+ * ⚠️ `whatsappLateFee: true` is doing real work here, not padding the object.
125
+ * Every account created before this field existed has NO stored preferences at
126
+ * all, so they all read through to this default — which is the intended
127
+ * outcome under the opt-out ruling, but it means the value below is the actual
128
+ * consent position of the entire existing member base. Changing it to `false`
129
+ * silently switches the feature off for everyone who has never opened the
130
+ * preference screen.
131
+ */
96
132
  export const DEFAULT_NOTIFICATION_PREFERENCES = {
97
133
  pauseAll: false,
98
134
  gymTime: true,
99
135
  dailyWorkout: true,
100
136
  calorieBurn: true,
101
137
  membershipRenewal: true,
138
+ whatsappLateFee: true,
102
139
  };
103
140
  export const updateNotificationPreferencesBodySchema = notificationPreferencesSchema.partial();
104
141
  // ─── Account deletion ────────────────────────────────────────────────────────
package/dist/user.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"user.js","sourceRoot":"","sources":["../src/user.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAE,qBAAqB,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAEpG,gFAAgF;AAEhF,kFAAkF;AAClF,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,eAAe,EAAE,WAAW,EAAE,SAAS,EAAE,YAAY,CAAC,CAAC,CAAC;AAG7F,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,UAAU,EAAE,WAAW,EAAE,SAAS,CAAC,CAAC,CAAC;AAGvF,gFAAgF;AAEhF,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,CAAC,MAAM,CAAC;IAC/B,EAAE,EAAE,cAAc;IAClB,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE;IACjB,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE;IACrB,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC/B,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC5B;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,aAAa,EAAE,CAAC,CAAC,OAAO,EAAE;IAC1B,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC7B,QAAQ,EAAE,CAAC,CAAC,KAAK,CAAC,aAAa,CAAC;IAChC,MAAM,EAAE,gBAAgB;IACxB,SAAS,EAAE,iBAAiB;IAC5B,SAAS,EAAE,iBAAiB;CAC7B,CAAC,CAAC;AAGH,gFAAgF;AAEhF;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC3C,SAAS,EAAE,qBAAqB,CAAC,EAAE,CAAC;IACpC,QAAQ,EAAE,qBAAqB,CAAC,EAAE,CAAC;IACnC,KAAK,EAAE,CAAC,CAAC,UAAU,CACjB,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,EACjE,WAAW,CAAC,QAAQ,EAAE,CACvB;IACD,MAAM,EAAE,qBAAqB,CAAC,IAAI,CAAC;CACpC,CAAC,CAAC;AAGH,gFAAgF;AAEhF;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,6BAA6B,GAAG,CAAC,CAAC,MAAM,CAAC;IACpD,QAAQ,EAAE,CAAC,CAAC,OAAO,EAAE;IACrB,yDAAyD;IACzD,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE;IACpB,uCAAuC;IACvC,YAAY,EAAE,CAAC,CAAC,OAAO,EAAE;IACzB,sDAAsD;IACtD,WAAW,EAAE,CAAC,CAAC,OAAO,EAAE;IACxB,2CAA2C;IAC3C,iBAAiB,EAAE,CAAC,CAAC,OAAO,EAAE;CAC/B,CAAC,CAAC;AAGH,+DAA+D;AAC/D,MAAM,CAAC,MAAM,gCAAgC,GAA4B;IACvE,QAAQ,EAAE,KAAK;IACf,OAAO,EAAE,IAAI;IACb,YAAY,EAAE,IAAI;IAClB,WAAW,EAAE,IAAI;IACjB,iBAAiB,EAAE,IAAI;CACxB,CAAC;AAEF,MAAM,CAAC,MAAM,uCAAuC,GAAG,6BAA6B,CAAC,OAAO,EAAE,CAAC;AAK/F,gFAAgF;AAEhF;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,4BAA4B,GAAG,CAAC,CAAC,MAAM,CAAC;IACnD,WAAW,EAAE,iBAAiB;CAC/B,CAAC,CAAC","sourcesContent":["/**\n * gymmonk-schema — Authenticated user identity (\"me\")\n * ===================================================\n * The identity the whole app boots from — the shape of `GET /users/me`, mirrored\n * from the auth-synced `User`. This is the SSOT for the signed-in user's name,\n * avatar, email, phone, platform roles and status; the member/owner profile\n * modules layer product-specific data on top of it.\n *\n * @module gymmonk-schema/user\n */\n\nimport { z } from 'zod';\nimport { isoDateTimeSchema, objectIdSchema, optionalTrimmedString, phoneSchema } from './common.js';\n\n// ─── Platform roles & status ─────────────────────────────────────────────────\n\n/** Platform-wide roles (empty for most users). Distinct from gym owner/member. */\nexport const appRoleSchema = z.enum(['support_agent', 'moderator', 'manager', 'superadmin']);\nexport type AppRole = z.infer<typeof appRoleSchema>;\n\nexport const userStatusSchema = z.enum(['active', 'inactive', 'suspended', 'deleted']);\nexport type UserStatus = z.infer<typeof userStatusSchema>;\n\n// ─── The \"me\" entity ─────────────────────────────────────────────────────────\n\nexport const meSchema = z.object({\n id: objectIdSchema,\n email: z.string(),\n firstName: z.string(),\n lastName: z.string().nullable(),\n phone: z.string().nullable(),\n /**\n * Whether we have evidence this number reaches this person.\n *\n * True only when they opened a gym's WhatsApp invite link: the gym sent it to\n * that number, so whoever opened it holds that WhatsApp. A number the person\n * typed themselves is false, however true it happens to be — this records\n * what we have SEEN, not what we believe.\n *\n * It matters because the phone is the join key between an app account and a\n * gym's member record, and when two numbers disagree we have to prefer one.\n * The verified one wins.\n *\n * ⚠️ NOT a proof of ownership, and nothing security-bearing may treat it as\n * one. A WhatsApp message forwards, so this says the opener had the message,\n * not that they hold the SIM. It is an OTP's poor relation and is only ever\n * used to break a tie.\n *\n * ⚠️ Distinct from the roster's `verified`, which means \"this person has\n * claimed their record\". Two different facts, and a screen that shows both\n * must not let them read as one.\n */\n phoneVerified: z.boolean(),\n avatar: z.string().nullable(),\n appRoles: z.array(appRoleSchema),\n status: userStatusSchema,\n createdAt: isoDateTimeSchema,\n updatedAt: isoDateTimeSchema,\n});\nexport type Me = z.infer<typeof meSchema>;\n\n// ─── Update the caller's own identity fields ─────────────────────────────────\n\n/**\n * Patch the product-owned identity fields (name/phone/avatar).\n * `email` is auth-owned and never updated here.\n *\n * A user has exactly TWO name fields and no denormalised third one. There used\n * to be a `displayName`, seeded once from the central-auth (Google) name and\n * never recomputed — and because every screen read it FIRST, the name a member\n * typed during onboarding was stored and never shown.\n *\n * The name is submitted as a UNIT: the profile screen edits it in one box and\n * splits on the first space, so a body carrying `firstName` is the complete\n * name and an absent `lastName` means the surname was deleted. The API applies\n * that rule (`shared/routes/user.route`), because `optionalTrimmedString` maps\n * an empty string to `undefined` and the wire cannot say \"clear it\" otherwise.\n */\nexport const updateUserBodySchema = z.object({\n firstName: optionalTrimmedString(80),\n lastName: optionalTrimmedString(80),\n phone: z.preprocess(\n (v) => (typeof v === 'string' && v.trim() === '' ? undefined : v),\n phoneSchema.optional(),\n ),\n avatar: optionalTrimmedString(2000),\n});\nexport type UpdateUserBody = z.infer<typeof updateUserBodySchema>;\n\n// ─── Notification preferences ────────────────────────────────────────────────\n\n/**\n * What a person wants to be told about.\n *\n * `pauseAll` is a MASTER switch, not a sixth category: it silences everything\n * without the user having to remember which four they had turned on, and\n * un-pausing restores exactly what they had. The dispatcher checks it first\n * (see `notification/gym.dispatch`), so a paused account cannot be woken by a\n * new notification kind added later that nobody thought to gate.\n */\nexport const notificationPreferencesSchema = z.object({\n pauseAll: z.boolean(),\n /** Reminders before a booked or habitual gym session. */\n gymTime: z.boolean(),\n /** The day's assigned workout plan. */\n dailyWorkout: z.boolean(),\n /** Estimated calorie-burn updates through the day. */\n calorieBurn: z.boolean(),\n /** \"Your membership expires in N days\". */\n membershipRenewal: z.boolean(),\n});\nexport type NotificationPreferences = z.infer<typeof notificationPreferencesSchema>;\n\n/** Everything on, nothing paused — what a new account gets. */\nexport const DEFAULT_NOTIFICATION_PREFERENCES: NotificationPreferences = {\n pauseAll: false,\n gymTime: true,\n dailyWorkout: true,\n calorieBurn: true,\n membershipRenewal: true,\n};\n\nexport const updateNotificationPreferencesBodySchema = notificationPreferencesSchema.partial();\nexport type UpdateNotificationPreferencesBody = z.infer<\n typeof updateNotificationPreferencesBodySchema\n>;\n\n// ─── Account deletion ────────────────────────────────────────────────────────\n\n/**\n * A request to erase the account.\n *\n * Recorded, not executed. Deleting a gym owner cascades into the organisation,\n * its centres, every member and staff record, the membership ledger and the\n * payment history — some of which the business is legally required to keep for\n * a period. So the account is marked, signed out and queued for an operator,\n * which is also what \"right to erasure with a verification step\" looks like.\n */\nexport const accountDeletionRequestSchema = z.object({\n requestedAt: isoDateTimeSchema,\n});\nexport type AccountDeletionRequest = z.infer<typeof accountDeletionRequestSchema>;\n"]}
1
+ {"version":3,"file":"user.js","sourceRoot":"","sources":["../src/user.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * gymmonk-schema — Authenticated user identity (\"me\")\n * ===================================================\n * The identity the whole app boots from — the shape of `GET /users/me`, mirrored\n * from the auth-synced `User`. This is the SSOT for the signed-in user's name,\n * avatar, email, phone, platform roles and status; the member/owner profile\n * modules layer product-specific data on top of it.\n *\n * @module gymmonk-schema/user\n */\n\nimport { z } from 'zod';\nimport { isoDateTimeSchema, objectIdSchema, optionalTrimmedString, phoneSchema } from './common.js';\n\n// ─── Platform roles & status ─────────────────────────────────────────────────\n\n/** Platform-wide roles (empty for most users). Distinct from gym owner/member. */\nexport const appRoleSchema = z.enum(['support_agent', 'moderator', 'manager', 'superadmin']);\nexport type AppRole = z.infer<typeof appRoleSchema>;\n\nexport const userStatusSchema = z.enum(['active', 'inactive', 'suspended', 'deleted']);\nexport type UserStatus = z.infer<typeof userStatusSchema>;\n\n// ─── The \"me\" entity ─────────────────────────────────────────────────────────\n\nexport const meSchema = z.object({\n id: objectIdSchema,\n email: z.string(),\n firstName: z.string(),\n lastName: z.string().nullable(),\n phone: z.string().nullable(),\n /**\n * Whether we have evidence this number reaches this person.\n *\n * True only when they opened a gym's WhatsApp invite link: the gym sent it to\n * that number, so whoever opened it holds that WhatsApp. A number the person\n * typed themselves is false, however true it happens to be — this records\n * what we have SEEN, not what we believe.\n *\n * It matters because the phone is the join key between an app account and a\n * gym's member record, and when two numbers disagree we have to prefer one.\n * The verified one wins.\n *\n * ⚠️ NOT a proof of ownership, and nothing security-bearing may treat it as\n * one. A WhatsApp message forwards, so this says the opener had the message,\n * not that they hold the SIM. It is an OTP's poor relation and is only ever\n * used to break a tie.\n *\n * ⚠️ Distinct from the roster's `verified`, which means \"this person has\n * claimed their record\". Two different facts, and a screen that shows both\n * must not let them read as one.\n */\n phoneVerified: z.boolean(),\n avatar: z.string().nullable(),\n appRoles: z.array(appRoleSchema),\n status: userStatusSchema,\n createdAt: isoDateTimeSchema,\n updatedAt: isoDateTimeSchema,\n});\nexport type Me = z.infer<typeof meSchema>;\n\n// ─── Update the caller's own identity fields ─────────────────────────────────\n\n/**\n * Patch the product-owned identity fields (name/phone/avatar).\n * `email` is auth-owned and never updated here.\n *\n * A user has exactly TWO name fields and no denormalised third one. There used\n * to be a `displayName`, seeded once from the central-auth (Google) name and\n * never recomputed — and because every screen read it FIRST, the name a member\n * typed during onboarding was stored and never shown.\n *\n * The name is submitted as a UNIT: the profile screen edits it in one box and\n * splits on the first space, so a body carrying `firstName` is the complete\n * name and an absent `lastName` means the surname was deleted. The API applies\n * that rule (`shared/routes/user.route`), because `optionalTrimmedString` maps\n * an empty string to `undefined` and the wire cannot say \"clear it\" otherwise.\n */\nexport const updateUserBodySchema = z.object({\n firstName: optionalTrimmedString(80),\n lastName: optionalTrimmedString(80),\n phone: z.preprocess(\n (v) => (typeof v === 'string' && v.trim() === '' ? undefined : v),\n phoneSchema.optional(),\n ),\n avatar: optionalTrimmedString(2000),\n});\nexport type UpdateUserBody = z.infer<typeof updateUserBodySchema>;\n\n// ─── Notification preferences ────────────────────────────────────────────────\n\n/**\n * What a person wants to be told about.\n *\n * `pauseAll` is a MASTER switch, not a sixth category: it silences everything\n * without the user having to remember which four they had turned on, and\n * un-pausing restores exactly what they had. The dispatcher checks it first\n * (see `notification/gym.dispatch`), so a paused account cannot be woken by a\n * new notification kind added later that nobody thought to gate.\n */\nexport const notificationPreferencesSchema = z.object({\n pauseAll: z.boolean(),\n /** Reminders before a booked or habitual gym session. */\n gymTime: z.boolean(),\n /** The day's assigned workout plan. */\n dailyWorkout: z.boolean(),\n /** Estimated calorie-burn updates through the day. */\n calorieBurn: z.boolean(),\n /** \"Your membership expires in N days\". */\n membershipRenewal: z.boolean(),\n /**\n * The WhatsApp late-fee reminder, and the ONLY switch a member has for it.\n *\n * ── WHY IT IS ON BY DEFAULT, WHICH NONE OF THE OTHERS HAD TO JUSTIFY ──────\n * Product ruling: enrolling at a gym is consent to be told over WhatsApp that\n * a fee is overdue. So this is opt-OUT, unlike anything else here, and that\n * asymmetry is deliberate rather than an oversight — a reminder nobody\n * receives is a debt nobody is told about.\n *\n * ⚠️ IT IS ALSO THE ONLY PREFERENCE THAT COSTS MONEY TO IGNORE. Meta bills\n * per conversation, so a send to somebody who has said no is a charge against\n * the gym's allowance AND a mark against the quality rating of the one\n * business number every gym on the platform shares.\n *\n * ⚠️ `pauseAll` SILENCES THIS TOO. That follows this object's own stated\n * contract — a paused account must not be woken by a kind added later — and\n * it is the safe direction: under-sending is recoverable, an unwanted paid\n * message is not.\n *\n * ⚠️ This flag is NOT the whole answer, because it can only speak for people\n * who have an account. A member the gym wrote down but who never claimed\n * theirs still receives reminders and can still reply STOP, which is recorded\n * against their number instead. Both are checked before a send.\n */\n whatsappLateFee: z.boolean(),\n});\nexport type NotificationPreferences = z.infer<typeof notificationPreferencesSchema>;\n\n/**\n * Everything on, nothing paused — what a new account gets, and what an account\n * with no stored block resolves to.\n *\n * ⚠️ `whatsappLateFee: true` is doing real work here, not padding the object.\n * Every account created before this field existed has NO stored preferences at\n * all, so they all read through to this default — which is the intended\n * outcome under the opt-out ruling, but it means the value below is the actual\n * consent position of the entire existing member base. Changing it to `false`\n * silently switches the feature off for everyone who has never opened the\n * preference screen.\n */\nexport const DEFAULT_NOTIFICATION_PREFERENCES: NotificationPreferences = {\n pauseAll: false,\n gymTime: true,\n dailyWorkout: true,\n calorieBurn: true,\n membershipRenewal: true,\n whatsappLateFee: true,\n};\n\nexport const updateNotificationPreferencesBodySchema = notificationPreferencesSchema.partial();\nexport type UpdateNotificationPreferencesBody = z.infer<\n typeof updateNotificationPreferencesBodySchema\n>;\n\n// ─── Account deletion ────────────────────────────────────────────────────────\n\n/**\n * A request to erase the account.\n *\n * Recorded, not executed. Deleting a gym owner cascades into the organisation,\n * its centres, every member and staff record, the membership ledger and the\n * payment history — some of which the business is legally required to keep for\n * a period. So the account is marked, signed out and queued for an operator,\n * which is also what \"right to erasure with a verification step\" looks like.\n */\nexport const accountDeletionRequestSchema = z.object({\n requestedAt: isoDateTimeSchema,\n});\nexport type AccountDeletionRequest = z.infer<typeof accountDeletionRequestSchema>;\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gymmonk-schema",
3
- "version": "0.65.0",
3
+ "version": "0.67.0",
4
4
  "description": "Shared Zod schemas, enums and domain types for GymMonk (fitness SaaS) — single source of truth (SSOT) consumed by gymmonk-backend and gymmonk-web-client.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",