@updraft-solutions/mcrit-sdk 0.3.1 → 0.5.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.
Files changed (49) hide show
  1. package/dist/chunk-AKLFMP7G.cjs +2525 -0
  2. package/dist/{chunk-MF7G76QS.cjs → chunk-IVMOR5SW.cjs} +26 -1
  3. package/dist/{chunk-D5DUVW34.js → chunk-KIMCBEY7.js} +26 -1
  4. package/dist/chunk-YCIXOVEC.js +2525 -0
  5. package/dist/format/index.cjs +2 -2
  6. package/dist/format/index.d.cts +13 -4
  7. package/dist/format/index.d.ts +13 -4
  8. package/dist/format/index.js +1 -1
  9. package/dist/index.cjs +201 -5
  10. package/dist/index.d.cts +3 -3
  11. package/dist/index.d.ts +3 -3
  12. package/dist/index.js +208 -12
  13. package/dist/schemas/index.cjs +198 -2
  14. package/dist/schemas/index.d.cts +3157 -128
  15. package/dist/schemas/index.d.ts +3157 -128
  16. package/dist/schemas/index.js +201 -5
  17. package/dist/{models-BWmpcfR8.d.cts → training-Der1DPU-.d.cts} +23757 -6528
  18. package/dist/{models-BWmpcfR8.d.ts → training-Der1DPU-.d.ts} +23757 -6528
  19. package/dist/types/index.cjs +1 -1
  20. package/dist/types/index.d.cts +277 -6
  21. package/dist/types/index.d.ts +277 -6
  22. package/dist/types/index.js +1 -1
  23. package/package.json +1 -1
  24. package/src/format/index.ts +51 -5
  25. package/src/index.ts +39 -0
  26. package/src/schemas/common.ts +37 -0
  27. package/src/schemas/course-milestone.ts +78 -0
  28. package/src/schemas/equipment.ts +27 -0
  29. package/src/schemas/form-template.ts +413 -0
  30. package/src/schemas/index.ts +7 -1
  31. package/src/schemas/location-map.ts +175 -0
  32. package/src/schemas/models.ts +110 -28
  33. package/src/schemas/newsletter.ts +362 -0
  34. package/src/schemas/todo.ts +46 -0
  35. package/src/schemas/training.ts +500 -0
  36. package/src/types/ai.ts +85 -0
  37. package/src/types/aircraft-block.ts +23 -0
  38. package/src/types/api.ts +3 -1
  39. package/src/types/background-task.ts +28 -0
  40. package/src/types/compliance.ts +27 -3
  41. package/src/types/index.ts +5 -0
  42. package/src/types/landing-fee.ts +92 -0
  43. package/src/types/models.ts +44 -0
  44. package/src/types/roles.ts +26 -0
  45. package/dist/chunk-7QF3OJ45.cjs +0 -1325
  46. package/dist/chunk-FS5I4MBG.js +0 -1325
  47. package/src/schemas/fee.ts +0 -37
  48. /package/dist/{chunk-LXNCAKJZ.js → chunk-2WJHTRIE.js} +0 -0
  49. /package/dist/{chunk-MOOGM3DW.cjs → chunk-DCEOET63.cjs} +0 -0
@@ -11,6 +11,8 @@ import {
11
11
  zNetGross,
12
12
  zPhpMoneyObject,
13
13
  } from "./common";
14
+ import { equipmentSchema } from "./equipment";
15
+ import { courseCheckSyllabusSchema } from "./training";
14
16
 
15
17
  export const baseMembershipSchema = z.object({
16
18
  type: z.literal("membership"),
@@ -88,33 +90,58 @@ export const feeSchema = BaseModel.extend({
88
90
  id: z.number(),
89
91
  price: priceSchema.optional(),
90
92
  prices: z.array(priceSchema).optional(),
91
- provider: z.string().optional(),
93
+ provider: z.string().nullable().optional(),
92
94
  company_id: z.number().optional(),
93
95
  company: companySchema.optional(),
94
96
  ident: z.string().optional(),
95
97
  category: z.string().optional(),
96
- subcategory: z.string().optional(),
98
+ subcategory: z.string().nullable().optional(),
97
99
  title: z.string().optional(),
100
+ internal_title: z.string().nullable().optional(),
101
+ // internal_title falling back to title, resolved server-side.
102
+ display_title: z.string().optional(),
103
+ // Course-derived fees (source course + which course fee spawned them).
104
+ source_course_id: z.number().nullable().optional(),
105
+ source_key: z.string().nullable().optional(),
106
+ is_derived: z.boolean().optional(),
98
107
  relation: z
99
108
  .array(z.enum(["booking", "flightLog", "invoice", "instructor"]))
109
+ .nullable()
100
110
  .optional(),
101
- payment_per: z.string().optional(),
102
- can: z
103
- .object({
104
- update: zNullableBool,
105
- delete: zNullableBool,
106
- })
107
- .optional(),
111
+ payment_per: z.string().nullable().optional(),
112
+ can: z.record(z.string(), zNullableBool).optional(),
108
113
  course_count: z.number().optional(),
114
+ // feeables-pivot fields, present when the fee is serialized through an
115
+ // attachment (course/enrollment listings).
109
116
  count: z.number().optional(),
110
- variant: z.string().optional(),
117
+ variant: z.string().nullable().optional(),
111
118
  is_optional: z.boolean().optional(),
119
+ sort_order: z.number().nullable().optional(),
120
+ is_initial_payment: z.boolean().optional(),
121
+ feeables: z
122
+ .array(
123
+ z.object({
124
+ feeable_id: z.number(),
125
+ feeable_type: z.string(),
126
+ count: z.number().nullable(),
127
+ variant: z.string().nullable(),
128
+ is_optional: z.boolean().nullable(),
129
+ is_initial_payment: z.boolean().nullable(),
130
+ sort_order: z.number().nullable(),
131
+ }),
132
+ )
133
+ .optional(),
112
134
  input_value: z.number().optional(),
113
135
  net_gross: zNetGross,
114
136
  tax_percentage: z.string().optional(),
115
137
  valid_from: z.string(),
116
138
  });
117
139
 
140
+ export const feeCategoryOptionsSchema = z.object({
141
+ categories: z.array(z.string()),
142
+ subcategories: z.record(z.string(), z.array(z.string())),
143
+ });
144
+
118
145
  export const courseFeeSchema = feeSchema.extend({
119
146
  count: z.number().optional(),
120
147
  variant: z.string().optional(),
@@ -265,11 +292,19 @@ export const userQualificationSchema = z.object({
265
292
  });
266
293
 
267
294
  export const airportSchema = z.object({
268
- type: z.literal("airport"),
295
+ // Aligned with what AirportResource actually serializes: `type` and
296
+ // `country` are absent on minified payloads (e.g. booking waypoint
297
+ // airports); coordinates + municipality/iso_country ride along on the
298
+ // full shape.
299
+ type: z.literal("airport").optional(),
269
300
  id: z.number(),
270
301
  ident: z.string(),
271
302
  name: z.string(),
272
- country: z.string(),
303
+ country: z.string().optional(),
304
+ municipality: z.string().nullable().optional(),
305
+ iso_country: z.string().nullable().optional(),
306
+ latitude_deg: z.number().nullable().optional(),
307
+ longitude_deg: z.number().nullable().optional(),
273
308
  });
274
309
 
275
310
  export const recurringFlightSchema = z.object({
@@ -340,7 +375,7 @@ export const extendedAirplaneSchema = airplaneSchema.extend({
340
375
  VFRNight: z.boolean().optional(),
341
376
  IFR: z.boolean().optional(),
342
377
  IFRKnownIcing: z.boolean().optional(),
343
- equipment: z.array(z.any()).optional(),
378
+ equipment: z.array(equipmentSchema).optional(),
344
379
  contact: contactAttributeSchema.optional(),
345
380
  hints: z
346
381
  .object({
@@ -387,6 +422,26 @@ export const availSchema = z.object({
387
422
  .optional(),
388
423
  });
389
424
 
425
+ // Ordered via-airport on a booking's route (BookingResource `waypoints`).
426
+ // Airports arrive minified (no `type` literal), hence the dedicated shape.
427
+ // Distinct from the nav-fix `waypointSchema` further down — do not merge.
428
+ export const bookingWaypointSchema = z.object({
429
+ sequence: z.number(),
430
+ airport_id: z.number(),
431
+ airport: z
432
+ .object({
433
+ id: z.number(),
434
+ ident: z.string(),
435
+ name: z.string(),
436
+ municipality: z.string().nullable().optional(),
437
+ iso_country: z.string().nullable().optional(),
438
+ latitude_deg: z.number().nullable().optional(),
439
+ longitude_deg: z.number().nullable().optional(),
440
+ })
441
+ .nullable()
442
+ .optional(),
443
+ });
444
+
390
445
  export const bookingSchema = BaseModel.extend({
391
446
  type: z.literal("booking"),
392
447
  ifr: z.boolean().default(false),
@@ -403,6 +458,15 @@ export const bookingSchema = BaseModel.extend({
403
458
  destination_airport_id: z.number().optional(),
404
459
  departure_airport: airportSchema.optional(),
405
460
  destination_airport: airportSchema.optional(),
461
+ waypoints: z.array(bookingWaypointSchema).optional(),
462
+ // Flat, always-present home timezone on BookingResource.
463
+ company_timezone: z.string().nullable().optional(),
464
+ // Slim per-booking training-record status, present when the backend
465
+ // eager-loaded `trainingRecord` (enrollment bookings surfaces).
466
+ training_record_status: z
467
+ .enum(["draft", "awaiting_signatures", "signed"])
468
+ .nullable()
469
+ .optional(),
406
470
  next_booking_id: z.number().nullable().optional(),
407
471
  previous_booking_id: z.number().nullable().optional(),
408
472
  maintenance: z.number().optional(),
@@ -797,6 +861,15 @@ export const expenseSchema = z.object({
797
861
  .optional(),
798
862
  });
799
863
 
864
+ // Company-scoped tag (shared pool across taggable models — see
865
+ // m-crit-api docs/features/tags.md).
866
+ export const tagSchema = z.object({
867
+ type: z.literal("tag").optional(),
868
+ id: z.number(),
869
+ name: z.string(),
870
+ color: z.string().nullable().optional(),
871
+ });
872
+
800
873
  export const courseSchema = z.object({
801
874
  type: z.literal("course"),
802
875
  id: z.number(),
@@ -813,15 +886,25 @@ export const courseSchema = z.object({
813
886
  aircraft_required: z.boolean().default(false),
814
887
  qualification_id: z.number().nullable().optional(),
815
888
  qualification: qualificationSchema.optional(),
889
+ is_archived: z.boolean().optional(),
890
+ archived_at: z.string().nullable().optional(),
891
+ category: z.string().nullable().optional(),
892
+ title: z.string().nullable().optional(),
893
+ subtitle: z.string().nullable().optional(),
894
+ content: z.string().nullable().optional(),
895
+ footer: z.string().nullable().optional(),
896
+ // Syllabus assignment pointers: `syllabus_id` drives the Übersicht,
897
+ // `booking_form_syllabus_id` the per-flight Ausbildungsnachweis.
898
+ syllabus_id: z.number().nullable().optional(),
899
+ booking_form_syllabus_id: z.number().nullable().optional(),
900
+ // Present only when the backend eager-loaded `checkSyllabi`.
901
+ check_syllabi: z.array(courseCheckSyllabusSchema).optional(),
902
+ tags: z.array(tagSchema).optional(),
816
903
  created_at: z.string(),
817
904
  updated_at: z.string(),
818
- can: z
819
- .object({
820
- update: zNullableBool,
821
- delete: zNullableBool,
822
- enroll: zNullableBool,
823
- })
824
- .optional(),
905
+ // Loose record on purpose: gates are policy-reflected server-side and
906
+ // grow over time (useAssistant, …) — same convention as companySchema.
907
+ can: z.record(z.string(), zNullableBool).optional(),
825
908
  });
826
909
 
827
910
  /**
@@ -854,20 +937,19 @@ export const courseEnrollmentSchema = z.object({
854
937
  comments: z.string().nullable().optional(),
855
938
  initial_payment_generated: z.boolean().optional(),
856
939
  initial_payment_removable: z.boolean().optional(),
940
+ // Element-driven training progress (see m-crit-api
941
+ // docs/features/training-tracking.md).
942
+ tracked_training_elements_count: z.number().optional(),
943
+ progress_source: z.enum(["training_elements", "milestones"]).optional(),
857
944
  metrics: courseEnrollmentMetricsSchema.optional(),
858
945
  created_at: z.string(),
859
946
  updated_at: z.string(),
860
947
  course: courseSchema.optional(),
861
948
  membership: membershipSchema.optional(),
862
949
  mentor: userSchema.optional(),
863
- can: z
864
- .object({
865
- view: zNullableBool,
866
- update: zNullableBool,
867
- delete: zNullableBool,
868
- complete: zNullableBool,
869
- })
870
- .optional(),
950
+ // Loose record on purpose: gates are policy-reflected server-side and
951
+ // grow over time (updateTraining, manageInitialPayment, …).
952
+ can: z.record(z.string(), zNullableBool).optional(),
871
953
  });
872
954
 
873
955
  export const eventSchema = z.object({
@@ -0,0 +1,362 @@
1
+ import { z } from "zod";
2
+
3
+ /*
4
+ |------------------------------------------------------------------------
5
+ | Block schemas
6
+ |------------------------------------------------------------------------
7
+ |
8
+ | v1 supports heading / paragraph / button / divider. Paragraph content
9
+ | is stored as TipTap JSON on `doc` so variable tokens survive
10
+ | sanitisation. The server converts `variable` nodes to `{{name}}` at
11
+ | render time.
12
+ */
13
+
14
+ export enum NewsletterBlockType {
15
+ Heading = "heading",
16
+ Paragraph = "paragraph",
17
+ Button = "button",
18
+ Divider = "divider",
19
+ }
20
+
21
+ export const TipTapNodeSchema: z.ZodType<TipTapNode> = z.lazy(() =>
22
+ z.object({
23
+ type: z.string(),
24
+ attrs: z.record(z.any()).optional(),
25
+ content: z.array(TipTapNodeSchema).optional(),
26
+ marks: z.array(z.record(z.any())).optional(),
27
+ text: z.string().optional(),
28
+ }),
29
+ );
30
+
31
+ export interface TipTapNode {
32
+ type: string;
33
+ attrs?: Record<string, unknown>;
34
+ content?: TipTapNode[];
35
+ marks?: Array<Record<string, unknown>>;
36
+ text?: string;
37
+ }
38
+
39
+ const Align = z.enum(["left", "center", "right"]).optional();
40
+
41
+ export const HeadingBlockSchema = z.object({
42
+ key: z.string(),
43
+ type: z.literal(NewsletterBlockType.Heading),
44
+ level: z.number().int().min(1).max(3).default(1),
45
+ text: z.string(),
46
+ align: Align,
47
+ });
48
+
49
+ export const ParagraphBlockSchema = z.object({
50
+ key: z.string(),
51
+ type: z.literal(NewsletterBlockType.Paragraph),
52
+ doc: TipTapNodeSchema,
53
+ align: Align,
54
+ });
55
+
56
+ export const ButtonBlockSchema = z.object({
57
+ key: z.string(),
58
+ type: z.literal(NewsletterBlockType.Button),
59
+ label: z.string(),
60
+ url: z.string(),
61
+ align: Align,
62
+ color: z.string().optional(),
63
+ });
64
+
65
+ export const DividerBlockSchema = z.object({
66
+ key: z.string(),
67
+ type: z.literal(NewsletterBlockType.Divider),
68
+ thickness: z.number().int().min(1).max(10).optional(),
69
+ color: z.string().optional(),
70
+ });
71
+
72
+ export const NewsletterBlockSchema = z.discriminatedUnion("type", [
73
+ HeadingBlockSchema,
74
+ ParagraphBlockSchema,
75
+ ButtonBlockSchema,
76
+ DividerBlockSchema,
77
+ ]);
78
+
79
+ export type HeadingBlock = z.infer<typeof HeadingBlockSchema>;
80
+ export type ParagraphBlock = z.infer<typeof ParagraphBlockSchema>;
81
+ export type ButtonBlock = z.infer<typeof ButtonBlockSchema>;
82
+ export type DividerBlock = z.infer<typeof DividerBlockSchema>;
83
+ export type NewsletterBlock = z.infer<typeof NewsletterBlockSchema>;
84
+
85
+ /*
86
+ |------------------------------------------------------------------------
87
+ | Category
88
+ |------------------------------------------------------------------------
89
+ */
90
+
91
+ export const NewsletterCategorySchema = z.object({
92
+ type: z.literal("newsletter_category").optional(),
93
+ id: z.number(),
94
+ key: z.string(),
95
+ name: z.string(),
96
+ description: z.string().nullable().optional(),
97
+ is_mandatory: z.boolean(),
98
+ is_active: z.boolean(),
99
+ sort_order: z.number().int(),
100
+ created_at: z.string().optional(),
101
+ updated_at: z.string().optional(),
102
+ });
103
+
104
+ export type NewsletterCategory = z.infer<typeof NewsletterCategorySchema>;
105
+
106
+ /*
107
+ |------------------------------------------------------------------------
108
+ | Template
109
+ |------------------------------------------------------------------------
110
+ */
111
+
112
+ const UserBasicSchema = z
113
+ .object({
114
+ id: z.number(),
115
+ name: z.string(),
116
+ email: z.string().optional(),
117
+ })
118
+ .nullable();
119
+
120
+ export const NewsletterTemplateSchemaSchema = z.object({
121
+ blocks: z.array(NewsletterBlockSchema),
122
+ variables_used: z.array(z.string()).optional(),
123
+ });
124
+
125
+ export type NewsletterTemplateDoc = z.infer<typeof NewsletterTemplateSchemaSchema>;
126
+
127
+ export const NewsletterTemplateSchema = z.object({
128
+ type: z.literal("newsletter_template").optional(),
129
+ id: z.number(),
130
+ company_id: z.number().nullable(),
131
+ is_system: z.boolean().optional(),
132
+ newsletter_category_id: z.number(),
133
+ category: NewsletterCategorySchema.nullish(),
134
+ name: z.string(),
135
+ slug: z.string(),
136
+ description: z.string().nullable().optional(),
137
+ subject: z.string(),
138
+ preheader: z.string().nullable().optional(),
139
+ schema: NewsletterTemplateSchemaSchema,
140
+ schema_version: z.number().int(),
141
+ is_active: z.boolean(),
142
+ created_by: z.number().nullable().optional(),
143
+ creator: UserBasicSchema.optional(),
144
+ created_at: z.string().optional(),
145
+ updated_at: z.string().optional(),
146
+ });
147
+
148
+ export type NewsletterTemplate = z.infer<typeof NewsletterTemplateSchema>;
149
+
150
+ export const CreateNewsletterTemplateInputSchema = z.object({
151
+ newsletter_category_id: z.number(),
152
+ name: z.string().min(1),
153
+ slug: z.string().optional(),
154
+ description: z.string().nullable().optional(),
155
+ subject: z.string().min(1),
156
+ preheader: z.string().nullable().optional(),
157
+ schema: NewsletterTemplateSchemaSchema,
158
+ is_active: z.boolean().optional(),
159
+ });
160
+
161
+ export type CreateNewsletterTemplateInput = z.infer<typeof CreateNewsletterTemplateInputSchema>;
162
+ export type UpdateNewsletterTemplateInput = Partial<CreateNewsletterTemplateInput>;
163
+
164
+ /*
165
+ |------------------------------------------------------------------------
166
+ | Audience
167
+ |------------------------------------------------------------------------
168
+ */
169
+
170
+ export enum AudienceBucketType {
171
+ AllUsers = "all_users",
172
+ CompanyUsers = "company_users",
173
+ RoleMembers = "role_members",
174
+ }
175
+
176
+ export const AllUsersBucketSchema = z.object({ type: z.literal(AudienceBucketType.AllUsers) });
177
+ export const CompanyUsersBucketSchema = z.object({
178
+ type: z.literal(AudienceBucketType.CompanyUsers),
179
+ company_ids: z.array(z.number()).min(1),
180
+ });
181
+ export const RoleMembersBucketSchema = z.object({
182
+ type: z.literal(AudienceBucketType.RoleMembers),
183
+ role_ids: z.array(z.number()).min(1),
184
+ });
185
+
186
+ export const AudienceBucketSchema = z.discriminatedUnion("type", [
187
+ AllUsersBucketSchema,
188
+ CompanyUsersBucketSchema,
189
+ RoleMembersBucketSchema,
190
+ ]);
191
+
192
+ export const NewsletterAudienceSchema = z.object({
193
+ version: z.literal(1),
194
+ include: z.array(AudienceBucketSchema).min(1),
195
+ exclude: z
196
+ .object({
197
+ emails: z.array(z.string().email()).optional(),
198
+ user_ids: z.array(z.number()).optional(),
199
+ })
200
+ .optional(),
201
+ });
202
+
203
+ export type AudienceBucket = z.infer<typeof AudienceBucketSchema>;
204
+ export type NewsletterAudience = z.infer<typeof NewsletterAudienceSchema>;
205
+
206
+ /**
207
+ * Company-admin audience rule: the role selection is the sole determinant.
208
+ * No roles selected → the whole company (company_users). One or more roles
209
+ * → only those role-holders (role_members), and the company-wide bucket is
210
+ * dropped so the send-time union (ResolveCampaignAudienceAction) cannot
211
+ * widen the audience back to everyone.
212
+ */
213
+ export function companyAudienceInclude(
214
+ companyId: number,
215
+ roleIds: number[],
216
+ ): AudienceBucket[] {
217
+ return roleIds.length > 0
218
+ ? [{ type: AudienceBucketType.RoleMembers, role_ids: roleIds }]
219
+ : [{ type: AudienceBucketType.CompanyUsers, company_ids: [companyId] }];
220
+ }
221
+
222
+ /*
223
+ |------------------------------------------------------------------------
224
+ | Campaign
225
+ |------------------------------------------------------------------------
226
+ */
227
+
228
+ export enum NewsletterCampaignStatus {
229
+ Draft = "draft",
230
+ Scheduled = "scheduled",
231
+ Sending = "sending",
232
+ Sent = "sent",
233
+ Failed = "failed",
234
+ Cancelled = "cancelled",
235
+ }
236
+
237
+ export const NewsletterCampaignSchema = z.object({
238
+ type: z.literal("newsletter_campaign").optional(),
239
+ id: z.number(),
240
+ company_id: z.number().nullable(),
241
+ is_system: z.boolean().optional(),
242
+ newsletter_template_id: z.number().nullable(),
243
+ template: NewsletterTemplateSchema.partial().nullish(),
244
+ newsletter_category_id: z.number(),
245
+ category: NewsletterCategorySchema.nullish(),
246
+ name: z.string(),
247
+ subject: z.string(),
248
+ preheader: z.string().nullable().optional(),
249
+ schema_snapshot: NewsletterTemplateSchemaSchema,
250
+ variables: z.record(z.string()).nullable().optional(),
251
+ audience_definition: NewsletterAudienceSchema,
252
+ status: z.nativeEnum(NewsletterCampaignStatus),
253
+ scheduled_at: z.string().nullable().optional(),
254
+ started_at: z.string().nullable().optional(),
255
+ finished_at: z.string().nullable().optional(),
256
+ cancelled_at: z.string().nullable().optional(),
257
+ counts: z.object({
258
+ queued: z.number().nullable(),
259
+ sent: z.number(),
260
+ failed: z.number(),
261
+ bounced: z.number(),
262
+ complained: z.number(),
263
+ }),
264
+ sent_by_user_id: z.number().nullable().optional(),
265
+ sent_by: UserBasicSchema.optional(),
266
+ can: z
267
+ .object({
268
+ update: z.boolean(),
269
+ schedule: z.boolean(),
270
+ send: z.boolean(),
271
+ cancel: z.boolean(),
272
+ test_send: z.boolean(),
273
+ delete: z.boolean(),
274
+ })
275
+ .partial()
276
+ .optional(),
277
+ created_at: z.string().optional(),
278
+ updated_at: z.string().optional(),
279
+ });
280
+
281
+ export type NewsletterCampaign = z.infer<typeof NewsletterCampaignSchema>;
282
+
283
+ export const CreateNewsletterCampaignInputSchema = z.object({
284
+ newsletter_template_id: z.number().nullable().optional(),
285
+ newsletter_category_id: z.number().optional(),
286
+ name: z.string().min(1),
287
+ subject: z.string().optional(),
288
+ preheader: z.string().nullable().optional(),
289
+ schema_snapshot: NewsletterTemplateSchemaSchema.optional(),
290
+ variables: z.record(z.string()).optional(),
291
+ audience_definition: NewsletterAudienceSchema,
292
+ });
293
+
294
+ export type CreateNewsletterCampaignInput = z.infer<typeof CreateNewsletterCampaignInputSchema>;
295
+ export type UpdateNewsletterCampaignInput = Partial<CreateNewsletterCampaignInput>;
296
+
297
+ /*
298
+ |------------------------------------------------------------------------
299
+ | Recipient
300
+ |------------------------------------------------------------------------
301
+ */
302
+
303
+ export enum NewsletterRecipientStatus {
304
+ Queued = "queued",
305
+ Sent = "sent",
306
+ Failed = "failed",
307
+ Bounced = "bounced",
308
+ Complained = "complained",
309
+ Suppressed = "suppressed",
310
+ OptedOut = "opted_out",
311
+ }
312
+
313
+ export const NewsletterCampaignRecipientSchema = z.object({
314
+ type: z.literal("newsletter_campaign_recipient").optional(),
315
+ id: z.number(),
316
+ newsletter_campaign_id: z.number(),
317
+ user_id: z.number().nullable(),
318
+ user: UserBasicSchema.optional(),
319
+ email: z.string(),
320
+ status: z.nativeEnum(NewsletterRecipientStatus),
321
+ error: z.string().nullable().optional(),
322
+ message_id: z.string().nullable().optional(),
323
+ queued_at: z.string().nullable().optional(),
324
+ sent_at: z.string().nullable().optional(),
325
+ bounced_at: z.string().nullable().optional(),
326
+ complained_at: z.string().nullable().optional(),
327
+ });
328
+
329
+ export type NewsletterCampaignRecipient = z.infer<typeof NewsletterCampaignRecipientSchema>;
330
+
331
+ /*
332
+ |------------------------------------------------------------------------
333
+ | Preference (per-user per-category)
334
+ |------------------------------------------------------------------------
335
+ */
336
+
337
+ export const NewsletterPreferenceRowSchema = z.object({
338
+ category_id: z.number(),
339
+ category_key: z.string(),
340
+ name: z.string(),
341
+ description: z.string().nullable().optional(),
342
+ is_mandatory: z.boolean(),
343
+ opted_in: z.boolean(),
344
+ });
345
+
346
+ export type NewsletterPreferenceRow = z.infer<typeof NewsletterPreferenceRowSchema>;
347
+
348
+ /*
349
+ |------------------------------------------------------------------------
350
+ | Variable allowlist (must mirror backend NewsletterBlocksValidator)
351
+ |------------------------------------------------------------------------
352
+ */
353
+
354
+ export const ALLOWED_NEWSLETTER_VARIABLES = [
355
+ "user.name",
356
+ "user.first_name",
357
+ "user.email",
358
+ "company.name",
359
+ "unsubscribe_url",
360
+ ] as const;
361
+
362
+ export type AllowedNewsletterVariable = (typeof ALLOWED_NEWSLETTER_VARIABLES)[number];
@@ -0,0 +1,46 @@
1
+ import { z } from "zod";
2
+
3
+ export const TodoActionSchema = z.object({
4
+ type: z.enum(["open_dialog", "navigate"]),
5
+ target: z.string(),
6
+ params: z.record(z.any()).optional(),
7
+ });
8
+
9
+ export const TodoKey = z.enum([
10
+ "fill_form",
11
+ "sign_form",
12
+ "review_form",
13
+ "log_booking",
14
+ "add_address",
15
+ "add_required_documents",
16
+ "fill_training_record",
17
+ "sign_training_record",
18
+ ]);
19
+
20
+ export const TodoSchema = z.object({
21
+ key: TodoKey,
22
+ ref_type: z.string(),
23
+ ref_id: z.number(),
24
+ title: z.string(),
25
+ subtitle: z.string().nullable().optional(),
26
+ due_date: z.string().nullable().optional(),
27
+ is_overdue: z.boolean().default(false),
28
+ priority: z.enum(["low", "normal", "high"]).default("normal"),
29
+ company_id: z.number().nullable().optional(),
30
+ action: TodoActionSchema,
31
+ meta: z.record(z.any()).optional(),
32
+ });
33
+
34
+ export type Todo = z.infer<typeof TodoSchema>;
35
+ export type TodoAction = z.infer<typeof TodoActionSchema>;
36
+
37
+ /**
38
+ * Shape of one entry in `meta.accepted_alternatives` on
39
+ * `add_required_documents` todos — the other templates of the requirement's
40
+ * OR bucket that would satisfy it equally (backend:
41
+ * MissingDocumentTodoSource::selectOrAlternative()).
42
+ */
43
+ export interface TodoAcceptedAlternative {
44
+ form_template_id: number;
45
+ form_template_name: string | null;
46
+ }