@dragonmastery/dragoncore-shared 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,1680 @@
1
+ import { z } from "zod";
2
+
3
+ //#region src/validation/app_settings/notification_emails_zod.ts
4
+ /**
5
+ * Schema for individual email entry
6
+ */
7
+ const EmailEntrySchema = z.object({
8
+ id: z.string().default(() => `email-${Math.random().toString(36).substr(2, 9)}`).optional(),
9
+ email: z.string().email("Must be a valid email address")
10
+ });
11
+ /**
12
+ * Schema for updating support ticket notification emails
13
+ * Uses an array of email objects for proper validation and UX
14
+ */
15
+ const UpdateNotificationEmailsSchema = z.object({ emails: z.array(EmailEntrySchema).max(10, "Maximum 10 email addresses allowed") });
16
+ /**
17
+ * Schema for reading support ticket notification emails
18
+ */
19
+ const ReadNotificationEmailsSchema = z.object({ emails: z.array(z.object({ email: z.string() })) });
20
+ /**
21
+ * Helper function to create a new email entry
22
+ */
23
+ function createNewEmailEntry() {
24
+ return {
25
+ id: `email-${Math.random().toString(36).substr(2, 9)}`,
26
+ email: ""
27
+ };
28
+ }
29
+
30
+ //#endregion
31
+ //#region src/validation/attachment/attachment_filters_zod.ts
32
+ const AttachmentFiltersSchema = z.object({
33
+ record_id: z.string(),
34
+ record_type: z.string(),
35
+ filters: z.object({
36
+ start_date: z.string().optional().nullable(),
37
+ end_date: z.string().optional().nullable(),
38
+ limit: z.number().optional().nullable(),
39
+ cursor: z.string().optional().nullable(),
40
+ folder_id: z.string().optional().nullable(),
41
+ include_folders: z.boolean().optional().default(true)
42
+ }).optional().nullable()
43
+ });
44
+
45
+ //#endregion
46
+ //#region src/validation/attachment/attachment_folder_input_zod.ts
47
+ const AttachmentFolderBaseSchema = z.object({
48
+ record_id: z.string(),
49
+ record_type: z.string(),
50
+ sanitized_name: z.string(),
51
+ original_name: z.string(),
52
+ description: z.string().optional().nullable(),
53
+ metadata: z.string().optional().nullable(),
54
+ parent_folder_id: z.string().optional().nullable(),
55
+ file_count: z.number().optional()
56
+ });
57
+ const AttachmentFolderCreateSchema = AttachmentFolderBaseSchema;
58
+ const AttachmentFolderUpdateSchema = AttachmentFolderBaseSchema.extend({
59
+ id: z.string(),
60
+ record_id: z.string().optional(),
61
+ record_type: z.string().optional(),
62
+ sanitized_name: z.string().optional(),
63
+ original_name: z.string().optional(),
64
+ parent_folder_id: z.string().optional().nullable()
65
+ });
66
+
67
+ //#endregion
68
+ //#region src/validation/attachment/attachment_folder_read_zod.ts
69
+ const AttachmentFolderReadSchema = AttachmentFolderCreateSchema.extend({
70
+ id: z.string(),
71
+ file_count: z.number().optional(),
72
+ created_at: z.string(),
73
+ created_by: z.string(),
74
+ updated_at: z.string().optional().nullable(),
75
+ updated_by: z.string().optional().nullable(),
76
+ archived_at: z.string().optional().nullable(),
77
+ archived_by: z.string().optional().nullable(),
78
+ deleted_at: z.string().optional().nullable(),
79
+ deleted_by: z.string().optional().nullable()
80
+ });
81
+
82
+ //#endregion
83
+ //#region src/validation/attachment/attachment_input_zod.ts
84
+ const AttachmentBaseSchema = z.object({
85
+ record_id: z.string(),
86
+ record_type: z.string(),
87
+ sanitized_name: z.string(),
88
+ original_name: z.string(),
89
+ content_type: z.string(),
90
+ file_size: z.string(),
91
+ description: z.string().optional().nullable(),
92
+ metadata: z.string().optional().nullable(),
93
+ folder_id: z.string().optional().nullable()
94
+ });
95
+ const AttachmentCreateSchema = AttachmentBaseSchema;
96
+ const AttachmentUpdateSchema = AttachmentBaseSchema.extend({
97
+ id: z.string(),
98
+ record_id: z.string().optional(),
99
+ record_type: z.string().optional(),
100
+ sanitized_name: z.string().optional(),
101
+ original_name: z.string().optional(),
102
+ content_type: z.string().optional(),
103
+ file_size: z.string().optional(),
104
+ folder_id: z.string().optional().nullable()
105
+ });
106
+
107
+ //#endregion
108
+ //#region src/validation/attachment/attachment_read_zod.ts
109
+ const AttachmentReadSchema = AttachmentCreateSchema.extend({
110
+ id: z.string(),
111
+ created_at: z.string(),
112
+ created_by: z.string(),
113
+ updated_at: z.string().optional().nullable(),
114
+ updated_by: z.string().optional().nullable(),
115
+ archived_at: z.string().optional().nullable(),
116
+ archived_by: z.string().optional().nullable(),
117
+ deleted_at: z.string().optional().nullable(),
118
+ deleted_by: z.string().optional().nullable()
119
+ });
120
+
121
+ //#endregion
122
+ //#region src/validation/attachment/attachment_page_zod.ts
123
+ const AttachmentPageSchema = z.object({
124
+ files: z.array(AttachmentReadSchema),
125
+ folders: z.array(AttachmentFolderReadSchema),
126
+ pageInfo: z.object({
127
+ hasNextPage: z.boolean(),
128
+ endCursor: z.string().optional().nullable()
129
+ })
130
+ });
131
+
132
+ //#endregion
133
+ //#region src/validation/login_zod.ts
134
+ const isCommonPassword = (password) => {
135
+ return [
136
+ "password",
137
+ "12345678",
138
+ "qwerty123",
139
+ "admin1234"
140
+ ].includes(password.toLowerCase());
141
+ };
142
+ const passwordSchema = z.string().min(8, { message: "Password must be at least 8 characters long" }).max(64, { message: "Password must not exceed 64 characters" }).refine((password) => !/^\s|\s$/.test(password), { message: "Password must not have leading or trailing whitespace" }).refine((password) => !isCommonPassword(password), { message: "Password is too common and easily guessed" });
143
+ const loginSchema = z.object({
144
+ email: z.string().trim().toLowerCase().min(3, "Please enter your email.").email("The email address is badly formatted."),
145
+ password: passwordSchema
146
+ });
147
+
148
+ //#endregion
149
+ //#region src/validation/change_password_zod.ts
150
+ const changePasswordSchema = z.object({ passwords: z.object({
151
+ current_password: z.string().min(8).max(64),
152
+ new_password: passwordSchema,
153
+ new_password_confirm: z.string()
154
+ }).refine((data) => data.new_password === data.new_password_confirm, {
155
+ message: "Passwords do not match",
156
+ path: ["new_password_confirm"]
157
+ }).refine((data) => data.new_password !== data.current_password, {
158
+ message: "New password must be different from current password",
159
+ path: ["new_password"]
160
+ }) });
161
+
162
+ //#endregion
163
+ //#region src/validation/common/filter_operators_zod.ts
164
+ /**
165
+ * Individual operator constants - use these instead of string literals
166
+ * All operators use shorthand names for URL compatibility
167
+ */
168
+ const OPERATORS = {
169
+ EQUALS: "eq",
170
+ NOT_EQUALS: "ne",
171
+ GREATER_THAN: "gt",
172
+ GREATER_THAN_OR_EQUAL: "gte",
173
+ LESS_THAN: "lt",
174
+ LESS_THAN_OR_EQUAL: "lte",
175
+ BETWEEN: "between",
176
+ CONTAINS: "contains",
177
+ STARTS_WITH: "sw",
178
+ ENDS_WITH: "ew",
179
+ IS_ONE_OF: "in",
180
+ IS_NOT_ONE_OF: "notIn",
181
+ IS_EMPTY: "isEmpty",
182
+ IS_NOT_EMPTY: "isNotEmpty"
183
+ };
184
+ /**
185
+ * String operators - shorthand names for URL compatibility
186
+ * Only operators that make sense for string filtering
187
+ */
188
+ const StringOperatorSchema = z.enum([
189
+ OPERATORS.EQUALS,
190
+ OPERATORS.NOT_EQUALS,
191
+ OPERATORS.CONTAINS,
192
+ OPERATORS.STARTS_WITH,
193
+ OPERATORS.ENDS_WITH,
194
+ OPERATORS.IS_ONE_OF,
195
+ OPERATORS.IS_NOT_ONE_OF
196
+ ]);
197
+ /**
198
+ * Date operators - shorthand names for URL compatibility
199
+ * Only operators that make sense for date filtering
200
+ */
201
+ const DateOperatorSchema = z.enum([
202
+ OPERATORS.EQUALS,
203
+ OPERATORS.NOT_EQUALS,
204
+ OPERATORS.GREATER_THAN,
205
+ OPERATORS.GREATER_THAN_OR_EQUAL,
206
+ OPERATORS.LESS_THAN,
207
+ OPERATORS.LESS_THAN_OR_EQUAL,
208
+ OPERATORS.BETWEEN,
209
+ OPERATORS.IS_EMPTY,
210
+ OPERATORS.IS_NOT_EMPTY
211
+ ]);
212
+ /**
213
+ * Number operators - shorthand names for URL compatibility
214
+ * Only operators that make sense for number filtering
215
+ */
216
+ const NumberOperatorSchema = z.enum([
217
+ OPERATORS.EQUALS,
218
+ OPERATORS.NOT_EQUALS,
219
+ OPERATORS.GREATER_THAN,
220
+ OPERATORS.GREATER_THAN_OR_EQUAL,
221
+ OPERATORS.LESS_THAN,
222
+ OPERATORS.LESS_THAN_OR_EQUAL,
223
+ OPERATORS.BETWEEN,
224
+ OPERATORS.IS_ONE_OF,
225
+ OPERATORS.IS_NOT_ONE_OF
226
+ ]);
227
+ /**
228
+ * Shared operator enum for equality comparisons (used by boolean and enum filters)
229
+ */
230
+ const EqualityOperatorSchema = z.enum([OPERATORS.EQUALS, OPERATORS.NOT_EQUALS]);
231
+ /**
232
+ * Shared operator enum for equality and array operations (used by enum filters)
233
+ */
234
+ const EqualityArrayOperatorSchema = z.enum([
235
+ OPERATORS.EQUALS,
236
+ OPERATORS.NOT_EQUALS,
237
+ OPERATORS.IS_ONE_OF,
238
+ OPERATORS.IS_NOT_ONE_OF
239
+ ]);
240
+ /**
241
+ * Data type enum for filters (spec-compliant)
242
+ */
243
+ const DataTypeSchema = z.enum([
244
+ "string",
245
+ "number",
246
+ "date",
247
+ "boolean",
248
+ "enum",
249
+ "money",
250
+ "percentage"
251
+ ]);
252
+ /**
253
+ * Base filter schema - spec-compliant structure
254
+ * Field name is the object key, not part of the filter object
255
+ * All filters must include: operator
256
+ * Optional: value, values, caseSensitive
257
+ * Type is inferred from registry using the field name (object key)
258
+ */
259
+ const BaseFilterSchema = z.object({
260
+ operator: z.string(),
261
+ value: z.any().optional(),
262
+ values: z.array(z.any()).optional(),
263
+ caseSensitive: z.boolean().optional()
264
+ });
265
+ /**
266
+ * String filter input - Spec-compliant structure
267
+ * Restricted operators: only operators that make sense for string filtering
268
+ * Field name is the object key, type is inferred from registry
269
+ *
270
+ * Examples:
271
+ * - Equality: { operator: "eq", value: "search" }
272
+ * - Contains: { operator: "contains", value: "search", caseSensitive: false }
273
+ * - Starts with: { operator: "sw", value: "prefix" }
274
+ * - In array: { operator: "in", values: ["option1", "option2"] }
275
+ */
276
+ const StringFilterSchema = BaseFilterSchema.extend({
277
+ operator: StringOperatorSchema,
278
+ value: z.string().optional(),
279
+ values: z.array(z.string()).optional(),
280
+ caseSensitive: z.boolean().optional()
281
+ }).refine((data) => {
282
+ if (data.operator === OPERATORS.IS_ONE_OF || data.operator === OPERATORS.IS_NOT_ONE_OF) return data.values !== void 0 && data.values.length > 0;
283
+ return data.value !== void 0;
284
+ }, { message: "value is required for non-array operators, values array is required for in/notIn" });
285
+ /**
286
+ * Boolean filter input - Spec-compliant structure
287
+ * Field name is the object key, type is inferred from registry
288
+ *
289
+ * Examples:
290
+ * - Equality: { operator: "eq", value: true }
291
+ * - Not equals: { operator: "ne", value: false }
292
+ */
293
+ const BooleanFilterSchema = BaseFilterSchema.extend({
294
+ operator: EqualityOperatorSchema,
295
+ value: z.boolean()
296
+ });
297
+ /**
298
+ * Number filter input - Spec-compliant structure
299
+ * Restricted operators: only operators that make sense for number filtering
300
+ * Useful for ranges (credits > 100, priority <= 3, etc.)
301
+ *
302
+ * Examples:
303
+ * - Equality: { field: "age", operator: "eq", value: 100, type: "number" }
304
+ * - Greater than: { field: "price", operator: "gt", value: 100, type: "number" }
305
+ * - Range: { field: "score", operator: "gte", value: 50, type: "number" }
306
+ * - In array: { field: "priority", operator: "in", values: [1, 2, 3], type: "number" }
307
+ */
308
+ const NumberFilterSchema = BaseFilterSchema.extend({
309
+ operator: NumberOperatorSchema,
310
+ value: z.number().optional(),
311
+ values: z.array(z.number()).optional()
312
+ }).refine((data) => {
313
+ if (data.operator === OPERATORS.BETWEEN) return data.values !== void 0 && data.values.length === 2;
314
+ if (data.operator === OPERATORS.IS_ONE_OF || data.operator === OPERATORS.IS_NOT_ONE_OF) return data.values !== void 0 && data.values.length > 0;
315
+ return data.value !== void 0;
316
+ }, { message: "value is required for non-array operators, values array is required for in/notIn, exactly 2 values required for between" }).refine((data) => {
317
+ if (data.operator === OPERATORS.BETWEEN && data.values && data.values.length === 2) {
318
+ const [min, max] = data.values;
319
+ return min !== void 0 && max !== void 0 && min <= max;
320
+ }
321
+ return true;
322
+ }, {
323
+ message: "For between operator, values[0] must be less than or equal to values[1]",
324
+ path: ["values"]
325
+ });
326
+ /**
327
+ * Date/DateTime string filter - Spec-compliant structure
328
+ * Restricted operators: only operators that make sense for date filtering
329
+ * Dates should be in ISO 8601 format: YYYY-MM-DD or full datetime
330
+ *
331
+ * Examples:
332
+ * - Exact match: { field: "createdAt", operator: "eq", value: "2024-01-01", type: "date" }
333
+ * - After date: { field: "expiresAt", operator: "gte", value: "2024-01-01", type: "date" }
334
+ * - Before date: { field: "deadline", operator: "lt", value: "2024-12-31", type: "date" }
335
+ * - Is empty: { field: "deletedAt", operator: "isEmpty", type: "date" } // No value property
336
+ * - Is not empty: { field: "publishedAt", operator: "isNotEmpty", type: "date" } // No value property
337
+ */
338
+ const DateFilterSchema = z.union([
339
+ BaseFilterSchema.extend({ operator: z.enum([OPERATORS.IS_EMPTY, OPERATORS.IS_NOT_EMPTY]) }).refine((data) => data.value === void 0 && data.values === void 0, { message: "isEmpty and isNotEmpty operators must not have value or values properties" }),
340
+ BaseFilterSchema.extend({
341
+ operator: z.literal(OPERATORS.BETWEEN),
342
+ values: z.array(z.string()).length(2)
343
+ }).refine((data) => data.values !== void 0 && data.values.length === 2, { message: "between operator requires exactly 2 values" }).refine((data) => {
344
+ if (data.values && data.values.length === 2) {
345
+ const [startDate, endDate] = data.values;
346
+ if (startDate !== void 0 && endDate !== void 0) return new Date(startDate) <= new Date(endDate);
347
+ }
348
+ return true;
349
+ }, {
350
+ message: "For between operator, values[0] must represent an earlier or equal date/time compared to values[1]",
351
+ path: ["values"]
352
+ }),
353
+ BaseFilterSchema.extend({
354
+ operator: z.enum([
355
+ OPERATORS.EQUALS,
356
+ OPERATORS.NOT_EQUALS,
357
+ OPERATORS.GREATER_THAN,
358
+ OPERATORS.GREATER_THAN_OR_EQUAL,
359
+ OPERATORS.LESS_THAN,
360
+ OPERATORS.LESS_THAN_OR_EQUAL
361
+ ]),
362
+ value: z.string()
363
+ }).refine((data) => data.value !== void 0, { message: "value is required for non-null operators" })
364
+ ]);
365
+ /**
366
+ * Enum filter input - Spec-compliant structure
367
+ * Supports both single value (eq/ne) and multiple values (in/notIn)
368
+ * Field name is the object key, type is inferred from registry
369
+ *
370
+ * Usage: createEnumFilter(MyEnumSchema)
371
+ *
372
+ * Examples:
373
+ * - Equality: { operator: "eq", value: "OPTION_A" }
374
+ * - Not equals: { operator: "ne", value: "OPTION_B" }
375
+ * - In array: { operator: "in", values: ["OPTION_A", "OPTION_B"] }
376
+ * - Not in array: { operator: "notIn", values: ["OPTION_C", "OPTION_D"] }
377
+ */
378
+ function createEnumFilter(enumSchema) {
379
+ return BaseFilterSchema.extend({
380
+ operator: EqualityArrayOperatorSchema,
381
+ value: enumSchema.optional(),
382
+ values: z.array(enumSchema).optional()
383
+ }).refine((data) => {
384
+ if (data.operator === OPERATORS.IS_ONE_OF || data.operator === OPERATORS.IS_NOT_ONE_OF) return data.values !== void 0 && data.values.length > 0;
385
+ return data.value !== void 0;
386
+ }, { message: "value is required for non-array operators, values array is required for in/notIn" });
387
+ }
388
+ /**
389
+ * Filter configuration - Spec-compliant structure
390
+ * Uses a flat object mapping field names to filter value objects
391
+ * Field name is the object key, type is inferred from registry
392
+ * All filters are combined with implicit AND logic
393
+ * OR logic within a single field is supported through isOneOf/isNotOneOf operators
394
+ *
395
+ * Examples:
396
+ * {
397
+ * "status": { operator: "eq", value: "active" },
398
+ * "age": { operator: "gt", value: 18 }
399
+ * }
400
+ */
401
+ const FilterConfigSchema = z.record(z.string(), BaseFilterSchema);
402
+
403
+ //#endregion
404
+ //#region src/validation/common/pagination_zod.ts
405
+ /**
406
+ * Sort direction enum
407
+ */
408
+ const SortDirectionSchema = z.enum(["asc", "desc"]);
409
+ const PaginationFiltersSchema = z.object({
410
+ first: z.number().min(1).max(100).optional(),
411
+ after: z.string().optional(),
412
+ sortBy: z.string().optional(),
413
+ sortDirection: SortDirectionSchema.optional(),
414
+ paginationToken: z.string().optional()
415
+ });
416
+ const PageInfoSchema = z.object({
417
+ hasNextPage: z.boolean(),
418
+ hasPreviousPage: z.boolean(),
419
+ prevPageCursor: z.string().optional(),
420
+ nextPageCursor: z.string().optional(),
421
+ currentPageIndex: z.number().optional(),
422
+ paginationToken: z.string().optional()
423
+ });
424
+ function createPaginatedSchema(itemSchema) {
425
+ return z.object({
426
+ items: z.array(itemSchema),
427
+ pageInfo: PageInfoSchema
428
+ });
429
+ }
430
+
431
+ //#endregion
432
+ //#region src/validation/common/regex_patterns.ts
433
+ /**
434
+ * Common regex patterns for validation
435
+ */
436
+ /**
437
+ * Decimal amount regex - allows up to 2 decimal places
438
+ * Matches: "100", "100.00", "100.5", "0.99", etc.
439
+ * Also allows empty string for optional fields
440
+ */
441
+ const DECIMAL_AMOUNT_REGEX = /^((\d*\.?\d{1,2}|\d+)|)$/;
442
+ /**
443
+ * Decimal amount regex error message
444
+ */
445
+ const DECIMAL_AMOUNT_ERROR_MESSAGE = "Must be a valid dollar amount with up to 2 decimal places or empty";
446
+ /**
447
+ * Decimal amount regex for 2 decimals or empty
448
+ * Matches: "100.00", "0.99", "1234.56", etc. or empty string
449
+ * Requires exactly 2 decimal places when not empty
450
+ */
451
+ const DECIMAL_AMOUNT_2_DECIMALS_OR_EMPTY_REGEX = /^((\d*\.?\d{1,2}|\d+)|)$/;
452
+ /**
453
+ * Decimal amount regex for 2 decimals or empty error message
454
+ */
455
+ const DECIMAL_AMOUNT_2_DECIMALS_OR_EMPTY_ERROR_MESSAGE = "Must be a valid dollar amount with up to 2 decimal places or empty";
456
+
457
+ //#endregion
458
+ //#region src/validation/user/user_enums_zod.ts
459
+ const USER_TYPES = [
460
+ "consumer",
461
+ "lead",
462
+ "staff",
463
+ "super_admin"
464
+ ];
465
+ const UserTypeEnum = z.enum([...USER_TYPES]);
466
+
467
+ //#endregion
468
+ //#region src/validation/user/user_create_zod.ts
469
+ const createUserSchema = z.object({
470
+ email: z.string().trim().toLowerCase().min(3, "Please enter your email.").email("The email address is badly formatted."),
471
+ user_type: UserTypeEnum,
472
+ password: z.string().optional().nullable()
473
+ });
474
+ const createUserSchemaOutput = z.object({
475
+ id: z.string(),
476
+ email: z.string()
477
+ });
478
+
479
+ //#endregion
480
+ //#region src/validation/credit_transaction/credit_balance_schema.ts
481
+ const CreditBalanceSchema = z.object({
482
+ monthly: z.string(),
483
+ rollover: z.string()
484
+ });
485
+ const AddCreditsSchema = z.object({
486
+ amount: z.string().regex(DECIMAL_AMOUNT_REGEX, DECIMAL_AMOUNT_ERROR_MESSAGE),
487
+ reason: z.string().optional()
488
+ });
489
+ const SetMonthlyAllocationSchema = z.object({ amount: z.string().regex(DECIMAL_AMOUNT_REGEX, DECIMAL_AMOUNT_ERROR_MESSAGE) });
490
+ const ResetMonthlyBalanceSchema = z.object({}).optional();
491
+
492
+ //#endregion
493
+ //#region src/validation/credit_transaction/credit_transaction_read_zod.ts
494
+ /**
495
+ * Transaction types for credit operations
496
+ */
497
+ const CreditTransactionTypeEnum = z.enum([
498
+ "DEDUCTION",
499
+ "REFUND",
500
+ "PURCHASE_ONETIME",
501
+ "PURCHASE_RECURRING",
502
+ "ADJUSTMENT"
503
+ ]);
504
+ /**
505
+ * Schema for a credit transaction record
506
+ */
507
+ const CreditTransactionReadSchema = z.object({
508
+ id: z.string(),
509
+ support_ticket_id: z.string().nullable().optional(),
510
+ amount: z.string(),
511
+ type: CreditTransactionTypeEnum,
512
+ description: z.string().nullable().optional(),
513
+ balance_after: z.string(),
514
+ created_at: z.string(),
515
+ created_by: z.string()
516
+ });
517
+ /**
518
+ * Paginated schema for credit transactions
519
+ */
520
+ const CreditTransactionPageSchema = createPaginatedSchema(CreditTransactionReadSchema);
521
+
522
+ //#endregion
523
+ //#region src/validation/credit_transaction/credit_transaction_filters_zod.ts
524
+ /**
525
+ * Filters for querying credit transactions
526
+ * Supports operator-based filtering for advanced queries
527
+ *
528
+ * All filters support operators for advanced filtering:
529
+ * - Enums: eq, ne, in, notIn
530
+ * - Strings: eq, ne, contains, startsWith, endsWith
531
+ * - Numbers: eq, ne, gt, gte, lt, lte, between
532
+ * - Dates: eq, ne, gt, gte, lt, lte, between
533
+ *
534
+ * Examples:
535
+ * - { type: { operator: "eq", value: "DEDUCTION" } }
536
+ * - { amount: { operator: "gte", value: 100 } }
537
+ * - { created_at: { operator: "gte", value: "2024-01-01" } }
538
+ */
539
+ const CreditTransactionFiltersSchema = PaginationFiltersSchema.extend({
540
+ type: createEnumFilter(CreditTransactionTypeEnum).optional(),
541
+ support_ticket_id: StringFilterSchema.optional(),
542
+ amount: NumberFilterSchema.optional(),
543
+ balance_after: NumberFilterSchema.optional(),
544
+ created_at: DateFilterSchema.optional(),
545
+ created_by: StringFilterSchema.optional(),
546
+ search: z.object({
547
+ query: z.string(),
548
+ searchableFields: z.array(z.string())
549
+ }).optional()
550
+ });
551
+
552
+ //#endregion
553
+ //#region src/validation/forgot_password_zod.ts
554
+ const forgot_password_zod = z.object({ email: z.string().email().trim().toLowerCase() });
555
+
556
+ //#endregion
557
+ //#region src/types/record_types.ts
558
+ /**
559
+ * Record type constants and types for the application
560
+ * These define all the different types of records that can exist in the system
561
+ */
562
+ const RecordTypeValues = [
563
+ "attachment_folder",
564
+ "attachment",
565
+ "user",
566
+ "user_session",
567
+ "user_profile",
568
+ "refresh_token",
569
+ "refresh_token_family",
570
+ "user_subscription",
571
+ "password_reset",
572
+ "record_version",
573
+ "support_ticket",
574
+ "credit_transaction",
575
+ "team_member",
576
+ "client_contact",
577
+ "client_location",
578
+ "client_profile",
579
+ "business_profile",
580
+ "tracker",
581
+ "team",
582
+ "quote",
583
+ "note",
584
+ "followup"
585
+ ];
586
+ const RecordConst = {
587
+ ATTACHMENT: "attachment",
588
+ ATTACHMENT_FOLDER: "attachment_folder",
589
+ USER: "user",
590
+ USER_SESSION: "user_session",
591
+ USER_PROFILE: "user_profile",
592
+ REFRESH_TOKEN: "refresh_token",
593
+ REFRESH_TOKEN_FAMILY: "refresh_token_family",
594
+ USER_SUBSCRIPTION: "user_subscription",
595
+ PASSWORD_RESET: "password_reset",
596
+ RECORD_VERSION: "record_version",
597
+ SUPPORT_TICKET: "support_ticket",
598
+ CREDIT_TRANSACTION: "credit_transaction",
599
+ TEAM_MEMBER: "team_member",
600
+ CLIENT_CONTACT: "client_contact",
601
+ CLIENT_LOCATION: "client_location",
602
+ CLIENT_PROFILE: "client_profile",
603
+ BUSINESS_PROFILE: "business_profile",
604
+ TRACKER: "tracker",
605
+ TEAM: "team",
606
+ NOTE: "note",
607
+ FOLLOWUP: "followup"
608
+ };
609
+
610
+ //#endregion
611
+ //#region src/validation/note/note_read_zod.ts
612
+ const RecordTypeEnum$3 = z.enum(RecordTypeValues);
613
+ const NoteReadSchema = z.object({
614
+ id: z.string(),
615
+ record_id: z.string(),
616
+ record_type: RecordTypeEnum$3,
617
+ tag: z.string().optional().nullable(),
618
+ title: z.string().optional().nullable(),
619
+ body: z.string().optional().nullable(),
620
+ original_id: z.number().optional().nullable(),
621
+ is_internal: z.boolean(),
622
+ created_by: z.string(),
623
+ created_at: z.string(),
624
+ updated_by: z.string().optional().nullable(),
625
+ updated_at: z.string().optional().nullable(),
626
+ archived_by: z.string().optional().nullable(),
627
+ archived_at: z.string().optional().nullable(),
628
+ deleted_by: z.string().optional().nullable(),
629
+ deleted_at: z.string().optional().nullable()
630
+ });
631
+
632
+ //#endregion
633
+ //#region src/validation/note/note_create_zod.ts
634
+ const RecordTypeEnum$2 = z.enum(RecordTypeValues);
635
+ const NoteCreateSchema = z.object({
636
+ record_id: z.string(),
637
+ record_type: RecordTypeEnum$2,
638
+ tag: z.string().optional().nullable(),
639
+ title: z.string().optional().nullable(),
640
+ body: z.string().optional().nullable(),
641
+ original_id: z.number().optional().nullable(),
642
+ is_internal: z.boolean().optional().default(false)
643
+ });
644
+
645
+ //#endregion
646
+ //#region src/validation/note/note_update_zod.ts
647
+ const RecordTypeEnum$1 = z.enum(RecordTypeValues);
648
+ const NoteUpdateSchema = z.object({
649
+ id: z.string(),
650
+ record_id: z.string().optional(),
651
+ record_type: RecordTypeEnum$1.optional(),
652
+ tag: z.string().optional().nullable(),
653
+ title: z.string().optional().nullable(),
654
+ body: z.string().optional().nullable(),
655
+ original_id: z.number().optional().nullable(),
656
+ is_internal: z.boolean().optional()
657
+ });
658
+
659
+ //#endregion
660
+ //#region src/validation/note/note_filters_zod.ts
661
+ const RecordTypeFilterEnum = z.enum(RecordTypeValues);
662
+ /**
663
+ * Filters for note
664
+ * Supports operator-based filtering for advanced queries
665
+ *
666
+ * All filters support operators for advanced filtering:
667
+ * - Booleans: eq, ne
668
+ * - Strings: eq, ne, contains, startsWith, endsWith
669
+ * - Dates: eq, ne, gt, gte, lt, lte
670
+ *
671
+ * Examples:
672
+ * - { record_type: { operator: "eq", value: "SUPPORT_TICKET" } }
673
+ * - { title: { operator: "contains", value: "search" } }
674
+ */
675
+ const NoteFiltersSchema = PaginationFiltersSchema.extend({
676
+ record_id: StringFilterSchema.optional(),
677
+ record_type: createEnumFilter(RecordTypeFilterEnum).optional(),
678
+ title: StringFilterSchema.optional(),
679
+ body: StringFilterSchema.optional(),
680
+ tag: StringFilterSchema.optional(),
681
+ created_at: DateFilterSchema.optional(),
682
+ updated_at: DateFilterSchema.optional(),
683
+ is_internal: BooleanFilterSchema.optional(),
684
+ search: z.object({
685
+ query: z.string(),
686
+ searchableFields: z.array(z.string())
687
+ }).optional()
688
+ });
689
+ const NoteFiltersSortDirectionEnum = z.enum(["asc", "desc"]);
690
+
691
+ //#endregion
692
+ //#region src/validation/record_version_zod.ts
693
+ const RecordTypeEnum = z.enum(RecordTypeValues);
694
+ const recordVersionSchema = z.object({
695
+ id: z.string(),
696
+ record_id: z.string(),
697
+ operation: z.string(),
698
+ recorded_at: z.string(),
699
+ record_type: z.string(),
700
+ record: z.string().optional().nullable(),
701
+ old_record: z.string().optional().nullable(),
702
+ auth_uid: z.string().optional().nullable(),
703
+ auth_role: z.string().optional().nullable(),
704
+ auth_username: z.string().optional().nullable()
705
+ });
706
+ const recordVersionPageSchema = z.object({
707
+ items: z.array(recordVersionSchema),
708
+ pageInfo: z.object({
709
+ hasNextPage: z.boolean(),
710
+ endCursor: z.string().optional().nullable()
711
+ })
712
+ });
713
+ const pageInfoSchema = z.object({
714
+ hasNextPage: z.boolean(),
715
+ endCursor: z.string().optional().nullable()
716
+ });
717
+ const recordVersionFiltersSchema = z.object({
718
+ start_date: z.string().optional().nullable(),
719
+ end_date: z.string().optional().nullable(),
720
+ limit: z.number().optional().nullable(),
721
+ cursor: z.string().optional().nullable()
722
+ });
723
+ const recordVersionFiltersInputSchema = z.object({
724
+ record_id: z.string(),
725
+ record_type: RecordTypeEnum,
726
+ filters: recordVersionFiltersSchema.optional().nullable()
727
+ });
728
+ /**
729
+ * NEW: Breadcrumb-paginated record version response
730
+ * Use this for new implementations
731
+ */
732
+ const recordVersionPageBreadcrumbSchema = createPaginatedSchema(recordVersionSchema);
733
+ /**
734
+ * NEW: Filters with breadcrumb pagination support
735
+ * Extends PaginationFiltersSchema for consistent pagination
736
+ */
737
+ const recordVersionFiltersBreadcrumbSchema = z.object({
738
+ first: z.number().min(1).max(100).optional(),
739
+ after: z.string().optional(),
740
+ last: z.number().min(1).max(100).optional(),
741
+ before: z.string().optional(),
742
+ sortBy: z.string().optional(),
743
+ sortDirection: SortDirectionSchema.optional(),
744
+ paginationToken: z.string().optional(),
745
+ start_date: z.string().optional().nullable(),
746
+ end_date: z.string().optional().nullable()
747
+ });
748
+ const recordVersionFiltersInputBreadcrumbSchema = z.object({
749
+ record_id: z.string(),
750
+ record_type: RecordTypeEnum,
751
+ filters: recordVersionFiltersBreadcrumbSchema.optional().nullable()
752
+ });
753
+
754
+ //#endregion
755
+ //#region src/validation/reset_password_zod.ts
756
+ const resetPasswordInputSchema = z.object({ passwords: z.object({
757
+ password: z.string().min(8, { message: "Password must be at least 8 characters long" }).max(64, { message: "Password must not exceed 64 characters" }).refine((password) => !/^\s|\s$/.test(password), { message: "Password must not have leading or trailing whitespace" }).refine((password) => !isCommonPassword(password), { message: "Password is too common and easily guessed" }),
758
+ password_confirm: z.string().min(8)
759
+ }).refine((data) => data.password === data.password_confirm, {
760
+ message: "Passwords do not match",
761
+ path: ["password_confirm"]
762
+ }) });
763
+ const resetPasswordSchema = resetPasswordInputSchema.extend({ token: z.string().min(20) });
764
+
765
+ //#endregion
766
+ //#region src/validation/signup_zod.ts
767
+ const signupSchema = z.object({
768
+ email: z.string().trim().toLowerCase().min(3, "Please enter your email.").email("The email address is badly formatted."),
769
+ passwords: z.object({
770
+ password: passwordSchema,
771
+ password_confirm: z.string().min(1)
772
+ }).refine((data) => data.password === data.password_confirm, {
773
+ message: "Passwords do not match",
774
+ path: ["password_confirm"]
775
+ })
776
+ });
777
+
778
+ //#endregion
779
+ //#region src/validation/support_ticket/support_ticket_shared/support_ticket_enums_zod.ts
780
+ const SupportTicketTypeEnum = [
781
+ "IMPROVEMENT",
782
+ "BUG",
783
+ "FEATURE_REQUEST",
784
+ "OPERATIONAL"
785
+ ];
786
+ const SupportTicketTypeSchema = z.enum(SupportTicketTypeEnum);
787
+ const SupportTicketPriorityEnum = [
788
+ "LOW",
789
+ "MEDIUM",
790
+ "HIGH",
791
+ "CRITICAL"
792
+ ];
793
+ /**
794
+ * Enum for feature request priority levels
795
+ */
796
+ const SupportTicketPrioritySchema = z.enum(SupportTicketPriorityEnum);
797
+ /**
798
+ * Enum for customer-facing support_ticket status (computed from approval_status + dev_lifecycle)
799
+ * - PENDING: Awaiting admin review
800
+ * - FOLLOWUP: Approved but not started
801
+ * - IN_PROGRESS: Actively being worked on
802
+ * - COMPLETED: Deployed to production
803
+ * - CANCELLED: Rejected by admin
804
+ */
805
+ const SupportTicketStatusEnum = [
806
+ "PENDING",
807
+ "FOLLOWUP",
808
+ "IN_PROGRESS",
809
+ "COMPLETED",
810
+ "CANCELLED"
811
+ ];
812
+ const SupportTicketStatusSchema = z.enum(SupportTicketStatusEnum);
813
+ /**
814
+ * Enum for feature request approval status
815
+ * - PENDING: Awaiting staff decision (customer-submitted)
816
+ * - APPROVED: Approved by staff (with customer credits)
817
+ * - REJECTED: Rejected by staff
818
+ * - INTERNAL: Internal staff support_ticket (no approval/credits needed)
819
+ */
820
+ const SupportTicketApprovalEnum = [
821
+ "PENDING",
822
+ "APPROVED",
823
+ "REJECTED",
824
+ "INTERNAL"
825
+ ];
826
+ const SupportTicketApprovalSchema = z.enum(SupportTicketApprovalEnum);
827
+ /**
828
+ * Enum for internal development lifecycle stages
829
+ * - BACKLOG: Approved but not started
830
+ * - PLANNING: Scoping/design phase
831
+ * - DEVELOPMENT: Actively coding
832
+ * - CODE_REVIEW: In review
833
+ * - TESTING: QA testing
834
+ * - STAGING: On staging environment
835
+ * - PO_APPROVAL: Waiting for PO/customer sign-off
836
+ * - DEPLOYED: Live in production
837
+ * - CANCELLED: Internal task cancelled (only for INTERNAL approval status)
838
+ */
839
+ const SupportTicketDevLifecycleEnum = [
840
+ "PENDING",
841
+ "BACKLOG",
842
+ "PLANNING",
843
+ "DEVELOPMENT",
844
+ "CODE_REVIEW",
845
+ "TESTING",
846
+ "STAGING",
847
+ "PO_APPROVAL",
848
+ "DEPLOYED",
849
+ "CANCELLED"
850
+ ];
851
+ const SupportTicketDevLifecycleSchema = z.enum(SupportTicketDevLifecycleEnum);
852
+ /**
853
+ * Dev lifecycle stages that can be manually set via update form
854
+ * Excludes workflow-only stages:
855
+ * - PENDING: Set by revert workflow
856
+ * - DEPLOYED: Set by completeSupportTicket workflow
857
+ * Staff can manually progress through: BACKLOG → PLANNING → DEVELOPMENT → CODE_REVIEW → TESTING → STAGING → PO_APPROVAL
858
+ */
859
+ const SupportTicketDevLifecycleUpdateEnum = [
860
+ "BACKLOG",
861
+ "PLANNING",
862
+ "DEVELOPMENT",
863
+ "CODE_REVIEW",
864
+ "TESTING",
865
+ "STAGING",
866
+ "PO_APPROVAL"
867
+ ];
868
+ const SupportTicketDevLifecycleUpdateSchema = z.enum(SupportTicketDevLifecycleUpdateEnum);
869
+ /**
870
+ * Filter-specific enum instances
871
+ * These are separate Zod objects with the same values as above,
872
+ * allowing GraphQL to generate distinct enum types for filters vs domain objects
873
+ */
874
+ const SupportTicketTypeFilterSchema = z.enum(SupportTicketTypeEnum);
875
+ const SupportTicketPriorityFilterSchema = z.enum(SupportTicketPriorityEnum);
876
+ const SupportTicketStatusFilterSchema = z.enum(SupportTicketStatusEnum);
877
+ const SupportTicketApprovalFilterSchema = z.enum(SupportTicketApprovalEnum);
878
+ const SupportTicketDevLifecycleFilterSchema = z.enum(SupportTicketDevLifecycleEnum);
879
+
880
+ //#endregion
881
+ //#region src/validation/support_ticket/support_ticket_customer/customer_input_zod.ts
882
+ /**
883
+ * Schema for creating new support_ticket (customer)
884
+ */
885
+ const CustomerSupportTicketCreateSchema = z.object({
886
+ title: z.string().min(5, "Please add a more descriptive title (at least 5 characters)").max(256, "Title is too long - keep it brief (under 256 characters)"),
887
+ description: z.string().optional().nullable(),
888
+ type: SupportTicketTypeSchema.default("FEATURE_REQUEST"),
889
+ priority: SupportTicketPrioritySchema.default("MEDIUM")
890
+ });
891
+ /**
892
+ * Schema for updating support_ticket (customer - PENDING items only)
893
+ */
894
+ const CustomerSupportTicketUpdateSchema = CustomerSupportTicketCreateSchema.extend({ id: z.string() });
895
+
896
+ //#endregion
897
+ //#region src/validation/support_ticket/support_ticket_customer/customer_read_zod.ts
898
+ /**
899
+ * Customer-specific read schema - ONLY includes fields customers can see
900
+ * Excludes all staff-only fields like internalNotes, devLifecycle, timeline tracking, etc.
901
+ */
902
+ const CustomerSupportTicketReadSchema = z.object({
903
+ id: z.string(),
904
+ display_id: z.string().optional().nullable(),
905
+ display_id_prefix: z.string().optional().nullable(),
906
+ title: z.string(),
907
+ description: z.string(),
908
+ type: SupportTicketTypeSchema,
909
+ priority: SupportTicketPrioritySchema,
910
+ status: SupportTicketStatusSchema,
911
+ is_locked: z.boolean(),
912
+ requester_name: z.string().optional().nullable(),
913
+ requester_email: z.string().email().optional().nullable(),
914
+ credit_value: z.string().optional().nullable(),
915
+ start_at: z.string().optional().nullable(),
916
+ target_at: z.string().optional().nullable(),
917
+ completed_at: z.string().optional().nullable(),
918
+ locked_approval_at: z.string().optional().nullable(),
919
+ created_by: z.string().optional().nullable(),
920
+ created_at: z.string().optional().nullable(),
921
+ updated_by: z.string().optional().nullable(),
922
+ updated_at: z.string().optional().nullable()
923
+ });
924
+ /**
925
+ * Paginated customer support_ticket response
926
+ */
927
+ const CustomerSupportTicketPageSchema = createPaginatedSchema(CustomerSupportTicketReadSchema);
928
+
929
+ //#endregion
930
+ //#region src/validation/support_ticket/support_ticket_customer/customer_support_ticket_filters_zod.ts
931
+ /**
932
+ * Customer-facing filters for support_ticket
933
+ * Includes all fields customers can see and filter on
934
+ *
935
+ * - Cannot filter by internal fields (isInternalOnly, approvalStatus, devLifecycle)
936
+ * - Can filter on all visible fields including credits, requester info, etc.
937
+ */
938
+ const CustomerSupportTicketFiltersSchema = PaginationFiltersSchema.extend({
939
+ type: createEnumFilter(SupportTicketTypeFilterSchema).optional(),
940
+ status: createEnumFilter(SupportTicketStatusFilterSchema).optional(),
941
+ priority: createEnumFilter(SupportTicketPriorityFilterSchema).optional(),
942
+ title: StringFilterSchema.optional(),
943
+ description: StringFilterSchema.optional(),
944
+ requester_name: StringFilterSchema.optional(),
945
+ requester_email: StringFilterSchema.optional(),
946
+ is_locked: BooleanFilterSchema.optional(),
947
+ credit_value: NumberFilterSchema.optional(),
948
+ created_at: DateFilterSchema.optional(),
949
+ updated_at: DateFilterSchema.optional(),
950
+ start_at: DateFilterSchema.optional(),
951
+ target_at: DateFilterSchema.optional(),
952
+ completed_at: DateFilterSchema.optional(),
953
+ search: z.object({
954
+ query: z.string(),
955
+ searchableFields: z.array(z.string())
956
+ }).optional()
957
+ });
958
+
959
+ //#endregion
960
+ //#region src/validation/support_ticket/support_ticket_staff/staff_read_zod.ts
961
+ /**
962
+ * Staff-specific read schema - includes ALL fields including staff-only data
963
+ * This is what staff see when they query support_ticket (includes internal notes, dev lifecycle, etc.)
964
+ */
965
+ const StaffSupportTicketReadSchema = z.object({
966
+ id: z.string(),
967
+ display_id: z.string().optional().nullable(),
968
+ display_id_prefix: z.string().optional().nullable(),
969
+ title: z.string(),
970
+ description: z.string(),
971
+ type: SupportTicketTypeSchema,
972
+ priority: SupportTicketPrioritySchema,
973
+ status: SupportTicketStatusSchema,
974
+ approval_status: SupportTicketApprovalSchema,
975
+ is_locked: z.boolean(),
976
+ can_delete: z.boolean(),
977
+ requester_name: z.string().optional().nullable(),
978
+ requester_email: z.string().email().optional().nullable(),
979
+ dev_lifecycle: SupportTicketDevLifecycleSchema.optional().nullable(),
980
+ credit_value: z.string().optional().nullable(),
981
+ delivered_value: z.string().optional().nullable(),
982
+ start_at: z.string().optional().nullable(),
983
+ target_at: z.string().optional().nullable(),
984
+ completed_at: z.string().optional().nullable(),
985
+ locked_approval_at: z.string().optional().nullable(),
986
+ created_by: z.string().optional().nullable(),
987
+ created_at: z.string().optional().nullable(),
988
+ updated_by: z.string().optional().nullable(),
989
+ updated_at: z.string().optional().nullable()
990
+ });
991
+ /**
992
+ * Paginated staff support_ticket response
993
+ */
994
+ const StaffSupportTicketPageSchema = createPaginatedSchema(StaffSupportTicketReadSchema);
995
+
996
+ //#endregion
997
+ //#region src/validation/support_ticket/support_ticket_staff/staff_support_ticket_filters_zod.ts
998
+ /**
999
+ * Staff/Admin filters for support_ticket
1000
+ * Full access to all filtering capabilities including internal fields
1001
+ *
1002
+ * All filters support operators for advanced filtering:
1003
+ * - Enums: eq, ne
1004
+ * - Booleans: eq, ne
1005
+ * - Strings: eq, ne, contains, startsWith, endsWith
1006
+ * - Numbers: eq, ne, gt, gte, lt, lte
1007
+ * - Dates: eq, ne, gt, gte, lt, lte
1008
+ *
1009
+ * Examples:
1010
+ * - { type: { operator: "eq", value: "BUG" } }
1011
+ * - { approvalStatus: { operator: "eq", value: "INTERNAL" } } // Filter for internal tasks
1012
+ * - { title: { operator: "contains", value: "search" } }
1013
+ * - { creditValue: { operator: "gte", value: 100 } }
1014
+ * - { createdAt: { operator: "gte", value: "2024-01-01" } }
1015
+ */
1016
+ const StaffSupportTicketFiltersSchema = PaginationFiltersSchema.extend({
1017
+ type: createEnumFilter(SupportTicketTypeFilterSchema).optional(),
1018
+ status: createEnumFilter(SupportTicketStatusFilterSchema).optional(),
1019
+ approval_status: createEnumFilter(SupportTicketApprovalFilterSchema).optional(),
1020
+ priority: createEnumFilter(SupportTicketPriorityFilterSchema).optional(),
1021
+ dev_lifecycle: createEnumFilter(SupportTicketDevLifecycleFilterSchema).optional(),
1022
+ title: StringFilterSchema.optional(),
1023
+ description: StringFilterSchema.optional(),
1024
+ requester_name: StringFilterSchema.optional(),
1025
+ requester_email: StringFilterSchema.optional(),
1026
+ is_locked: BooleanFilterSchema.optional(),
1027
+ credit_value: NumberFilterSchema.optional(),
1028
+ delivered_value: NumberFilterSchema.optional(),
1029
+ created_at: DateFilterSchema.optional(),
1030
+ updated_at: DateFilterSchema.optional(),
1031
+ start_at: DateFilterSchema.optional(),
1032
+ target_at: DateFilterSchema.optional(),
1033
+ completed_at: DateFilterSchema.optional(),
1034
+ search: z.object({
1035
+ query: z.string(),
1036
+ searchableFields: z.array(z.string())
1037
+ }).optional()
1038
+ });
1039
+
1040
+ //#endregion
1041
+ //#region src/validation/support_ticket/support_ticket_staff/staff_update_zod.ts
1042
+ /**
1043
+ * Base schema for staff support_ticket updates (general editing)
1044
+ *
1045
+ * Workflow-specific fields NOT included here:
1046
+ * - approvalStatus: Use approve/reject/revert workflows, or set to INTERNAL at creation
1047
+ *
1048
+ * Note: completeSupportTicket workflow sets deliveredValue + completedAt initially,
1049
+ * but they can be adjusted here afterwards for corrections.
1050
+ *
1051
+ * Business rules validated in backend:
1052
+ * - Can't edit creditValue unless PENDING (INTERNAL support_ticket can't have credits)
1053
+ * - Can't edit devLifecycle unless APPROVED or INTERNAL
1054
+ * - Can't edit deliveredValue unless DEPLOYED
1055
+ * - Can't edit completedAt unless DEPLOYED
1056
+ * - etc.
1057
+ */
1058
+ const StaffSupportTicketInputBaseSchema = z.object({
1059
+ title: z.string().min(5, "Please add a more descriptive title (at least 5 characters)").max(256, "Title is too long - keep it brief (under 256 characters)"),
1060
+ description: z.string().optional().nullable(),
1061
+ type: SupportTicketTypeSchema,
1062
+ priority: SupportTicketPrioritySchema,
1063
+ dev_lifecycle: SupportTicketDevLifecycleUpdateSchema.optional(),
1064
+ credit_value: z.string().regex(DECIMAL_AMOUNT_REGEX, DECIMAL_AMOUNT_ERROR_MESSAGE).optional().nullable(),
1065
+ delivered_value: z.string().regex(DECIMAL_AMOUNT_2_DECIMALS_OR_EMPTY_REGEX, DECIMAL_AMOUNT_2_DECIMALS_OR_EMPTY_ERROR_MESSAGE).optional().nullable(),
1066
+ start_at: z.string().regex(/\d{4}-\d{2}-\d{2}/, "Date must be in the format YYYY-MM-DD").optional().nullable(),
1067
+ target_at: z.string().regex(/\d{4}-\d{2}-\d{2}/, "Date must be in the format YYYY-MM-DD").optional().nullable(),
1068
+ completed_at: z.string().regex(/\d{4}-\d{2}-\d{2}/, "Date must be in the format YYYY-MM-DD").optional().nullable()
1069
+ });
1070
+ const StaffSupportTicketCreateSchema = StaffSupportTicketInputBaseSchema.extend({ is_internal: z.boolean().optional() });
1071
+ const StaffSupportTicketUpdateSchema = StaffSupportTicketInputBaseSchema.extend({ id: z.string() });
1072
+ const StaffSupportTicketInputSchema = StaffSupportTicketUpdateSchema;
1073
+
1074
+ //#endregion
1075
+ //#region src/validation/support_ticket/support_ticket_staff/staff_workflow_zod.ts
1076
+ /**
1077
+ * Staff workflow schemas
1078
+ * These are for critical state transitions in the support_ticket lifecycle
1079
+ */
1080
+ /**
1081
+ * Approve support_ticket (PENDING → APPROVED)
1082
+ * Deducts credits and sets dev_lifecycle to BACKLOG
1083
+ */
1084
+ const ApproveSupportTicketSchema = z.object({ id: z.string() });
1085
+ /**
1086
+ * Reject support_ticket (PENDING → REJECTED)
1087
+ * Locks the support_ticket without deducting credits
1088
+ */
1089
+ const RejectSupportTicketSchema = z.object({ id: z.string() });
1090
+ /**
1091
+ * Revert support_ticket (APPROVED/REJECTED → PENDING)
1092
+ * Refunds credits if applicable and unlocks the support_ticket
1093
+ */
1094
+ const RevertSupportTicketSchema = z.object({ id: z.string() });
1095
+ /**
1096
+ * Complete support_ticket (devLifecycle → DEPLOYED)
1097
+ * Marks support_ticket as deployed and captures actual delivered value
1098
+ * Auto-sets completedAt timestamp
1099
+ */
1100
+ const CompleteSupportTicketSchema = z.object({
1101
+ id: z.string(),
1102
+ delivered_value: z.string().regex(/^(\d*\.?\d{1,2}|\d+)$/, "Must be a valid dollar amount with up to 2 decimal places")
1103
+ });
1104
+ /**
1105
+ * Delete support_ticket (soft delete)
1106
+ * Cannot delete APPROVED tickets (charged to customer)
1107
+ * Can delete PENDING, REJECTED, INTERNAL tickets
1108
+ */
1109
+ const DeleteSupportTicketSchema = z.object({ id: z.string() });
1110
+ /**
1111
+ * Cancel internal task (devLifecycle → CANCELLED)
1112
+ * Only for INTERNAL approval_status tickets
1113
+ * Customer tickets use reject workflow instead
1114
+ */
1115
+ const CancelInternalTaskSchema = z.object({ id: z.string() });
1116
+ /**
1117
+ * Reactivate terminal internal task (CANCELLED or DEPLOYED → BACKLOG)
1118
+ * Only for INTERNAL approval_status tickets with dev_lifecycle = CANCELLED or DEPLOYED
1119
+ * Clears completed_at timestamp and reopens task for more work
1120
+ */
1121
+ const ReactivateInternalTaskSchema = z.object({ id: z.string() });
1122
+
1123
+ //#endregion
1124
+ //#region src/validation/support_ticket/support_ticket_enrichment.ts
1125
+ /**
1126
+ * Schema for support ticket record data in enriched contexts
1127
+ * This defines the minimal fields needed for display/enrichment
1128
+ * Includes id for mapping purposes, even though it's also in record_id
1129
+ */
1130
+ const SupportTicketRecordDataSchema = z.object({
1131
+ id: z.string(),
1132
+ display_id: z.string().optional().nullable(),
1133
+ title: z.string()
1134
+ });
1135
+
1136
+ //#endregion
1137
+ //#region src/validation/team/team_enums_zod.ts
1138
+ const TeamStatusSchema = z.enum(["active", "inactive"]).describe("TeamStatus");
1139
+
1140
+ //#endregion
1141
+ //#region src/validation/team/team_filters_zod.ts
1142
+ /**
1143
+ * Filters for teams
1144
+ *
1145
+ * All filters support operators for advanced filtering:
1146
+ * - Booleans: eq, ne
1147
+ * - Strings: eq, ne, contains, startsWith, endsWith
1148
+ * - Dates: eq, ne, gt, gte, lt, lte
1149
+ *
1150
+ * Examples:
1151
+ * - { display_name: { operator: "contains", value: "search" } }
1152
+ * - { contact_email: { operator: "eq", value: "test@example.com" } }
1153
+ * - { created_at: { operator: "gte", value: "2024-01-01" } }
1154
+ */
1155
+ const TeamFiltersSchema = PaginationFiltersSchema.extend({
1156
+ unique_name: StringFilterSchema.optional(),
1157
+ display_name: StringFilterSchema.optional(),
1158
+ legal_name: StringFilterSchema.optional(),
1159
+ contact_email: StringFilterSchema.optional(),
1160
+ address_city: StringFilterSchema.optional(),
1161
+ address_zip: StringFilterSchema.optional(),
1162
+ includeArchived: BooleanFilterSchema.optional(),
1163
+ created_at: DateFilterSchema.optional(),
1164
+ updated_at: DateFilterSchema.optional(),
1165
+ archived_at: DateFilterSchema.optional(),
1166
+ search: z.object({
1167
+ query: z.string(),
1168
+ searchableFields: z.array(z.string())
1169
+ }).optional()
1170
+ });
1171
+
1172
+ //#endregion
1173
+ //#region src/validation/team/team_input_zod.ts
1174
+ const TeamInputBaseSchema = z.object({
1175
+ unique_name: z.string().min(3, "Unique name must be between 3 and 64 characters").max(64).optional().nullable(),
1176
+ display_name: z.string().min(3, "Display name must be between 3 and 64 characters").max(64),
1177
+ legal_name: z.string().optional().nullable(),
1178
+ description: z.string().optional().nullable(),
1179
+ contact_name: z.string().optional().nullable(),
1180
+ contact_email: z.string().email().optional().nullable(),
1181
+ contact_business_phone: z.string().optional().nullable(),
1182
+ contact_mobile_phone: z.string().optional().nullable(),
1183
+ contact_time_zone: z.string().optional().nullable(),
1184
+ address_full: z.string().optional().nullable(),
1185
+ address_city: z.string().optional().nullable(),
1186
+ address_zip: z.string().optional().nullable(),
1187
+ twitter_username: z.string().optional().nullable(),
1188
+ url: z.string().url().optional().nullable(),
1189
+ logo: z.string().optional().nullable(),
1190
+ email_sent_from: z.string().email().optional().nullable(),
1191
+ email_reply_to: z.string().email().optional().nullable()
1192
+ });
1193
+ const TeamCreateSchema = TeamInputBaseSchema;
1194
+ const TeamUpdateSchema = TeamInputBaseSchema.extend({ id: z.string() });
1195
+
1196
+ //#endregion
1197
+ //#region src/validation/team/team_read_zod.ts
1198
+ const TeamReadSchema = TeamCreateSchema.extend({
1199
+ id: z.string(),
1200
+ original_id: z.string().optional().nullable(),
1201
+ path: z.string().optional().nullable(),
1202
+ created_at: z.string(),
1203
+ updated_at: z.string().optional().nullable(),
1204
+ created_by: z.string(),
1205
+ updated_by: z.string().optional().nullable(),
1206
+ archived_at: z.string().optional().nullable(),
1207
+ archived_by: z.string().optional().nullable()
1208
+ });
1209
+
1210
+ //#endregion
1211
+ //#region src/validation/team/team_page_zod.ts
1212
+ const TeamPageSchema = createPaginatedSchema(TeamReadSchema);
1213
+
1214
+ //#endregion
1215
+ //#region src/validation/team/team_user_teams_zod.ts
1216
+ /**
1217
+ * Schema for a team option in user teams list
1218
+ * Includes all team fields to match the GraphQL query
1219
+ */
1220
+ const TeamOptionSchema = TeamReadSchema;
1221
+ /**
1222
+ * Schema for the list of teams that a user is part of
1223
+ */
1224
+ const UserTeamsSchema = z.object({ items: z.array(TeamOptionSchema) });
1225
+
1226
+ //#endregion
1227
+ //#region src/validation/team_member/team_member_enums_zod.ts
1228
+ /**
1229
+ * Team Member Role Enum
1230
+ * - owner: Full permissions (create, read, update, delete)
1231
+ * - manager: Can create and read (cannot update or delete)
1232
+ * - member: Can create and read (cannot update or delete)
1233
+ * - client: Client role (permissions TBD)
1234
+ */
1235
+ const TeamMemberRoleEnum = [
1236
+ "owner",
1237
+ "manager",
1238
+ "member",
1239
+ "client"
1240
+ ];
1241
+ const TeamMemberRoleSchema = z.enum(TeamMemberRoleEnum);
1242
+ /**
1243
+ * Filter-specific enum instance
1244
+ * This is a separate Zod object with the same values as above,
1245
+ * allowing GraphQL to generate distinct enum types for filters vs domain objects
1246
+ */
1247
+ const TeamMemberRoleFilterSchema = z.enum(TeamMemberRoleEnum);
1248
+
1249
+ //#endregion
1250
+ //#region src/validation/team_member/team_member_filters_zod.ts
1251
+ /**
1252
+ * Filters for team_member
1253
+ * Supports operator-based filtering for advanced queries
1254
+ *
1255
+ * All filters support operators for advanced filtering:
1256
+ * - Numbers: eq, ne, gt, gte, lt, lte
1257
+ * - Strings: eq, ne, contains, startsWith, endsWith
1258
+ * - Dates: eq, ne, gt, gte, lt, lte
1259
+ *
1260
+ * Examples:
1261
+ * - { original_id: { operator: "eq", value: 123 } }
1262
+ * - { team_id: { operator: "eq", value: "team_123" } }
1263
+ * - { display_name: { operator: "contains", value: "John" } }
1264
+ * - { created_at: { operator: "gte", value: "2024-01-01" } }
1265
+ */
1266
+ const TeamMemberFiltersSchema = PaginationFiltersSchema.extend({
1267
+ original_id: NumberFilterSchema.optional(),
1268
+ team_id: StringFilterSchema.optional(),
1269
+ original_team_id: StringFilterSchema.optional(),
1270
+ user_id: StringFilterSchema.optional(),
1271
+ original_user_id: StringFilterSchema.optional(),
1272
+ role: createEnumFilter(TeamMemberRoleFilterSchema).optional(),
1273
+ display_name: StringFilterSchema.optional(),
1274
+ business_phone: StringFilterSchema.optional(),
1275
+ mobile_phone: StringFilterSchema.optional(),
1276
+ email_address: StringFilterSchema.optional(),
1277
+ website_address: StringFilterSchema.optional(),
1278
+ time_zone: StringFilterSchema.optional(),
1279
+ created_at: DateFilterSchema.optional(),
1280
+ updated_at: DateFilterSchema.optional(),
1281
+ deleted_at: DateFilterSchema.optional(),
1282
+ search: z.object({
1283
+ query: z.string(),
1284
+ searchableFields: z.array(z.string())
1285
+ }).optional()
1286
+ });
1287
+
1288
+ //#endregion
1289
+ //#region src/validation/team_member/team_member_input_zod.ts
1290
+ const TeamMemberBaseSchema = z.object({
1291
+ team_id: z.string(),
1292
+ user_id: z.string(),
1293
+ role: TeamMemberRoleSchema,
1294
+ display_name: z.string().min(3, "Display name must be at least 3 characters").max(64, "Display name must be at most 64 characters"),
1295
+ business_phone: z.string().optional().nullable(),
1296
+ mobile_phone: z.string().optional().nullable(),
1297
+ email_address: z.string().email(),
1298
+ website_address: z.string().url().optional().nullable(),
1299
+ time_zone: z.string().optional().nullable()
1300
+ });
1301
+ const TeamMemberCreateSchema = TeamMemberBaseSchema;
1302
+ const TeamMemberUpdateSchema = TeamMemberBaseSchema.extend({ id: z.string() });
1303
+
1304
+ //#endregion
1305
+ //#region src/validation/team_member/team_member_read_zod.ts
1306
+ const TeamMemberReadSchema = z.object({
1307
+ id: z.string(),
1308
+ original_id: z.number().optional().nullable(),
1309
+ team_id: z.string(),
1310
+ original_team_id: z.string().optional().nullable(),
1311
+ user_id: z.string(),
1312
+ original_user_id: z.string().optional().nullable(),
1313
+ role: TeamMemberRoleSchema,
1314
+ display_name: z.string().optional().nullable(),
1315
+ business_phone: z.string().optional().nullable(),
1316
+ mobile_phone: z.string().optional().nullable(),
1317
+ email_address: z.string().email(),
1318
+ website_address: z.string().optional().nullable(),
1319
+ time_zone: z.string().optional().nullable(),
1320
+ created_at: z.string(),
1321
+ created_by: z.string(),
1322
+ updated_at: z.string().optional().nullable(),
1323
+ updated_by: z.string().optional().nullable(),
1324
+ deleted_at: z.string().optional().nullable(),
1325
+ deleted_by: z.string().optional().nullable()
1326
+ });
1327
+
1328
+ //#endregion
1329
+ //#region src/validation/team_member/team_member_user_team_members_zod.ts
1330
+ /**
1331
+ * Schema for a team member option in user team members list
1332
+ * Includes all team member fields to match the query
1333
+ */
1334
+ const TeamMemberOptionSchema = TeamMemberReadSchema;
1335
+ /**
1336
+ * Schema for the list of team members that a user can see (from their teams)
1337
+ */
1338
+ const UserTeamMembersSchema = z.object({ items: z.array(TeamMemberOptionSchema) });
1339
+
1340
+ //#endregion
1341
+ //#region src/validation/user/user_read_zod.ts
1342
+ const UserReadSchema = z.object({
1343
+ id: z.string(),
1344
+ username: z.string(),
1345
+ email: z.string(),
1346
+ email_verified: z.boolean(),
1347
+ user_type: UserTypeEnum,
1348
+ created_at: z.string(),
1349
+ updated_at: z.string().nullable()
1350
+ });
1351
+
1352
+ //#endregion
1353
+ //#region src/validation/user/user_update_zod.ts
1354
+ const UserUpdateSchema = z.object({
1355
+ id: z.string(),
1356
+ user_type: UserTypeEnum
1357
+ });
1358
+
1359
+ //#endregion
1360
+ //#region src/validation/user_profile_zod.ts
1361
+ /**
1362
+ * Base schema for user profile
1363
+ */
1364
+ const UserProfileBaseSchema = z.object({
1365
+ first_name: z.string().max(255, "First name is too long").nullable().optional(),
1366
+ last_name: z.string().max(255, "Last name is too long").nullable().optional(),
1367
+ bio: z.string().max(1e3, "Bio is too long").nullable().optional()
1368
+ });
1369
+ /**
1370
+ * Schema for updating a user profile
1371
+ */
1372
+ const UserProfileUpdateSchema = UserProfileBaseSchema.extend({ user_id: z.string().min(1, "User ID is required") });
1373
+ /**
1374
+ * Schema for reading a user profile
1375
+ */
1376
+ const UserProfileReadSchema = UserProfileBaseSchema.extend({
1377
+ id: z.string().min(1, "ID is required"),
1378
+ user_id: z.string().min(1, "User ID is required"),
1379
+ created_at: z.string().datetime(),
1380
+ created_by: z.string().min(1, "Created by is required"),
1381
+ updated_at: z.string().datetime().nullable(),
1382
+ updated_by: z.string().nullable()
1383
+ });
1384
+
1385
+ //#endregion
1386
+ //#region src/validation/user_session_zod.ts
1387
+ const userSessionSchema = z.object({
1388
+ created_at: z.string(),
1389
+ expires_at: z.string(),
1390
+ status: z.string(),
1391
+ user_agent: z.string().optional().nullable(),
1392
+ ip_address: z.string().optional().nullable(),
1393
+ user: z.object({
1394
+ userId: z.string(),
1395
+ email: z.string(),
1396
+ username: z.string(),
1397
+ email_verified: z.boolean(),
1398
+ user_type: z.string(),
1399
+ first_name: z.string().optional().nullable(),
1400
+ last_name: z.string().optional().nullable(),
1401
+ avatar_url: z.string().optional().nullable(),
1402
+ subscriptions: z.array(z.object({
1403
+ subscription_id: z.string(),
1404
+ subscription_status: z.string().optional().nullable(),
1405
+ subscription_created: z.string(),
1406
+ subscription_current_period_start: z.string(),
1407
+ subscription_current_period_end: z.string(),
1408
+ subscription_cancel_at: z.string().optional().nullable(),
1409
+ subscription_canceled_at: z.string().optional().nullable(),
1410
+ product_name: z.string().optional().nullable(),
1411
+ price_amount: z.number().nullable(),
1412
+ price_currency: z.string().optional().nullable()
1413
+ })).optional().nullable()
1414
+ })
1415
+ });
1416
+ const loginResponseSchema = z.object({
1417
+ frontend_session: userSessionSchema,
1418
+ access_token: z.string(),
1419
+ refresh_token: z.string(),
1420
+ user_details_token: z.string()
1421
+ });
1422
+
1423
+ //#endregion
1424
+ //#region src/utils/enum_labels.ts
1425
+ /**
1426
+ * Utility functions for converting enum values to human-readable display labels
1427
+ */
1428
+ /**
1429
+ * Normalizes and validates input
1430
+ */
1431
+ function normalizeInput(value) {
1432
+ if (typeof value !== "string") return String(value);
1433
+ return value.trim().toLowerCase();
1434
+ }
1435
+ /**
1436
+ * Converts a snake_case string to Title Case
1437
+ * Handles edge cases like empty strings, single characters, numbers, etc.
1438
+ *
1439
+ * Examples:
1440
+ * - "order_signed" -> "Order Signed"
1441
+ * - "active_mtm" -> "Active Mtm"
1442
+ * - "create_a_quote" -> "Create A Quote"
1443
+ */
1444
+ function snakeCaseToTitleCase(value) {
1445
+ const normalized = normalizeInput(value);
1446
+ if (!normalized) return value;
1447
+ const parts = normalized.split("_").filter((part) => part.length > 0);
1448
+ if (parts.length === 0) return value;
1449
+ return parts.map((part) => {
1450
+ if (part.length === 0) return part;
1451
+ return part.charAt(0).toUpperCase() + part.slice(1);
1452
+ }).join(" ");
1453
+ }
1454
+ /**
1455
+ * Determines if a value should use dash formatting based on structure
1456
+ * Uses heuristics: length of first part, total parts, etc.
1457
+ */
1458
+ function shouldUseDashes(parts) {
1459
+ if (parts.length < 2) return false;
1460
+ const firstPart = parts[0];
1461
+ if (!firstPart) return false;
1462
+ if (firstPart.length <= 2) return true;
1463
+ if (firstPart.length >= 3 && firstPart.length <= 8 && parts.length >= 2) return true;
1464
+ if (parts.length >= 3) return true;
1465
+ return parts.length >= 2;
1466
+ }
1467
+ /**
1468
+ * Formats a value with dashes - intelligently splits based on structure
1469
+ *
1470
+ * Examples:
1471
+ * - "order_signed" -> "Order - Signed"
1472
+ * - "closed_lost" -> "Closed - Lost"
1473
+ * - "active_in_term" -> "Active - In Term"
1474
+ * - "on_hold_broker" -> "On Hold - Broker"
1475
+ */
1476
+ function formatWithDashes(value) {
1477
+ const parts = normalizeInput(value).split("_").filter((part) => part.length > 0);
1478
+ if (parts.length < 2) return snakeCaseToTitleCase(value);
1479
+ const firstPart = parts[0];
1480
+ const secondPart = parts[1];
1481
+ if (!firstPart) return snakeCaseToTitleCase(value);
1482
+ if (parts.length === 2) {
1483
+ if (!secondPart) return snakeCaseToTitleCase(value);
1484
+ return `${snakeCaseToTitleCase(firstPart)} - ${snakeCaseToTitleCase(secondPart)}`;
1485
+ }
1486
+ if (firstPart.length <= 3) return `${snakeCaseToTitleCase(firstPart)} - ${snakeCaseToTitleCase(parts.slice(1).join("_"))}`;
1487
+ if (secondPart && firstPart.length > secondPart.length) return `${snakeCaseToTitleCase(firstPart)} - ${snakeCaseToTitleCase(parts.slice(1).join("_"))}`;
1488
+ else return `${snakeCaseToTitleCase(parts.slice(0, 2).join("_"))} - ${snakeCaseToTitleCase(parts.slice(2).join("_"))}`;
1489
+ }
1490
+ /**
1491
+ * Converts an enum value to a display label
1492
+ * Uses intelligent formatting based on structure
1493
+ *
1494
+ * @param value - The enum value in snake_case (or any case)
1495
+ * @param customOverrides - Optional map of custom overrides for specific values
1496
+ * @returns The formatted display label
1497
+ */
1498
+ function enumToDisplayLabel(value, customOverrides) {
1499
+ if (!value || typeof value !== "string") return String(value || "");
1500
+ const normalized = normalizeInput(value);
1501
+ if (customOverrides) {
1502
+ if (customOverrides[value]) return customOverrides[value];
1503
+ if (customOverrides[normalized]) return customOverrides[normalized];
1504
+ }
1505
+ const parts = normalized.split("_").filter((part) => part.length > 0);
1506
+ if (parts.length === 0) return value;
1507
+ if (shouldUseDashes(parts)) return formatWithDashes(value);
1508
+ return snakeCaseToTitleCase(value);
1509
+ }
1510
+ /**
1511
+ * Creates a label map for an array of enum values
1512
+ *
1513
+ * @param enumValues - Array of enum values
1514
+ * @param customOverrides - Optional map of custom overrides for specific values
1515
+ * @returns Record mapping enum values to display labels
1516
+ */
1517
+ function createEnumLabelMap(enumValues, customOverrides) {
1518
+ const labelMap = {};
1519
+ for (const value of enumValues) labelMap[value] = enumToDisplayLabel(value, customOverrides);
1520
+ return labelMap;
1521
+ }
1522
+ /**
1523
+ * Converts an enum label map to select options for dropdowns
1524
+ *
1525
+ * @param labelMap - Record mapping enum values to display labels
1526
+ * @returns Array of select options
1527
+ */
1528
+ function createSelectOptionsFromLabelMap(labelMap) {
1529
+ return Object.entries(labelMap).map(([value, label]) => ({
1530
+ value,
1531
+ label
1532
+ }));
1533
+ }
1534
+
1535
+ //#endregion
1536
+ //#region src/utils/file.ts
1537
+ /**
1538
+ * File utility functions for sanitizing and processing file names.
1539
+ */
1540
+ /**
1541
+ * Sanitizes a file name by replacing non-alphanumeric characters with underscores.
1542
+ * Provides several improvements over basic sanitization:
1543
+ * - Converts dashes to underscores first (to avoid mixed dash/underscore patterns)
1544
+ * - Collapses multiple consecutive underscores
1545
+ * - Trims leading/trailing underscores
1546
+ * - Handles files without extensions properly
1547
+ * - Sanitizes the extension too
1548
+ * - Enforces 800-byte limit for R2 compatibility
1549
+ * - Provides fallback for completely invalid names
1550
+ *
1551
+ * This function enforces an 800-byte limit to comply with Cloudflare R2's 1,024 byte
1552
+ * object key limit (leaving room for path overhead).
1553
+ *
1554
+ * @param fileName - The original file name to sanitize
1555
+ * @returns The sanitized file name with underscores replacing non-alphanumeric characters
1556
+ *
1557
+ * @example
1558
+ * ```typescript
1559
+ * sanitizeFileName('Sun Life Sheet 2021-10-13.xls')
1560
+ * // Returns: 'Sun_Life_Sheet_2021_10_13.xls'
1561
+ *
1562
+ * sanitizeFileName('my-file@2023.pdf')
1563
+ * // Returns: 'my_file_2023.pdf'
1564
+ *
1565
+ * sanitizeFileName('document.txt')
1566
+ * // Returns: 'document.txt'
1567
+ *
1568
+ * sanitizeFileName('file---name.txt')
1569
+ * // Returns: 'file_name.txt' (collapsed underscores)
1570
+ *
1571
+ * sanitizeFileName('file-name_test.txt')
1572
+ * // Returns: 'file_name_test.txt' (dashes converted first, no mixed patterns)
1573
+ *
1574
+ * sanitizeFileName('!!!file.txt')
1575
+ * // Returns: 'file.txt' (trimmed underscores)
1576
+ *
1577
+ * sanitizeFileName('')
1578
+ * // Returns: 'unnamed_file' (fallback)
1579
+ * ```
1580
+ */
1581
+ function sanitizeFileName(fileName) {
1582
+ if (!fileName || fileName.trim() === "") return "unnamed_file";
1583
+ const lastDotIndex = fileName.lastIndexOf(".");
1584
+ const hasExtension = lastDotIndex > 0;
1585
+ let name = hasExtension ? fileName.substring(0, lastDotIndex) : fileName;
1586
+ let extension = hasExtension ? fileName.substring(lastDotIndex + 1) : "";
1587
+ const sanitize = (str) => str.replace(/-/g, "_").replace(/[^a-zA-Z0-9]/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "");
1588
+ name = sanitize(name);
1589
+ extension = sanitize(extension);
1590
+ if (!name) name = "file";
1591
+ const sanitized = extension ? `${name}.${extension}` : name;
1592
+ const encoder = new TextEncoder();
1593
+ if (encoder.encode(sanitized).length > 800) {
1594
+ const maxNameBytes = 800 - (extension ? encoder.encode(`.${extension}`).length : 0);
1595
+ let truncatedName = "";
1596
+ let currentBytes = 0;
1597
+ for (const char of name) {
1598
+ const charBytes = encoder.encode(char).length;
1599
+ if (currentBytes + charBytes > maxNameBytes) break;
1600
+ truncatedName += char;
1601
+ currentBytes += charBytes;
1602
+ }
1603
+ return extension ? `${truncatedName}.${extension}` : truncatedName;
1604
+ }
1605
+ return sanitized;
1606
+ }
1607
+
1608
+ //#endregion
1609
+ //#region src/utils/money.ts
1610
+ /**
1611
+ * Money calculation utilities using strings for storage and numbers for calculations.
1612
+ * All monetary values are stored as strings.
1613
+ */
1614
+ /**
1615
+ * Adds two or more monetary values
1616
+ * @param values Monetary values as strings
1617
+ * @returns Sum as string
1618
+ */
1619
+ const addMoney = (...values) => values.reduce((sum, value) => sum + Number(value), 0).toString();
1620
+ /**
1621
+ * Subtracts monetary values from the first value
1622
+ * @param minuend Value to subtract from
1623
+ * @param subtrahends Values to subtract
1624
+ * @returns Difference as string
1625
+ */
1626
+ const subtractMoney = (minuend, ...subtrahends) => subtrahends.reduce((diff, value) => diff - Number(value), Number(minuend)).toString();
1627
+ /**
1628
+ * Multiplies a monetary value by a number
1629
+ * @param amount Amount as string
1630
+ * @param multiplier Number to multiply by
1631
+ * @returns Product as string
1632
+ */
1633
+ const multiplyMoney = (amount, multiplier) => (Number(amount) * multiplier).toString();
1634
+ /**
1635
+ * Divides a monetary value by a number
1636
+ * @param amount Amount as string
1637
+ * @param divisor Number to divide by
1638
+ * @returns Quotient as string
1639
+ */
1640
+ const divideMoney = (amount, divisor) => (Number(amount) / divisor).toString();
1641
+ /**
1642
+ * Applies a percentage to a monetary value
1643
+ * @param amount Amount as string
1644
+ * @param percentage Percentage as decimal (e.g., 0.15 for 15%)
1645
+ * @returns Result as string
1646
+ */
1647
+ const applyPercentage = (amount, percentage) => multiplyMoney(amount, 1 + percentage);
1648
+ /**
1649
+ * Rounds a monetary value
1650
+ * @param amount Amount to round
1651
+ * @returns Rounded amount as string
1652
+ */
1653
+ const roundMoney = (amount) => (Math.round(Number(amount) * 100) / 100).toString();
1654
+ /**
1655
+ * Formats a number as currency
1656
+ * @param value Number to format
1657
+ * @returns Formatted currency string
1658
+ */
1659
+ const formatCurrency = (value) => {
1660
+ return new Intl.NumberFormat("en-US", {
1661
+ style: "currency",
1662
+ currency: "USD"
1663
+ }).format(value);
1664
+ };
1665
+ /**
1666
+ * Formats a dollar amount as currency
1667
+ * @param amount Amount as string
1668
+ * @returns Formatted currency string
1669
+ */
1670
+ const formatDollar = (amount) => {
1671
+ if (amount === null || amount === void 0) return "$0.00";
1672
+ return new Intl.NumberFormat("en-US", {
1673
+ style: "currency",
1674
+ currency: "USD"
1675
+ }).format(Number(amount));
1676
+ };
1677
+
1678
+ //#endregion
1679
+ export { AddCreditsSchema, ApproveSupportTicketSchema, AttachmentCreateSchema, AttachmentFiltersSchema, AttachmentFiltersSchema as attachmentFilters_zod, AttachmentFolderCreateSchema, AttachmentFolderReadSchema, AttachmentFolderUpdateSchema, AttachmentPageSchema, AttachmentPageSchema as attachmentPage_zod, AttachmentReadSchema, AttachmentReadSchema as attachment_zod, AttachmentUpdateSchema, BaseFilterSchema, BooleanFilterSchema, CancelInternalTaskSchema, CompleteSupportTicketSchema, CreditBalanceSchema, CreditTransactionFiltersSchema, CreditTransactionPageSchema, CreditTransactionReadSchema, CreditTransactionTypeEnum, CustomerSupportTicketCreateSchema, CustomerSupportTicketFiltersSchema, CustomerSupportTicketPageSchema, CustomerSupportTicketReadSchema, CustomerSupportTicketUpdateSchema, DECIMAL_AMOUNT_2_DECIMALS_OR_EMPTY_ERROR_MESSAGE, DECIMAL_AMOUNT_2_DECIMALS_OR_EMPTY_REGEX, DECIMAL_AMOUNT_ERROR_MESSAGE, DECIMAL_AMOUNT_REGEX, DataTypeSchema, DateFilterSchema, DateOperatorSchema, DeleteSupportTicketSchema, EmailEntrySchema, EqualityArrayOperatorSchema, EqualityOperatorSchema, FilterConfigSchema, NoteCreateSchema, NoteFiltersSchema, NoteFiltersSortDirectionEnum, NoteReadSchema, NoteUpdateSchema, NumberFilterSchema, NumberOperatorSchema, OPERATORS, PageInfoSchema, PaginationFiltersSchema, ReactivateInternalTaskSchema, ReadNotificationEmailsSchema, RecordConst, RecordTypeValues, RejectSupportTicketSchema, ResetMonthlyBalanceSchema, RevertSupportTicketSchema, SetMonthlyAllocationSchema, SortDirectionSchema, StaffSupportTicketCreateSchema, StaffSupportTicketFiltersSchema, StaffSupportTicketInputSchema, StaffSupportTicketPageSchema, StaffSupportTicketReadSchema, StaffSupportTicketUpdateSchema, StringFilterSchema, StringOperatorSchema, SupportTicketApprovalEnum, SupportTicketApprovalFilterSchema, SupportTicketApprovalSchema, SupportTicketDevLifecycleEnum, SupportTicketDevLifecycleFilterSchema, SupportTicketDevLifecycleSchema, SupportTicketDevLifecycleUpdateEnum, SupportTicketDevLifecycleUpdateSchema, SupportTicketPriorityEnum, SupportTicketPriorityFilterSchema, SupportTicketPrioritySchema, SupportTicketRecordDataSchema, SupportTicketStatusEnum, SupportTicketStatusFilterSchema, SupportTicketStatusSchema, SupportTicketTypeEnum, SupportTicketTypeFilterSchema, SupportTicketTypeSchema, TeamCreateSchema, TeamFiltersSchema, TeamInputBaseSchema, TeamMemberCreateSchema, TeamMemberFiltersSchema, TeamMemberOptionSchema, TeamMemberReadSchema, TeamMemberRoleEnum, TeamMemberRoleFilterSchema, TeamMemberRoleSchema, TeamMemberUpdateSchema, TeamOptionSchema, TeamPageSchema, TeamReadSchema, TeamStatusSchema, TeamUpdateSchema, USER_TYPES, UpdateNotificationEmailsSchema, UserProfileBaseSchema, UserProfileReadSchema, UserProfileUpdateSchema, UserReadSchema, UserTeamMembersSchema, UserTeamsSchema, UserTypeEnum, UserUpdateSchema, addMoney, applyPercentage, changePasswordSchema, createEnumFilter, createEnumLabelMap, createNewEmailEntry, createPaginatedSchema, createSelectOptionsFromLabelMap, createUserSchema, createUserSchemaOutput, divideMoney, enumToDisplayLabel, forgot_password_zod, formatCurrency, formatDollar, isCommonPassword, loginResponseSchema, loginSchema, multiplyMoney, pageInfoSchema, passwordSchema, recordVersionFiltersBreadcrumbSchema, recordVersionFiltersInputBreadcrumbSchema, recordVersionFiltersInputSchema, recordVersionFiltersSchema, recordVersionPageBreadcrumbSchema, recordVersionPageSchema, recordVersionSchema, resetPasswordInputSchema, resetPasswordSchema, roundMoney, sanitizeFileName, signupSchema, subtractMoney, userSessionSchema };
1680
+ //# sourceMappingURL=index.mjs.map