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 +3 -1
- package/dist/file.d.ts.map +1 -1
- package/dist/file.js +2 -0
- package/dist/file.js.map +1 -1
- package/dist/member.d.ts +18 -1
- package/dist/member.d.ts.map +1 -1
- package/dist/membership-plan.d.ts +40 -6
- package/dist/membership-plan.d.ts.map +1 -1
- package/dist/membership-plan.js +43 -7
- package/dist/membership-plan.js.map +1 -1
- package/dist/membership-request.d.ts +5 -5
- package/dist/membership.d.ts +112 -5
- package/dist/membership.d.ts.map +1 -1
- package/dist/membership.js +95 -2
- package/dist/membership.js.map +1 -1
- package/dist/messages.d.ts +43 -1
- package/dist/messages.d.ts.map +1 -1
- package/dist/messages.js +46 -1
- package/dist/messages.js.map +1 -1
- package/dist/onboarding.d.ts +4 -4
- package/dist/organisation.d.ts +5 -5
- package/dist/platform.d.ts +7 -7
- package/dist/staff.d.ts +3 -3
- package/package.json +1 -1
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;
|
package/dist/file.d.ts.map
CHANGED
|
@@ -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,
|
|
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>>;
|
package/dist/member.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"member.d.ts","sourceRoot":"","sources":["../src/member.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAsBxB,kDAAkD;AAClD,eAAO,MAAM,mBAAmB;;;;EAAyC,CAAC;AAC1E,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAIhE,eAAO,MAAM,sBAAsB
|
|
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
|
-
|
|
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, '
|
|
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' | '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"membership-plan.d.ts","sourceRoot":"","sources":["../src/membership-plan.ts"],"names":[],"mappings":"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"}
|
package/dist/membership-plan.js
CHANGED
|
@@ -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
|
-
/**
|
|
51
|
-
|
|
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
|
-
|
|
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 -
|
|
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(
|
|
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(
|
|
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":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EACL,iBAAiB,EACjB,cAAc,EACd,qBAAqB,EACrB,cAAc,GACf,MAAM,aAAa,CAAC;AAErB,gFAAgF;AAEhF,0CAA0C;AAC1C,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC;AAG9E,gFAAgF;AAEhF;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC;AAG3E,+EAA+E;AAC/E,MAAM,CAAC,MAAM,mBAAmB,GAAwD;IACtF,GAAG,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE;IACjC,IAAI,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE;IACpC,KAAK,EAAE,EAAE,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE;IACvC,IAAI,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE;CACrC,CAAC;AAEF;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,CAAC,MAAM,CAAC;IACxC;;;;OAIG;IACH,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC;IACpC,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC;IAC/C,YAAY,EAAE,kBAAkB;IAChC,uEAAuE;IACvE,gBAAgB,EAAE,cAAc;IAChC,mEAAmE;IACnE,eAAe,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;IAC5D,kEAAkE;IAClE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE;CAChC,CAAC,CAAC;AAGH,sDAAsD;AACtD,MAAM,UAAU,iBAAiB,CAC/B,OAAkE;IAElE,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC,GAAG,GAAG,CAAC,CAAC;IACnF,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,gBAAgB,GAAG,GAAG,CAAC,CAAC;AACrD,CAAC;AAED,2DAA2D;AAC3D,MAAM,UAAU,cAAc,CAC5B,OAAkE;IAElE,OAAO,OAAO,CAAC,gBAAgB,GAAG,iBAAiB,CAAC,OAAO,CAAC,CAAC;AAC/D,CAAC;AAED,+EAA+E;AAC/E,MAAM,UAAU,YAAY,CAAC,OAA4D;IACvF,MAAM,KAAK,GAAG,mBAAmB,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC;IACxD,OAAO,GAAG,OAAO,CAAC,aAAa,IAAI,OAAO,CAAC,aAAa,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;AAC5F,CAAC;AAED,gFAAgF;AAEhF;;;GAGG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC,CAAC;AAGjE,MAAM,CAAC,MAAM,gBAAgB,GAA8B;IACzD,OAAO,EAAE,cAAc;IACvB,YAAY,EAAE,YAAY;CAC3B,CAAC;AAEF,gFAAgF;AAEhF;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG;IAChC,UAAU;IACV,SAAS;IACT,YAAY;IACZ,UAAU;IACV,UAAU;IACV,QAAQ;CACA,CAAC;AAGX,MAAM,CAAC,MAAM,sBAAsB,GAAoC;IACrE,QAAQ,EAAE,UAAU;IACpB,OAAO,EAAE,SAAS;IAClB,UAAU,EAAE,YAAY;IACxB,QAAQ,EAAE,qBAAqB;IAC/B,QAAQ,EAAE,UAAU;IACpB,MAAM,EAAE,QAAQ;CACjB,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG;IAC7B,8EAA8E;IAC9E,MAAM,EAAE,EAAE,KAAK,EAAE,iBAAiB,EAAE,QAAQ,EAAE,UAAU,EAAE;IAC1D,QAAQ,EAAE,EAAE,KAAK,EAAE,mBAAmB,EAAE,QAAQ,EAAE,UAAU,EAAE;IAC9D,WAAW,EAAE,EAAE,KAAK,EAAE,cAAc,EAAE,QAAQ,EAAE,UAAU,EAAE;IAC5D,QAAQ,EAAE,EAAE,KAAK,EAAE,iBAAiB,EAAE,QAAQ,EAAE,UAAU,EAAE;IAC5D,UAAU,EAAE,EAAE,KAAK,EAAE,iBAAiB,EAAE,QAAQ,EAAE,UAAU,EAAE;IAC9D,QAAQ,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,UAAU,EAAE;IACrD,YAAY,EAAE,EAAE,KAAK,EAAE,cAAc,EAAE,QAAQ,EAAE,UAAU,EAAE;IAC7D,YAAY,EAAE,EAAE,KAAK,EAAE,cAAc,EAAE,QAAQ,EAAE,UAAU,EAAE;IAC7D,MAAM,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,UAAU,EAAE;IACjD,WAAW,EAAE,EAAE,KAAK,EAAE,cAAc,EAAE,QAAQ,EAAE,UAAU,EAAE;IAC5D,SAAS,EAAE,EAAE,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,UAAU,EAAE;IACxD,OAAO,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,UAAU,EAAE;IACnD,MAAM,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,UAAU,EAAE;IACjD,WAAW,EAAE,EAAE,KAAK,EAAE,iBAAiB,EAAE,QAAQ,EAAE,UAAU,EAAE;IAC/D,QAAQ,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,UAAU,EAAE;IAEtD,8EAA8E;IAC9E,OAAO,EAAE,EAAE,KAAK,EAAE,eAAe,EAAE,QAAQ,EAAE,SAAS,EAAE;IACxD,IAAI,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE;IAC5C,QAAQ,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,SAAS,EAAE;IACrD,KAAK,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE;IAC9C,OAAO,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS,EAAE;IAClD,QAAQ,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,SAAS,EAAE;IACpD,KAAK,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE;IAC9C,QAAQ,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,SAAS,EAAE;IACpD,QAAQ,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,SAAS,EAAE;IACpD,UAAU,EAAE,EAAE,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,SAAS,EAAE;IAExD,8EAA8E;IAC9E,EAAE,EAAE,EAAE,KAAK,EAAE,iBAAiB,EAAE,QAAQ,EAAE,YAAY,EAAE;IACxD,aAAa,EAAE,EAAE,KAAK,EAAE,gBAAgB,EAAE,QAAQ,EAAE,YAAY,EAAE;IAClE,QAAQ,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,YAAY,EAAE;IACvD,QAAQ,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,YAAY,EAAE;IACvD,MAAM,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,YAAY,EAAE;IACpD,YAAY,EAAE,EAAE,KAAK,EAAE,eAAe,EAAE,QAAQ,EAAE,YAAY,EAAE;IAChE,MAAM,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,YAAY,EAAE;IACpD,OAAO,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,YAAY,EAAE;IACrD,IAAI,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,YAAY,EAAE;IACpD,WAAW,EAAE,EAAE,KAAK,EAAE,cAAc,EAAE,QAAQ,EAAE,YAAY,EAAE;IAC9D,WAAW,EAAE,EAAE,KAAK,EAAE,cAAc,EAAE,QAAQ,EAAE,YAAY,EAAE;IAC9D,IAAI,EAAE,EAAE,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,YAAY,EAAE;IACtD,IAAI,EAAE,EAAE,KAAK,EAAE,eAAe,EAAE,QAAQ,EAAE,YAAY,EAAE;IACxD,gBAAgB,EAAE,EAAE,KAAK,EAAE,mBAAmB,EAAE,QAAQ,EAAE,YAAY,EAAE;IAExE,8EAA8E;IAC9E,KAAK,EAAE,EAAE,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,UAAU,EAAE;IACpD,KAAK,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE;IAC/C,GAAG,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE;IAC3C,OAAO,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,UAAU,EAAE;IACnD,OAAO,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,UAAU,EAAE;IACpD,IAAI,EAAE,EAAE,KAAK,EAAE,eAAe,EAAE,QAAQ,EAAE,UAAU,EAAE;IACtD,aAAa,EAAE,EAAE,KAAK,EAAE,eAAe,EAAE,QAAQ,EAAE,UAAU,EAAE;IAE/D,8EAA8E;IAC9E,OAAO,EAAE,EAAE,KAAK,EAAE,iBAAiB,EAAE,QAAQ,EAAE,UAAU,EAAE;IAC3D,gBAAgB,EAAE,EAAE,KAAK,EAAE,mBAAmB,EAAE,QAAQ,EAAE,UAAU,EAAE;IACtE,IAAI,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,UAAU,EAAE;IAClD,YAAY,EAAE,EAAE,KAAK,EAAE,cAAc,EAAE,QAAQ,EAAE,UAAU,EAAE;IAC7D,MAAM,EAAE,EAAE,KAAK,EAAE,cAAc,EAAE,QAAQ,EAAE,UAAU,EAAE;IACvD,eAAe,EAAE,EAAE,KAAK,EAAE,kBAAkB,EAAE,QAAQ,EAAE,UAAU,EAAE;IACpE,aAAa,EAAE,EAAE,KAAK,EAAE,qBAAqB,EAAE,QAAQ,EAAE,UAAU,EAAE;IACrE,QAAQ,EAAE,EAAE,KAAK,EAAE,kBAAkB,EAAE,QAAQ,EAAE,UAAU,EAAE;IAE7D,8EAA8E;IAC9E,IAAI,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE;IAC3C,aAAa,EAAE,EAAE,KAAK,EAAE,gBAAgB,EAAE,QAAQ,EAAE,QAAQ,EAAE;IAC9D,UAAU,EAAE,EAAE,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,QAAQ,EAAE;IACxD,eAAe,EAAE,EAAE,KAAK,EAAE,kBAAkB,EAAE,QAAQ,EAAE,QAAQ,EAAE;IAClE,SAAS,EAAE,EAAE,KAAK,EAAE,gBAAgB,EAAE,QAAQ,EAAE,QAAQ,EAAE;IAC1D,YAAY,EAAE,EAAE,KAAK,EAAE,eAAe,EAAE,QAAQ,EAAE,QAAQ,EAAE;IAC5D,OAAO,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,EAAE;IACjD,SAAS,EAAE,EAAE,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,QAAQ,EAAE;IACtD,SAAS,EAAE,EAAE,KAAK,EAAE,kBAAkB,EAAE,QAAQ,EAAE,QAAQ,EAAE;IAC5D,WAAW,EAAE,EAAE,KAAK,EAAE,cAAc,EAAE,QAAQ,EAAE,QAAQ,EAAE;CACqB,CAAC;AAElF,6CAA6C;AAC7C,MAAM,CAAC,MAAM,aAAa,GAAG,MAAM,CAAC,IAAI,CAAC,eAAe,CAAoC,CAAC;AAG7F,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,CAAC,IAAI,CACrC,MAAM,CAAC,IAAI,CAAC,eAAe,CAAoC,CAChE,CAAC;AAEF,8DAA8D;AAC9D,MAAM,UAAU,mBAAmB,CAAC,QAAyB;IAC3D,OAAQ,MAAM,CAAC,IAAI,CAAC,eAAe,CAAmB,CAAC,MAAM,CAC3D,CAAC,GAAG,EAAE,EAAE,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC,QAAQ,KAAK,QAAQ,CACpD,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,CAAC,MAAM,CAAC;IACxC,IAAI,EAAE,iBAAiB;IACvB,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC;CACxC,CAAC,CAAC;AAGH,gFAAgF;AAEhF,MAAM,CAAC,MAAM,8BAA8B,GAAG,CAAC,CAAC,MAAM,CAAC;IACrD,QAAQ,EAAE,cAAc;IACxB,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,uBAAuB,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC;IAC/D;;;;;;OAMG;IACH,WAAW,EAAE,qBAAqB,CAAC,GAAG,CAAC;IACvC,MAAM,EAAE,gBAAgB;IACxB;2CACuC;IACvC,KAAK,EAAE,eAAe,CAAC,OAAO,EAAE;IAChC,QAAQ,EAAE,CAAC,CAAC,KAAK,CAAC,iBAAiB,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IACxD,QAAQ,EAAE,CAAC,CAAC,KAAK,CAAC,iBAAiB,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,0BAA0B,CAAC;CACxE,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,8BAA8B,GAAG,8BAA8B;KACzE,IAAI,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;KACxB,OAAO,EAAE;KACT,MAAM,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;AAGhD,gFAAgF;AAEhF,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC3C,EAAE,EAAE,cAAc;IAClB,QAAQ,EAAE,cAAc;IACxB,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;IAChB,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAClC,MAAM,EAAE,gBAAgB;IACxB,KAAK,EAAE,eAAe,CAAC,QAAQ,EAAE;IACjC,QAAQ,EAAE,CAAC,CAAC,KAAK,CAAC,iBAAiB,CAAC;IACpC,QAAQ,EAAE,CAAC,CAAC,KAAK,CAAC,iBAAiB,CAAC;IACpC,QAAQ,EAAE,CAAC,CAAC,OAAO,EAAE;IACrB,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE;IAC3B,2EAA2E;IAC3E,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;IAC3C,SAAS,EAAE,iBAAiB;IAC5B,SAAS,EAAE,iBAAiB;CAC7B,CAAC,CAAC","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>;
|