@voyant-travel/charters 0.117.2

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 (108) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +16 -0
  3. package/dist/adapters/index.d.ts +254 -0
  4. package/dist/adapters/index.d.ts.map +1 -0
  5. package/dist/adapters/index.js +16 -0
  6. package/dist/adapters/memoize.d.ts +28 -0
  7. package/dist/adapters/memoize.d.ts.map +1 -0
  8. package/dist/adapters/memoize.js +121 -0
  9. package/dist/adapters/mock.d.ts +50 -0
  10. package/dist/adapters/mock.d.ts.map +1 -0
  11. package/dist/adapters/mock.js +194 -0
  12. package/dist/adapters/registry.d.ts +24 -0
  13. package/dist/adapters/registry.d.ts.map +1 -0
  14. package/dist/adapters/registry.js +40 -0
  15. package/dist/booking-extension.d.ts +895 -0
  16. package/dist/booking-extension.d.ts.map +1 -0
  17. package/dist/booking-extension.js +339 -0
  18. package/dist/catalog-policy.d.ts +23 -0
  19. package/dist/catalog-policy.d.ts.map +1 -0
  20. package/dist/catalog-policy.js +400 -0
  21. package/dist/content-shape.d.ts +5 -0
  22. package/dist/content-shape.d.ts.map +1 -0
  23. package/dist/content-shape.js +13 -0
  24. package/dist/draft-shape.d.ts +29 -0
  25. package/dist/draft-shape.d.ts.map +1 -0
  26. package/dist/draft-shape.js +63 -0
  27. package/dist/index.d.ts +31 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +55 -0
  30. package/dist/lib/key.d.ts +22 -0
  31. package/dist/lib/key.d.ts.map +1 -0
  32. package/dist/lib/key.js +24 -0
  33. package/dist/routes-public.d.ts +785 -0
  34. package/dist/routes-public.d.ts.map +1 -0
  35. package/dist/routes-public.js +234 -0
  36. package/dist/routes.d.ts +1744 -0
  37. package/dist/routes.d.ts.map +1 -0
  38. package/dist/routes.js +543 -0
  39. package/dist/schema-core.d.ts +815 -0
  40. package/dist/schema-core.d.ts.map +1 -0
  41. package/dist/schema-core.js +98 -0
  42. package/dist/schema-itinerary.d.ts +239 -0
  43. package/dist/schema-itinerary.d.ts.map +1 -0
  44. package/dist/schema-itinerary.js +30 -0
  45. package/dist/schema-pricing.d.ts +385 -0
  46. package/dist/schema-pricing.d.ts.map +1 -0
  47. package/dist/schema-pricing.js +62 -0
  48. package/dist/schema-shared.d.ts +8 -0
  49. package/dist/schema-shared.d.ts.map +1 -0
  50. package/dist/schema-shared.js +37 -0
  51. package/dist/schema-sourced-content.d.ts +253 -0
  52. package/dist/schema-sourced-content.d.ts.map +1 -0
  53. package/dist/schema-sourced-content.js +44 -0
  54. package/dist/schema-yachts.d.ts +367 -0
  55. package/dist/schema-yachts.d.ts.map +1 -0
  56. package/dist/schema-yachts.js +30 -0
  57. package/dist/schema.d.ts +8 -0
  58. package/dist/schema.d.ts.map +1 -0
  59. package/dist/schema.js +7 -0
  60. package/dist/service-bookings-helpers.d.ts +20 -0
  61. package/dist/service-bookings-helpers.d.ts.map +1 -0
  62. package/dist/service-bookings-helpers.js +67 -0
  63. package/dist/service-bookings-local.d.ts +5 -0
  64. package/dist/service-bookings-local.d.ts.map +1 -0
  65. package/dist/service-bookings-local.js +177 -0
  66. package/dist/service-bookings-types.d.ts +88 -0
  67. package/dist/service-bookings-types.d.ts.map +1 -0
  68. package/dist/service-bookings-types.js +1 -0
  69. package/dist/service-bookings.d.ts +36 -0
  70. package/dist/service-bookings.d.ts.map +1 -0
  71. package/dist/service-bookings.js +267 -0
  72. package/dist/service-catalog-plane.d.ts +58 -0
  73. package/dist/service-catalog-plane.d.ts.map +1 -0
  74. package/dist/service-catalog-plane.js +145 -0
  75. package/dist/service-content-synthesizer.d.ts +42 -0
  76. package/dist/service-content-synthesizer.d.ts.map +1 -0
  77. package/dist/service-content-synthesizer.js +122 -0
  78. package/dist/service-content.d.ts +43 -0
  79. package/dist/service-content.d.ts.map +1 -0
  80. package/dist/service-content.js +248 -0
  81. package/dist/service-myba.d.ts +85 -0
  82. package/dist/service-myba.d.ts.map +1 -0
  83. package/dist/service-myba.js +88 -0
  84. package/dist/service-pricing.d.ts +64 -0
  85. package/dist/service-pricing.d.ts.map +1 -0
  86. package/dist/service-pricing.js +167 -0
  87. package/dist/service.d.ts +131 -0
  88. package/dist/service.d.ts.map +1 -0
  89. package/dist/service.js +279 -0
  90. package/dist/validation-core.d.ts +152 -0
  91. package/dist/validation-core.d.ts.map +1 -0
  92. package/dist/validation-core.js +66 -0
  93. package/dist/validation-itinerary.d.ts +43 -0
  94. package/dist/validation-itinerary.d.ts.map +1 -0
  95. package/dist/validation-itinerary.js +19 -0
  96. package/dist/validation-pricing.d.ts +103 -0
  97. package/dist/validation-pricing.d.ts.map +1 -0
  98. package/dist/validation-pricing.js +28 -0
  99. package/dist/validation-shared.d.ts +61 -0
  100. package/dist/validation-shared.d.ts.map +1 -0
  101. package/dist/validation-shared.js +60 -0
  102. package/dist/validation-yachts.d.ts +76 -0
  103. package/dist/validation-yachts.d.ts.map +1 -0
  104. package/dist/validation-yachts.js +36 -0
  105. package/dist/validation.d.ts +6 -0
  106. package/dist/validation.d.ts.map +1 -0
  107. package/dist/validation.js +5 -0
  108. package/package.json +116 -0
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Charter content service — `getCharterContent` with locale-resolved
3
+ * cache reads, SWR refresh, and synthesizer fallback.
4
+ *
5
+ * Mirrors `service-content.ts` in the products / cruises / accommodations
6
+ * packages but charter-shaped. The charter content aggregate (§3.2 /
7
+ * §3.6) is `{ charter, yacht, voyages[], suites[], schedule_days[],
8
+ * policies[] }` — one payload returned by a single getContent.
9
+ * Pricing stays out (volatile-live, flows through `liveResolve`).
10
+ */
11
+ import { type ContentLocaleResolution, type InvalidateOnDrift, type SourceAdapter, type SourceAdapterContext } from "@voyant-travel/catalog";
12
+ import type { SourceAdapterRegistry } from "@voyant-travel/catalog/booking-engine";
13
+ import type { AnyDrizzleDb } from "@voyant-travel/db";
14
+ import { type CharterContent } from "./content-shape.js";
15
+ export interface CharterContentScope {
16
+ preferredLocales: ReadonlyArray<string>;
17
+ market?: string;
18
+ currency?: string;
19
+ acceptMachineTranslated?: boolean;
20
+ }
21
+ export interface GetCharterContentOptions {
22
+ registry: SourceAdapterRegistry;
23
+ buildAdapterContext?: (adapter: SourceAdapter) => SourceAdapterContext;
24
+ onOverlayError?: (event: {
25
+ field_path: string;
26
+ reason: string;
27
+ }) => void;
28
+ }
29
+ export interface ResolvedCharterContent {
30
+ content: CharterContent;
31
+ resolution: ContentLocaleResolution<{
32
+ locale: string;
33
+ payload: CharterContent;
34
+ }>;
35
+ source: "sourced-cache" | "sourced-fresh" | "synthesized";
36
+ served_stale: boolean;
37
+ synthesized: boolean;
38
+ machine_translated: boolean;
39
+ }
40
+ export declare function getCharterContent(db: AnyDrizzleDb, entityId: string, scope: CharterContentScope, options: GetCharterContentOptions): Promise<ResolvedCharterContent | null>;
41
+ /** Drift event consumer for the charters content cache. Per §3.4.1. */
42
+ export declare const invalidateCharterContentOnDrift: InvalidateOnDrift;
43
+ //# sourceMappingURL=service-content.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-content.d.ts","sourceRoot":"","sources":["../src/service-content.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EACL,KAAK,uBAAuB,EAK5B,KAAK,iBAAiB,EAKtB,KAAK,aAAa,EAClB,KAAK,oBAAoB,EAE1B,MAAM,wBAAwB,CAAA;AAC/B,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,uCAAuC,CAAA;AAClF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAGrD,OAAO,EAEL,KAAK,cAAc,EAIpB,MAAM,oBAAoB,CAAA;AAc3B,MAAM,WAAW,mBAAmB;IAClC,gBAAgB,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;IACvC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,uBAAuB,CAAC,EAAE,OAAO,CAAA;CAClC;AAED,MAAM,WAAW,wBAAwB;IACvC,QAAQ,EAAE,qBAAqB,CAAA;IAC/B,mBAAmB,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,oBAAoB,CAAA;IACtE,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAA;CACzE;AAED,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,cAAc,CAAA;IACvB,UAAU,EAAE,uBAAuB,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,cAAc,CAAA;KAAE,CAAC,CAAA;IAChF,MAAM,EAAE,eAAe,GAAG,eAAe,GAAG,aAAa,CAAA;IACzD,YAAY,EAAE,OAAO,CAAA;IACrB,WAAW,EAAE,OAAO,CAAA;IACpB,kBAAkB,EAAE,OAAO,CAAA;CAC5B;AAED,wBAAsB,iBAAiB,CACrC,EAAE,EAAE,YAAY,EAChB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,mBAAmB,EAC1B,OAAO,EAAE,wBAAwB,GAChC,OAAO,CAAC,sBAAsB,GAAG,IAAI,CAAC,CA0FxC;AA2ND,uEAAuE;AACvE,eAAO,MAAM,+BAA+B,EAAE,iBAG7C,CAAA"}
@@ -0,0 +1,248 @@
1
+ /**
2
+ * Charter content service — `getCharterContent` with locale-resolved
3
+ * cache reads, SWR refresh, and synthesizer fallback.
4
+ *
5
+ * Mirrors `service-content.ts` in the products / cruises / accommodations
6
+ * packages but charter-shaped. The charter content aggregate (§3.2 /
7
+ * §3.6) is `{ charter, yacht, voyages[], suites[], schedule_days[],
8
+ * policies[] }` — one payload returned by a single getContent.
9
+ * Pricing stays out (volatile-live, flows through `liveResolve`).
10
+ */
11
+ import { createInvalidateOnDrift, fetchOverlaysForEntity, isStale, pickBestCachedLocale, readSourcedEntry, withContentRefreshLock, } from "@voyant-travel/catalog";
12
+ import { and, eq } from "drizzle-orm";
13
+ import { CHARTERS_CONTENT_SCHEMA_VERSION, charterContentSchema, mergeOverlaysIntoCharterContent, validateCharterContent, } from "./content-shape.js";
14
+ import { CHARTERS_CONTENT_MARKET_ANY, chartersSourcedContentTable, } from "./schema-sourced-content.js";
15
+ import { synthesizeCharterContent, } from "./service-content-synthesizer.js";
16
+ /** Charters cache TTL is 24h — same as products / cruises. */
17
+ const CHARTERS_DEFAULT_TTL_MS = 24 * 60 * 60 * 1000;
18
+ export async function getCharterContent(db, entityId, scope, options) {
19
+ const sourcedEntry = await readSourcedEntry(db, "charters", entityId);
20
+ if (!sourcedEntry)
21
+ return null;
22
+ const provenance = {
23
+ kind: "sourced",
24
+ provenance: {
25
+ source_kind: sourcedEntry.source_kind,
26
+ source_provider: sourcedEntry.source_provider ?? undefined,
27
+ source_connection_id: sourcedEntry.source_connection_id ?? undefined,
28
+ source_ref: sourcedEntry.source_ref ?? undefined,
29
+ source_freshness: sourcedEntry.source_freshness,
30
+ last_sourced_at: sourcedEntry.last_sourced_at ?? undefined,
31
+ },
32
+ entry_id: sourcedEntry.id,
33
+ status: sourcedEntry.status,
34
+ projection: sourcedEntry.projection,
35
+ projection_etag: sourcedEntry.projection_etag,
36
+ projection_seen_at: sourcedEntry.projection_seen_at,
37
+ first_seen_at: sourcedEntry.first_seen_at,
38
+ last_seen_at: sourcedEntry.last_seen_at,
39
+ };
40
+ const adapter = sourcedEntry.source_connection_id
41
+ ? (options.registry.resolveByConnection(sourcedEntry.source_connection_id) ??
42
+ options.registry.byKind(sourcedEntry.source_kind)[0]?.adapter)
43
+ : options.registry.byKind(sourcedEntry.source_kind)[0]?.adapter;
44
+ const adapterCtx = options.buildAdapterContext?.(adapter) ?? {
45
+ connection_id: sourcedEntry.source_connection_id ?? sourcedEntry.source_kind,
46
+ };
47
+ const market = scope.market ?? CHARTERS_CONTENT_MARKET_ANY;
48
+ const acceptMT = scope.acceptMachineTranslated ?? true;
49
+ const cachedRows = await fetchCacheCandidates(db, entityId, market);
50
+ const eligibleRows = acceptMT ? cachedRows : cachedRows.filter((r) => !r.machine_translated);
51
+ const best = pickBestCachedLocale(eligibleRows.map((row) => ({ ...row, locale: row.locale })), scope.preferredLocales);
52
+ if (best && !isStale(best.candidate)) {
53
+ return finalizeFromCache(db, entityId, best, false, options);
54
+ }
55
+ if (best && isStale(best.candidate)) {
56
+ if (adapter?.getContent) {
57
+ void scheduleRefresh(db, adapter, adapterCtx, {
58
+ entity_module: "charters",
59
+ entity_id: entityId,
60
+ locale: scope.preferredLocales[0] ?? best.candidate.locale,
61
+ market,
62
+ currency: scope.currency,
63
+ });
64
+ }
65
+ return finalizeFromCache(db, entityId, best, true, options);
66
+ }
67
+ if (!adapter?.getContent) {
68
+ const overlays = await fetchOverlaysForEntity(db, "charters", entityId);
69
+ const synthesized = synthesizeCharterContent({ locale: scope.preferredLocales[0] ?? "en-GB" }, {
70
+ provenance,
71
+ overlays: overlays.map((o) => ({ field_path: o.field_path, value: o.value })),
72
+ });
73
+ return wrapSynthesized(synthesized, scope, false);
74
+ }
75
+ const fresh = await fetchFreshContent(db, adapter, adapterCtx, {
76
+ entity_module: "charters",
77
+ entity_id: entityId,
78
+ locale: scope.preferredLocales[0] ?? "en-GB",
79
+ market,
80
+ currency: scope.currency,
81
+ });
82
+ if (!fresh) {
83
+ const overlays = await fetchOverlaysForEntity(db, "charters", entityId);
84
+ const synthesized = synthesizeCharterContent({ locale: scope.preferredLocales[0] ?? "en-GB" }, {
85
+ provenance,
86
+ overlays: overlays.map((o) => ({ field_path: o.field_path, value: o.value })),
87
+ });
88
+ return wrapSynthesized(synthesized, scope, false);
89
+ }
90
+ return finalizeFresh(db, entityId, fresh, scope, options);
91
+ }
92
+ async function fetchCacheCandidates(db, entityId, market) {
93
+ return db
94
+ .select()
95
+ .from(chartersSourcedContentTable)
96
+ .where(and(eq(chartersSourcedContentTable.entity_id, entityId), eq(chartersSourcedContentTable.market, market), eq(chartersSourcedContentTable.content_schema_version, CHARTERS_CONTENT_SCHEMA_VERSION)));
97
+ }
98
+ async function fetchFreshContent(db, adapter, ctx, request) {
99
+ const result = await withContentRefreshLock(db, {
100
+ entityModule: request.entity_module,
101
+ entityId: request.entity_id,
102
+ locale: request.locale,
103
+ market: request.market,
104
+ }, async () => {
105
+ const got = await adapter.getContent(ctx, request);
106
+ const validation = validateCharterContent(got.content);
107
+ if (!validation.valid) {
108
+ throw new Error(`charters getContent for ${request.entity_id} failed validation: ${validation.reason}`);
109
+ }
110
+ await writeCacheRow(db, request, got);
111
+ return got;
112
+ });
113
+ return result ?? null;
114
+ }
115
+ function scheduleRefresh(db, adapter, ctx, request) {
116
+ void withContentRefreshLock(db, {
117
+ entityModule: request.entity_module,
118
+ entityId: request.entity_id,
119
+ locale: request.locale,
120
+ market: request.market,
121
+ }, async () => {
122
+ const got = await adapter.getContent(ctx, request);
123
+ const validation = validateCharterContent(got.content);
124
+ if (!validation.valid)
125
+ return;
126
+ await writeCacheRow(db, request, got);
127
+ }).catch(() => {
128
+ // intentional swallow — see §3.4 SWR refresh contract
129
+ });
130
+ }
131
+ async function writeCacheRow(db, request, result) {
132
+ const market = request.market ?? CHARTERS_CONTENT_MARKET_ANY;
133
+ const now = new Date();
134
+ // Coerce JSON-string dates to Date — see products writeCacheRow.
135
+ const sourceUpdatedAt = toDateOrNull(result.source_updated_at);
136
+ const freshUntil = toDateOrNull(result.fresh_until) ?? new Date(now.getTime() + CHARTERS_DEFAULT_TTL_MS);
137
+ await db
138
+ .insert(chartersSourcedContentTable)
139
+ .values({
140
+ entity_id: request.entity_id,
141
+ locale: request.locale,
142
+ market,
143
+ payload: result.content,
144
+ content_schema_version: result.content_schema_version,
145
+ returned_locale: result.returned_locale,
146
+ machine_translated: result.machine_translated ?? false,
147
+ source_updated_at: sourceUpdatedAt,
148
+ fetched_at: now,
149
+ fresh_until: freshUntil,
150
+ etag: result.etag ?? null,
151
+ fetch_status: "ok",
152
+ fetch_error: null,
153
+ })
154
+ .onConflictDoUpdate({
155
+ target: [
156
+ chartersSourcedContentTable.entity_id,
157
+ chartersSourcedContentTable.locale,
158
+ chartersSourcedContentTable.market,
159
+ ],
160
+ set: {
161
+ payload: result.content,
162
+ content_schema_version: result.content_schema_version,
163
+ returned_locale: result.returned_locale,
164
+ machine_translated: result.machine_translated ?? false,
165
+ source_updated_at: sourceUpdatedAt,
166
+ fetched_at: now,
167
+ fresh_until: freshUntil,
168
+ etag: result.etag ?? null,
169
+ fetch_status: "ok",
170
+ fetch_error: null,
171
+ },
172
+ });
173
+ }
174
+ async function finalizeFromCache(db, entityId, best, servedStale, options) {
175
+ const validation = validateCharterContent(best.candidate.payload);
176
+ if (!validation.valid) {
177
+ throw new Error(`charters cache row for ${entityId} (${best.candidate.locale}) failed validation: ${validation.reason}`);
178
+ }
179
+ const overlays = await fetchOverlaysForEntity(db, "charters", entityId);
180
+ const merged = mergeOverlaysIntoCharterContent(validation.content, overlays.map((o) => ({ field_path: o.field_path, value: o.value })), {
181
+ onOverlayError: options.onOverlayError
182
+ ? (e) => options.onOverlayError({
183
+ field_path: e.overlay.field_path,
184
+ reason: e.reason,
185
+ })
186
+ : undefined,
187
+ });
188
+ return {
189
+ content: merged,
190
+ resolution: {
191
+ candidate: { locale: best.candidate.locale, payload: merged },
192
+ served_locale: best.candidate.returned_locale,
193
+ match_kind: best.match_kind,
194
+ },
195
+ source: "sourced-cache",
196
+ served_stale: servedStale,
197
+ synthesized: false,
198
+ machine_translated: best.candidate.machine_translated,
199
+ };
200
+ }
201
+ async function finalizeFresh(db, entityId, fresh, scope, options) {
202
+ const cachedContent = charterContentSchema.parse(fresh.content);
203
+ const overlays = await fetchOverlaysForEntity(db, "charters", entityId);
204
+ const merged = mergeOverlaysIntoCharterContent(cachedContent, overlays.map((o) => ({ field_path: o.field_path, value: o.value })), {
205
+ onOverlayError: options.onOverlayError
206
+ ? (e) => options.onOverlayError({
207
+ field_path: e.overlay.field_path,
208
+ reason: e.reason,
209
+ })
210
+ : undefined,
211
+ });
212
+ return {
213
+ content: merged,
214
+ resolution: {
215
+ candidate: { locale: scope.preferredLocales[0] ?? fresh.returned_locale, payload: merged },
216
+ served_locale: fresh.returned_locale,
217
+ match_kind: scope.preferredLocales[0] === fresh.returned_locale ? "exact" : "language_match",
218
+ },
219
+ source: "sourced-fresh",
220
+ served_stale: false,
221
+ synthesized: false,
222
+ machine_translated: fresh.machine_translated ?? false,
223
+ };
224
+ }
225
+ function wrapSynthesized(synthesized, scope, servedStale) {
226
+ return {
227
+ content: synthesized.content,
228
+ resolution: {
229
+ candidate: { locale: synthesized.served_locale, payload: synthesized.content },
230
+ served_locale: synthesized.served_locale,
231
+ match_kind: scope.preferredLocales[0] === synthesized.served_locale ? "exact" : "any",
232
+ },
233
+ source: "synthesized",
234
+ served_stale: servedStale,
235
+ synthesized: true,
236
+ machine_translated: false,
237
+ };
238
+ }
239
+ /** Drift event consumer for the charters content cache. Per §3.4.1. */
240
+ export const invalidateCharterContentOnDrift = createInvalidateOnDrift(chartersSourcedContentTable, { entityModule: "charters" });
241
+ function toDateOrNull(value) {
242
+ if (!value)
243
+ return null;
244
+ if (value instanceof Date)
245
+ return value;
246
+ const parsed = new Date(value);
247
+ return Number.isNaN(parsed.getTime()) ? null : parsed;
248
+ }
@@ -0,0 +1,85 @@
1
+ import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
2
+ import { type BookingCharterDetail } from "./booking-extension.js";
3
+ /**
4
+ * Minimal subset of `@voyant-travel/legal`'s `contractsService` we depend on.
5
+ * Defined structurally so charters does NOT take a hard dependency on
6
+ * the legal package — templates wire the real service in. Mirrors the
7
+ * dependency-inversion pattern legal already uses for document
8
+ * generators / event resolveDb.
9
+ */
10
+ export interface CharterContractsService {
11
+ getDefaultTemplate(db: PostgresJsDatabase, query: {
12
+ scope?: string;
13
+ language?: string;
14
+ slug?: string;
15
+ }): Promise<{
16
+ id: string;
17
+ currentVersionId: string | null;
18
+ slug: string;
19
+ } | null>;
20
+ getTemplateById(db: PostgresJsDatabase, id: string): Promise<{
21
+ id: string;
22
+ currentVersionId: string | null;
23
+ slug: string;
24
+ } | null>;
25
+ createContract(db: PostgresJsDatabase, data: {
26
+ scope: "customer" | "supplier" | "partner" | "channel" | "other";
27
+ title: string;
28
+ templateVersionId?: string | null;
29
+ personId?: string | null;
30
+ organizationId?: string | null;
31
+ bookingId?: string | null;
32
+ variables?: Record<string, unknown> | null;
33
+ language?: string;
34
+ metadata?: Record<string, unknown> | null;
35
+ }): Promise<{
36
+ id: string;
37
+ } | null>;
38
+ }
39
+ export type GenerateMybaContractInput = {
40
+ bookingId: string;
41
+ /** Override the template that the booking_charter_details snapshotted. */
42
+ templateIdOverride?: string | null;
43
+ /** Locale for the contract; defaults to "en". */
44
+ language?: string;
45
+ /** Extra Liquid variables passed to the template renderer. Merged on top of
46
+ * the defaults generated from the booking + charter snapshot. */
47
+ extraVariables?: Record<string, unknown>;
48
+ title?: string;
49
+ };
50
+ export type GenerateMybaContractResult = {
51
+ status: "ok";
52
+ contractId: string;
53
+ detail: BookingCharterDetail;
54
+ } | {
55
+ status: "not_found";
56
+ } | {
57
+ status: "wrong_mode";
58
+ bookingMode: string;
59
+ } | {
60
+ status: "no_template";
61
+ detail: BookingCharterDetail;
62
+ } | {
63
+ status: "template_not_found";
64
+ templateId: string;
65
+ } | {
66
+ status: "contract_create_failed";
67
+ };
68
+ /**
69
+ * Generate a MYBA contract for a whole-yacht booking and link it back via
70
+ * `booking_charter_details.mybaContractId`. Idempotent in the
71
+ * already-generated case: if `mybaContractId` is set, returns ok without
72
+ * recreating.
73
+ *
74
+ * Resolves the template id with this precedence:
75
+ * 1. `input.templateIdOverride`
76
+ * 2. `booking_charter_details.mybaTemplateIdSnapshot` (recorded at
77
+ * booking-creation time from voyage override or product default)
78
+ * 3. legal's default contract template (scope='customer', the slug
79
+ * conventionally used for MYBA — caller can also pass a slug via
80
+ * override).
81
+ */
82
+ export declare const mybaService: {
83
+ generateContract(db: PostgresJsDatabase, contractsService: CharterContractsService, input: GenerateMybaContractInput): Promise<GenerateMybaContractResult>;
84
+ };
85
+ //# sourceMappingURL=service-myba.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-myba.d.ts","sourceRoot":"","sources":["../src/service-myba.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAEjE,OAAO,EAAE,KAAK,oBAAoB,EAAyB,MAAM,wBAAwB,CAAA;AAEzF;;;;;;GAMG;AACH,MAAM,WAAW,uBAAuB;IACtC,kBAAkB,CAChB,EAAE,EAAE,kBAAkB,EACtB,KAAK,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GAC1D,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAA;IAEhF,eAAe,CACb,EAAE,EAAE,kBAAkB,EACtB,EAAE,EAAE,MAAM,GACT,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAA;IAEhF,cAAc,CACZ,EAAE,EAAE,kBAAkB,EACtB,IAAI,EAAE;QACJ,KAAK,EAAE,UAAU,GAAG,UAAU,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,CAAA;QAChE,KAAK,EAAE,MAAM,CAAA;QACb,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;QACjC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;QACxB,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;QAC9B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;QACzB,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;QAC1C,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;KAC1C,GACA,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAA;CAClC;AAED,MAAM,MAAM,yBAAyB,GAAG;IACtC,SAAS,EAAE,MAAM,CAAA;IACjB,0EAA0E;IAC1E,kBAAkB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAClC,iDAAiD;IACjD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;sEACkE;IAClE,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACxC,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,CAAA;AAED,MAAM,MAAM,0BAA0B,GAClC;IAAE,MAAM,EAAE,IAAI,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,oBAAoB,CAAA;CAAE,GAClE;IAAE,MAAM,EAAE,WAAW,CAAA;CAAE,GACvB;IAAE,MAAM,EAAE,YAAY,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GAC7C;IAAE,MAAM,EAAE,aAAa,CAAC;IAAC,MAAM,EAAE,oBAAoB,CAAA;CAAE,GACvD;IAAE,MAAM,EAAE,oBAAoB,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GACpD;IAAE,MAAM,EAAE,wBAAwB,CAAA;CAAE,CAAA;AAExC;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,WAAW;yBAEhB,kBAAkB,oBACJ,uBAAuB,SAClC,yBAAyB,GAC/B,OAAO,CAAC,0BAA0B,CAAC;CAqEvC,CAAA"}
@@ -0,0 +1,88 @@
1
+ import { eq } from "drizzle-orm";
2
+ import { bookingCharterDetails } from "./booking-extension.js";
3
+ /**
4
+ * Generate a MYBA contract for a whole-yacht booking and link it back via
5
+ * `booking_charter_details.mybaContractId`. Idempotent in the
6
+ * already-generated case: if `mybaContractId` is set, returns ok without
7
+ * recreating.
8
+ *
9
+ * Resolves the template id with this precedence:
10
+ * 1. `input.templateIdOverride`
11
+ * 2. `booking_charter_details.mybaTemplateIdSnapshot` (recorded at
12
+ * booking-creation time from voyage override or product default)
13
+ * 3. legal's default contract template (scope='customer', the slug
14
+ * conventionally used for MYBA — caller can also pass a slug via
15
+ * override).
16
+ */
17
+ export const mybaService = {
18
+ async generateContract(db, contractsService, input) {
19
+ const [detail] = await db
20
+ .select()
21
+ .from(bookingCharterDetails)
22
+ .where(eq(bookingCharterDetails.bookingId, input.bookingId))
23
+ .limit(1);
24
+ if (!detail)
25
+ return { status: "not_found" };
26
+ if (detail.bookingMode !== "whole_yacht") {
27
+ return { status: "wrong_mode", bookingMode: detail.bookingMode };
28
+ }
29
+ if (detail.mybaContractId) {
30
+ return { status: "ok", contractId: detail.mybaContractId, detail };
31
+ }
32
+ const templateId = input.templateIdOverride ?? detail.mybaTemplateIdSnapshot;
33
+ let template = null;
34
+ if (templateId) {
35
+ template = await contractsService.getTemplateById(db, templateId);
36
+ if (!template)
37
+ return { status: "template_not_found", templateId };
38
+ }
39
+ else {
40
+ template = await contractsService.getDefaultTemplate(db, {
41
+ scope: "customer",
42
+ language: input.language ?? "en",
43
+ });
44
+ if (!template)
45
+ return { status: "no_template", detail };
46
+ }
47
+ const variables = {
48
+ bookingId: detail.bookingId,
49
+ voyageId: detail.voyageId,
50
+ voyageDisplayName: detail.voyageDisplayName,
51
+ yachtName: detail.yachtName,
52
+ yachtId: detail.yachtId,
53
+ charterArea: detail.charterAreaSnapshot,
54
+ guestCount: detail.guestCount,
55
+ currency: detail.quotedCurrency,
56
+ charterFee: detail.quotedCharterFee,
57
+ apaPercent: detail.apaPercent,
58
+ apaAmount: detail.apaAmount,
59
+ total: detail.quotedTotal,
60
+ ...(input.extraVariables ?? {}),
61
+ };
62
+ const contract = await contractsService.createContract(db, {
63
+ scope: "customer",
64
+ title: input.title ??
65
+ `MYBA charter agreement — ${detail.voyageDisplayName ?? detail.voyageId ?? detail.bookingId}`,
66
+ templateVersionId: template.currentVersionId,
67
+ bookingId: detail.bookingId,
68
+ variables,
69
+ language: input.language ?? "en",
70
+ metadata: {
71
+ source: "charters",
72
+ bookingMode: "whole_yacht",
73
+ templateId: template.id,
74
+ templateSlug: template.slug,
75
+ },
76
+ });
77
+ if (!contract)
78
+ return { status: "contract_create_failed" };
79
+ const [updated] = await db
80
+ .update(bookingCharterDetails)
81
+ .set({ mybaContractId: contract.id, updatedAt: new Date() })
82
+ .where(eq(bookingCharterDetails.bookingId, input.bookingId))
83
+ .returning();
84
+ if (!updated)
85
+ return { status: "contract_create_failed" };
86
+ return { status: "ok", contractId: contract.id, detail: updated };
87
+ },
88
+ };
@@ -0,0 +1,64 @@
1
+ import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
2
+ import { type CharterVoyage } from "./schema-core.js";
3
+ import { type CharterSuite } from "./schema-pricing.js";
4
+ export type PerSuiteQuote = {
5
+ mode: "per_suite";
6
+ voyageId: string;
7
+ suiteId: string;
8
+ suiteName: string;
9
+ currency: string;
10
+ suitePrice: string;
11
+ portFee: string | null;
12
+ total: string;
13
+ };
14
+ export type ComposePerSuiteQuoteInput = {
15
+ voyageId: string;
16
+ suite: Pick<CharterSuite, "id" | "suiteName" | "pricesByCurrency" | "portFeesByCurrency">;
17
+ currency: string;
18
+ };
19
+ export declare function composePerSuiteQuote(input: ComposePerSuiteQuoteInput): PerSuiteQuote;
20
+ export type WholeYachtQuote = {
21
+ mode: "whole_yacht";
22
+ voyageId: string;
23
+ currency: string;
24
+ charterFee: string;
25
+ apaPercent: string;
26
+ apaAmount: string;
27
+ total: string;
28
+ };
29
+ export type ComposeWholeYachtQuoteInput = {
30
+ voyage: Pick<CharterVoyage, "id" | "wholeYachtPricesByCurrency" | "apaPercentOverride">;
31
+ /**
32
+ * The product's defaultApaPercent — passed in by the caller so this stays
33
+ * a pure function. Voyage-level override takes precedence.
34
+ */
35
+ productDefaultApaPercent: string | null;
36
+ currency: string;
37
+ };
38
+ export declare function composeWholeYachtQuote(input: ComposeWholeYachtQuoteInput): WholeYachtQuote;
39
+ /**
40
+ * Compute APA amount from a charter fee and a percent. Useful for finance-side
41
+ * recalculation without needing to re-quote the whole voyage.
42
+ */
43
+ export declare function computeApaAmount(charterFee: string, apaPercent: string): string;
44
+ export declare const pricingService: {
45
+ quotePerSuite(db: PostgresJsDatabase, args: {
46
+ suiteId: string;
47
+ currency: string;
48
+ }): Promise<PerSuiteQuote>;
49
+ quoteWholeYacht(db: PostgresJsDatabase, args: {
50
+ voyageId: string;
51
+ currency: string;
52
+ }): Promise<WholeYachtQuote>;
53
+ /**
54
+ * Lowest published per-suite price for a voyage, in the requested currency.
55
+ * Returns `null` when no available suite has that currency. The previous
56
+ * `lowestSuitePriceUSD` shape was renamed and generalized as part of #355
57
+ * (browse-currency policy is a deployment choice, not a hardcoded USD).
58
+ */
59
+ lowestSuitePriceForCurrency(db: PostgresJsDatabase, voyageId: string, currency: string): Promise<{
60
+ suiteId: string;
61
+ price: string;
62
+ } | null>;
63
+ };
64
+ //# sourceMappingURL=service-pricing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-pricing.d.ts","sourceRoot":"","sources":["../src/service-pricing.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAEjE,OAAO,EAAE,KAAK,aAAa,EAAmC,MAAM,kBAAkB,CAAA;AACtF,OAAO,EAAE,KAAK,YAAY,EAAiB,MAAM,qBAAqB,CAAA;AAgEtE,MAAM,MAAM,aAAa,GAAG;IAC1B,IAAI,EAAE,WAAW,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,EAAE,MAAM,CAAA;IAClB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,KAAK,EAAE,MAAM,CAAA;CACd,CAAA;AAED,MAAM,MAAM,yBAAyB,GAAG;IACtC,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,IAAI,CAAC,YAAY,EAAE,IAAI,GAAG,WAAW,GAAG,kBAAkB,GAAG,oBAAoB,CAAC,CAAA;IACzF,QAAQ,EAAE,MAAM,CAAA;CACjB,CAAA;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,yBAAyB,GAAG,aAAa,CAqBpF;AAID,MAAM,MAAM,eAAe,GAAG;IAC5B,IAAI,EAAE,aAAa,CAAA;IACnB,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;CAMd,CAAA;AAED,MAAM,MAAM,2BAA2B,GAAG;IACxC,MAAM,EAAE,IAAI,CAAC,aAAa,EAAE,IAAI,GAAG,4BAA4B,GAAG,oBAAoB,CAAC,CAAA;IACvF;;;OAGG;IACH,wBAAwB,EAAE,MAAM,GAAG,IAAI,CAAA;IACvC,QAAQ,EAAE,MAAM,CAAA;CACjB,CAAA;AAED,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,2BAA2B,GAAG,eAAe,CA2B1F;AAID;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAG/E;AAID,eAAO,MAAM,cAAc;sBAEnB,kBAAkB,QAChB;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,GAC1C,OAAO,CAAC,aAAa,CAAC;wBAenB,kBAAkB,QAChB;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,GAC3C,OAAO,CAAC,eAAe,CAAC;IAqB3B;;;;;OAKG;oCAEG,kBAAkB,YACZ,MAAM,YACN,MAAM,GACf,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;CAsBtD,CAAA"}