@updraft-solutions/mcrit-sdk 0.14.12 → 0.14.13

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.
@@ -1,14 +1,18 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
  import {
3
3
  fboActivitySchema,
4
+ fboCompanyGrantSchema,
4
5
  fboCompanyMemberSchema,
6
+ fboCompanySchema,
5
7
  fboDutyAffectedSchema,
6
8
  fboDutySchema,
7
9
  fboDutySeriesResponseSchema,
8
10
  fboFlightDocumentSchema,
9
11
  fboFlightSchema,
10
12
  fboFlightStatusSchema,
13
+ fboGrantAbilitySchema,
11
14
  fboMaterialiseResultSchema,
15
+ fboMetaSchema,
12
16
  fboOccupationSchema,
13
17
  fboOperationsSchema,
14
18
  fboPersonSchema,
@@ -473,3 +477,115 @@ describe("fboFlightSchema — POB, documents and the customs mail stamp", () =>
473
477
  expect(fboFlightSchema.safeParse(bad).success).toBe(false);
474
478
  });
475
479
  });
480
+
481
+ /**
482
+ * Fixtures copied from `CompanyGrantResource`, the FBO meta endpoint and the
483
+ * attached-companies index, so a schema that drifts from the payload fails
484
+ * here rather than at runtime in the app.
485
+ */
486
+ const grantPayload = {
487
+ id: 12,
488
+ grantee: { id: 34, name: "Alpha Handling" },
489
+ target: { id: 35, name: "Bravo Charter" },
490
+ ability: "duties.view",
491
+ ability_label: "Dienstpläne einsehen",
492
+ granted_by: { id: 7, name: "Erika Muster" },
493
+ created_at: "2026-08-29T09:00:00.000000Z",
494
+ can: { delete: true },
495
+ };
496
+
497
+ describe("fboCompanyGrantSchema", () => {
498
+ it("parses a grant exactly as the register emits it", () => {
499
+ expect(fboCompanyGrantSchema.parse(grantPayload).ability).toBe("duties.view");
500
+ });
501
+
502
+ it("parses a migrated grant nobody signed — granted_by is null", () => {
503
+ expect(fboCompanyGrantSchema.parse({ ...grantPayload, granted_by: null }).granted_by).toBeNull();
504
+ });
505
+
506
+ it("parses a side whose company left the FBO domain — the ref is null, not missing", () => {
507
+ expect(fboCompanyGrantSchema.parse({ ...grantPayload, target: null }).target).toBeNull();
508
+ });
509
+
510
+ it("rejects an ability outside the register's vocabulary", () => {
511
+ expect(fboCompanyGrantSchema.safeParse({ ...grantPayload, ability: "flights.view" }).success).toBe(false);
512
+ });
513
+
514
+ it("rejects a row without its ability label — the UI has no copy of its own", () => {
515
+ const { ability_label: _label, ...withoutLabel } = grantPayload;
516
+ expect(fboCompanyGrantSchema.safeParse(withoutLabel).success).toBe(false);
517
+ });
518
+ });
519
+
520
+ describe("fboGrantAbilitySchema", () => {
521
+ it("accepts the one ability the register knows", () => {
522
+ expect(fboGrantAbilitySchema.parse("duties.view")).toBe("duties.view");
523
+ });
524
+
525
+ it("rejects an ability the client cannot render", () => {
526
+ expect(fboGrantAbilitySchema.safeParse("traffic.details").success).toBe(false);
527
+ });
528
+ });
529
+
530
+ describe("fboMetaSchema", () => {
531
+ const metaPayload = {
532
+ roles: ["fbo", "operator"],
533
+ root_id: 34,
534
+ can_decide: true,
535
+ can_view_own_flights: false,
536
+ grant_abilities: [{ value: "duties.view", label: "View duty rosters" }],
537
+ can_manage_grants: true,
538
+ companies: [{ id: 34, name: "Alpha Handling" }],
539
+ };
540
+
541
+ it("parses the meta payload as the API emits it", () => {
542
+ const parsed = fboMetaSchema.parse(metaPayload);
543
+ expect(parsed.grant_abilities?.[0]?.value).toBe("duties.view");
544
+ expect(parsed.can_manage_grants).toBe(true);
545
+ });
546
+
547
+ it("parses a payload from an API that predates the grant register — only roles is required", () => {
548
+ const parsed = fboMetaSchema.parse({ roles: [] });
549
+ expect(parsed.grant_abilities).toBeUndefined();
550
+ expect(parsed.can_view_own_flights).toBeUndefined();
551
+ });
552
+
553
+ it("rejects a grant ability option the client cannot render", () => {
554
+ const bad = { ...metaPayload, grant_abilities: [{ value: "duties.edit", label: "Edit" }] };
555
+ expect(fboMetaSchema.safeParse(bad).success).toBe(false);
556
+ });
557
+ });
558
+
559
+ describe("fboCompanySchema", () => {
560
+ const companyPayload = {
561
+ id: 35,
562
+ name: "Bravo Charter",
563
+ email: "ops@bravo.test",
564
+ phone: null,
565
+ description: null,
566
+ contact_name: "Erika Muster",
567
+ companyType: "interface",
568
+ is_ima_imported: true,
569
+ fbo: { role: "operator", flights_require_approval: true, notify_on_movement: false, has_duties: true, has_airplanes: false },
570
+ members_count: 4,
571
+ movements_count: 92,
572
+ grants_out_count: 1,
573
+ grants_in_count: 0,
574
+ can: { update: true, updateIdentity: true },
575
+ };
576
+
577
+ it("parses an attached company with both grant counts", () => {
578
+ const parsed = fboCompanySchema.parse(companyPayload);
579
+ expect(parsed.grants_out_count).toBe(1);
580
+ expect(parsed.grants_in_count).toBe(0);
581
+ });
582
+
583
+ it("parses a payload from an API that does not count grants yet", () => {
584
+ const { grants_out_count: _out, grants_in_count: _in, ...withoutCounts } = companyPayload;
585
+ expect(fboCompanySchema.parse(withoutCounts).grants_out_count).toBeUndefined();
586
+ });
587
+
588
+ it("parses a company with no FBO profile at all", () => {
589
+ expect(fboCompanySchema.parse({ ...companyPayload, fbo: null }).fbo).toBeNull();
590
+ });
591
+ });
@@ -436,3 +436,102 @@ export const fboMaterialiseResultSchema = z.object({
436
436
  skipped_existing: z.number(),
437
437
  skipped_closed: z.number(),
438
438
  });
439
+
440
+ /**
441
+ * What one company may exercise on another company's data inside the same FBO
442
+ * tree — the vocabulary of the root's grant register. Mirrors the API enum
443
+ * exactly; a value outside it means the register grew an ability this client
444
+ * does not know how to render, and failing here is the point.
445
+ */
446
+ export const fboGrantAbilitySchema = z.enum(["duties.view"]);
447
+
448
+ /**
449
+ * One entry of `GET fbo/meta`'s `grant_abilities` — the ability vocabulary
450
+ * with the label the API already translated, so no client hardcodes copy for
451
+ * an ability it may not know.
452
+ */
453
+ export const fboGrantAbilityOptionSchema = z.object({
454
+ value: fboGrantAbilitySchema,
455
+ label: z.string(),
456
+ });
457
+
458
+ /**
459
+ * A company side of a grant. Rendered as `{id, name}` rather than a full
460
+ * company payload: a grant is a relation between two companies, not a view of
461
+ * either. Null only when that company stopped taking part in the FBO domain.
462
+ */
463
+ export const fboGrantCompanyRefSchema = z.object({
464
+ id: z.number(),
465
+ name: z.string().nullable(),
466
+ });
467
+
468
+ /**
469
+ * One row of the root's company-grant register — `CompanyGrantResource`.
470
+ * `granted_by` is null for a grant the migration wrote rather than a person.
471
+ * `can.delete` is always true on a row that reached the client at all (only
472
+ * `fbo.company.manage` at the tree root can read the register), but it is
473
+ * read as a gate like every other row's, never assumed.
474
+ */
475
+ export const fboCompanyGrantSchema = z.object({
476
+ id: z.number(),
477
+ grantee: fboGrantCompanyRefSchema.nullable(),
478
+ target: fboGrantCompanyRefSchema.nullable(),
479
+ ability: fboGrantAbilitySchema,
480
+ ability_label: z.string(),
481
+ granted_by: z.object({ id: z.number(), name: z.string() }).nullable(),
482
+ created_at: z.string().nullable(),
483
+ can: z.object({ delete: z.boolean() }).optional(),
484
+ });
485
+
486
+ /** `POST companies/{id}/fbo/company-grants` — the register's write shape. */
487
+ export const fboCompanyGrantInputSchema = z.object({
488
+ grantee_company_id: z.number(),
489
+ target_company_id: z.number(),
490
+ ability: fboGrantAbilitySchema,
491
+ });
492
+
493
+ /**
494
+ * `GET companies/{id}/fbo/meta` — the FBO vocabulary and the caller's
495
+ * root-evaluated capabilities, served so no client hardcodes either.
496
+ *
497
+ * Every key but `roles` is optional: the endpoint is the one surface an
498
+ * own-author filer can reach before anything else is known, and it has grown
499
+ * keys over time — a client pinned to an older API must still parse it.
500
+ *
501
+ * `can_view_own_flights` is the TIER, not the bare permission: true only when
502
+ * this caller's sight of movements is the own-author one (false for a decider
503
+ * or an operator, who hold the wider `fbo.flight.viewAny`).
504
+ */
505
+ export const fboMetaSchema = z.object({
506
+ roles: z.array(z.string()),
507
+ root_id: z.number().nullable().optional(),
508
+ can_decide: z.boolean().optional(),
509
+ can_view_own_flights: z.boolean().optional(),
510
+ grant_abilities: z.array(fboGrantAbilityOptionSchema).optional(),
511
+ can_manage_grants: z.boolean().optional(),
512
+ companies: z.array(fboGrantCompanyRefSchema).optional(),
513
+ });
514
+
515
+ /**
516
+ * A company attached to the FBO as its root's admin reads it —
517
+ * `CompanyResource` under `GET companies/{fbo}/fbo/companies`. The three
518
+ * counts are aggregates the index adds (`withCount`) and are null on any
519
+ * payload that did not compute them; `grants_out_count`/`grants_in_count`
520
+ * are additionally optional, being newer than the surface itself.
521
+ */
522
+ export const fboCompanySchema = z.object({
523
+ id: z.number(),
524
+ name: z.string(),
525
+ email: z.string().nullable(),
526
+ phone: z.string().nullable(),
527
+ description: z.string().nullable(),
528
+ contact_name: z.string().nullable(),
529
+ companyType: z.string(),
530
+ is_ima_imported: z.boolean(),
531
+ fbo: fboCompanyProfileSchema.nullable(),
532
+ members_count: z.number().nullable(),
533
+ movements_count: z.number().nullable(),
534
+ grants_out_count: z.number().nullable().optional(),
535
+ grants_in_count: z.number().nullable().optional(),
536
+ can: z.object({ update: z.boolean(), updateIdentity: z.boolean() }),
537
+ });
package/src/types/fbo.ts CHANGED
@@ -36,3 +36,10 @@ export type FboActivityChanges = z.infer<typeof Fbo.fboActivityChangesSchema>;
36
36
  export type FboActivity = z.infer<typeof Fbo.fboActivitySchema>;
37
37
  export type FboRecurringFlight = z.infer<typeof Fbo.fboRecurringFlightSchema>;
38
38
  export type FboMaterialiseResult = z.infer<typeof Fbo.fboMaterialiseResultSchema>;
39
+ export type FboGrantAbility = z.infer<typeof Fbo.fboGrantAbilitySchema>;
40
+ export type FboGrantAbilityOption = z.infer<typeof Fbo.fboGrantAbilityOptionSchema>;
41
+ export type FboGrantCompanyRef = z.infer<typeof Fbo.fboGrantCompanyRefSchema>;
42
+ export type FboCompanyGrant = z.infer<typeof Fbo.fboCompanyGrantSchema>;
43
+ export type FboCompanyGrantInput = z.infer<typeof Fbo.fboCompanyGrantInputSchema>;
44
+ export type FboMeta = z.infer<typeof Fbo.fboMetaSchema>;
45
+ export type FboCompany = z.infer<typeof Fbo.fboCompanySchema>;