@voyant-travel/bookings 0.119.3

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 (204) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +84 -0
  3. package/dist/action-ledger-capabilities.d.ts +306 -0
  4. package/dist/action-ledger-capabilities.d.ts.map +1 -0
  5. package/dist/action-ledger-capabilities.js +92 -0
  6. package/dist/action-ledger-drift-remediation.d.ts +30 -0
  7. package/dist/action-ledger-drift-remediation.d.ts.map +1 -0
  8. package/dist/action-ledger-drift-remediation.js +85 -0
  9. package/dist/action-ledger-drift.d.ts +29 -0
  10. package/dist/action-ledger-drift.d.ts.map +1 -0
  11. package/dist/action-ledger-drift.js +222 -0
  12. package/dist/availability-ref.d.ts +418 -0
  13. package/dist/availability-ref.d.ts.map +1 -0
  14. package/dist/availability-ref.js +28 -0
  15. package/dist/checkout-capability.d.ts +30 -0
  16. package/dist/checkout-capability.d.ts.map +1 -0
  17. package/dist/checkout-capability.js +112 -0
  18. package/dist/extensions/suppliers.d.ts +3 -0
  19. package/dist/extensions/suppliers.d.ts.map +1 -0
  20. package/dist/extensions/suppliers.js +109 -0
  21. package/dist/extras/product-extra-ref.d.ts +343 -0
  22. package/dist/extras/product-extra-ref.d.ts.map +1 -0
  23. package/dist/extras/product-extra-ref.js +31 -0
  24. package/dist/extras/routes.d.ts +515 -0
  25. package/dist/extras/routes.d.ts.map +1 -0
  26. package/dist/extras/routes.js +60 -0
  27. package/dist/extras/schema.d.ts +660 -0
  28. package/dist/extras/schema.d.ts.map +1 -0
  29. package/dist/extras/schema.js +102 -0
  30. package/dist/extras/service-manifest.d.ts +213 -0
  31. package/dist/extras/service-manifest.d.ts.map +1 -0
  32. package/dist/extras/service-manifest.js +329 -0
  33. package/dist/extras/service.d.ts +341 -0
  34. package/dist/extras/service.d.ts.map +1 -0
  35. package/dist/extras/service.js +52 -0
  36. package/dist/extras/validation.d.ts +223 -0
  37. package/dist/extras/validation.d.ts.map +1 -0
  38. package/dist/extras/validation.js +101 -0
  39. package/dist/extras.d.ts +12 -0
  40. package/dist/extras.d.ts.map +1 -0
  41. package/dist/extras.js +12 -0
  42. package/dist/index.d.ts +37 -0
  43. package/dist/index.d.ts.map +1 -0
  44. package/dist/index.js +50 -0
  45. package/dist/markets-ref.d.ts +151 -0
  46. package/dist/markets-ref.d.ts.map +1 -0
  47. package/dist/markets-ref.js +19 -0
  48. package/dist/pii-redaction.d.ts +88 -0
  49. package/dist/pii-redaction.d.ts.map +1 -0
  50. package/dist/pii-redaction.js +121 -0
  51. package/dist/pii.d.ts +63 -0
  52. package/dist/pii.d.ts.map +1 -0
  53. package/dist/pii.js +242 -0
  54. package/dist/pricing-assignment/age.d.ts +33 -0
  55. package/dist/pricing-assignment/age.d.ts.map +1 -0
  56. package/dist/pricing-assignment/age.js +109 -0
  57. package/dist/pricing-assignment/draft.d.ts +69 -0
  58. package/dist/pricing-assignment/draft.d.ts.map +1 -0
  59. package/dist/pricing-assignment/draft.js +284 -0
  60. package/dist/pricing-assignment/types.d.ts +113 -0
  61. package/dist/pricing-assignment/types.d.ts.map +1 -0
  62. package/dist/pricing-assignment/types.js +30 -0
  63. package/dist/pricing-assignment/unit-helpers.d.ts +11 -0
  64. package/dist/pricing-assignment/unit-helpers.d.ts.map +1 -0
  65. package/dist/pricing-assignment/unit-helpers.js +19 -0
  66. package/dist/pricing-assignment/verify.d.ts +67 -0
  67. package/dist/pricing-assignment/verify.d.ts.map +1 -0
  68. package/dist/pricing-assignment/verify.js +121 -0
  69. package/dist/pricing-assignment.d.ts +5 -0
  70. package/dist/pricing-assignment.d.ts.map +1 -0
  71. package/dist/pricing-assignment.js +4 -0
  72. package/dist/pricing-ref.d.ts +1116 -0
  73. package/dist/pricing-ref.d.ts.map +1 -0
  74. package/dist/pricing-ref.js +77 -0
  75. package/dist/products-ref.d.ts +1472 -0
  76. package/dist/products-ref.d.ts.map +1 -0
  77. package/dist/products-ref.js +110 -0
  78. package/dist/requirements/index.d.ts +19 -0
  79. package/dist/requirements/index.d.ts.map +1 -0
  80. package/dist/requirements/index.js +24 -0
  81. package/dist/requirements/routes-public.d.ts +112 -0
  82. package/dist/requirements/routes-public.d.ts.map +1 -0
  83. package/dist/requirements/routes-public.js +15 -0
  84. package/dist/requirements/routes.d.ts +1211 -0
  85. package/dist/requirements/routes.d.ts.map +1 -0
  86. package/dist/requirements/routes.js +221 -0
  87. package/dist/requirements/schema.d.ts +1504 -0
  88. package/dist/requirements/schema.d.ts.map +1 -0
  89. package/dist/requirements/schema.js +260 -0
  90. package/dist/requirements/service-answers.d.ts +71 -0
  91. package/dist/requirements/service-answers.d.ts.map +1 -0
  92. package/dist/requirements/service-answers.js +47 -0
  93. package/dist/requirements/service-public.d.ts +35 -0
  94. package/dist/requirements/service-public.d.ts.map +1 -0
  95. package/dist/requirements/service-public.js +56 -0
  96. package/dist/requirements/service-questions.d.ts +391 -0
  97. package/dist/requirements/service-questions.d.ts.map +1 -0
  98. package/dist/requirements/service-questions.js +319 -0
  99. package/dist/requirements/service-shared.d.ts +37 -0
  100. package/dist/requirements/service-shared.d.ts.map +1 -0
  101. package/dist/requirements/service-shared.js +4 -0
  102. package/dist/requirements/service.d.ts +47 -0
  103. package/dist/requirements/service.d.ts.map +1 -0
  104. package/dist/requirements/service.js +46 -0
  105. package/dist/requirements/validation.d.ts +649 -0
  106. package/dist/requirements/validation.d.ts.map +1 -0
  107. package/dist/requirements/validation.js +221 -0
  108. package/dist/reservation-plans.d.ts +78 -0
  109. package/dist/reservation-plans.d.ts.map +1 -0
  110. package/dist/reservation-plans.js +102 -0
  111. package/dist/route-runtime.d.ts +87 -0
  112. package/dist/route-runtime.d.ts.map +1 -0
  113. package/dist/route-runtime.js +26 -0
  114. package/dist/routes-admin.d.ts +2798 -0
  115. package/dist/routes-admin.d.ts.map +1 -0
  116. package/dist/routes-admin.js +2065 -0
  117. package/dist/routes-groups.d.ts +405 -0
  118. package/dist/routes-groups.d.ts.map +1 -0
  119. package/dist/routes-groups.js +62 -0
  120. package/dist/routes-public.d.ts +685 -0
  121. package/dist/routes-public.d.ts.map +1 -0
  122. package/dist/routes-public.js +257 -0
  123. package/dist/routes-shared.d.ts +60 -0
  124. package/dist/routes-shared.d.ts.map +1 -0
  125. package/dist/routes-shared.js +10 -0
  126. package/dist/routes.d.ts +2 -0
  127. package/dist/routes.d.ts.map +1 -0
  128. package/dist/routes.js +1 -0
  129. package/dist/schema/travel-details.d.ts +404 -0
  130. package/dist/schema/travel-details.d.ts.map +1 -0
  131. package/dist/schema/travel-details.js +158 -0
  132. package/dist/schema-core.d.ts +1248 -0
  133. package/dist/schema-core.d.ts.map +1 -0
  134. package/dist/schema-core.js +120 -0
  135. package/dist/schema-groups.d.ts +261 -0
  136. package/dist/schema-groups.d.ts.map +1 -0
  137. package/dist/schema-groups.js +38 -0
  138. package/dist/schema-items.d.ts +1363 -0
  139. package/dist/schema-items.d.ts.map +1 -0
  140. package/dist/schema-items.js +160 -0
  141. package/dist/schema-operations.d.ts +800 -0
  142. package/dist/schema-operations.d.ts.map +1 -0
  143. package/dist/schema-operations.js +95 -0
  144. package/dist/schema-origin.d.ts +328 -0
  145. package/dist/schema-origin.d.ts.map +1 -0
  146. package/dist/schema-origin.js +45 -0
  147. package/dist/schema-relations.d.ts +87 -0
  148. package/dist/schema-relations.d.ts.map +1 -0
  149. package/dist/schema-relations.js +132 -0
  150. package/dist/schema-shared.d.ts +20 -0
  151. package/dist/schema-shared.d.ts.map +1 -0
  152. package/dist/schema-shared.js +150 -0
  153. package/dist/schema-staff.d.ts +267 -0
  154. package/dist/schema-staff.d.ts.map +1 -0
  155. package/dist/schema-staff.js +31 -0
  156. package/dist/schema.d.ts +12 -0
  157. package/dist/schema.d.ts.map +1 -0
  158. package/dist/schema.js +11 -0
  159. package/dist/service-core.d.ts +6233 -0
  160. package/dist/service-core.d.ts.map +1 -0
  161. package/dist/service-core.js +3776 -0
  162. package/dist/service-groups.d.ts +65 -0
  163. package/dist/service-groups.d.ts.map +1 -0
  164. package/dist/service-groups.js +195 -0
  165. package/dist/service-origin.d.ts +55 -0
  166. package/dist/service-origin.d.ts.map +1 -0
  167. package/dist/service-origin.js +135 -0
  168. package/dist/service-public-core.d.ts +907 -0
  169. package/dist/service-public-core.d.ts.map +1 -0
  170. package/dist/service-public-core.js +1182 -0
  171. package/dist/service-public.d.ts +2 -0
  172. package/dist/service-public.d.ts.map +1 -0
  173. package/dist/service-public.js +1 -0
  174. package/dist/service.d.ts +2 -0
  175. package/dist/service.d.ts.map +1 -0
  176. package/dist/service.js +1 -0
  177. package/dist/state-machine.d.ts +38 -0
  178. package/dist/state-machine.d.ts.map +1 -0
  179. package/dist/state-machine.js +48 -0
  180. package/dist/status-dispatch.d.ts +32 -0
  181. package/dist/status-dispatch.d.ts.map +1 -0
  182. package/dist/status-dispatch.js +56 -0
  183. package/dist/tasks/expire-stale-holds.d.ts +13 -0
  184. package/dist/tasks/expire-stale-holds.d.ts.map +1 -0
  185. package/dist/tasks/expire-stale-holds.js +7 -0
  186. package/dist/tasks/index.d.ts +2 -0
  187. package/dist/tasks/index.d.ts.map +1 -0
  188. package/dist/tasks/index.js +1 -0
  189. package/dist/transactions-ref.d.ts +3189 -0
  190. package/dist/transactions-ref.d.ts.map +1 -0
  191. package/dist/transactions-ref.js +207 -0
  192. package/dist/validation-public.d.ts +2 -0
  193. package/dist/validation-public.d.ts.map +1 -0
  194. package/dist/validation-public.js +1 -0
  195. package/dist/validation-shared.d.ts +2 -0
  196. package/dist/validation-shared.d.ts.map +1 -0
  197. package/dist/validation-shared.js +1 -0
  198. package/dist/validation.d.ts +2 -0
  199. package/dist/validation.d.ts.map +1 -0
  200. package/dist/validation.js +1 -0
  201. package/dist/workflows/refund-booking.d.ts +87 -0
  202. package/dist/workflows/refund-booking.d.ts.map +1 -0
  203. package/dist/workflows/refund-booking.js +216 -0
  204. package/package.json +159 -0
@@ -0,0 +1,30 @@
1
+ import type { AnyDrizzleDb } from "@voyant-travel/db";
2
+ import type { BookingActionLedgerDriftCheck, CheckBookingActionLedgerDriftInput, CheckBookingActionLedgerDriftResult } from "./action-ledger-drift.js";
3
+ export interface BookingActionLedgerDriftRemediationItem {
4
+ check: BookingActionLedgerDriftCheck;
5
+ missingCount: number;
6
+ sampleTargetIds: string[];
7
+ sampleTruncated: boolean;
8
+ recommendedBackfillActionName: string;
9
+ targetType: string;
10
+ targetIdKind: string;
11
+ mode: "dry_run";
12
+ note: string;
13
+ }
14
+ export interface BookingActionLedgerDriftRemediationPlan {
15
+ mode: "dry_run";
16
+ generatedAt: string;
17
+ createdAtFrom: string | null;
18
+ sampleLimit: number | null;
19
+ totalMissingCount: number;
20
+ items: BookingActionLedgerDriftRemediationItem[];
21
+ }
22
+ export interface BuildBookingActionLedgerDriftRemediationPlanInput {
23
+ drift: CheckBookingActionLedgerDriftResult;
24
+ createdAtFrom?: CheckBookingActionLedgerDriftInput["createdAtFrom"];
25
+ sampleLimit?: number | null;
26
+ generatedAt?: Date | string;
27
+ }
28
+ export declare function planBookingActionLedgerDriftRemediation(db: AnyDrizzleDb, input?: CheckBookingActionLedgerDriftInput): Promise<BookingActionLedgerDriftRemediationPlan>;
29
+ export declare function buildBookingActionLedgerDriftRemediationPlan({ drift, createdAtFrom, sampleLimit, generatedAt, }: BuildBookingActionLedgerDriftRemediationPlanInput): BookingActionLedgerDriftRemediationPlan;
30
+ //# sourceMappingURL=action-ledger-drift-remediation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"action-ledger-drift-remediation.d.ts","sourceRoot":"","sources":["../src/action-ledger-drift-remediation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AACrD,OAAO,KAAK,EACV,6BAA6B,EAC7B,kCAAkC,EAClC,mCAAmC,EACpC,MAAM,0BAA0B,CAAA;AAgDjC,MAAM,WAAW,uCAAuC;IACtD,KAAK,EAAE,6BAA6B,CAAA;IACpC,YAAY,EAAE,MAAM,CAAA;IACpB,eAAe,EAAE,MAAM,EAAE,CAAA;IACzB,eAAe,EAAE,OAAO,CAAA;IACxB,6BAA6B,EAAE,MAAM,CAAA;IACrC,UAAU,EAAE,MAAM,CAAA;IAClB,YAAY,EAAE,MAAM,CAAA;IACpB,IAAI,EAAE,SAAS,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;CACb;AAED,MAAM,WAAW,uCAAuC;IACtD,IAAI,EAAE,SAAS,CAAA;IACf,WAAW,EAAE,MAAM,CAAA;IACnB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,iBAAiB,EAAE,MAAM,CAAA;IACzB,KAAK,EAAE,uCAAuC,EAAE,CAAA;CACjD;AAED,MAAM,WAAW,iDAAiD;IAChE,KAAK,EAAE,mCAAmC,CAAA;IAC1C,aAAa,CAAC,EAAE,kCAAkC,CAAC,eAAe,CAAC,CAAA;IACnE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,WAAW,CAAC,EAAE,IAAI,GAAG,MAAM,CAAA;CAC5B;AAED,wBAAsB,uCAAuC,CAC3D,EAAE,EAAE,YAAY,EAChB,KAAK,GAAE,kCAAuC,GAC7C,OAAO,CAAC,uCAAuC,CAAC,CAOlD;AAED,wBAAgB,4CAA4C,CAAC,EAC3D,KAAK,EACL,aAAa,EACb,WAAW,EACX,WAAwB,GACzB,EAAE,iDAAiD,GAAG,uCAAuC,CA+B7F"}
@@ -0,0 +1,85 @@
1
+ import { checkBookingActionLedgerDrift } from "./action-ledger-drift.js";
2
+ const BACKFILL_ACTION_BY_CHECK = {
3
+ booking_confirmed: {
4
+ actionName: "booking.status.confirm",
5
+ targetType: "booking",
6
+ targetIdKind: "booking_id",
7
+ },
8
+ booking_expired: {
9
+ actionName: "booking.status.expire",
10
+ targetType: "booking",
11
+ targetIdKind: "booking_id",
12
+ },
13
+ booking_cancelled: {
14
+ actionName: "booking.status.cancel",
15
+ targetType: "booking",
16
+ targetIdKind: "booking_id",
17
+ },
18
+ booking_completed: {
19
+ actionName: "booking.status.complete",
20
+ targetType: "booking",
21
+ targetIdKind: "booking_id",
22
+ },
23
+ booking_item: {
24
+ actionName: "booking.item.create",
25
+ targetType: "booking_item",
26
+ targetIdKind: "booking_item_id",
27
+ },
28
+ booking_traveler: {
29
+ actionName: "booking.traveler.create",
30
+ targetType: "booking_traveler",
31
+ targetIdKind: "booking_traveler_id",
32
+ },
33
+ booking_traveler_travel_details: {
34
+ actionName: "booking.traveler_travel_details.update",
35
+ targetType: "booking_traveler",
36
+ targetIdKind: "booking_traveler_id",
37
+ },
38
+ };
39
+ export async function planBookingActionLedgerDriftRemediation(db, input = {}) {
40
+ const drift = await checkBookingActionLedgerDrift(db, input);
41
+ return buildBookingActionLedgerDriftRemediationPlan({
42
+ drift,
43
+ createdAtFrom: input.createdAtFrom,
44
+ sampleLimit: input.sampleLimit,
45
+ });
46
+ }
47
+ export function buildBookingActionLedgerDriftRemediationPlan({ drift, createdAtFrom, sampleLimit, generatedAt = new Date(), }) {
48
+ const generatedAtDate = generatedAt instanceof Date ? generatedAt : new Date(generatedAt);
49
+ if (Number.isNaN(generatedAtDate.getTime())) {
50
+ throw new Error("generatedAt must be a valid date");
51
+ }
52
+ const items = drift.rows
53
+ .filter((row) => row.missingCount > 0)
54
+ .map((row) => {
55
+ const action = BACKFILL_ACTION_BY_CHECK[row.check];
56
+ return {
57
+ check: row.check,
58
+ missingCount: row.missingCount,
59
+ sampleTargetIds: row.sampleIds,
60
+ sampleTruncated: row.missingCount > row.sampleIds.length,
61
+ recommendedBackfillActionName: action.actionName,
62
+ targetType: action.targetType,
63
+ targetIdKind: action.targetIdKind,
64
+ mode: "dry_run",
65
+ note: "Dry run only. Review source rows and choose an explicit historical actor before writing backfill ledger entries.",
66
+ };
67
+ });
68
+ return {
69
+ mode: "dry_run",
70
+ generatedAt: generatedAtDate.toISOString(),
71
+ createdAtFrom: normalizeNullableDate(createdAtFrom),
72
+ sampleLimit: sampleLimit ?? null,
73
+ totalMissingCount: items.reduce((sum, item) => sum + item.missingCount, 0),
74
+ items,
75
+ };
76
+ }
77
+ function normalizeNullableDate(value) {
78
+ if (!value)
79
+ return null;
80
+ const date = value instanceof Date ? value : new Date(value);
81
+ if (Number.isNaN(date.getTime())) {
82
+ throw new Error("createdAtFrom must be a valid date");
83
+ }
84
+ return date.toISOString();
85
+ }
@@ -0,0 +1,29 @@
1
+ import type { AnyDrizzleDb } from "@voyant-travel/db";
2
+ import { type SQL } from "drizzle-orm";
3
+ export type BookingActionLedgerDriftCheck = "booking_confirmed" | "booking_expired" | "booking_cancelled" | "booking_completed" | "booking_item" | "booking_traveler" | "booking_traveler_travel_details";
4
+ export interface CheckBookingActionLedgerDriftInput {
5
+ createdAtFrom?: Date | string | null;
6
+ sampleLimit?: number | null;
7
+ }
8
+ export interface BookingActionLedgerDriftRow {
9
+ check: BookingActionLedgerDriftCheck;
10
+ missingCount: number;
11
+ sampleIds: string[];
12
+ }
13
+ export interface CheckBookingActionLedgerDriftResult {
14
+ ok: boolean;
15
+ rows: BookingActionLedgerDriftRow[];
16
+ }
17
+ interface BookingActionLedgerDriftQueryRow extends Record<string, unknown> {
18
+ check: BookingActionLedgerDriftCheck;
19
+ missing_count: number | string;
20
+ sample_ids: string[] | null;
21
+ }
22
+ export declare function buildBookingActionLedgerDriftQueries(input?: CheckBookingActionLedgerDriftInput): Record<BookingActionLedgerDriftCheck, SQL<BookingActionLedgerDriftQueryRow>>;
23
+ export declare function checkBookingActionLedgerDrift(db: AnyDrizzleDb, input?: CheckBookingActionLedgerDriftInput): Promise<CheckBookingActionLedgerDriftResult>;
24
+ declare function normalizeRow(row: BookingActionLedgerDriftQueryRow): BookingActionLedgerDriftRow;
25
+ export declare const __test__: {
26
+ normalizeRow: typeof normalizeRow;
27
+ };
28
+ export {};
29
+ //# sourceMappingURL=action-ledger-drift.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"action-ledger-drift.d.ts","sourceRoot":"","sources":["../src/action-ledger-drift.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AACrD,OAAO,EAAE,KAAK,GAAG,EAAwB,MAAM,aAAa,CAAA;AAsB5D,MAAM,MAAM,6BAA6B,GACrC,mBAAmB,GACnB,iBAAiB,GACjB,mBAAmB,GACnB,mBAAmB,GACnB,cAAc,GACd,kBAAkB,GAClB,iCAAiC,CAAA;AAErC,MAAM,WAAW,kCAAkC;IACjD,aAAa,CAAC,EAAE,IAAI,GAAG,MAAM,GAAG,IAAI,CAAA;IACpC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC5B;AAED,MAAM,WAAW,2BAA2B;IAC1C,KAAK,EAAE,6BAA6B,CAAA;IACpC,YAAY,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,MAAM,EAAE,CAAA;CACpB;AAED,MAAM,WAAW,mCAAmC;IAClD,EAAE,EAAE,OAAO,CAAA;IACX,IAAI,EAAE,2BAA2B,EAAE,CAAA;CACpC;AAED,UAAU,gCAAiC,SAAQ,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IACxE,KAAK,EAAE,6BAA6B,CAAA;IACpC,aAAa,EAAE,MAAM,GAAG,MAAM,CAAA;IAC9B,UAAU,EAAE,MAAM,EAAE,GAAG,IAAI,CAAA;CAC5B;AAED,wBAAgB,oCAAoC,CAClD,KAAK,GAAE,kCAAuC,GAC7C,MAAM,CAAC,6BAA6B,EAAE,GAAG,CAAC,gCAAgC,CAAC,CAAC,CA8H9E;AAED,wBAAsB,6BAA6B,CACjD,EAAE,EAAE,YAAY,EAChB,KAAK,GAAE,kCAAuC,GAC7C,OAAO,CAAC,mCAAmC,CAAC,CAmB9C;AA+DD,iBAAS,YAAY,CAAC,GAAG,EAAE,gCAAgC,GAAG,2BAA2B,CAMxF;AAED,eAAO,MAAM,QAAQ;;CAEpB,CAAA"}
@@ -0,0 +1,222 @@
1
+ import { actionLedgerEntries } from "@voyant-travel/action-ledger/schema";
2
+ import { sql } from "drizzle-orm";
3
+ import { bookingTravelerTravelDetails } from "./schema/travel-details.js";
4
+ import { bookings, bookingTravelers } from "./schema-core.js";
5
+ import { bookingItems } from "./schema-items.js";
6
+ const DEFAULT_SAMPLE_LIMIT = 20;
7
+ const MAX_SAMPLE_LIMIT = 100;
8
+ const BOOKING_CONFIRM_ACTION_NAME = "booking.status.confirm";
9
+ const BOOKING_EXPIRE_ACTION_NAME = "booking.status.expire";
10
+ const BOOKING_CANCEL_ACTION_NAME = "booking.status.cancel";
11
+ const BOOKING_COMPLETE_ACTION_NAME = "booking.status.complete";
12
+ const BOOKING_TRAVELER_CREATE_ACTION_NAMES = [
13
+ "booking.traveler.create",
14
+ "booking.traveler_with_travel_details.create",
15
+ ];
16
+ const BOOKING_TRAVELER_TRAVEL_DETAILS_ACTION_NAMES = [
17
+ "booking.traveler_with_travel_details.create",
18
+ "booking.traveler_travel_details.update",
19
+ ];
20
+ const BOOKING_ITEM_CREATE_ACTION_NAME = "booking.item.create";
21
+ export function buildBookingActionLedgerDriftQueries(input = {}) {
22
+ const sampleLimit = normalizeSampleLimit(input.sampleLimit);
23
+ return {
24
+ booking_confirmed: buildBookingStatusDriftQuery({
25
+ check: "booking_confirmed",
26
+ actionName: BOOKING_CONFIRM_ACTION_NAME,
27
+ timestampColumn: bookings.confirmedAt,
28
+ input,
29
+ sampleLimit,
30
+ }),
31
+ booking_expired: buildBookingStatusDriftQuery({
32
+ check: "booking_expired",
33
+ actionName: BOOKING_EXPIRE_ACTION_NAME,
34
+ timestampColumn: bookings.expiredAt,
35
+ input,
36
+ sampleLimit,
37
+ }),
38
+ booking_cancelled: buildBookingStatusDriftQuery({
39
+ check: "booking_cancelled",
40
+ actionName: BOOKING_CANCEL_ACTION_NAME,
41
+ timestampColumn: bookings.cancelledAt,
42
+ input,
43
+ sampleLimit,
44
+ }),
45
+ booking_completed: buildBookingStatusDriftQuery({
46
+ check: "booking_completed",
47
+ actionName: BOOKING_COMPLETE_ACTION_NAME,
48
+ timestampColumn: bookings.completedAt,
49
+ input,
50
+ sampleLimit,
51
+ }),
52
+ booking_item: sql `
53
+ SELECT
54
+ 'booking_item' AS check,
55
+ count(*)::int AS missing_count,
56
+ coalesce(
57
+ array_agg(candidate_id ORDER BY created_at DESC, candidate_id DESC)
58
+ FILTER (WHERE sample_ordinal <= ${sampleLimit}),
59
+ ARRAY[]::text[]
60
+ ) AS sample_ids
61
+ FROM (
62
+ SELECT
63
+ ${bookingItems.id} AS candidate_id,
64
+ ${bookingItems.createdAt} AS created_at,
65
+ row_number() OVER (
66
+ ORDER BY ${bookingItems.createdAt} DESC, ${bookingItems.id} DESC
67
+ ) AS sample_ordinal
68
+ FROM ${bookingItems}
69
+ WHERE 1 = 1
70
+ ${buildCreatedAtCondition(bookingItems.createdAt, input.createdAtFrom)}
71
+ AND NOT EXISTS (
72
+ SELECT 1
73
+ FROM ${actionLedgerEntries}
74
+ WHERE ${actionLedgerEntries.actionName} = ${BOOKING_ITEM_CREATE_ACTION_NAME}
75
+ AND ${actionLedgerEntries.targetType} = ${"booking_item"}
76
+ AND ${actionLedgerEntries.targetId} = ${bookingItems.id}
77
+ )
78
+ ) missing
79
+ `,
80
+ booking_traveler: sql `
81
+ SELECT
82
+ 'booking_traveler' AS check,
83
+ count(*)::int AS missing_count,
84
+ coalesce(
85
+ array_agg(candidate_id ORDER BY created_at DESC, candidate_id DESC)
86
+ FILTER (WHERE sample_ordinal <= ${sampleLimit}),
87
+ ARRAY[]::text[]
88
+ ) AS sample_ids
89
+ FROM (
90
+ SELECT
91
+ ${bookingTravelers.id} AS candidate_id,
92
+ ${bookingTravelers.createdAt} AS created_at,
93
+ row_number() OVER (
94
+ ORDER BY ${bookingTravelers.createdAt} DESC, ${bookingTravelers.id} DESC
95
+ ) AS sample_ordinal
96
+ FROM ${bookingTravelers}
97
+ WHERE 1 = 1
98
+ ${buildCreatedAtCondition(bookingTravelers.createdAt, input.createdAtFrom)}
99
+ AND NOT EXISTS (
100
+ SELECT 1
101
+ FROM ${actionLedgerEntries}
102
+ WHERE ${actionLedgerEntries.actionName} IN (${sql.join(
103
+ // agent-quality: raw-sql reviewed -- owner: bookings; dynamic SQL interpolation uses Drizzle parameter binding or vetted SQL identifiers.
104
+ BOOKING_TRAVELER_CREATE_ACTION_NAMES.map((actionName) => sql `${actionName}`), sql `, `)})
105
+ AND ${actionLedgerEntries.targetType} = ${"booking_traveler"}
106
+ AND ${actionLedgerEntries.targetId} = ${bookingTravelers.id}
107
+ )
108
+ ) missing
109
+ `,
110
+ booking_traveler_travel_details: sql `
111
+ SELECT
112
+ 'booking_traveler_travel_details' AS check,
113
+ count(*)::int AS missing_count,
114
+ coalesce(
115
+ array_agg(candidate_id ORDER BY created_at DESC, candidate_id DESC)
116
+ FILTER (WHERE sample_ordinal <= ${sampleLimit}),
117
+ ARRAY[]::text[]
118
+ ) AS sample_ids
119
+ FROM (
120
+ SELECT
121
+ ${bookingTravelerTravelDetails.travelerId} AS candidate_id,
122
+ ${bookingTravelerTravelDetails.createdAt} AS created_at,
123
+ row_number() OVER (
124
+ ORDER BY ${bookingTravelerTravelDetails.createdAt} DESC,
125
+ ${bookingTravelerTravelDetails.travelerId} DESC
126
+ ) AS sample_ordinal
127
+ FROM ${bookingTravelerTravelDetails}
128
+ WHERE 1 = 1
129
+ ${buildCreatedAtCondition(bookingTravelerTravelDetails.createdAt, input.createdAtFrom)}
130
+ AND NOT EXISTS (
131
+ SELECT 1
132
+ FROM ${actionLedgerEntries}
133
+ WHERE ${actionLedgerEntries.actionName} IN (${sql.join(
134
+ // agent-quality: raw-sql reviewed -- owner: bookings; dynamic SQL interpolation uses Drizzle parameter binding or vetted SQL identifiers.
135
+ BOOKING_TRAVELER_TRAVEL_DETAILS_ACTION_NAMES.map((actionName) => sql `${actionName}`), sql `, `)})
136
+ AND ${actionLedgerEntries.targetType} = ${"booking_traveler"}
137
+ AND ${actionLedgerEntries.targetId} = ${bookingTravelerTravelDetails.travelerId}
138
+ )
139
+ ) missing
140
+ `,
141
+ };
142
+ }
143
+ export async function checkBookingActionLedgerDrift(db, input = {}) {
144
+ const queries = buildBookingActionLedgerDriftQueries(input);
145
+ const results = await Promise.all([
146
+ db.execute(queries.booking_confirmed),
147
+ db.execute(queries.booking_expired),
148
+ db.execute(queries.booking_cancelled),
149
+ db.execute(queries.booking_completed),
150
+ db.execute(queries.booking_item),
151
+ db.execute(queries.booking_traveler),
152
+ db.execute(queries.booking_traveler_travel_details),
153
+ ]);
154
+ const rows = results
155
+ .flatMap((result) => extractRows(result))
156
+ .map((row) => normalizeRow(row));
157
+ return {
158
+ ok: rows.every((row) => row.missingCount === 0),
159
+ rows,
160
+ };
161
+ }
162
+ function buildBookingStatusDriftQuery(input) {
163
+ return sql `
164
+ SELECT
165
+ ${input.check} AS check,
166
+ count(*)::int AS missing_count,
167
+ coalesce(
168
+ array_agg(candidate_id ORDER BY created_at DESC, candidate_id DESC)
169
+ FILTER (WHERE sample_ordinal <= ${input.sampleLimit}),
170
+ ARRAY[]::text[]
171
+ ) AS sample_ids
172
+ FROM (
173
+ SELECT
174
+ ${bookings.id} AS candidate_id,
175
+ ${input.timestampColumn} AS created_at,
176
+ row_number() OVER (
177
+ ORDER BY ${input.timestampColumn} DESC, ${bookings.id} DESC
178
+ ) AS sample_ordinal
179
+ FROM ${bookings}
180
+ WHERE ${input.timestampColumn} IS NOT NULL
181
+ ${buildCreatedAtCondition(input.timestampColumn, input.input.createdAtFrom)}
182
+ AND NOT EXISTS (
183
+ SELECT 1
184
+ FROM ${actionLedgerEntries}
185
+ WHERE ${actionLedgerEntries.actionName} = ${input.actionName}
186
+ AND ${actionLedgerEntries.targetType} = ${"booking"}
187
+ AND ${actionLedgerEntries.targetId} = ${bookings.id}
188
+ )
189
+ ) missing
190
+ `;
191
+ }
192
+ function normalizeSampleLimit(limit) {
193
+ if (!limit)
194
+ return DEFAULT_SAMPLE_LIMIT;
195
+ return Math.min(Math.max(Math.trunc(limit), 1), MAX_SAMPLE_LIMIT);
196
+ }
197
+ function buildCreatedAtCondition(column, value) {
198
+ if (!value)
199
+ return sql ``;
200
+ const date = value instanceof Date ? value : new Date(value);
201
+ if (Number.isNaN(date.getTime())) {
202
+ throw new Error("createdAtFrom must be a valid date");
203
+ }
204
+ // agent-quality: raw-sql reviewed -- owner: bookings; dynamic SQL interpolation uses Drizzle parameter binding or vetted SQL identifiers.
205
+ return sql `AND ${column} >= ${date}`;
206
+ }
207
+ function extractRows(result) {
208
+ if (Array.isArray(result))
209
+ return result;
210
+ const maybeRows = result.rows;
211
+ return Array.isArray(maybeRows) ? maybeRows : [];
212
+ }
213
+ function normalizeRow(row) {
214
+ return {
215
+ check: row.check,
216
+ missingCount: Number(row.missing_count),
217
+ sampleIds: row.sample_ids ?? [],
218
+ };
219
+ }
220
+ export const __test__ = {
221
+ normalizeRow,
222
+ };