gymmonk-schema 0.38.0 → 0.40.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/file.d.ts CHANGED
@@ -25,7 +25,7 @@ import { z } from 'zod';
25
25
  * `<folder>/<userId>/<timestamp>-<random><ext>`. That per-user prefix is what
26
26
  * makes ownership checkable on cleanup.
27
27
  */
28
- export declare const UPLOAD_FOLDERS: readonly ["profile-photos", "center-photos", "logos", "member-documents", "equipment-photos", "announcement-media", "exercise-media", "progress-photos", "feedback-media"];
28
+ export declare const UPLOAD_FOLDERS: readonly ["profile-photos", "center-photos", "logos", "member-documents", "equipment-photos", "announcement-media", "exercise-media", "progress-photos", "payment-proofs", "feedback-media"];
29
29
  export declare const uploadFolderSchema: z.ZodEnum<{
30
30
  "profile-photos": "profile-photos";
31
31
  "center-photos": "center-photos";
@@ -35,6 +35,7 @@ export declare const uploadFolderSchema: z.ZodEnum<{
35
35
  "announcement-media": "announcement-media";
36
36
  "exercise-media": "exercise-media";
37
37
  "progress-photos": "progress-photos";
38
+ "payment-proofs": "payment-proofs";
38
39
  "feedback-media": "feedback-media";
39
40
  }>;
40
41
  export type UploadFolder = z.infer<typeof uploadFolderSchema>;
@@ -82,6 +83,7 @@ export declare const signedUploadUrlBodySchema: z.ZodObject<{
82
83
  "announcement-media": "announcement-media";
83
84
  "exercise-media": "exercise-media";
84
85
  "progress-photos": "progress-photos";
86
+ "payment-proofs": "payment-proofs";
85
87
  "feedback-media": "feedback-media";
86
88
  }>;
87
89
  fileSize: z.ZodNumber;
@@ -1 +1 @@
1
- {"version":3,"file":"file.d.ts","sourceRoot":"","sources":["../src/file.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB;;;;;;;;;GASG;AACH,eAAO,MAAM,cAAc,4KAmBjB,CAAC;AAEX,eAAO,MAAM,kBAAkB;;;;;;;;;;EAAyB,CAAC;AACzD,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAI9D,wDAAwD;AACxD,eAAO,MAAM,yBAAyB,oFAM5B,CAAC;AAEX,eAAO,MAAM,oBAAoB;;;;;;EAAoC,CAAC;AACtE,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAIlE;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,KAAK,CAAC;AAErC,wEAAwE;AACxE,eAAO,MAAM,kBAAkB,EAAE,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,CAEpE,CAAC;AAMF;;;;;;GAMG;AACH,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;;;;;iBA0BlC,CAAC;AAEL,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAE5E,mDAAmD;AACnD,eAAO,MAAM,6BAA6B;;;;;iBAUxC,CAAC;AAEH,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,6BAA6B,CAAC,CAAC;AAIpF;;;;;;;;GAQG;AACH,eAAO,MAAM,wBAAwB;;iBAEnC,CAAC;AAEH,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAE1E,eAAO,MAAM,4BAA4B;;;iBAGvC,CAAC;AAEH,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,4BAA4B,CAAC,CAAC"}
1
+ {"version":3,"file":"file.d.ts","sourceRoot":"","sources":["../src/file.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB;;;;;;;;;GASG;AACH,eAAO,MAAM,cAAc,8LAqBjB,CAAC;AAEX,eAAO,MAAM,kBAAkB;;;;;;;;;;;EAAyB,CAAC;AACzD,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAI9D,wDAAwD;AACxD,eAAO,MAAM,yBAAyB,oFAM5B,CAAC;AAEX,eAAO,MAAM,oBAAoB;;;;;;EAAoC,CAAC;AACtE,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAIlE;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,KAAK,CAAC;AAErC,wEAAwE;AACxE,eAAO,MAAM,kBAAkB,EAAE,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,CAEpE,CAAC;AAMF;;;;;;GAMG;AACH,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;;;;;;iBA0BlC,CAAC;AAEL,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAE5E,mDAAmD;AACnD,eAAO,MAAM,6BAA6B;;;;;iBAUxC,CAAC;AAEH,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,6BAA6B,CAAC,CAAC;AAIpF;;;;;;;;GAQG;AACH,eAAO,MAAM,wBAAwB;;iBAEnC,CAAC;AAEH,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAE1E,eAAO,MAAM,4BAA4B;;;iBAGvC,CAAC;AAEH,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,4BAA4B,CAAC,CAAC"}
package/dist/file.js CHANGED
@@ -43,6 +43,8 @@ export const UPLOAD_FOLDERS = [
43
43
  'exercise-media',
44
44
  /** Progress photos a member uploads against a measurement entry. */
45
45
  'progress-photos',
46
+ /** Screenshot of a transfer, attached to a membership awaiting confirmation. */
47
+ 'payment-proofs',
46
48
  /** Screenshots and photos attached to feedback or a complaint. */
47
49
  'feedback-media',
48
50
  ];
package/dist/file.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"file.js","sourceRoot":"","sources":["../src/file.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * gymmonk-schema — File uploads\n * =============================\n * Contracts for the signed-URL upload flow. GymMonk never streams file bytes\n * through the API: the browser asks the backend for a short-lived v4 signed\n * PUT URL, uploads straight to Google Cloud Storage, and stores the returned\n * PERMANENT public URL.\n *\n * The download URL is deliberately NOT signed. A signed read URL expires,\n * which would rot every image reference already written to Mongo; the bucket\n * instead grants `allUsers` object-viewer and we store the plain public URL.\n *\n * Mirrors reform-backend's `shared/schemas/file.schema.ts`.\n *\n * @module gymmonk-schema/file\n */\n\nimport { z } from 'zod';\n\n// ─── Folders ─────────────────────────────────────────────────────────────────\n\n/**\n * Every folder an upload may target. This is an allow-list, not a hint: the\n * backend rejects anything outside it, so a compromised client cannot scatter\n * objects across the bucket. Adding a new upload surface means adding it here\n * FIRST, then publishing the package.\n *\n * `userId` is appended by the backend, so the effective path is\n * `<folder>/<userId>/<timestamp>-<random><ext>`. That per-user prefix is what\n * makes ownership checkable on cleanup.\n */\nexport const UPLOAD_FOLDERS = [\n /** Member + owner + staff avatars. Square, cropped round in the UI. */\n 'profile-photos',\n /** Gym/center gallery photos. Multi-select, uncropped. */\n 'center-photos',\n /** Organisation and center logos. */\n 'logos',\n /** Member ID proof / address proof captured at the front desk. */\n 'member-documents',\n /** Equipment photos in the gym inventory. */\n 'equipment-photos',\n /** Images attached to an announcement. */\n 'announcement-media',\n /** Custom exercise thumbnails added by a gym. */\n 'exercise-media',\n /** Progress photos a member uploads against a measurement entry. */\n 'progress-photos',\n /** Screenshots and photos attached to feedback or a complaint. */\n 'feedback-media',\n] as const;\n\nexport const uploadFolderSchema = z.enum(UPLOAD_FOLDERS);\nexport type UploadFolder = z.infer<typeof uploadFolderSchema>;\n\n// ─── Content types ───────────────────────────────────────────────────────────\n\n/** MIME types the signed-URL endpoint will sign for. */\nexport const ALLOWED_UPLOAD_MIME_TYPES = [\n 'image/jpeg',\n 'image/png',\n 'image/webp',\n 'image/gif',\n 'application/pdf',\n] as const;\n\nexport const uploadMimeTypeSchema = z.enum(ALLOWED_UPLOAD_MIME_TYPES);\nexport type UploadMimeType = z.infer<typeof uploadMimeTypeSchema>;\n\n// ─── Size limits ─────────────────────────────────────────────────────────────\n\n/**\n * Default per-file ceiling, in MB. Validation runs on the RAW pick, before the\n * client-side optimizer shrinks it — a modern phone camera produces 8-12 MB\n * JPEGs that compress to well under 1 MB, so a tighter cap would reject\n * perfectly ordinary photos.\n */\nexport const MAX_UPLOAD_SIZE_MB = 10;\n\n/** Per-folder overrides. Documents may be larger scans than a photo. */\nexport const FOLDER_MAX_SIZE_MB: Partial<Record<UploadFolder, number>> = {\n 'member-documents': 20,\n};\n\nconst HARD_CAP_MB = Math.max(MAX_UPLOAD_SIZE_MB, ...Object.values(FOLDER_MAX_SIZE_MB));\n\n// ─── Signed upload URL ───────────────────────────────────────────────────────\n\n/**\n * Request body for `POST /api/v1/files/signed-url`.\n *\n * `fileName` is restricted to a safe charset because it becomes part of the GCS\n * object path — the client sanitises before sending, and this rejects anything\n * that slipped through (path separators, control characters, unicode tricks).\n */\nexport const signedUploadUrlBodySchema = z\n .object({\n fileName: z\n .string()\n .min(1)\n .max(255)\n .regex(/^[a-zA-Z0-9_.-]+$/, 'Invalid file name'),\n contentType: uploadMimeTypeSchema,\n folder: uploadFolderSchema,\n fileSize: z\n .number()\n .int()\n .min(1)\n .max(HARD_CAP_MB * 1024 * 1024, `File must be smaller than ${HARD_CAP_MB}MB`),\n })\n .superRefine((data, ctx) => {\n // Tighten to the per-folder cap. The outer `.max` is only the widest\n // ceiling any folder allows; this is the one that actually applies.\n const capMb = FOLDER_MAX_SIZE_MB[data.folder] ?? MAX_UPLOAD_SIZE_MB;\n if (data.fileSize > capMb * 1024 * 1024) {\n ctx.addIssue({\n code: 'custom',\n path: ['fileSize'],\n message: `File must be smaller than ${capMb}MB for ${data.folder}`,\n });\n }\n });\n\nexport type SignedUploadUrlBody = z.infer<typeof signedUploadUrlBodySchema>;\n\n/** Response of `POST /api/v1/files/signed-url`. */\nexport const signedUploadUrlResponseSchema = z.object({\n /** Short-lived v4 signed URL — PUT the bytes here. */\n uploadUrl: z.string(),\n /** Permanent public URL. This is what gets stored in Mongo. */\n downloadUrl: z.string(),\n /** Object path inside the bucket, without the bucket prefix. Used to\n * delete an object the user abandoned before saving. */\n filePath: z.string(),\n /** When `uploadUrl` stops working. */\n expiresAt: z.iso.datetime(),\n});\n\nexport type SignedUploadUrlResponse = z.infer<typeof signedUploadUrlResponseSchema>;\n\n// ─── Cleanup ─────────────────────────────────────────────────────────────────\n\n/**\n * Body for `POST /api/v1/files/cleanup-uploads`. Called when a user removes a\n * photo they uploaded but never saved, so the orphaned object does not sit in\n * the bucket forever.\n *\n * Ownership is enforced per-path from the `userId` segment the backend embedded\n * at signing time. Paths belonging to someone else are skipped silently rather\n * than 403-ing, so one foreign entry cannot void a legitimate batch.\n */\nexport const cleanupUploadsBodySchema = z.object({\n filePaths: z.array(z.string().min(1)).min(1).max(20),\n});\n\nexport type CleanupUploadsBody = z.infer<typeof cleanupUploadsBodySchema>;\n\nexport const cleanupUploadsResponseSchema = z.object({\n deleted: z.number().int().min(0),\n skipped: z.number().int().min(0),\n});\n\nexport type CleanupUploadsResponse = z.infer<typeof cleanupUploadsResponseSchema>;\n"]}
1
+ {"version":3,"file":"file.js","sourceRoot":"","sources":["../src/file.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * gymmonk-schema — File uploads\n * =============================\n * Contracts for the signed-URL upload flow. GymMonk never streams file bytes\n * through the API: the browser asks the backend for a short-lived v4 signed\n * PUT URL, uploads straight to Google Cloud Storage, and stores the returned\n * PERMANENT public URL.\n *\n * The download URL is deliberately NOT signed. A signed read URL expires,\n * which would rot every image reference already written to Mongo; the bucket\n * instead grants `allUsers` object-viewer and we store the plain public URL.\n *\n * Mirrors reform-backend's `shared/schemas/file.schema.ts`.\n *\n * @module gymmonk-schema/file\n */\n\nimport { z } from 'zod';\n\n// ─── Folders ─────────────────────────────────────────────────────────────────\n\n/**\n * Every folder an upload may target. This is an allow-list, not a hint: the\n * backend rejects anything outside it, so a compromised client cannot scatter\n * objects across the bucket. Adding a new upload surface means adding it here\n * FIRST, then publishing the package.\n *\n * `userId` is appended by the backend, so the effective path is\n * `<folder>/<userId>/<timestamp>-<random><ext>`. That per-user prefix is what\n * makes ownership checkable on cleanup.\n */\nexport const UPLOAD_FOLDERS = [\n /** Member + owner + staff avatars. Square, cropped round in the UI. */\n 'profile-photos',\n /** Gym/center gallery photos. Multi-select, uncropped. */\n 'center-photos',\n /** Organisation and center logos. */\n 'logos',\n /** Member ID proof / address proof captured at the front desk. */\n 'member-documents',\n /** Equipment photos in the gym inventory. */\n 'equipment-photos',\n /** Images attached to an announcement. */\n 'announcement-media',\n /** Custom exercise thumbnails added by a gym. */\n 'exercise-media',\n /** Progress photos a member uploads against a measurement entry. */\n 'progress-photos',\n /** Screenshot of a transfer, attached to a membership awaiting confirmation. */\n 'payment-proofs',\n /** Screenshots and photos attached to feedback or a complaint. */\n 'feedback-media',\n] as const;\n\nexport const uploadFolderSchema = z.enum(UPLOAD_FOLDERS);\nexport type UploadFolder = z.infer<typeof uploadFolderSchema>;\n\n// ─── Content types ───────────────────────────────────────────────────────────\n\n/** MIME types the signed-URL endpoint will sign for. */\nexport const ALLOWED_UPLOAD_MIME_TYPES = [\n 'image/jpeg',\n 'image/png',\n 'image/webp',\n 'image/gif',\n 'application/pdf',\n] as const;\n\nexport const uploadMimeTypeSchema = z.enum(ALLOWED_UPLOAD_MIME_TYPES);\nexport type UploadMimeType = z.infer<typeof uploadMimeTypeSchema>;\n\n// ─── Size limits ─────────────────────────────────────────────────────────────\n\n/**\n * Default per-file ceiling, in MB. Validation runs on the RAW pick, before the\n * client-side optimizer shrinks it — a modern phone camera produces 8-12 MB\n * JPEGs that compress to well under 1 MB, so a tighter cap would reject\n * perfectly ordinary photos.\n */\nexport const MAX_UPLOAD_SIZE_MB = 10;\n\n/** Per-folder overrides. Documents may be larger scans than a photo. */\nexport const FOLDER_MAX_SIZE_MB: Partial<Record<UploadFolder, number>> = {\n 'member-documents': 20,\n};\n\nconst HARD_CAP_MB = Math.max(MAX_UPLOAD_SIZE_MB, ...Object.values(FOLDER_MAX_SIZE_MB));\n\n// ─── Signed upload URL ───────────────────────────────────────────────────────\n\n/**\n * Request body for `POST /api/v1/files/signed-url`.\n *\n * `fileName` is restricted to a safe charset because it becomes part of the GCS\n * object path — the client sanitises before sending, and this rejects anything\n * that slipped through (path separators, control characters, unicode tricks).\n */\nexport const signedUploadUrlBodySchema = z\n .object({\n fileName: z\n .string()\n .min(1)\n .max(255)\n .regex(/^[a-zA-Z0-9_.-]+$/, 'Invalid file name'),\n contentType: uploadMimeTypeSchema,\n folder: uploadFolderSchema,\n fileSize: z\n .number()\n .int()\n .min(1)\n .max(HARD_CAP_MB * 1024 * 1024, `File must be smaller than ${HARD_CAP_MB}MB`),\n })\n .superRefine((data, ctx) => {\n // Tighten to the per-folder cap. The outer `.max` is only the widest\n // ceiling any folder allows; this is the one that actually applies.\n const capMb = FOLDER_MAX_SIZE_MB[data.folder] ?? MAX_UPLOAD_SIZE_MB;\n if (data.fileSize > capMb * 1024 * 1024) {\n ctx.addIssue({\n code: 'custom',\n path: ['fileSize'],\n message: `File must be smaller than ${capMb}MB for ${data.folder}`,\n });\n }\n });\n\nexport type SignedUploadUrlBody = z.infer<typeof signedUploadUrlBodySchema>;\n\n/** Response of `POST /api/v1/files/signed-url`. */\nexport const signedUploadUrlResponseSchema = z.object({\n /** Short-lived v4 signed URL — PUT the bytes here. */\n uploadUrl: z.string(),\n /** Permanent public URL. This is what gets stored in Mongo. */\n downloadUrl: z.string(),\n /** Object path inside the bucket, without the bucket prefix. Used to\n * delete an object the user abandoned before saving. */\n filePath: z.string(),\n /** When `uploadUrl` stops working. */\n expiresAt: z.iso.datetime(),\n});\n\nexport type SignedUploadUrlResponse = z.infer<typeof signedUploadUrlResponseSchema>;\n\n// ─── Cleanup ─────────────────────────────────────────────────────────────────\n\n/**\n * Body for `POST /api/v1/files/cleanup-uploads`. Called when a user removes a\n * photo they uploaded but never saved, so the orphaned object does not sit in\n * the bucket forever.\n *\n * Ownership is enforced per-path from the `userId` segment the backend embedded\n * at signing time. Paths belonging to someone else are skipped silently rather\n * than 403-ing, so one foreign entry cannot void a legitimate batch.\n */\nexport const cleanupUploadsBodySchema = z.object({\n filePaths: z.array(z.string().min(1)).min(1).max(20),\n});\n\nexport type CleanupUploadsBody = z.infer<typeof cleanupUploadsBodySchema>;\n\nexport const cleanupUploadsResponseSchema = z.object({\n deleted: z.number().int().min(0),\n skipped: z.number().int().min(0),\n});\n\nexport type CleanupUploadsResponse = z.infer<typeof cleanupUploadsResponseSchema>;\n"]}
package/dist/member.d.ts CHANGED
@@ -35,6 +35,8 @@ export declare const memberRosterItemSchema: z.ZodObject<{
35
35
  status: z.ZodNullable<z.ZodEnum<{
36
36
  invited: "invited";
37
37
  active: "active";
38
+ pending: "pending";
39
+ rejected: "rejected";
38
40
  paused: "paused";
39
41
  expired: "expired";
40
42
  cancelled: "cancelled";
@@ -201,6 +203,8 @@ export declare const memberDetailSchema: z.ZodObject<{
201
203
  status: z.ZodEnum<{
202
204
  invited: "invited";
203
205
  active: "active";
206
+ pending: "pending";
207
+ rejected: "rejected";
204
208
  paused: "paused";
205
209
  expired: "expired";
206
210
  cancelled: "cancelled";
@@ -209,6 +213,11 @@ export declare const memberDetailSchema: z.ZodObject<{
209
213
  endDate: z.ZodString;
210
214
  pausedFrom: z.ZodNullable<z.ZodString>;
211
215
  pausedTo: z.ZodNullable<z.ZodString>;
216
+ paymentProofUrl: z.ZodNullable<z.ZodString>;
217
+ requestedAt: z.ZodISODateTime;
218
+ decidedAt: z.ZodNullable<z.ZodISODateTime>;
219
+ decidedBy: z.ZodNullable<z.ZodString>;
220
+ decisionNote: z.ZodNullable<z.ZodString>;
212
221
  createdAt: z.ZodISODateTime;
213
222
  updatedAt: z.ZodISODateTime;
214
223
  }, z.core.$strip>>;
@@ -285,6 +294,7 @@ export declare const createMemberBodySchema: z.ZodObject<{
285
294
  planId: z.ZodString;
286
295
  variantId: z.ZodString;
287
296
  startDate: z.ZodOptional<z.ZodString>;
297
+ paymentProofUrl: z.ZodOptional<z.ZodString>;
288
298
  payment: z.ZodOptional<z.ZodObject<{
289
299
  amountInr: z.ZodNumber;
290
300
  method: z.ZodEnum<{
@@ -294,8 +304,8 @@ export declare const createMemberBodySchema: z.ZodObject<{
294
304
  }>;
295
305
  status: z.ZodDefault<z.ZodEnum<{
296
306
  success: "success";
297
- failed: "failed";
298
307
  pending: "pending";
308
+ failed: "failed";
299
309
  }>>;
300
310
  }, z.core.$strip>>;
301
311
  }, z.core.$strip>>;
@@ -429,6 +439,8 @@ export declare const provisionResultSchema: z.ZodObject<{
429
439
  status: z.ZodEnum<{
430
440
  invited: "invited";
431
441
  active: "active";
442
+ pending: "pending";
443
+ rejected: "rejected";
432
444
  paused: "paused";
433
445
  expired: "expired";
434
446
  cancelled: "cancelled";
@@ -437,6 +449,11 @@ export declare const provisionResultSchema: z.ZodObject<{
437
449
  endDate: z.ZodString;
438
450
  pausedFrom: z.ZodNullable<z.ZodString>;
439
451
  pausedTo: z.ZodNullable<z.ZodString>;
452
+ paymentProofUrl: z.ZodNullable<z.ZodString>;
453
+ requestedAt: z.ZodISODateTime;
454
+ decidedAt: z.ZodNullable<z.ZodISODateTime>;
455
+ decidedBy: z.ZodNullable<z.ZodString>;
456
+ decisionNote: z.ZodNullable<z.ZodString>;
440
457
  createdAt: z.ZodISODateTime;
441
458
  updatedAt: z.ZodISODateTime;
442
459
  }, z.core.$strip>>;
@@ -1 +1 @@
1
- {"version":3,"file":"member.d.ts","sourceRoot":"","sources":["../src/member.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"member.d.ts","sourceRoot":"","sources":["../src/member.ts"],"names":[],"mappings":"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"}
@@ -53,14 +53,48 @@ export declare const planVariantSchema: z.ZodObject<{
53
53
  month: "month";
54
54
  }>;
55
55
  originalPriceInr: z.ZodNumber;
56
- discountPercent: z.ZodDefault<z.ZodNumber>;
56
+ finalPriceInr: z.ZodNumber;
57
57
  popular: z.ZodOptional<z.ZodBoolean>;
58
58
  }, z.core.$strip>;
59
59
  export type PlanVariant = z.infer<typeof planVariantSchema>;
60
+ /**
61
+ * The same variant, with the one rule the two prices have between them.
62
+ *
63
+ * Separate from `planVariantSchema` because a `.refine()` turns a schema into
64
+ * a ZodEffects, and `.partial()` or `.omit()` on one of those throws at MODULE
65
+ * LOAD — invisible to a build, and the page simply dies on import. Keeping
66
+ * the plain object available means the plan bodies can go on composing it
67
+ * while the API still validates the pair.
68
+ */
69
+ export declare const checkedPlanVariantSchema: z.ZodObject<{
70
+ id: z.ZodString;
71
+ durationCount: z.ZodNumber;
72
+ durationUnit: z.ZodEnum<{
73
+ year: "year";
74
+ day: "day";
75
+ week: "week";
76
+ month: "month";
77
+ }>;
78
+ originalPriceInr: z.ZodNumber;
79
+ finalPriceInr: z.ZodNumber;
80
+ popular: z.ZodOptional<z.ZodBoolean>;
81
+ }, z.core.$strip>;
60
82
  /** What the member actually pays, in whole rupees. */
61
- export declare function variantFinalPrice(variant: Pick<PlanVariant, 'originalPriceInr' | 'discountPercent'>): number;
83
+ export declare function variantFinalPrice(variant: Pick<PlanVariant, 'finalPriceInr'>): number;
62
84
  /** Whole rupees saved, or 0 on an undiscounted variant. */
63
- export declare function variantSavings(variant: Pick<PlanVariant, 'originalPriceInr' | 'discountPercent'>): number;
85
+ export declare function variantSavings(variant: Pick<PlanVariant, 'originalPriceInr' | 'finalPriceInr'>): number;
86
+ /**
87
+ * The discount as a percentage, for the "25% off" ribbon.
88
+ *
89
+ * DERIVED, and rounded only here where rounding is harmless: this number is
90
+ * shown, never stored and never used to compute a price. That is the whole
91
+ * point of the change — the rounding used to happen before the value was
92
+ * saved, so it moved the money.
93
+ *
94
+ * Zero when there is no discount, or when the original price is zero and the
95
+ * question has no answer.
96
+ */
97
+ export declare function variantDiscountPercent(variant: Pick<PlanVariant, 'originalPriceInr' | 'finalPriceInr'>): number;
64
98
  /** `{ 3, 'month' }` → `"3 Months"`. One spelling of a duration, everywhere. */
65
99
  export declare function variantLabel(variant: Pick<PlanVariant, 'durationCount' | 'durationUnit'>): string;
66
100
  /**
@@ -589,7 +623,7 @@ export declare const createMembershipPlanBodySchema: z.ZodObject<{
589
623
  month: "month";
590
624
  }>;
591
625
  originalPriceInr: z.ZodNumber;
592
- discountPercent: z.ZodDefault<z.ZodNumber>;
626
+ finalPriceInr: z.ZodNumber;
593
627
  popular: z.ZodOptional<z.ZodBoolean>;
594
628
  }, z.core.$strip>>;
595
629
  }, z.core.$strip>;
@@ -686,7 +720,7 @@ export declare const updateMembershipPlanBodySchema: z.ZodObject<{
686
720
  month: "month";
687
721
  }>;
688
722
  originalPriceInr: z.ZodNumber;
689
- discountPercent: z.ZodDefault<z.ZodNumber>;
723
+ finalPriceInr: z.ZodNumber;
690
724
  popular: z.ZodOptional<z.ZodBoolean>;
691
725
  }, z.core.$strip>>>;
692
726
  isActive: z.ZodOptional<z.ZodBoolean>;
@@ -786,7 +820,7 @@ export declare const membershipPlanSchema: z.ZodObject<{
786
820
  month: "month";
787
821
  }>;
788
822
  originalPriceInr: z.ZodNumber;
789
- discountPercent: z.ZodDefault<z.ZodNumber>;
823
+ finalPriceInr: z.ZodNumber;
790
824
  popular: z.ZodOptional<z.ZodBoolean>;
791
825
  }, z.core.$strip>>;
792
826
  isActive: z.ZodBoolean;
@@ -1 +1 @@
1
- {"version":3,"file":"membership-plan.d.ts","sourceRoot":"","sources":["../src/membership-plan.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"membership-plan.d.ts","sourceRoot":"","sources":["../src/membership-plan.ts"],"names":[],"mappings":"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"}
@@ -7,6 +7,7 @@
7
7
  * @module gymmonk-schema/membership-plan
8
8
  */
9
9
  import { z } from 'zod';
10
+ import { ERROR_MESSAGE } from './messages.js';
10
11
  import { isoDateTimeSchema, objectIdSchema, optionalTrimmedString, priceInrSchema, } from './common.js';
11
12
  // ─── Accent ──────────────────────────────────────────────────────────────────
12
13
  /** UI accent colour for the plan card. */
@@ -47,19 +48,54 @@ export const planVariantSchema = z.object({
47
48
  durationUnit: durationUnitSchema,
48
49
  /** The list price, before any discount. Struck through on the card. */
49
50
  originalPriceInr: priceInrSchema,
50
- /** Whole percent off. 0 means the plan sells at its list price. */
51
- discountPercent: z.number().int().min(0).max(100).default(0),
51
+ /**
52
+ * What the member actually pays. THE STORED TRUTH, not a derivation.
53
+ *
54
+ * It used to be a whole-number `discountPercent` with the price computed
55
+ * from it, which cannot represent most prices a gym would name. A gym
56
+ * pricing ${'₹'}1000 down to ${'₹'}999 is offering 0.1% off; rounded to the nearest
57
+ * whole percent that is 0, so the price came back as ${'₹'}1000 and the ${'₹'}999
58
+ * they typed was quietly gone. Owners think in the price on the poster, not
59
+ * in percentages, so the price is what is kept and the percentage is worked
60
+ * out for display.
61
+ */
62
+ finalPriceInr: priceInrSchema,
52
63
  /** Ribbon on the variant tile. Editorial, like the plan badge. */
53
64
  popular: z.boolean().optional(),
54
65
  });
66
+ /**
67
+ * The same variant, with the one rule the two prices have between them.
68
+ *
69
+ * Separate from `planVariantSchema` because a `.refine()` turns a schema into
70
+ * a ZodEffects, and `.partial()` or `.omit()` on one of those throws at MODULE
71
+ * LOAD — invisible to a build, and the page simply dies on import. Keeping
72
+ * the plain object available means the plan bodies can go on composing it
73
+ * while the API still validates the pair.
74
+ */
75
+ export const checkedPlanVariantSchema = planVariantSchema.refine((v) => v.finalPriceInr <= v.originalPriceInr, { message: ERROR_MESSAGE.planFinalPriceAboveOriginal, path: ['finalPriceInr'] });
55
76
  /** What the member actually pays, in whole rupees. */
56
77
  export function variantFinalPrice(variant) {
57
- const off = Math.round((variant.originalPriceInr * variant.discountPercent) / 100);
58
- return Math.max(0, variant.originalPriceInr - off);
78
+ return variant.finalPriceInr;
59
79
  }
60
80
  /** Whole rupees saved, or 0 on an undiscounted variant. */
61
81
  export function variantSavings(variant) {
62
- return variant.originalPriceInr - variantFinalPrice(variant);
82
+ return Math.max(0, variant.originalPriceInr - variant.finalPriceInr);
83
+ }
84
+ /**
85
+ * The discount as a percentage, for the "25% off" ribbon.
86
+ *
87
+ * DERIVED, and rounded only here where rounding is harmless: this number is
88
+ * shown, never stored and never used to compute a price. That is the whole
89
+ * point of the change — the rounding used to happen before the value was
90
+ * saved, so it moved the money.
91
+ *
92
+ * Zero when there is no discount, or when the original price is zero and the
93
+ * question has no answer.
94
+ */
95
+ export function variantDiscountPercent(variant) {
96
+ if (variant.originalPriceInr <= 0)
97
+ return 0;
98
+ return Math.round((variantSavings(variant) / variant.originalPriceInr) * 100);
63
99
  }
64
100
  /** `{ 3, 'month' }` → `"3 Months"`. One spelling of a duration, everywhere. */
65
101
  export function variantLabel(variant) {
@@ -214,7 +250,7 @@ export const createMembershipPlanBodySchema = z.object({
214
250
  * equally is pushing none of them. */
215
251
  badge: planBadgeSchema.nullish(),
216
252
  features: z.array(planFeatureSchema).max(40).default([]),
217
- variants: z.array(planVariantSchema).min(1, 'Add at least one variant'),
253
+ variants: z.array(checkedPlanVariantSchema).min(1, 'Add at least one variant'),
218
254
  });
219
255
  export const updateMembershipPlanBodySchema = createMembershipPlanBodySchema
220
256
  .omit({ centerId: true })
@@ -229,7 +265,7 @@ export const membershipPlanSchema = z.object({
229
265
  accent: planAccentSchema,
230
266
  badge: planBadgeSchema.nullable(),
231
267
  features: z.array(planFeatureSchema),
232
- variants: z.array(planVariantSchema),
268
+ variants: z.array(checkedPlanVariantSchema),
233
269
  isActive: z.boolean(),
234
270
  sortOrder: z.number().int(),
235
271
  /** Denormalised count of active members on this plan (owner list view). */
@@ -1 +1 @@
1
- {"version":3,"file":"membership-plan.js","sourceRoot":"","sources":["../src/membership-plan.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * gymmonk-schema — Membership plan (owner-defined catalog)\n * ========================================================\n * The plans a gym owner defines and sells to members, scoped to a center. A\n * member's active `membership` references one of these by id + variant.\n *\n * @module gymmonk-schema/membership-plan\n */\n\nimport { z } from 'zod';\nimport {\n isoDateTimeSchema,\n objectIdSchema,\n optionalTrimmedString,\n priceInrSchema,\n} from './common.js';\n\n// ─── Accent ──────────────────────────────────────────────────────────────────\n\n/** UI accent colour for the plan card. */\nexport const planAccentSchema = z.enum(['neutral', 'teal', 'purple', 'gold']);\nexport type PlanAccent = z.infer<typeof planAccentSchema>;\n\n// ─── Variants (duration + price) ─────────────────────────────────────────────\n\n/**\n * The unit a plan's duration is counted in.\n *\n * `day` earns its place: a gym selling a one-day trial pass or a ten-day\n * tourist package cannot express either in months, and the fixed\n * 1m/3m/6m/1y list it replaces forced them to.\n */\nexport const durationUnitSchema = z.enum(['day', 'week', 'month', 'year']);\nexport type DurationUnit = z.infer<typeof durationUnitSchema>;\n\n/** Singular / plural label for each unit, so no caller writes `unit + 's'`. */\nexport const DURATION_UNIT_LABEL: Record<DurationUnit, { one: string; many: string }> = {\n day: { one: 'Day', many: 'Days' },\n week: { one: 'Week', many: 'Weeks' },\n month: { one: 'Month', many: 'Months' },\n year: { one: 'Year', many: 'Years' },\n};\n\n/**\n * One buyable duration of a plan: how long it runs, what it lists at, and what\n * comes off.\n *\n * The gym enters the ORIGINAL price and a DISCOUNT; the selling price is\n * derived (`variantFinalPrice`) rather than stored. Storing all three invites\n * the arithmetic to drift — a gym edits the discount, forgets the total, and\n * the card advertises a saving the checkout does not honour.\n */\nexport const planVariantSchema = z.object({\n /**\n * Stable per plan, minted by the client when a variant is added, because a\n * live `membership` points back at it. Reusing an array index instead would\n * silently re-point every membership when a variant is deleted.\n */\n id: z.string().trim().min(1).max(40),\n durationCount: z.number().int().min(1).max(365),\n durationUnit: durationUnitSchema,\n /** The list price, before any discount. Struck through on the card. */\n originalPriceInr: priceInrSchema,\n /** Whole percent off. 0 means the plan sells at its list price. */\n discountPercent: z.number().int().min(0).max(100).default(0),\n /** Ribbon on the variant tile. Editorial, like the plan badge. */\n popular: z.boolean().optional(),\n});\nexport type PlanVariant = z.infer<typeof planVariantSchema>;\n\n/** What the member actually pays, in whole rupees. */\nexport function variantFinalPrice(\n variant: Pick<PlanVariant, 'originalPriceInr' | 'discountPercent'>,\n): number {\n const off = Math.round((variant.originalPriceInr * variant.discountPercent) / 100);\n return Math.max(0, variant.originalPriceInr - off);\n}\n\n/** Whole rupees saved, or 0 on an undiscounted variant. */\nexport function variantSavings(\n variant: Pick<PlanVariant, 'originalPriceInr' | 'discountPercent'>,\n): number {\n return variant.originalPriceInr - variantFinalPrice(variant);\n}\n\n/** `{ 3, 'month' }` → `\"3 Months\"`. One spelling of a duration, everywhere. */\nexport function variantLabel(variant: Pick<PlanVariant, 'durationCount' | 'durationUnit'>): string {\n const label = DURATION_UNIT_LABEL[variant.durationUnit];\n return `${variant.durationCount} ${variant.durationCount === 1 ? label.one : label.many}`;\n}\n\n// ─── Badge ───────────────────────────────────────────────────────────────────\n\n/**\n * The ribbon above a plan card. Editorial, not derived: which plan a gym wants\n * to push is a commercial decision, not something to infer from price.\n */\nexport const planBadgeSchema = z.enum(['popular', 'best-value']);\nexport type PlanBadge = z.infer<typeof planBadgeSchema>;\n\nexport const PLAN_BADGE_LABEL: Record<PlanBadge, string> = {\n popular: 'Most Popular',\n 'best-value': 'Best Value',\n};\n\n// ─── Amenities ───────────────────────────────────────────────────────────────\n\n/**\n * What a gym can advertise on a plan, grouped the way the picker shows them.\n *\n * Deliberately EXHAUSTIVE. The old sixteen-key list made an owner describe a\n * real gym in someone else's vocabulary: no drinking water, no washroom, no\n * air conditioning, no parking — the things members actually ask about before\n * they join — so plans all read the same and the chips stopped meaning\n * anything.\n *\n * Keys are stable ids, never shown. The LABEL is the default caption and a gym\n * may override it per plan (see `planFeatureSchema.label`), which is how a\n * studio calls `classes` \"Group Sessions\" without needing a new key.\n */\nexport const AMENITY_CATEGORIES = [\n 'training',\n 'classes',\n 'facilities',\n 'wellness',\n 'services',\n 'extras',\n] as const;\nexport type AmenityCategory = (typeof AMENITY_CATEGORIES)[number];\n\nexport const AMENITY_CATEGORY_LABEL: Record<AmenityCategory, string> = {\n training: 'Training',\n classes: 'Classes',\n facilities: 'Facilities',\n wellness: 'Wellness & Recovery',\n services: 'Services',\n extras: 'Extras',\n};\n\n/**\n * The full catalogue: key → default caption + which group it sits in.\n *\n * Labels live HERE rather than in the client. They used to be duplicated in\n * two frontend maps that had drifted apart — the same key drew a different\n * icon on the owner's screen and the member's, and three keys drew nothing at\n * all on one of them. One source, and the client maps keys to icons only.\n */\nexport const AMENITY_CATALOG = {\n // ── Training floor ─────────────────────────────────────────────────────────\n cardio: { label: 'Cardio Machines', category: 'training' },\n strength: { label: 'Strength Training', category: 'training' },\n freeWeights: { label: 'Free Weights', category: 'training' },\n machines: { label: 'Weight Machines', category: 'training' },\n functional: { label: 'Functional Area', category: 'training' },\n crossfit: { label: 'CrossFit', category: 'training' },\n calisthenics: { label: 'Calisthenics', category: 'training' },\n powerlifting: { label: 'Powerlifting', category: 'training' },\n boxing: { label: 'Boxing', category: 'training' },\n martialArts: { label: 'Martial Arts', category: 'training' },\n treadmill: { label: 'Treadmills', category: 'training' },\n cycling: { label: 'Cycling', category: 'training' },\n rowing: { label: 'Rowing', category: 'training' },\n stretchZone: { label: 'Stretching Zone', category: 'training' },\n turfArea: { label: 'Turf Area', category: 'training' },\n\n // ── Classes ────────────────────────────────────────────────────────────────\n classes: { label: 'Group Classes', category: 'classes' },\n yoga: { label: 'Yoga', category: 'classes' },\n yogaArea: { label: 'Yoga Area', category: 'classes' },\n zumba: { label: 'Zumba', category: 'classes' },\n pilates: { label: 'Pilates', category: 'classes' },\n aerobics: { label: 'Aerobics', category: 'classes' },\n dance: { label: 'Dance', category: 'classes' },\n spinning: { label: 'Spinning', category: 'classes' },\n bootcamp: { label: 'Bootcamp', category: 'classes' },\n meditation: { label: 'Meditation', category: 'classes' },\n\n // ── Facilities ─────────────────────────────────────────────────────────────\n ac: { label: 'Air Conditioned', category: 'facilities' },\n drinkingWater: { label: 'Drinking Water', category: 'facilities' },\n washroom: { label: 'Washroom', category: 'facilities' },\n restroom: { label: 'Restroom', category: 'facilities' },\n shower: { label: 'Showers', category: 'facilities' },\n changingRoom: { label: 'Changing Room', category: 'facilities' },\n locker: { label: 'Lockers', category: 'facilities' },\n parking: { label: 'Parking', category: 'facilities' },\n wifi: { label: 'Free WiFi', category: 'facilities' },\n musicSystem: { label: 'Music System', category: 'facilities' },\n powerBackup: { label: 'Power Backup', category: 'facilities' },\n lift: { label: 'Lift Access', category: 'facilities' },\n cctv: { label: 'CCTV Security', category: 'facilities' },\n wheelchairAccess: { label: 'Wheelchair Access', category: 'facilities' },\n\n // ── Wellness & recovery ────────────────────────────────────────────────────\n steam: { label: 'Steam Room', category: 'wellness' },\n sauna: { label: 'Sauna', category: 'wellness' },\n spa: { label: 'Spa', category: 'wellness' },\n massage: { label: 'Massage', category: 'wellness' },\n iceBath: { label: 'Ice Bath', category: 'wellness' },\n pool: { label: 'Swimming Pool', category: 'wellness' },\n physiotherapy: { label: 'Physiotherapy', category: 'wellness' },\n\n // ── Services ───────────────────────────────────────────────────────────────\n trainer: { label: 'Trainer Support', category: 'services' },\n personalTraining: { label: 'Personal Training', category: 'services' },\n diet: { label: 'Diet Plan', category: 'services' },\n nutritionist: { label: 'Nutritionist', category: 'services' },\n health: { label: 'Health Stats', category: 'services' },\n bodyComposition: { label: 'Body Composition', category: 'services' },\n doctorConsult: { label: 'Doctor Consultation', category: 'services' },\n priority: { label: 'Priority Support', category: 'services' },\n\n // ── Extras ─────────────────────────────────────────────────────────────────\n cafe: { label: 'Cafe', category: 'extras' },\n coffeeMachine: { label: 'Coffee Machine', category: 'extras' },\n proteinBar: { label: 'Protein Bar', category: 'extras' },\n supplementStore: { label: 'Supplement Store', category: 'extras' },\n childArea: { label: 'Kids Play Area', category: 'extras' },\n towelService: { label: 'Towel Service', category: 'extras' },\n laundry: { label: 'Laundry', category: 'extras' },\n guestPass: { label: 'Guest Pass', category: 'extras' },\n unlimited: { label: 'Unlimited Access', category: 'extras' },\n multiCenter: { label: 'All Branches', category: 'extras' },\n} as const satisfies Record<string, { label: string; category: AmenityCategory }>;\n\n/** Every amenity key, in catalogue order. */\nexport const FEATURE_ICONS = Object.keys(AMENITY_CATALOG) as [FeatureIcon, ...FeatureIcon[]];\nexport type FeatureIcon = keyof typeof AMENITY_CATALOG;\n\nexport const featureIconSchema = z.enum(\n Object.keys(AMENITY_CATALOG) as [FeatureIcon, ...FeatureIcon[]],\n);\n\n/** The amenity keys in one category, for a grouped picker. */\nexport function amenitiesInCategory(category: AmenityCategory): FeatureIcon[] {\n return (Object.keys(AMENITY_CATALOG) as FeatureIcon[]).filter(\n (key) => AMENITY_CATALOG[key].category === category,\n );\n}\n\nexport const planFeatureSchema = z.object({\n icon: featureIconSchema,\n label: z.string().trim().min(1).max(80),\n});\nexport type PlanFeature = z.infer<typeof planFeatureSchema>;\n\n// ─── Create / update body ────────────────────────────────────────────────────\n\nexport const createMembershipPlanBodySchema = z.object({\n centerId: objectIdSchema,\n name: z.string().trim().min(1, 'Plan name is required').max(80),\n /**\n * What the gym wants to say about the plan in its own words.\n *\n * Replaces a 60-character `note` that the card rendered but no screen could\n * ever set — a caption nobody could write. One optional field, roomy enough\n * for a sentence or two.\n */\n description: optionalTrimmedString(300),\n accent: planAccentSchema,\n /** Ribbon above the card. Null on most plans — a gym pushing every plan\n * equally is pushing none of them. */\n badge: planBadgeSchema.nullish(),\n features: z.array(planFeatureSchema).max(40).default([]),\n variants: z.array(planVariantSchema).min(1, 'Add at least one variant'),\n});\nexport type CreateMembershipPlanBody = z.infer<typeof createMembershipPlanBodySchema>;\n\nexport const updateMembershipPlanBodySchema = createMembershipPlanBodySchema\n .omit({ centerId: true })\n .partial()\n .extend({ isActive: z.boolean().optional() });\nexport type UpdateMembershipPlanBody = z.infer<typeof updateMembershipPlanBodySchema>;\n\n// ─── Entity (API response) ───────────────────────────────────────────────────\n\nexport const membershipPlanSchema = z.object({\n id: objectIdSchema,\n centerId: objectIdSchema,\n name: z.string(),\n description: z.string().nullable(),\n accent: planAccentSchema,\n badge: planBadgeSchema.nullable(),\n features: z.array(planFeatureSchema),\n variants: z.array(planVariantSchema),\n isActive: z.boolean(),\n sortOrder: z.number().int(),\n /** Denormalised count of active members on this plan (owner list view). */\n memberCount: z.number().int().nonnegative(),\n createdAt: isoDateTimeSchema,\n updatedAt: isoDateTimeSchema,\n});\nexport type MembershipPlan = z.infer<typeof membershipPlanSchema>;\n"]}
1
+ {"version":3,"file":"membership-plan.js","sourceRoot":"","sources":["../src/membership-plan.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * gymmonk-schema — Membership plan (owner-defined catalog)\n * ========================================================\n * The plans a gym owner defines and sells to members, scoped to a center. A\n * member's active `membership` references one of these by id + variant.\n *\n * @module gymmonk-schema/membership-plan\n */\n\nimport { z } from 'zod';\nimport { ERROR_MESSAGE } from './messages.js';\nimport {\n isoDateTimeSchema,\n objectIdSchema,\n optionalTrimmedString,\n priceInrSchema,\n} from './common.js';\n\n// ─── Accent ──────────────────────────────────────────────────────────────────\n\n/** UI accent colour for the plan card. */\nexport const planAccentSchema = z.enum(['neutral', 'teal', 'purple', 'gold']);\nexport type PlanAccent = z.infer<typeof planAccentSchema>;\n\n// ─── Variants (duration + price) ─────────────────────────────────────────────\n\n/**\n * The unit a plan's duration is counted in.\n *\n * `day` earns its place: a gym selling a one-day trial pass or a ten-day\n * tourist package cannot express either in months, and the fixed\n * 1m/3m/6m/1y list it replaces forced them to.\n */\nexport const durationUnitSchema = z.enum(['day', 'week', 'month', 'year']);\nexport type DurationUnit = z.infer<typeof durationUnitSchema>;\n\n/** Singular / plural label for each unit, so no caller writes `unit + 's'`. */\nexport const DURATION_UNIT_LABEL: Record<DurationUnit, { one: string; many: string }> = {\n day: { one: 'Day', many: 'Days' },\n week: { one: 'Week', many: 'Weeks' },\n month: { one: 'Month', many: 'Months' },\n year: { one: 'Year', many: 'Years' },\n};\n\n/**\n * One buyable duration of a plan: how long it runs, what it lists at, and what\n * comes off.\n *\n * The gym enters the ORIGINAL price and a DISCOUNT; the selling price is\n * derived (`variantFinalPrice`) rather than stored. Storing all three invites\n * the arithmetic to drift — a gym edits the discount, forgets the total, and\n * the card advertises a saving the checkout does not honour.\n */\nexport const planVariantSchema = z.object({\n /**\n * Stable per plan, minted by the client when a variant is added, because a\n * live `membership` points back at it. Reusing an array index instead would\n * silently re-point every membership when a variant is deleted.\n */\n id: z.string().trim().min(1).max(40),\n durationCount: z.number().int().min(1).max(365),\n durationUnit: durationUnitSchema,\n /** The list price, before any discount. Struck through on the card. */\n originalPriceInr: priceInrSchema,\n /**\n * What the member actually pays. THE STORED TRUTH, not a derivation.\n *\n * It used to be a whole-number `discountPercent` with the price computed\n * from it, which cannot represent most prices a gym would name. A gym\n * pricing ${'₹'}1000 down to ${'₹'}999 is offering 0.1% off; rounded to the nearest\n * whole percent that is 0, so the price came back as ${'₹'}1000 and the ${'₹'}999\n * they typed was quietly gone. Owners think in the price on the poster, not\n * in percentages, so the price is what is kept and the percentage is worked\n * out for display.\n */\n finalPriceInr: priceInrSchema,\n /** Ribbon on the variant tile. Editorial, like the plan badge. */\n popular: z.boolean().optional(),\n});\nexport type PlanVariant = z.infer<typeof planVariantSchema>;\n\n/**\n * The same variant, with the one rule the two prices have between them.\n *\n * Separate from `planVariantSchema` because a `.refine()` turns a schema into\n * a ZodEffects, and `.partial()` or `.omit()` on one of those throws at MODULE\n * LOAD — invisible to a build, and the page simply dies on import. Keeping\n * the plain object available means the plan bodies can go on composing it\n * while the API still validates the pair.\n */\nexport const checkedPlanVariantSchema = planVariantSchema.refine(\n (v) => v.finalPriceInr <= v.originalPriceInr,\n { message: ERROR_MESSAGE.planFinalPriceAboveOriginal, path: ['finalPriceInr'] },\n);\n\n/** What the member actually pays, in whole rupees. */\nexport function variantFinalPrice(variant: Pick<PlanVariant, 'finalPriceInr'>): number {\n return variant.finalPriceInr;\n}\n\n/** Whole rupees saved, or 0 on an undiscounted variant. */\nexport function variantSavings(\n variant: Pick<PlanVariant, 'originalPriceInr' | 'finalPriceInr'>,\n): number {\n return Math.max(0, variant.originalPriceInr - variant.finalPriceInr);\n}\n\n/**\n * The discount as a percentage, for the \"25% off\" ribbon.\n *\n * DERIVED, and rounded only here where rounding is harmless: this number is\n * shown, never stored and never used to compute a price. That is the whole\n * point of the change — the rounding used to happen before the value was\n * saved, so it moved the money.\n *\n * Zero when there is no discount, or when the original price is zero and the\n * question has no answer.\n */\nexport function variantDiscountPercent(\n variant: Pick<PlanVariant, 'originalPriceInr' | 'finalPriceInr'>,\n): number {\n if (variant.originalPriceInr <= 0) return 0;\n return Math.round((variantSavings(variant) / variant.originalPriceInr) * 100);\n}\n\n/** `{ 3, 'month' }` → `\"3 Months\"`. One spelling of a duration, everywhere. */\nexport function variantLabel(variant: Pick<PlanVariant, 'durationCount' | 'durationUnit'>): string {\n const label = DURATION_UNIT_LABEL[variant.durationUnit];\n return `${variant.durationCount} ${variant.durationCount === 1 ? label.one : label.many}`;\n}\n\n// ─── Badge ───────────────────────────────────────────────────────────────────\n\n/**\n * The ribbon above a plan card. Editorial, not derived: which plan a gym wants\n * to push is a commercial decision, not something to infer from price.\n */\nexport const planBadgeSchema = z.enum(['popular', 'best-value']);\nexport type PlanBadge = z.infer<typeof planBadgeSchema>;\n\nexport const PLAN_BADGE_LABEL: Record<PlanBadge, string> = {\n popular: 'Most Popular',\n 'best-value': 'Best Value',\n};\n\n// ─── Amenities ───────────────────────────────────────────────────────────────\n\n/**\n * What a gym can advertise on a plan, grouped the way the picker shows them.\n *\n * Deliberately EXHAUSTIVE. The old sixteen-key list made an owner describe a\n * real gym in someone else's vocabulary: no drinking water, no washroom, no\n * air conditioning, no parking — the things members actually ask about before\n * they join — so plans all read the same and the chips stopped meaning\n * anything.\n *\n * Keys are stable ids, never shown. The LABEL is the default caption and a gym\n * may override it per plan (see `planFeatureSchema.label`), which is how a\n * studio calls `classes` \"Group Sessions\" without needing a new key.\n */\nexport const AMENITY_CATEGORIES = [\n 'training',\n 'classes',\n 'facilities',\n 'wellness',\n 'services',\n 'extras',\n] as const;\nexport type AmenityCategory = (typeof AMENITY_CATEGORIES)[number];\n\nexport const AMENITY_CATEGORY_LABEL: Record<AmenityCategory, string> = {\n training: 'Training',\n classes: 'Classes',\n facilities: 'Facilities',\n wellness: 'Wellness & Recovery',\n services: 'Services',\n extras: 'Extras',\n};\n\n/**\n * The full catalogue: key → default caption + which group it sits in.\n *\n * Labels live HERE rather than in the client. They used to be duplicated in\n * two frontend maps that had drifted apart — the same key drew a different\n * icon on the owner's screen and the member's, and three keys drew nothing at\n * all on one of them. One source, and the client maps keys to icons only.\n */\nexport const AMENITY_CATALOG = {\n // ── Training floor ─────────────────────────────────────────────────────────\n cardio: { label: 'Cardio Machines', category: 'training' },\n strength: { label: 'Strength Training', category: 'training' },\n freeWeights: { label: 'Free Weights', category: 'training' },\n machines: { label: 'Weight Machines', category: 'training' },\n functional: { label: 'Functional Area', category: 'training' },\n crossfit: { label: 'CrossFit', category: 'training' },\n calisthenics: { label: 'Calisthenics', category: 'training' },\n powerlifting: { label: 'Powerlifting', category: 'training' },\n boxing: { label: 'Boxing', category: 'training' },\n martialArts: { label: 'Martial Arts', category: 'training' },\n treadmill: { label: 'Treadmills', category: 'training' },\n cycling: { label: 'Cycling', category: 'training' },\n rowing: { label: 'Rowing', category: 'training' },\n stretchZone: { label: 'Stretching Zone', category: 'training' },\n turfArea: { label: 'Turf Area', category: 'training' },\n\n // ── Classes ────────────────────────────────────────────────────────────────\n classes: { label: 'Group Classes', category: 'classes' },\n yoga: { label: 'Yoga', category: 'classes' },\n yogaArea: { label: 'Yoga Area', category: 'classes' },\n zumba: { label: 'Zumba', category: 'classes' },\n pilates: { label: 'Pilates', category: 'classes' },\n aerobics: { label: 'Aerobics', category: 'classes' },\n dance: { label: 'Dance', category: 'classes' },\n spinning: { label: 'Spinning', category: 'classes' },\n bootcamp: { label: 'Bootcamp', category: 'classes' },\n meditation: { label: 'Meditation', category: 'classes' },\n\n // ── Facilities ─────────────────────────────────────────────────────────────\n ac: { label: 'Air Conditioned', category: 'facilities' },\n drinkingWater: { label: 'Drinking Water', category: 'facilities' },\n washroom: { label: 'Washroom', category: 'facilities' },\n restroom: { label: 'Restroom', category: 'facilities' },\n shower: { label: 'Showers', category: 'facilities' },\n changingRoom: { label: 'Changing Room', category: 'facilities' },\n locker: { label: 'Lockers', category: 'facilities' },\n parking: { label: 'Parking', category: 'facilities' },\n wifi: { label: 'Free WiFi', category: 'facilities' },\n musicSystem: { label: 'Music System', category: 'facilities' },\n powerBackup: { label: 'Power Backup', category: 'facilities' },\n lift: { label: 'Lift Access', category: 'facilities' },\n cctv: { label: 'CCTV Security', category: 'facilities' },\n wheelchairAccess: { label: 'Wheelchair Access', category: 'facilities' },\n\n // ── Wellness & recovery ────────────────────────────────────────────────────\n steam: { label: 'Steam Room', category: 'wellness' },\n sauna: { label: 'Sauna', category: 'wellness' },\n spa: { label: 'Spa', category: 'wellness' },\n massage: { label: 'Massage', category: 'wellness' },\n iceBath: { label: 'Ice Bath', category: 'wellness' },\n pool: { label: 'Swimming Pool', category: 'wellness' },\n physiotherapy: { label: 'Physiotherapy', category: 'wellness' },\n\n // ── Services ───────────────────────────────────────────────────────────────\n trainer: { label: 'Trainer Support', category: 'services' },\n personalTraining: { label: 'Personal Training', category: 'services' },\n diet: { label: 'Diet Plan', category: 'services' },\n nutritionist: { label: 'Nutritionist', category: 'services' },\n health: { label: 'Health Stats', category: 'services' },\n bodyComposition: { label: 'Body Composition', category: 'services' },\n doctorConsult: { label: 'Doctor Consultation', category: 'services' },\n priority: { label: 'Priority Support', category: 'services' },\n\n // ── Extras ─────────────────────────────────────────────────────────────────\n cafe: { label: 'Cafe', category: 'extras' },\n coffeeMachine: { label: 'Coffee Machine', category: 'extras' },\n proteinBar: { label: 'Protein Bar', category: 'extras' },\n supplementStore: { label: 'Supplement Store', category: 'extras' },\n childArea: { label: 'Kids Play Area', category: 'extras' },\n towelService: { label: 'Towel Service', category: 'extras' },\n laundry: { label: 'Laundry', category: 'extras' },\n guestPass: { label: 'Guest Pass', category: 'extras' },\n unlimited: { label: 'Unlimited Access', category: 'extras' },\n multiCenter: { label: 'All Branches', category: 'extras' },\n} as const satisfies Record<string, { label: string; category: AmenityCategory }>;\n\n/** Every amenity key, in catalogue order. */\nexport const FEATURE_ICONS = Object.keys(AMENITY_CATALOG) as [FeatureIcon, ...FeatureIcon[]];\nexport type FeatureIcon = keyof typeof AMENITY_CATALOG;\n\nexport const featureIconSchema = z.enum(\n Object.keys(AMENITY_CATALOG) as [FeatureIcon, ...FeatureIcon[]],\n);\n\n/** The amenity keys in one category, for a grouped picker. */\nexport function amenitiesInCategory(category: AmenityCategory): FeatureIcon[] {\n return (Object.keys(AMENITY_CATALOG) as FeatureIcon[]).filter(\n (key) => AMENITY_CATALOG[key].category === category,\n );\n}\n\nexport const planFeatureSchema = z.object({\n icon: featureIconSchema,\n label: z.string().trim().min(1).max(80),\n});\nexport type PlanFeature = z.infer<typeof planFeatureSchema>;\n\n// ─── Create / update body ────────────────────────────────────────────────────\n\nexport const createMembershipPlanBodySchema = z.object({\n centerId: objectIdSchema,\n name: z.string().trim().min(1, 'Plan name is required').max(80),\n /**\n * What the gym wants to say about the plan in its own words.\n *\n * Replaces a 60-character `note` that the card rendered but no screen could\n * ever set — a caption nobody could write. One optional field, roomy enough\n * for a sentence or two.\n */\n description: optionalTrimmedString(300),\n accent: planAccentSchema,\n /** Ribbon above the card. Null on most plans — a gym pushing every plan\n * equally is pushing none of them. */\n badge: planBadgeSchema.nullish(),\n features: z.array(planFeatureSchema).max(40).default([]),\n variants: z.array(checkedPlanVariantSchema).min(1, 'Add at least one variant'),\n});\nexport type CreateMembershipPlanBody = z.infer<typeof createMembershipPlanBodySchema>;\n\nexport const updateMembershipPlanBodySchema = createMembershipPlanBodySchema\n .omit({ centerId: true })\n .partial()\n .extend({ isActive: z.boolean().optional() });\nexport type UpdateMembershipPlanBody = z.infer<typeof updateMembershipPlanBodySchema>;\n\n// ─── Entity (API response) ───────────────────────────────────────────────────\n\nexport const membershipPlanSchema = z.object({\n id: objectIdSchema,\n centerId: objectIdSchema,\n name: z.string(),\n description: z.string().nullable(),\n accent: planAccentSchema,\n badge: planBadgeSchema.nullable(),\n features: z.array(planFeatureSchema),\n variants: z.array(checkedPlanVariantSchema),\n isActive: z.boolean(),\n sortOrder: z.number().int(),\n /** Denormalised count of active members on this plan (owner list view). */\n memberCount: z.number().int().nonnegative(),\n createdAt: isoDateTimeSchema,\n updatedAt: isoDateTimeSchema,\n});\nexport type MembershipPlan = z.infer<typeof membershipPlanSchema>;\n"]}
@@ -28,8 +28,8 @@
28
28
  import { z } from 'zod';
29
29
  export declare const membershipRequestStatusSchema: z.ZodEnum<{
30
30
  pending: "pending";
31
- approved: "approved";
32
31
  rejected: "rejected";
32
+ approved: "approved";
33
33
  }>;
34
34
  export type MembershipRequestStatus = z.infer<typeof membershipRequestStatusSchema>;
35
35
  export declare const membershipRequestSchema: z.ZodObject<{
@@ -38,8 +38,8 @@ export declare const membershipRequestSchema: z.ZodObject<{
38
38
  centerId: z.ZodString;
39
39
  status: z.ZodEnum<{
40
40
  pending: "pending";
41
- approved: "approved";
42
41
  rejected: "rejected";
42
+ approved: "approved";
43
43
  }>;
44
44
  requestedAt: z.ZodISODateTime;
45
45
  decidedAt: z.ZodNullable<z.ZodISODateTime>;
@@ -57,8 +57,8 @@ export declare const membershipRequestItemSchema: z.ZodObject<{
57
57
  centerId: z.ZodString;
58
58
  status: z.ZodEnum<{
59
59
  pending: "pending";
60
- approved: "approved";
61
60
  rejected: "rejected";
61
+ approved: "approved";
62
62
  }>;
63
63
  requestedAt: z.ZodISODateTime;
64
64
  decidedAt: z.ZodNullable<z.ZodISODateTime>;
@@ -94,8 +94,8 @@ export declare const membershipRequestQuerySchema: z.ZodObject<{
94
94
  limit: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
95
95
  status: z.ZodOptional<z.ZodEnum<{
96
96
  pending: "pending";
97
- approved: "approved";
98
97
  rejected: "rejected";
98
+ approved: "approved";
99
99
  }>>;
100
100
  }, z.core.$strip>;
101
101
  export type MembershipRequestQuery = z.infer<typeof membershipRequestQuerySchema>;
@@ -109,8 +109,8 @@ export declare const myMembershipRequestSchema: z.ZodNullable<z.ZodObject<{
109
109
  centerId: z.ZodString;
110
110
  status: z.ZodEnum<{
111
111
  pending: "pending";
112
- approved: "approved";
113
112
  rejected: "rejected";
113
+ approved: "approved";
114
114
  }>;
115
115
  requestedAt: z.ZodISODateTime;
116
116
  decidedAt: z.ZodNullable<z.ZodISODateTime>;