@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.
- package/LICENSE +201 -0
- package/README.md +16 -0
- package/dist/adapters/index.d.ts +254 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +16 -0
- package/dist/adapters/memoize.d.ts +28 -0
- package/dist/adapters/memoize.d.ts.map +1 -0
- package/dist/adapters/memoize.js +121 -0
- package/dist/adapters/mock.d.ts +50 -0
- package/dist/adapters/mock.d.ts.map +1 -0
- package/dist/adapters/mock.js +194 -0
- package/dist/adapters/registry.d.ts +24 -0
- package/dist/adapters/registry.d.ts.map +1 -0
- package/dist/adapters/registry.js +40 -0
- package/dist/booking-extension.d.ts +895 -0
- package/dist/booking-extension.d.ts.map +1 -0
- package/dist/booking-extension.js +339 -0
- package/dist/catalog-policy.d.ts +23 -0
- package/dist/catalog-policy.d.ts.map +1 -0
- package/dist/catalog-policy.js +400 -0
- package/dist/content-shape.d.ts +5 -0
- package/dist/content-shape.d.ts.map +1 -0
- package/dist/content-shape.js +13 -0
- package/dist/draft-shape.d.ts +29 -0
- package/dist/draft-shape.d.ts.map +1 -0
- package/dist/draft-shape.js +63 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +55 -0
- package/dist/lib/key.d.ts +22 -0
- package/dist/lib/key.d.ts.map +1 -0
- package/dist/lib/key.js +24 -0
- package/dist/routes-public.d.ts +785 -0
- package/dist/routes-public.d.ts.map +1 -0
- package/dist/routes-public.js +234 -0
- package/dist/routes.d.ts +1744 -0
- package/dist/routes.d.ts.map +1 -0
- package/dist/routes.js +543 -0
- package/dist/schema-core.d.ts +815 -0
- package/dist/schema-core.d.ts.map +1 -0
- package/dist/schema-core.js +98 -0
- package/dist/schema-itinerary.d.ts +239 -0
- package/dist/schema-itinerary.d.ts.map +1 -0
- package/dist/schema-itinerary.js +30 -0
- package/dist/schema-pricing.d.ts +385 -0
- package/dist/schema-pricing.d.ts.map +1 -0
- package/dist/schema-pricing.js +62 -0
- package/dist/schema-shared.d.ts +8 -0
- package/dist/schema-shared.d.ts.map +1 -0
- package/dist/schema-shared.js +37 -0
- package/dist/schema-sourced-content.d.ts +253 -0
- package/dist/schema-sourced-content.d.ts.map +1 -0
- package/dist/schema-sourced-content.js +44 -0
- package/dist/schema-yachts.d.ts +367 -0
- package/dist/schema-yachts.d.ts.map +1 -0
- package/dist/schema-yachts.js +30 -0
- package/dist/schema.d.ts +8 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +7 -0
- package/dist/service-bookings-helpers.d.ts +20 -0
- package/dist/service-bookings-helpers.d.ts.map +1 -0
- package/dist/service-bookings-helpers.js +67 -0
- package/dist/service-bookings-local.d.ts +5 -0
- package/dist/service-bookings-local.d.ts.map +1 -0
- package/dist/service-bookings-local.js +177 -0
- package/dist/service-bookings-types.d.ts +88 -0
- package/dist/service-bookings-types.d.ts.map +1 -0
- package/dist/service-bookings-types.js +1 -0
- package/dist/service-bookings.d.ts +36 -0
- package/dist/service-bookings.d.ts.map +1 -0
- package/dist/service-bookings.js +267 -0
- package/dist/service-catalog-plane.d.ts +58 -0
- package/dist/service-catalog-plane.d.ts.map +1 -0
- package/dist/service-catalog-plane.js +145 -0
- package/dist/service-content-synthesizer.d.ts +42 -0
- package/dist/service-content-synthesizer.d.ts.map +1 -0
- package/dist/service-content-synthesizer.js +122 -0
- package/dist/service-content.d.ts +43 -0
- package/dist/service-content.d.ts.map +1 -0
- package/dist/service-content.js +248 -0
- package/dist/service-myba.d.ts +85 -0
- package/dist/service-myba.d.ts.map +1 -0
- package/dist/service-myba.js +88 -0
- package/dist/service-pricing.d.ts +64 -0
- package/dist/service-pricing.d.ts.map +1 -0
- package/dist/service-pricing.js +167 -0
- package/dist/service.d.ts +131 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +279 -0
- package/dist/validation-core.d.ts +152 -0
- package/dist/validation-core.d.ts.map +1 -0
- package/dist/validation-core.js +66 -0
- package/dist/validation-itinerary.d.ts +43 -0
- package/dist/validation-itinerary.d.ts.map +1 -0
- package/dist/validation-itinerary.js +19 -0
- package/dist/validation-pricing.d.ts +103 -0
- package/dist/validation-pricing.d.ts.map +1 -0
- package/dist/validation-pricing.js +28 -0
- package/dist/validation-shared.d.ts +61 -0
- package/dist/validation-shared.d.ts.map +1 -0
- package/dist/validation-shared.js +60 -0
- package/dist/validation-yachts.d.ts +76 -0
- package/dist/validation-yachts.d.ts.map +1 -0
- package/dist/validation-yachts.js +36 -0
- package/dist/validation.d.ts +6 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +5 -0
- package/package.json +116 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"booking-extension.d.ts","sourceRoot":"","sources":["../src/booking-extension.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAA;AAY/D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAEjE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAMvB,MAAM,MAAM,gBAAgB,GAAG;IAC7B,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,UAAU,EAAE,MAAM,CAAA;IAClB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CACvB,CAAA;AAID;;;;;;;;;;GAUG;AACH,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA+DjC,CAAA;AAED,MAAM,MAAM,oBAAoB,GAAG,OAAO,qBAAqB,CAAC,YAAY,CAAA;AAC5E,MAAM,MAAM,uBAAuB,GAAG,OAAO,qBAAqB,CAAC,YAAY,CAAA;AAa/E,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAoFlC,CAAA;AAEJ,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAA;AAgB3E,eAAO,MAAM,4BAA4B;YACzB,kBAAkB,aAAa,MAAM,GAAG,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC;eAUpF,kBAAkB,aACX,MAAM,QACX,mBAAmB,GACxB,OAAO,CAAC,oBAAoB,CAAC;eAsBf,kBAAkB,aAAa,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAQzE;;;;OAIG;yBAEG,kBAAkB,aACX,MAAM,QACX;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAC7C,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC;IAqBvC;;;;;OAKG;qBAEG,kBAAkB,aACX,MAAM,QACX;QACJ,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,YAAY,CAAC,EAAE,MAAM,CAAA;QACrB,MAAM,CAAC,EAAE,OAAO,CAAA;QAChB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KACrB,GACA,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC;CAkBxC,CAAA;AA4BD,KAAK,GAAG,GAAG;IACT,SAAS,EAAE;QACT,EAAE,EAAE,kBAAkB,CAAA;QACtB,MAAM,CAAC,EAAE,MAAM,CAAA;KAChB,CAAA;CACF,CAAA;AAED,eAAO,MAAM,8BAA8B;;;;;;;;;;;;;;;;;;;;;;;;;;;uCArV1B,MAAM;oCACT,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;uCADH,MAAM;oCACT,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;uCADH,MAAM;oCACT,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;uCADH,MAAM;oCACT,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;oDAgYhB,CAAA;AASJ,eAAO,MAAM,wBAAwB,EAAE,aAGtC,CAAA"}
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import { typeIdRef } from "@voyant-travel/db/lib/typeid-column";
|
|
2
|
+
import { parseJsonBody } from "@voyant-travel/hono";
|
|
3
|
+
import { eq } from "drizzle-orm";
|
|
4
|
+
import { char, index, jsonb, numeric, pgTable, smallint, text, timestamp, } from "drizzle-orm/pg-core";
|
|
5
|
+
import { Hono } from "hono";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { charterBookingModeEnum, charterSourceEnum } from "./schema-shared.js";
|
|
8
|
+
// ---------- schema ----------
|
|
9
|
+
/**
|
|
10
|
+
* 1:1 booking extension for charters. Holds the mode discriminator
|
|
11
|
+
* (`per_suite` | `whole_yacht`), provenance, snapshot pricing fields
|
|
12
|
+
* for both modes, and APA reconciliation state for whole-yacht charters.
|
|
13
|
+
*
|
|
14
|
+
* Soft FKs to charter_voyages / charter_suites (text columns; no
|
|
15
|
+
* cross-module .references()) follow the schema discipline rule.
|
|
16
|
+
*
|
|
17
|
+
* APA fields are only meaningful when bookingMode = 'whole_yacht'. They
|
|
18
|
+
* stay null for per_suite bookings.
|
|
19
|
+
*/
|
|
20
|
+
export const bookingCharterDetails = pgTable("booking_charter_details", {
|
|
21
|
+
bookingId: text("booking_id").primaryKey(),
|
|
22
|
+
bookingMode: charterBookingModeEnum("booking_mode").notNull(),
|
|
23
|
+
// Provenance — local rows reference local TypeIDs; external rows carry
|
|
24
|
+
// a sourceRef back to the upstream adapter and have nullable local FKs.
|
|
25
|
+
source: charterSourceEnum("source").notNull().default("local"),
|
|
26
|
+
sourceProvider: text("source_provider"),
|
|
27
|
+
sourceRef: jsonb("source_ref").$type(),
|
|
28
|
+
voyageId: typeIdRef("voyage_id"),
|
|
29
|
+
suiteId: typeIdRef("suite_id"),
|
|
30
|
+
yachtId: typeIdRef("yacht_id"),
|
|
31
|
+
// Display hints — populated for both local and external so the UI can
|
|
32
|
+
// render booking history without re-resolving the voyage every time.
|
|
33
|
+
voyageDisplayName: text("voyage_display_name"),
|
|
34
|
+
suiteDisplayName: text("suite_display_name"),
|
|
35
|
+
yachtName: text("yacht_name"),
|
|
36
|
+
charterAreaSnapshot: text("charter_area_snapshot"),
|
|
37
|
+
guestCount: smallint("guest_count").notNull(),
|
|
38
|
+
quotedCurrency: char("quoted_currency", { length: 3 }).notNull(),
|
|
39
|
+
// per_suite pricing snapshot
|
|
40
|
+
quotedSuitePrice: numeric("quoted_suite_price", { precision: 12, scale: 2 }),
|
|
41
|
+
quotedPortFee: numeric("quoted_port_fee", { precision: 12, scale: 2 }),
|
|
42
|
+
// whole_yacht pricing snapshot
|
|
43
|
+
quotedCharterFee: numeric("quoted_charter_fee", { precision: 15, scale: 2 }),
|
|
44
|
+
apaPercent: numeric("apa_percent", { precision: 5, scale: 2 }),
|
|
45
|
+
apaAmount: numeric("apa_amount", { precision: 15, scale: 2 }),
|
|
46
|
+
quotedTotal: numeric("quoted_total", { precision: 15, scale: 2 }).notNull(),
|
|
47
|
+
// MYBA contract — soft FK; nullable until generateContract() runs.
|
|
48
|
+
mybaTemplateIdSnapshot: text("myba_template_id_snapshot"),
|
|
49
|
+
mybaContractId: typeIdRef("myba_contract_id"),
|
|
50
|
+
// APA reconciliation state (whole_yacht only). All optional & default 0.
|
|
51
|
+
apaPaidAmount: numeric("apa_paid_amount", { precision: 15, scale: 2 }).default("0.00"),
|
|
52
|
+
apaSpentAmount: numeric("apa_spent_amount", { precision: 15, scale: 2 }).default("0.00"),
|
|
53
|
+
apaRefundAmount: numeric("apa_refund_amount", { precision: 15, scale: 2 }).default("0.00"),
|
|
54
|
+
apaSettledAt: timestamp("apa_settled_at", { withTimezone: true }),
|
|
55
|
+
connectorBookingRef: text("connector_booking_ref"),
|
|
56
|
+
connectorStatus: text("connector_status"),
|
|
57
|
+
notes: text("notes"),
|
|
58
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
59
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
|
60
|
+
}, (t) => [
|
|
61
|
+
index("idx_bchd_mode").on(t.bookingMode),
|
|
62
|
+
index("idx_bchd_source").on(t.source),
|
|
63
|
+
index("idx_bchd_voyage").on(t.voyageId),
|
|
64
|
+
index("idx_bchd_suite").on(t.suiteId),
|
|
65
|
+
index("idx_bchd_yacht").on(t.yachtId),
|
|
66
|
+
index("idx_bchd_myba_contract").on(t.mybaContractId),
|
|
67
|
+
index("idx_bchd_connector_ref").on(t.connectorBookingRef),
|
|
68
|
+
index("idx_bchd_provider").on(t.sourceProvider),
|
|
69
|
+
]);
|
|
70
|
+
// ---------- validation ----------
|
|
71
|
+
const sourceRefValueSchema = z
|
|
72
|
+
.object({
|
|
73
|
+
connectionId: z.string().optional(),
|
|
74
|
+
externalId: z.string(),
|
|
75
|
+
})
|
|
76
|
+
.catchall(z.unknown());
|
|
77
|
+
const moneyString = z.string().regex(/^-?\d+(\.\d{1,2})?$/);
|
|
78
|
+
export const charterDetailUpsertSchema = z
|
|
79
|
+
.object({
|
|
80
|
+
bookingMode: z.enum(["per_suite", "whole_yacht"]),
|
|
81
|
+
source: z.enum(["local", "external"]).default("local"),
|
|
82
|
+
sourceProvider: z.string().optional().nullable(),
|
|
83
|
+
sourceRef: sourceRefValueSchema.optional().nullable(),
|
|
84
|
+
voyageId: z.string().optional().nullable(),
|
|
85
|
+
suiteId: z.string().optional().nullable(),
|
|
86
|
+
yachtId: z.string().optional().nullable(),
|
|
87
|
+
voyageDisplayName: z.string().optional().nullable(),
|
|
88
|
+
suiteDisplayName: z.string().optional().nullable(),
|
|
89
|
+
yachtName: z.string().optional().nullable(),
|
|
90
|
+
charterAreaSnapshot: z.string().optional().nullable(),
|
|
91
|
+
guestCount: z.number().int().min(1).max(50),
|
|
92
|
+
quotedCurrency: z.string().length(3),
|
|
93
|
+
quotedSuitePrice: moneyString.optional().nullable(),
|
|
94
|
+
quotedPortFee: moneyString.optional().nullable(),
|
|
95
|
+
quotedCharterFee: moneyString.optional().nullable(),
|
|
96
|
+
apaPercent: moneyString.optional().nullable(),
|
|
97
|
+
apaAmount: moneyString.optional().nullable(),
|
|
98
|
+
quotedTotal: moneyString,
|
|
99
|
+
mybaTemplateIdSnapshot: z.string().optional().nullable(),
|
|
100
|
+
mybaContractId: z.string().optional().nullable(),
|
|
101
|
+
apaPaidAmount: moneyString.optional().nullable(),
|
|
102
|
+
apaSpentAmount: moneyString.optional().nullable(),
|
|
103
|
+
apaRefundAmount: moneyString.optional().nullable(),
|
|
104
|
+
connectorBookingRef: z.string().optional().nullable(),
|
|
105
|
+
connectorStatus: z.string().optional().nullable(),
|
|
106
|
+
notes: z.string().optional().nullable(),
|
|
107
|
+
})
|
|
108
|
+
.superRefine((value, ctx) => {
|
|
109
|
+
if (value.source === "local") {
|
|
110
|
+
if (!value.voyageId) {
|
|
111
|
+
ctx.addIssue({
|
|
112
|
+
code: "custom",
|
|
113
|
+
path: ["voyageId"],
|
|
114
|
+
message: "voyageId is required when source='local'",
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
if (value.bookingMode === "per_suite" && !value.suiteId) {
|
|
118
|
+
ctx.addIssue({
|
|
119
|
+
code: "custom",
|
|
120
|
+
path: ["suiteId"],
|
|
121
|
+
message: "suiteId is required for local per_suite bookings",
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
if (!value.sourceProvider) {
|
|
127
|
+
ctx.addIssue({
|
|
128
|
+
code: "custom",
|
|
129
|
+
path: ["sourceProvider"],
|
|
130
|
+
message: "sourceProvider is required when source='external'",
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
if (!value.sourceRef) {
|
|
134
|
+
ctx.addIssue({
|
|
135
|
+
code: "custom",
|
|
136
|
+
path: ["sourceRef"],
|
|
137
|
+
message: "sourceRef is required when source='external'",
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (value.bookingMode === "whole_yacht") {
|
|
142
|
+
if (!value.quotedCharterFee) {
|
|
143
|
+
ctx.addIssue({
|
|
144
|
+
code: "custom",
|
|
145
|
+
path: ["quotedCharterFee"],
|
|
146
|
+
message: "quotedCharterFee is required for whole_yacht bookings",
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
if (!value.apaPercent || !value.apaAmount) {
|
|
150
|
+
ctx.addIssue({
|
|
151
|
+
code: "custom",
|
|
152
|
+
path: ["apaPercent"],
|
|
153
|
+
message: "apaPercent + apaAmount required for whole_yacht bookings",
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
else if (value.bookingMode === "per_suite" && !value.quotedSuitePrice) {
|
|
158
|
+
ctx.addIssue({
|
|
159
|
+
code: "custom",
|
|
160
|
+
path: ["quotedSuitePrice"],
|
|
161
|
+
message: "quotedSuitePrice is required for per_suite bookings",
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
const apaPaymentSchema = z.object({
|
|
166
|
+
amount: moneyString,
|
|
167
|
+
note: z.string().optional().nullable(),
|
|
168
|
+
});
|
|
169
|
+
const apaReconcileSchema = z.object({
|
|
170
|
+
spentAmount: moneyString.optional(),
|
|
171
|
+
refundAmount: moneyString.optional(),
|
|
172
|
+
settle: z.boolean().default(false),
|
|
173
|
+
note: z.string().optional().nullable(),
|
|
174
|
+
});
|
|
175
|
+
// ---------- services ----------
|
|
176
|
+
export const bookingCharterDetailsService = {
|
|
177
|
+
async get(db, bookingId) {
|
|
178
|
+
const [row] = await db
|
|
179
|
+
.select()
|
|
180
|
+
.from(bookingCharterDetails)
|
|
181
|
+
.where(eq(bookingCharterDetails.bookingId, bookingId))
|
|
182
|
+
.limit(1);
|
|
183
|
+
return row ?? null;
|
|
184
|
+
},
|
|
185
|
+
async upsert(db, bookingId, data) {
|
|
186
|
+
const payload = { ...data, bookingId };
|
|
187
|
+
const [existing] = await db
|
|
188
|
+
.select({ bookingId: bookingCharterDetails.bookingId })
|
|
189
|
+
.from(bookingCharterDetails)
|
|
190
|
+
.where(eq(bookingCharterDetails.bookingId, bookingId))
|
|
191
|
+
.limit(1);
|
|
192
|
+
if (existing) {
|
|
193
|
+
const [row] = await db
|
|
194
|
+
.update(bookingCharterDetails)
|
|
195
|
+
.set({ ...payload, updatedAt: new Date() })
|
|
196
|
+
.where(eq(bookingCharterDetails.bookingId, bookingId))
|
|
197
|
+
.returning();
|
|
198
|
+
if (!row)
|
|
199
|
+
throw new Error("Failed to update booking charter details");
|
|
200
|
+
return row;
|
|
201
|
+
}
|
|
202
|
+
const [row] = await db.insert(bookingCharterDetails).values(payload).returning();
|
|
203
|
+
if (!row)
|
|
204
|
+
throw new Error("Failed to insert booking charter details");
|
|
205
|
+
return row;
|
|
206
|
+
},
|
|
207
|
+
async remove(db, bookingId) {
|
|
208
|
+
const result = await db
|
|
209
|
+
.delete(bookingCharterDetails)
|
|
210
|
+
.where(eq(bookingCharterDetails.bookingId, bookingId))
|
|
211
|
+
.returning({ id: bookingCharterDetails.bookingId });
|
|
212
|
+
return result.length > 0;
|
|
213
|
+
},
|
|
214
|
+
/**
|
|
215
|
+
* Record an APA payment. Adds to apaPaidAmount; does not validate
|
|
216
|
+
* against quoted apaAmount because real-world APA settlements may
|
|
217
|
+
* involve top-ups during the charter.
|
|
218
|
+
*/
|
|
219
|
+
async recordApaPayment(db, bookingId, args) {
|
|
220
|
+
const existing = await this.get(db, bookingId);
|
|
221
|
+
if (!existing)
|
|
222
|
+
return null;
|
|
223
|
+
if (existing.bookingMode !== "whole_yacht") {
|
|
224
|
+
throw new Error("APA payments only apply to whole_yacht bookings");
|
|
225
|
+
}
|
|
226
|
+
const newPaid = addDecimal(existing.apaPaidAmount ?? "0.00", args.amount);
|
|
227
|
+
const [row] = await db
|
|
228
|
+
.update(bookingCharterDetails)
|
|
229
|
+
.set({
|
|
230
|
+
apaPaidAmount: newPaid,
|
|
231
|
+
notes: args.note
|
|
232
|
+
? appendNote(existing.notes, `APA payment ${args.amount}: ${args.note}`)
|
|
233
|
+
: existing.notes,
|
|
234
|
+
updatedAt: new Date(),
|
|
235
|
+
})
|
|
236
|
+
.where(eq(bookingCharterDetails.bookingId, bookingId))
|
|
237
|
+
.returning();
|
|
238
|
+
return row ?? null;
|
|
239
|
+
},
|
|
240
|
+
/**
|
|
241
|
+
* Post-charter APA reconciliation. Records what was actually spent on
|
|
242
|
+
* board + any refund due back to the charterer. When `settle: true`
|
|
243
|
+
* stamps `apaSettledAt`. Caller is responsible for kicking off any
|
|
244
|
+
* downstream finance flow (refund issuance, etc.).
|
|
245
|
+
*/
|
|
246
|
+
async reconcileApa(db, bookingId, args) {
|
|
247
|
+
const existing = await this.get(db, bookingId);
|
|
248
|
+
if (!existing)
|
|
249
|
+
return null;
|
|
250
|
+
if (existing.bookingMode !== "whole_yacht") {
|
|
251
|
+
throw new Error("APA reconciliation only applies to whole_yacht bookings");
|
|
252
|
+
}
|
|
253
|
+
const set = { updatedAt: new Date() };
|
|
254
|
+
if (args.spentAmount !== undefined)
|
|
255
|
+
set.apaSpentAmount = args.spentAmount;
|
|
256
|
+
if (args.refundAmount !== undefined)
|
|
257
|
+
set.apaRefundAmount = args.refundAmount;
|
|
258
|
+
if (args.settle)
|
|
259
|
+
set.apaSettledAt = new Date();
|
|
260
|
+
if (args.note)
|
|
261
|
+
set.notes = appendNote(existing.notes, `APA reconcile: ${args.note}`);
|
|
262
|
+
const [row] = await db
|
|
263
|
+
.update(bookingCharterDetails)
|
|
264
|
+
.set(set)
|
|
265
|
+
.where(eq(bookingCharterDetails.bookingId, bookingId))
|
|
266
|
+
.returning();
|
|
267
|
+
return row ?? null;
|
|
268
|
+
},
|
|
269
|
+
};
|
|
270
|
+
function addDecimal(a, b) {
|
|
271
|
+
// Both validated to ^-?\d+(\.\d{1,2})?$. Convert to cents (BigInt) then back.
|
|
272
|
+
return centsToString(stringToCents(a) + stringToCents(b));
|
|
273
|
+
}
|
|
274
|
+
function stringToCents(s) {
|
|
275
|
+
const negative = s.startsWith("-");
|
|
276
|
+
const abs = negative ? s.slice(1) : s;
|
|
277
|
+
const [whole = "0", frac = ""] = abs.split(".");
|
|
278
|
+
const fracPadded = `${frac}00`.slice(0, 2);
|
|
279
|
+
const cents = BigInt(whole) * 100n + BigInt(fracPadded);
|
|
280
|
+
return negative ? -cents : cents;
|
|
281
|
+
}
|
|
282
|
+
function centsToString(c) {
|
|
283
|
+
const negative = c < 0n;
|
|
284
|
+
const abs = negative ? -c : c;
|
|
285
|
+
const whole = abs / 100n;
|
|
286
|
+
const frac = (abs % 100n).toString().padStart(2, "0");
|
|
287
|
+
return `${negative ? "-" : ""}${whole.toString()}.${frac}`;
|
|
288
|
+
}
|
|
289
|
+
function appendNote(existing, addition) {
|
|
290
|
+
if (!existing)
|
|
291
|
+
return addition;
|
|
292
|
+
return `${existing}\n${addition}`;
|
|
293
|
+
}
|
|
294
|
+
export const chartersBookingExtensionRoutes = new Hono()
|
|
295
|
+
.get("/:bookingId/charter-details", async (c) => {
|
|
296
|
+
const row = await bookingCharterDetailsService.get(c.get("db"), c.req.param("bookingId"));
|
|
297
|
+
if (!row)
|
|
298
|
+
return c.json({ error: "not_found" }, 404);
|
|
299
|
+
return c.json({ data: row });
|
|
300
|
+
})
|
|
301
|
+
.put("/:bookingId/charter-details", async (c) => {
|
|
302
|
+
const data = await parseJsonBody(c, charterDetailUpsertSchema);
|
|
303
|
+
const row = await bookingCharterDetailsService.upsert(c.get("db"), c.req.param("bookingId"), data);
|
|
304
|
+
return c.json({ data: row });
|
|
305
|
+
})
|
|
306
|
+
.delete("/:bookingId/charter-details", async (c) => {
|
|
307
|
+
const ok = await bookingCharterDetailsService.remove(c.get("db"), c.req.param("bookingId"));
|
|
308
|
+
if (!ok)
|
|
309
|
+
return c.json({ error: "not_found" }, 404);
|
|
310
|
+
return c.body(null, 204);
|
|
311
|
+
})
|
|
312
|
+
.post("/:bookingId/charter-details/apa/payment", async (c) => {
|
|
313
|
+
const data = await parseJsonBody(c, apaPaymentSchema);
|
|
314
|
+
const row = await bookingCharterDetailsService.recordApaPayment(c.get("db"), c.req.param("bookingId"), { amount: data.amount, note: data.note ?? null });
|
|
315
|
+
if (!row)
|
|
316
|
+
return c.json({ error: "not_found" }, 404);
|
|
317
|
+
return c.json({ data: row });
|
|
318
|
+
})
|
|
319
|
+
.post("/:bookingId/charter-details/apa/reconcile", async (c) => {
|
|
320
|
+
const data = await parseJsonBody(c, apaReconcileSchema);
|
|
321
|
+
const row = await bookingCharterDetailsService.reconcileApa(c.get("db"), c.req.param("bookingId"), {
|
|
322
|
+
spentAmount: data.spentAmount,
|
|
323
|
+
refundAmount: data.refundAmount,
|
|
324
|
+
settle: data.settle,
|
|
325
|
+
note: data.note ?? null,
|
|
326
|
+
});
|
|
327
|
+
if (!row)
|
|
328
|
+
return c.json({ error: "not_found" }, 404);
|
|
329
|
+
return c.json({ data: row });
|
|
330
|
+
});
|
|
331
|
+
// ---------- HonoExtension export ----------
|
|
332
|
+
const chartersBookingExtensionDef = {
|
|
333
|
+
name: "charters-booking",
|
|
334
|
+
module: "bookings",
|
|
335
|
+
};
|
|
336
|
+
export const chartersBookingExtension = {
|
|
337
|
+
extension: chartersBookingExtensionDef,
|
|
338
|
+
adminRoutes: chartersBookingExtensionRoutes,
|
|
339
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catalog plane field policy for `packages/charters`.
|
|
3
|
+
*
|
|
4
|
+
* The charters vertical models luxury yacht-style products: per-suite or
|
|
5
|
+
* whole-yacht booking, MYBA contracts, APA (Advance Provisioning Allowance)
|
|
6
|
+
* as a first-class concern. See `docs/architecture/charters-module.md` for
|
|
7
|
+
* the vertical's design.
|
|
8
|
+
*
|
|
9
|
+
* Scope of this file:
|
|
10
|
+
* - The root `charter_products` table (from `schema-core.ts`).
|
|
11
|
+
* - Provenance + identity fields the catalog plane needs to track.
|
|
12
|
+
*
|
|
13
|
+
* Out of scope (deferred to follow-up adoption passes):
|
|
14
|
+
* - `charter_voyages` — own lifecycle (sales status), own query surface.
|
|
15
|
+
* - `charter_yachts` — vessel reference data; cross-module link target.
|
|
16
|
+
* - `charter_suites` — selectable variant axis with own micro-registry.
|
|
17
|
+
* - `charter_schedule_days` — composite list (mixed-class leaves).
|
|
18
|
+
*/
|
|
19
|
+
import { type FieldPolicyInput } from "@voyant-travel/catalog/contract";
|
|
20
|
+
declare const CHARTER_FIELD_POLICY: FieldPolicyInput[];
|
|
21
|
+
export declare const charterCatalogPolicy: import("@voyant-travel/catalog").FieldPolicy[];
|
|
22
|
+
export { CHARTER_FIELD_POLICY };
|
|
23
|
+
//# sourceMappingURL=catalog-policy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"catalog-policy.d.ts","sourceRoot":"","sources":["../src/catalog-policy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAqB,KAAK,gBAAgB,EAAE,MAAM,iCAAiC,CAAA;AAE1F,QAAA,MAAM,oBAAoB,EAAE,gBAAgB,EA+X3C,CAAA;AAED,eAAO,MAAM,oBAAoB,gDAA0C,CAAA;AAE3E,OAAO,EAAE,oBAAoB,EAAE,CAAA"}
|