@voyant-travel/cruises 0.118.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 +50 -0
- package/dist/adapters/connect-compat.d.ts +20 -0
- package/dist/adapters/connect-compat.d.ts.map +1 -0
- package/dist/adapters/connect-compat.js +71 -0
- package/dist/adapters/contract-fixture.d.ts +32 -0
- package/dist/adapters/contract-fixture.d.ts.map +1 -0
- package/dist/adapters/contract-fixture.js +152 -0
- package/dist/adapters/index.d.ts +331 -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 +131 -0
- package/dist/adapters/mock.d.ts +44 -0
- package/dist/adapters/mock.d.ts.map +1 -0
- package/dist/adapters/mock.js +192 -0
- package/dist/adapters/registry.d.ts +26 -0
- package/dist/adapters/registry.d.ts.map +1 -0
- package/dist/adapters/registry.js +42 -0
- package/dist/adapters/source-adapter-shim.d.ts +80 -0
- package/dist/adapters/source-adapter-shim.d.ts.map +1 -0
- package/dist/adapters/source-adapter-shim.js +390 -0
- package/dist/booking-engine/handler.d.ts +108 -0
- package/dist/booking-engine/handler.d.ts.map +1 -0
- package/dist/booking-engine/handler.js +225 -0
- package/dist/booking-engine/index.d.ts +9 -0
- package/dist/booking-engine/index.d.ts.map +1 -0
- package/dist/booking-engine/index.js +8 -0
- package/dist/booking-extension.d.ts +1179 -0
- package/dist/booking-extension.d.ts.map +1 -0
- package/dist/booking-extension.js +342 -0
- package/dist/cabin-features.d.ts +8 -0
- package/dist/cabin-features.d.ts.map +1 -0
- package/dist/cabin-features.js +7 -0
- package/dist/catalog-policy-cabins.d.ts +18 -0
- package/dist/catalog-policy-cabins.d.ts.map +1 -0
- package/dist/catalog-policy-cabins.js +96 -0
- package/dist/catalog-policy-core.d.ts +3 -0
- package/dist/catalog-policy-core.d.ts.map +1 -0
- package/dist/catalog-policy-core.js +247 -0
- package/dist/catalog-policy-structure.d.ts +3 -0
- package/dist/catalog-policy-structure.d.ts.map +1 -0
- package/dist/catalog-policy-structure.js +387 -0
- package/dist/catalog-policy.d.ts +15 -0
- package/dist/catalog-policy.d.ts.map +1 -0
- package/dist/catalog-policy.js +19 -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 +59 -0
- package/dist/draft-shape.d.ts.map +1 -0
- package/dist/draft-shape.js +98 -0
- package/dist/events.d.ts +21 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +21 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +66 -0
- package/dist/lib/key.d.ts +41 -0
- package/dist/lib/key.d.ts.map +1 -0
- package/dist/lib/key.js +100 -0
- package/dist/routes-booking-payloads.d.ts +133 -0
- package/dist/routes-booking-payloads.d.ts.map +1 -0
- package/dist/routes-booking-payloads.js +142 -0
- package/dist/routes-content.d.ts +53 -0
- package/dist/routes-content.d.ts.map +1 -0
- package/dist/routes-content.js +158 -0
- package/dist/routes-core.d.ts +4 -0
- package/dist/routes-core.d.ts.map +1 -0
- package/dist/routes-core.js +68 -0
- package/dist/routes-detail.d.ts +4 -0
- package/dist/routes-detail.d.ts.map +1 -0
- package/dist/routes-detail.js +261 -0
- package/dist/routes-env.d.ts +13 -0
- package/dist/routes-env.d.ts.map +1 -0
- package/dist/routes-env.js +1 -0
- package/dist/routes-keying.d.ts +28 -0
- package/dist/routes-keying.d.ts.map +1 -0
- package/dist/routes-keying.js +70 -0
- package/dist/routes-public.d.ts +911 -0
- package/dist/routes-public.d.ts.map +1 -0
- package/dist/routes-public.js +252 -0
- package/dist/routes-sailings-prices.d.ts +4 -0
- package/dist/routes-sailings-prices.d.ts.map +1 -0
- package/dist/routes-sailings-prices.js +278 -0
- package/dist/routes-search-index.d.ts +4 -0
- package/dist/routes-search-index.d.ts.map +1 -0
- package/dist/routes-search-index.js +25 -0
- package/dist/routes-ships.d.ts +4 -0
- package/dist/routes-ships.d.ts.map +1 -0
- package/dist/routes-ships.js +147 -0
- package/dist/routes-voyage-groups.d.ts +4 -0
- package/dist/routes-voyage-groups.d.ts.map +1 -0
- package/dist/routes-voyage-groups.js +85 -0
- package/dist/routes.d.ts +5 -0
- package/dist/routes.d.ts.map +1 -0
- package/dist/routes.js +14 -0
- package/dist/schema-cabins.d.ts +1098 -0
- package/dist/schema-cabins.d.ts.map +1 -0
- package/dist/schema-cabins.js +105 -0
- package/dist/schema-content.d.ts +577 -0
- package/dist/schema-content.d.ts.map +1 -0
- package/dist/schema-content.js +63 -0
- package/dist/schema-core.d.ts +1790 -0
- package/dist/schema-core.d.ts.map +1 -0
- package/dist/schema-core.js +171 -0
- package/dist/schema-itinerary.d.ts +556 -0
- package/dist/schema-itinerary.d.ts.map +1 -0
- package/dist/schema-itinerary.js +50 -0
- package/dist/schema-pricing.d.ts +633 -0
- package/dist/schema-pricing.d.ts.map +1 -0
- package/dist/schema-pricing.js +73 -0
- package/dist/schema-search.d.ts +611 -0
- package/dist/schema-search.d.ts.map +1 -0
- package/dist/schema-search.js +64 -0
- package/dist/schema-shared.d.ts +23 -0
- package/dist/schema-shared.d.ts.map +1 -0
- package/dist/schema-shared.js +107 -0
- package/dist/schema-sourced-content.d.ts +247 -0
- package/dist/schema-sourced-content.d.ts.map +1 -0
- package/dist/schema-sourced-content.js +38 -0
- package/dist/schema.d.ts +10 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +9 -0
- package/dist/service-booking-helpers.d.ts +12 -0
- package/dist/service-booking-helpers.d.ts.map +1 -0
- package/dist/service-booking-helpers.js +94 -0
- package/dist/service-booking-types.d.ts +101 -0
- package/dist/service-booking-types.d.ts.map +1 -0
- package/dist/service-booking-types.js +1 -0
- package/dist/service-bookings.d.ts +46 -0
- package/dist/service-bookings.d.ts.map +1 -0
- package/dist/service-bookings.js +420 -0
- package/dist/service-catalog-plane-cabins.d.ts +24 -0
- package/dist/service-catalog-plane-cabins.d.ts.map +1 -0
- package/dist/service-catalog-plane-cabins.js +90 -0
- package/dist/service-catalog-plane.d.ts +74 -0
- package/dist/service-catalog-plane.d.ts.map +1 -0
- package/dist/service-catalog-plane.js +194 -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 +144 -0
- package/dist/service-content.d.ts +74 -0
- package/dist/service-content.d.ts.map +1 -0
- package/dist/service-content.js +315 -0
- package/dist/service-core.d.ts +134 -0
- package/dist/service-core.d.ts.map +1 -0
- package/dist/service-core.js +257 -0
- package/dist/service-detach.d.ts +18 -0
- package/dist/service-detach.d.ts.map +1 -0
- package/dist/service-detach.js +199 -0
- package/dist/service-enrichment.d.ts +11 -0
- package/dist/service-enrichment.d.ts.map +1 -0
- package/dist/service-enrichment.js +47 -0
- package/dist/service-external-refresh.d.ts +39 -0
- package/dist/service-external-refresh.d.ts.map +1 -0
- package/dist/service-external-refresh.js +47 -0
- package/dist/service-itinerary.d.ts +22 -0
- package/dist/service-itinerary.d.ts.map +1 -0
- package/dist/service-itinerary.js +34 -0
- package/dist/service-prices.d.ts +46 -0
- package/dist/service-prices.d.ts.map +1 -0
- package/dist/service-prices.js +89 -0
- package/dist/service-pricing.d.ts +97 -0
- package/dist/service-pricing.d.ts.map +1 -0
- package/dist/service-pricing.js +198 -0
- package/dist/service-sailings.d.ts +48 -0
- package/dist/service-sailings.d.ts.map +1 -0
- package/dist/service-sailings.js +145 -0
- package/dist/service-search-types.d.ts +54 -0
- package/dist/service-search-types.d.ts.map +1 -0
- package/dist/service-search-types.js +1 -0
- package/dist/service-search.d.ts +65 -0
- package/dist/service-search.d.ts.map +1 -0
- package/dist/service-search.js +467 -0
- package/dist/service-shared.d.ts +22 -0
- package/dist/service-shared.d.ts.map +1 -0
- package/dist/service-shared.js +22 -0
- package/dist/service-ships.d.ts +47 -0
- package/dist/service-ships.d.ts.map +1 -0
- package/dist/service-ships.js +156 -0
- package/dist/service.d.ts +255 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +12 -0
- package/dist/validation-cabins.d.ts +267 -0
- package/dist/validation-cabins.d.ts.map +1 -0
- package/dist/validation-cabins.js +77 -0
- package/dist/validation-content.d.ts +123 -0
- package/dist/validation-content.d.ts.map +1 -0
- package/dist/validation-content.js +40 -0
- package/dist/validation-core.d.ts +393 -0
- package/dist/validation-core.d.ts.map +1 -0
- package/dist/validation-core.js +162 -0
- package/dist/validation-itinerary.d.ts +123 -0
- package/dist/validation-itinerary.d.ts.map +1 -0
- package/dist/validation-itinerary.js +47 -0
- package/dist/validation-pricing.d.ts +137 -0
- package/dist/validation-pricing.d.ts.map +1 -0
- package/dist/validation-pricing.js +49 -0
- package/dist/validation-search.d.ts +118 -0
- package/dist/validation-search.d.ts.map +1 -0
- package/dist/validation-search.js +60 -0
- package/dist/validation-shared.d.ts +123 -0
- package/dist/validation-shared.d.ts.map +1 -0
- package/dist/validation-shared.js +103 -0
- package/dist/validation.d.ts +8 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +7 -0
- package/package.json +146 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export function mergeDay(base, override) {
|
|
2
|
+
if (!override) {
|
|
3
|
+
return {
|
|
4
|
+
dayNumber: base.dayNumber,
|
|
5
|
+
title: base.title,
|
|
6
|
+
description: base.description,
|
|
7
|
+
portFacilityId: base.portFacilityId,
|
|
8
|
+
portCanonicalPlaceId: base.portCanonicalPlaceId,
|
|
9
|
+
arrivalTime: base.arrivalTime,
|
|
10
|
+
departureTime: base.departureTime,
|
|
11
|
+
isOvernight: base.isOvernight,
|
|
12
|
+
isSeaDay: base.isSeaDay,
|
|
13
|
+
isExpeditionLanding: base.isExpeditionLanding,
|
|
14
|
+
isSkipped: false,
|
|
15
|
+
meals: base.meals ?? {},
|
|
16
|
+
hasOverride: false,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
dayNumber: base.dayNumber,
|
|
21
|
+
title: override.title ?? base.title,
|
|
22
|
+
description: override.description ?? base.description,
|
|
23
|
+
portFacilityId: override.portFacilityId ?? base.portFacilityId,
|
|
24
|
+
portCanonicalPlaceId: override.portCanonicalPlaceId ?? base.portCanonicalPlaceId,
|
|
25
|
+
arrivalTime: override.arrivalTime ?? base.arrivalTime,
|
|
26
|
+
departureTime: override.departureTime ?? base.departureTime,
|
|
27
|
+
isOvernight: override.isOvernight ?? base.isOvernight,
|
|
28
|
+
isSeaDay: override.isSeaDay ?? base.isSeaDay,
|
|
29
|
+
isExpeditionLanding: override.isExpeditionLanding ?? base.isExpeditionLanding,
|
|
30
|
+
isSkipped: override.isSkipped,
|
|
31
|
+
meals: override.meals ?? base.meals ?? {},
|
|
32
|
+
hasOverride: true,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
|
2
|
+
import type { CruisePrice } from "./schema-pricing.js";
|
|
3
|
+
import type { InsertPrice, InsertPriceComponent, PriceListQuery, UpdatePrice } from "./validation-pricing.js";
|
|
4
|
+
export declare const cruisePriceRowsService: {
|
|
5
|
+
listPrices(db: PostgresJsDatabase, query: PriceListQuery): Promise<{
|
|
6
|
+
data: {
|
|
7
|
+
id: string;
|
|
8
|
+
sailingId: string;
|
|
9
|
+
cabinCategoryId: string;
|
|
10
|
+
occupancy: number;
|
|
11
|
+
fareCode: string | null;
|
|
12
|
+
fareCodeName: string | null;
|
|
13
|
+
fareVariant: "cruise_only" | "air_inclusive";
|
|
14
|
+
currency: string;
|
|
15
|
+
pricePerPerson: string;
|
|
16
|
+
originalPricePerPerson: string | null;
|
|
17
|
+
secondGuestPricePerPerson: string | null;
|
|
18
|
+
singlePricePerPerson: string | null;
|
|
19
|
+
singleSupplementPercent: string | null;
|
|
20
|
+
availability: "on_request" | "wait_list" | "sold_out" | "available" | "limited";
|
|
21
|
+
availabilityCount: number | null;
|
|
22
|
+
priceCatalogId: string | null;
|
|
23
|
+
priceScheduleId: string | null;
|
|
24
|
+
bookingDeadline: string | null;
|
|
25
|
+
earlyBookingDeadline: string | null;
|
|
26
|
+
earlyBookingBonusDescription: string | null;
|
|
27
|
+
requiresRequest: boolean;
|
|
28
|
+
notes: string | null;
|
|
29
|
+
externalRefs: Record<string, string> | null;
|
|
30
|
+
lastSyncedAt: Date | null;
|
|
31
|
+
createdAt: Date;
|
|
32
|
+
updatedAt: Date;
|
|
33
|
+
}[];
|
|
34
|
+
total: number;
|
|
35
|
+
limit: number;
|
|
36
|
+
offset: number;
|
|
37
|
+
}>;
|
|
38
|
+
createPrice(db: PostgresJsDatabase, data: InsertPrice): Promise<CruisePrice>;
|
|
39
|
+
updatePrice(db: PostgresJsDatabase, id: string, data: UpdatePrice): Promise<CruisePrice | null>;
|
|
40
|
+
replaceSailingPricing(db: PostgresJsDatabase, sailingId: string, payload: {
|
|
41
|
+
prices: Array<InsertPrice & {
|
|
42
|
+
components?: Array<Omit<InsertPriceComponent, "priceId">>;
|
|
43
|
+
}>;
|
|
44
|
+
}): Promise<CruisePrice[]>;
|
|
45
|
+
};
|
|
46
|
+
//# sourceMappingURL=service-prices.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-prices.d.ts","sourceRoot":"","sources":["../src/service-prices.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAGjE,OAAO,KAAK,EAAE,WAAW,EAA2C,MAAM,qBAAqB,CAAA;AAG/F,OAAO,KAAK,EACV,WAAW,EACX,oBAAoB,EACpB,cAAc,EACd,WAAW,EACZ,MAAM,yBAAyB,CAAA;AAEhC,eAAO,MAAM,sBAAsB;mBACZ,kBAAkB,SAAS,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;oBA0BxC,kBAAkB,QAAQ,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC;oBAU5E,kBAAkB,MAClB,MAAM,QACJ,WAAW,GAChB,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;8BAUxB,kBAAkB,aACX,MAAM,WACR;QACP,MAAM,EAAE,KAAK,CAAC,WAAW,GAAG;YAAE,UAAU,CAAC,EAAE,KAAK,CAAC,IAAI,CAAC,oBAAoB,EAAE,SAAS,CAAC,CAAC,CAAA;SAAE,CAAC,CAAA;KAC3F,GACA,OAAO,CAAC,WAAW,EAAE,CAAC;CAyC1B,CAAA"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { and, asc, count, eq } from "drizzle-orm";
|
|
2
|
+
import { cruiseSailings } from "./schema-core.js";
|
|
3
|
+
import { cruisePriceComponents, cruisePrices } from "./schema-pricing.js";
|
|
4
|
+
import { paginate, reprojectIfPossible, setUpdated } from "./service-shared.js";
|
|
5
|
+
export const cruisePriceRowsService = {
|
|
6
|
+
async listPrices(db, query) {
|
|
7
|
+
const conditions = [];
|
|
8
|
+
if (query.sailingId)
|
|
9
|
+
conditions.push(eq(cruisePrices.sailingId, query.sailingId));
|
|
10
|
+
if (query.cabinCategoryId)
|
|
11
|
+
conditions.push(eq(cruisePrices.cabinCategoryId, query.cabinCategoryId));
|
|
12
|
+
if (query.occupancy)
|
|
13
|
+
conditions.push(eq(cruisePrices.occupancy, query.occupancy));
|
|
14
|
+
if (query.fareCode)
|
|
15
|
+
conditions.push(eq(cruisePrices.fareCode, query.fareCode));
|
|
16
|
+
if (query.fareVariant)
|
|
17
|
+
conditions.push(eq(cruisePrices.fareVariant, query.fareVariant));
|
|
18
|
+
if (query.availability)
|
|
19
|
+
conditions.push(eq(cruisePrices.availability, query.availability));
|
|
20
|
+
if (query.priceCatalogId)
|
|
21
|
+
conditions.push(eq(cruisePrices.priceCatalogId, query.priceCatalogId));
|
|
22
|
+
const where = conditions.length > 0 ? and(...conditions) : undefined;
|
|
23
|
+
const { limit, offset } = paginate(query);
|
|
24
|
+
const [rows, totalRows] = await Promise.all([
|
|
25
|
+
db
|
|
26
|
+
.select()
|
|
27
|
+
.from(cruisePrices)
|
|
28
|
+
.where(where)
|
|
29
|
+
.orderBy(asc(cruisePrices.cabinCategoryId), asc(cruisePrices.occupancy))
|
|
30
|
+
.limit(limit)
|
|
31
|
+
.offset(offset),
|
|
32
|
+
db.select({ value: count() }).from(cruisePrices).where(where),
|
|
33
|
+
]);
|
|
34
|
+
return { data: rows, total: totalRows[0]?.value ?? 0, limit, offset };
|
|
35
|
+
},
|
|
36
|
+
async createPrice(db, data) {
|
|
37
|
+
const [row] = await db
|
|
38
|
+
.insert(cruisePrices)
|
|
39
|
+
.values({ ...data, lastSyncedAt: new Date() })
|
|
40
|
+
.returning();
|
|
41
|
+
if (!row)
|
|
42
|
+
throw new Error("Failed to create price");
|
|
43
|
+
return row;
|
|
44
|
+
},
|
|
45
|
+
async updatePrice(db, id, data) {
|
|
46
|
+
const [row] = await db
|
|
47
|
+
.update(cruisePrices)
|
|
48
|
+
.set({ ...data, ...setUpdated })
|
|
49
|
+
.where(eq(cruisePrices.id, id))
|
|
50
|
+
.returning();
|
|
51
|
+
return row ?? null;
|
|
52
|
+
},
|
|
53
|
+
async replaceSailingPricing(db, sailingId, payload) {
|
|
54
|
+
const result = await db.transaction(async (tx) => {
|
|
55
|
+
// Cascade-delete existing prices for this sailing — components go with them via FK.
|
|
56
|
+
await tx.delete(cruisePrices).where(eq(cruisePrices.sailingId, sailingId));
|
|
57
|
+
if (payload.prices.length === 0)
|
|
58
|
+
return [];
|
|
59
|
+
const insertedPrices = [];
|
|
60
|
+
for (const p of payload.prices) {
|
|
61
|
+
const { components, ...priceFields } = p;
|
|
62
|
+
const [priceRow] = await tx
|
|
63
|
+
.insert(cruisePrices)
|
|
64
|
+
.values({ ...priceFields, sailingId, lastSyncedAt: new Date() })
|
|
65
|
+
.returning();
|
|
66
|
+
if (!priceRow)
|
|
67
|
+
throw new Error("Failed to insert price");
|
|
68
|
+
insertedPrices.push(priceRow);
|
|
69
|
+
if (components && components.length > 0) {
|
|
70
|
+
await tx
|
|
71
|
+
.insert(cruisePriceComponents)
|
|
72
|
+
.values(components.map((c) => ({ ...c, priceId: priceRow.id })));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return insertedPrices;
|
|
76
|
+
});
|
|
77
|
+
// Bulk pricing changes likely move the lowest-price aggregate. Re-project
|
|
78
|
+
// the parent cruise so the storefront index stays current.
|
|
79
|
+
const [sailing] = await db
|
|
80
|
+
.select({ cruiseId: cruiseSailings.cruiseId })
|
|
81
|
+
.from(cruiseSailings)
|
|
82
|
+
.where(eq(cruiseSailings.id, sailingId))
|
|
83
|
+
.limit(1);
|
|
84
|
+
if (sailing)
|
|
85
|
+
await reprojectIfPossible(db, sailing.cruiseId);
|
|
86
|
+
return result;
|
|
87
|
+
},
|
|
88
|
+
// ---------- enrichment programs (expedition-focused) ----------
|
|
89
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
|
2
|
+
import { type CruisePrice, type CruisePriceComponent } from "./schema-pricing.js";
|
|
3
|
+
export type QuotePriceComponentKind = CruisePriceComponent["kind"] | "single_supplement" | "other";
|
|
4
|
+
export type QuoteBookingTerms = {
|
|
5
|
+
cancellationPolicy?: {
|
|
6
|
+
summary?: string | null;
|
|
7
|
+
rules?: Array<{
|
|
8
|
+
from?: string | null;
|
|
9
|
+
until?: string | null;
|
|
10
|
+
penaltyAmount?: string | null;
|
|
11
|
+
penaltyCurrency?: string | null;
|
|
12
|
+
penaltyPercent?: string | null;
|
|
13
|
+
description?: string | null;
|
|
14
|
+
}>;
|
|
15
|
+
[key: string]: unknown;
|
|
16
|
+
} | null;
|
|
17
|
+
paymentTerms?: {
|
|
18
|
+
summary?: string | null;
|
|
19
|
+
depositAmount?: string | null;
|
|
20
|
+
depositCurrency?: string | null;
|
|
21
|
+
depositPercent?: string | null;
|
|
22
|
+
dueDate?: string | null;
|
|
23
|
+
schedule?: Array<Record<string, unknown>>;
|
|
24
|
+
[key: string]: unknown;
|
|
25
|
+
} | null;
|
|
26
|
+
supplierTermsUrl?: string | null;
|
|
27
|
+
notes?: string | null;
|
|
28
|
+
[key: string]: unknown;
|
|
29
|
+
};
|
|
30
|
+
export type QuoteComponent = {
|
|
31
|
+
kind: QuotePriceComponentKind;
|
|
32
|
+
label: string | null;
|
|
33
|
+
amount: string;
|
|
34
|
+
currency: string;
|
|
35
|
+
direction: CruisePriceComponent["direction"];
|
|
36
|
+
perPerson: boolean;
|
|
37
|
+
};
|
|
38
|
+
export type Quote = {
|
|
39
|
+
fareCode: string | null;
|
|
40
|
+
fareCodeName: string | null;
|
|
41
|
+
fareVariant: CruisePrice["fareVariant"];
|
|
42
|
+
currency: string;
|
|
43
|
+
occupancy: number;
|
|
44
|
+
guestCount: number;
|
|
45
|
+
basePerPerson: string;
|
|
46
|
+
originalPricePerPerson: string | null;
|
|
47
|
+
singlePricePerPerson: string | null;
|
|
48
|
+
earlyBookingDeadline: string | null;
|
|
49
|
+
earlyBookingBonusDescription: string | null;
|
|
50
|
+
components: QuoteComponent[];
|
|
51
|
+
totalPerPerson: string;
|
|
52
|
+
totalForCabin: string;
|
|
53
|
+
bookingTerms?: QuoteBookingTerms | null;
|
|
54
|
+
};
|
|
55
|
+
export type ComposeQuoteInput = {
|
|
56
|
+
price: Pick<CruisePrice, "pricePerPerson" | "originalPricePerPerson" | "secondGuestPricePerPerson" | "singlePricePerPerson" | "singleSupplementPercent" | "currency" | "fareCode" | "fareCodeName" | "fareVariant" | "earlyBookingDeadline" | "earlyBookingBonusDescription">;
|
|
57
|
+
components: Array<Pick<CruisePriceComponent, "label" | "amount" | "currency" | "direction" | "perPerson"> & {
|
|
58
|
+
kind: QuotePriceComponentKind;
|
|
59
|
+
}>;
|
|
60
|
+
occupancy: number;
|
|
61
|
+
guestCount: number;
|
|
62
|
+
bookingTerms?: QuoteBookingTerms | null;
|
|
63
|
+
};
|
|
64
|
+
export declare function composeQuote(input: ComposeQuoteInput): Quote;
|
|
65
|
+
export type LowestPriceResult = {
|
|
66
|
+
pricePerPerson: string;
|
|
67
|
+
currency: string;
|
|
68
|
+
cabinCategoryId: string;
|
|
69
|
+
fareCode: string | null;
|
|
70
|
+
fareVariant: CruisePrice["fareVariant"];
|
|
71
|
+
} | null;
|
|
72
|
+
export type GridCell = {
|
|
73
|
+
cabinCategoryId: string;
|
|
74
|
+
occupancy: number;
|
|
75
|
+
fareCode: string | null;
|
|
76
|
+
fareVariant: CruisePrice["fareVariant"];
|
|
77
|
+
pricePerPerson: string;
|
|
78
|
+
originalPricePerPerson: string | null;
|
|
79
|
+
currency: string;
|
|
80
|
+
availability: CruisePrice["availability"];
|
|
81
|
+
};
|
|
82
|
+
export declare const pricingService: {
|
|
83
|
+
lowestAvailablePrice(db: PostgresJsDatabase, args: {
|
|
84
|
+
sailingId: string;
|
|
85
|
+
occupancy: number;
|
|
86
|
+
}): Promise<LowestPriceResult>;
|
|
87
|
+
gridForSailing(db: PostgresJsDatabase, sailingId: string): Promise<GridCell[]>;
|
|
88
|
+
assembleQuote(db: PostgresJsDatabase, args: {
|
|
89
|
+
sailingId: string;
|
|
90
|
+
cabinCategoryId: string;
|
|
91
|
+
occupancy: number;
|
|
92
|
+
guestCount: number;
|
|
93
|
+
fareCode?: string | null;
|
|
94
|
+
fareVariant?: CruisePrice["fareVariant"] | null;
|
|
95
|
+
}): Promise<Quote>;
|
|
96
|
+
};
|
|
97
|
+
//# 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":"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"}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { and, asc, eq, sql } from "drizzle-orm";
|
|
2
|
+
import { cruiseCabinCategories } from "./schema-cabins.js";
|
|
3
|
+
import { cruisePriceComponents, cruisePrices, } from "./schema-pricing.js";
|
|
4
|
+
// ---------- money helpers ----------
|
|
5
|
+
// All math is performed in integer cents to avoid float drift.
|
|
6
|
+
const CENTS_PER_UNIT = 100n;
|
|
7
|
+
function decimalStringToCents(s) {
|
|
8
|
+
const trimmed = s.trim();
|
|
9
|
+
if (!/^-?\d+(\.\d{1,2})?$/.test(trimmed)) {
|
|
10
|
+
throw new Error(`Invalid money string: ${s}`);
|
|
11
|
+
}
|
|
12
|
+
const negative = trimmed.startsWith("-");
|
|
13
|
+
const abs = negative ? trimmed.slice(1) : trimmed;
|
|
14
|
+
const parts = abs.split(".");
|
|
15
|
+
const whole = parts[0] ?? "0";
|
|
16
|
+
const frac = parts[1] ?? "";
|
|
17
|
+
const fracPadded = `${frac}00`.slice(0, 2);
|
|
18
|
+
const cents = BigInt(whole) * CENTS_PER_UNIT + BigInt(fracPadded);
|
|
19
|
+
return negative ? -cents : cents;
|
|
20
|
+
}
|
|
21
|
+
function centsToDecimalString(c) {
|
|
22
|
+
const negative = c < 0n;
|
|
23
|
+
const abs = negative ? -c : c;
|
|
24
|
+
const whole = abs / CENTS_PER_UNIT;
|
|
25
|
+
const frac = abs % CENTS_PER_UNIT;
|
|
26
|
+
const fracStr = frac.toString().padStart(2, "0");
|
|
27
|
+
return `${negative ? "-" : ""}${whole.toString()}.${fracStr}`;
|
|
28
|
+
}
|
|
29
|
+
function percentOf(cents, percentString) {
|
|
30
|
+
// percent stored with up to 2 decimal places (e.g. "75.50")
|
|
31
|
+
const trimmed = percentString.trim();
|
|
32
|
+
if (!/^-?\d+(\.\d{1,2})?$/.test(trimmed)) {
|
|
33
|
+
throw new Error(`Invalid percent string: ${percentString}`);
|
|
34
|
+
}
|
|
35
|
+
const parts = trimmed.split(".");
|
|
36
|
+
const whole = parts[0] ?? "0";
|
|
37
|
+
const frac = parts[1] ?? "";
|
|
38
|
+
const fracPadded = `${frac}00`.slice(0, 2);
|
|
39
|
+
// percent * 100 → integer basis points; multiply cents, divide by 10000
|
|
40
|
+
const basisPoints = BigInt(whole) * 100n + BigInt(fracPadded);
|
|
41
|
+
return (cents * basisPoints) / 10000n;
|
|
42
|
+
}
|
|
43
|
+
export function composeQuote(input) {
|
|
44
|
+
const { price, components, occupancy, guestCount } = input;
|
|
45
|
+
if (occupancy < 1)
|
|
46
|
+
throw new Error("occupancy must be >= 1");
|
|
47
|
+
if (guestCount < 1)
|
|
48
|
+
throw new Error("guestCount must be >= 1");
|
|
49
|
+
if (guestCount > occupancy)
|
|
50
|
+
throw new Error(`guestCount (${guestCount}) cannot exceed occupancy (${occupancy})`);
|
|
51
|
+
const basePerPersonCents = decimalStringToCents(price.pricePerPerson);
|
|
52
|
+
// Resolve effective base per cabin, accounting for second-guest reduction and single supplement.
|
|
53
|
+
let baseCabinCents;
|
|
54
|
+
if (occupancy === 1 && price.singlePricePerPerson && guestCount === 1) {
|
|
55
|
+
baseCabinCents = decimalStringToCents(price.singlePricePerPerson);
|
|
56
|
+
}
|
|
57
|
+
else if (occupancy === 1 && price.singleSupplementPercent && guestCount === 1) {
|
|
58
|
+
const supplementCents = percentOf(basePerPersonCents, price.singleSupplementPercent);
|
|
59
|
+
baseCabinCents = basePerPersonCents + supplementCents;
|
|
60
|
+
}
|
|
61
|
+
else if (occupancy === 2 && price.secondGuestPricePerPerson && guestCount === 2) {
|
|
62
|
+
const secondCents = decimalStringToCents(price.secondGuestPricePerPerson);
|
|
63
|
+
baseCabinCents = basePerPersonCents + secondCents;
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
baseCabinCents = basePerPersonCents * BigInt(guestCount);
|
|
67
|
+
}
|
|
68
|
+
// Apply price components.
|
|
69
|
+
let cabinAdjustmentCents = 0n;
|
|
70
|
+
const renderedComponents = [];
|
|
71
|
+
for (const c of components) {
|
|
72
|
+
if (c.currency !== price.currency) {
|
|
73
|
+
throw new Error(`Component currency ${c.currency} does not match price currency ${price.currency}`);
|
|
74
|
+
}
|
|
75
|
+
const amountCents = decimalStringToCents(c.amount);
|
|
76
|
+
const componentTotalCents = c.perPerson ? amountCents * BigInt(guestCount) : amountCents;
|
|
77
|
+
if (c.direction === "addition") {
|
|
78
|
+
cabinAdjustmentCents += componentTotalCents;
|
|
79
|
+
}
|
|
80
|
+
else if (c.direction === "credit") {
|
|
81
|
+
cabinAdjustmentCents -= componentTotalCents;
|
|
82
|
+
}
|
|
83
|
+
// 'inclusion' is display-only — does not affect totals.
|
|
84
|
+
renderedComponents.push({
|
|
85
|
+
kind: c.kind,
|
|
86
|
+
label: c.label ?? null,
|
|
87
|
+
amount: c.amount,
|
|
88
|
+
currency: c.currency,
|
|
89
|
+
direction: c.direction,
|
|
90
|
+
perPerson: c.perPerson,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
const totalForCabinCents = baseCabinCents + cabinAdjustmentCents;
|
|
94
|
+
const totalPerPersonCents = totalForCabinCents / BigInt(guestCount);
|
|
95
|
+
return {
|
|
96
|
+
fareCode: price.fareCode ?? null,
|
|
97
|
+
fareCodeName: price.fareCodeName ?? null,
|
|
98
|
+
fareVariant: price.fareVariant,
|
|
99
|
+
currency: price.currency,
|
|
100
|
+
occupancy,
|
|
101
|
+
guestCount,
|
|
102
|
+
basePerPerson: centsToDecimalString(basePerPersonCents),
|
|
103
|
+
originalPricePerPerson: price.originalPricePerPerson ?? null,
|
|
104
|
+
singlePricePerPerson: price.singlePricePerPerson ?? null,
|
|
105
|
+
earlyBookingDeadline: price.earlyBookingDeadline ?? null,
|
|
106
|
+
earlyBookingBonusDescription: price.earlyBookingBonusDescription ?? null,
|
|
107
|
+
components: renderedComponents,
|
|
108
|
+
totalPerPerson: centsToDecimalString(totalPerPersonCents),
|
|
109
|
+
totalForCabin: centsToDecimalString(totalForCabinCents),
|
|
110
|
+
bookingTerms: input.bookingTerms ?? null,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
export const pricingService = {
|
|
114
|
+
async lowestAvailablePrice(db, args) {
|
|
115
|
+
const [row] = await db
|
|
116
|
+
.select({
|
|
117
|
+
pricePerPerson: cruisePrices.pricePerPerson,
|
|
118
|
+
currency: cruisePrices.currency,
|
|
119
|
+
cabinCategoryId: cruisePrices.cabinCategoryId,
|
|
120
|
+
fareCode: cruisePrices.fareCode,
|
|
121
|
+
fareVariant: cruisePrices.fareVariant,
|
|
122
|
+
})
|
|
123
|
+
.from(cruisePrices)
|
|
124
|
+
.where(and(eq(cruisePrices.sailingId, args.sailingId), eq(cruisePrices.occupancy, args.occupancy),
|
|
125
|
+
// agent-quality: raw-sql reviewed -- owner: cruises; dynamic SQL interpolation uses Drizzle parameter binding or vetted SQL identifiers.
|
|
126
|
+
sql `${cruisePrices.availability} <> 'sold_out'`))
|
|
127
|
+
// agent-quality: raw-sql reviewed -- owner: cruises; dynamic SQL interpolation uses Drizzle parameter binding or vetted SQL identifiers.
|
|
128
|
+
.orderBy(asc(sql `${cruisePrices.pricePerPerson}::numeric`))
|
|
129
|
+
.limit(1);
|
|
130
|
+
return row ?? null;
|
|
131
|
+
},
|
|
132
|
+
async gridForSailing(db, sailingId) {
|
|
133
|
+
const rows = await db
|
|
134
|
+
.select({
|
|
135
|
+
cabinCategoryId: cruisePrices.cabinCategoryId,
|
|
136
|
+
occupancy: cruisePrices.occupancy,
|
|
137
|
+
fareCode: cruisePrices.fareCode,
|
|
138
|
+
fareVariant: cruisePrices.fareVariant,
|
|
139
|
+
pricePerPerson: cruisePrices.pricePerPerson,
|
|
140
|
+
originalPricePerPerson: cruisePrices.originalPricePerPerson,
|
|
141
|
+
currency: cruisePrices.currency,
|
|
142
|
+
availability: cruisePrices.availability,
|
|
143
|
+
})
|
|
144
|
+
.from(cruisePrices)
|
|
145
|
+
.where(eq(cruisePrices.sailingId, sailingId))
|
|
146
|
+
.orderBy(asc(cruisePrices.cabinCategoryId), asc(cruisePrices.occupancy),
|
|
147
|
+
// agent-quality: raw-sql reviewed -- owner: cruises; dynamic SQL interpolation uses Drizzle parameter binding or vetted SQL identifiers.
|
|
148
|
+
asc(sql `${cruisePrices.pricePerPerson}::numeric`));
|
|
149
|
+
return rows;
|
|
150
|
+
},
|
|
151
|
+
async assembleQuote(db, args) {
|
|
152
|
+
// Validate cabin category exists and respects occupancy bounds.
|
|
153
|
+
const [category] = await db
|
|
154
|
+
.select({
|
|
155
|
+
id: cruiseCabinCategories.id,
|
|
156
|
+
minOccupancy: cruiseCabinCategories.minOccupancy,
|
|
157
|
+
maxOccupancy: cruiseCabinCategories.maxOccupancy,
|
|
158
|
+
})
|
|
159
|
+
.from(cruiseCabinCategories)
|
|
160
|
+
.where(eq(cruiseCabinCategories.id, args.cabinCategoryId))
|
|
161
|
+
.limit(1);
|
|
162
|
+
if (!category)
|
|
163
|
+
throw new Error(`Cabin category ${args.cabinCategoryId} not found`);
|
|
164
|
+
if (args.guestCount < category.minOccupancy || args.guestCount > category.maxOccupancy) {
|
|
165
|
+
throw new Error(`guestCount ${args.guestCount} outside category bounds [${category.minOccupancy}, ${category.maxOccupancy}]`);
|
|
166
|
+
}
|
|
167
|
+
// Pick the cheapest matching price row.
|
|
168
|
+
const conditions = [
|
|
169
|
+
eq(cruisePrices.sailingId, args.sailingId),
|
|
170
|
+
eq(cruisePrices.cabinCategoryId, args.cabinCategoryId),
|
|
171
|
+
eq(cruisePrices.occupancy, args.occupancy),
|
|
172
|
+
];
|
|
173
|
+
if (args.fareCode)
|
|
174
|
+
conditions.push(eq(cruisePrices.fareCode, args.fareCode));
|
|
175
|
+
if (args.fareVariant)
|
|
176
|
+
conditions.push(eq(cruisePrices.fareVariant, args.fareVariant));
|
|
177
|
+
const [price] = await db
|
|
178
|
+
.select()
|
|
179
|
+
.from(cruisePrices)
|
|
180
|
+
.where(and(...conditions))
|
|
181
|
+
// agent-quality: raw-sql reviewed -- owner: cruises; dynamic SQL interpolation uses Drizzle parameter binding or vetted SQL identifiers.
|
|
182
|
+
.orderBy(asc(sql `${cruisePrices.pricePerPerson}::numeric`))
|
|
183
|
+
.limit(1);
|
|
184
|
+
if (!price) {
|
|
185
|
+
throw new Error(`No price found for sailing=${args.sailingId} category=${args.cabinCategoryId} occupancy=${args.occupancy}${args.fareCode ? ` fareCode=${args.fareCode}` : ""}${args.fareVariant ? ` fareVariant=${args.fareVariant}` : ""}`);
|
|
186
|
+
}
|
|
187
|
+
const components = await db
|
|
188
|
+
.select()
|
|
189
|
+
.from(cruisePriceComponents)
|
|
190
|
+
.where(eq(cruisePriceComponents.priceId, price.id));
|
|
191
|
+
return composeQuote({
|
|
192
|
+
price,
|
|
193
|
+
components,
|
|
194
|
+
occupancy: args.occupancy,
|
|
195
|
+
guestCount: args.guestCount,
|
|
196
|
+
});
|
|
197
|
+
},
|
|
198
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
|
2
|
+
import type { CruiseSailing } from "./schema-core.js";
|
|
3
|
+
import type { CruiseDay, CruiseSailingDay } from "./schema-itinerary.js";
|
|
4
|
+
import type { CruisePrice, CruisePriceComponent } from "./schema-pricing.js";
|
|
5
|
+
import { type EffectiveItineraryDay } from "./service-itinerary.js";
|
|
6
|
+
import type { InsertSailing, SailingListQuery, UpdateSailing } from "./validation-core.js";
|
|
7
|
+
import type { ReplaceCruiseDays, ReplaceSailingDays } from "./validation-itinerary.js";
|
|
8
|
+
export declare const cruiseSailingsService: {
|
|
9
|
+
listSailings(db: PostgresJsDatabase, query: SailingListQuery): Promise<{
|
|
10
|
+
data: {
|
|
11
|
+
id: string;
|
|
12
|
+
cruiseId: string;
|
|
13
|
+
shipId: string;
|
|
14
|
+
departureDate: string;
|
|
15
|
+
returnDate: string;
|
|
16
|
+
embarkPortFacilityId: string | null;
|
|
17
|
+
embarkPortCanonicalPlaceId: string | null;
|
|
18
|
+
disembarkPortFacilityId: string | null;
|
|
19
|
+
disembarkPortCanonicalPlaceId: string | null;
|
|
20
|
+
direction: "upstream" | "downstream" | "round_trip" | "one_way" | null;
|
|
21
|
+
availabilityNote: string | null;
|
|
22
|
+
isCharter: boolean;
|
|
23
|
+
salesStatus: "open" | "on_request" | "wait_list" | "sold_out" | "closed";
|
|
24
|
+
externalRefs: Record<string, string> | null;
|
|
25
|
+
customerPaymentPolicy: unknown;
|
|
26
|
+
lastSyncedAt: Date | null;
|
|
27
|
+
createdAt: Date;
|
|
28
|
+
updatedAt: Date;
|
|
29
|
+
}[];
|
|
30
|
+
total: number;
|
|
31
|
+
limit: number;
|
|
32
|
+
offset: number;
|
|
33
|
+
}>;
|
|
34
|
+
getSailingById(db: PostgresJsDatabase, id: string, options?: {
|
|
35
|
+
withPricing?: boolean;
|
|
36
|
+
withItinerary?: boolean;
|
|
37
|
+
}): Promise<(CruiseSailing & {
|
|
38
|
+
prices?: CruisePrice[];
|
|
39
|
+
priceComponents?: CruisePriceComponent[];
|
|
40
|
+
effectiveDays?: EffectiveItineraryDay[];
|
|
41
|
+
}) | null>;
|
|
42
|
+
upsertSailing(db: PostgresJsDatabase, data: InsertSailing): Promise<CruiseSailing>;
|
|
43
|
+
updateSailing(db: PostgresJsDatabase, id: string, data: UpdateSailing): Promise<CruiseSailing | null>;
|
|
44
|
+
getEffectiveItinerary(db: PostgresJsDatabase, sailingId: string): Promise<EffectiveItineraryDay[]>;
|
|
45
|
+
replaceCruiseDays(db: PostgresJsDatabase, payload: ReplaceCruiseDays): Promise<CruiseDay[]>;
|
|
46
|
+
replaceSailingDays(db: PostgresJsDatabase, payload: ReplaceSailingDays): Promise<CruiseSailingDay[]>;
|
|
47
|
+
};
|
|
48
|
+
//# sourceMappingURL=service-sailings.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-sailings.d.ts","sourceRoot":"","sources":["../src/service-sailings.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAEjE,OAAO,KAAK,EAAE,aAAa,EAAoB,MAAM,kBAAkB,CAAA;AAEvE,OAAO,KAAK,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AAExE,OAAO,KAAK,EAAE,WAAW,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAA;AAE5E,OAAO,EAAE,KAAK,qBAAqB,EAAY,MAAM,wBAAwB,CAAA;AAE7E,OAAO,KAAK,EAAE,aAAa,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA;AAC1F,OAAO,KAAK,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAA;AAEtF,eAAO,MAAM,qBAAqB;qBACT,kBAAkB,SAAS,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;uBAwB5D,kBAAkB,MAClB,MAAM,YACD;QAAE,WAAW,CAAC,EAAE,OAAO,CAAC;QAAC,aAAa,CAAC,EAAE,OAAO,CAAA;KAAE,GAC1D,OAAO,CACN,CAAC,aAAa,GAAG;QACf,MAAM,CAAC,EAAE,WAAW,EAAE,CAAA;QACtB,eAAe,CAAC,EAAE,oBAAoB,EAAE,CAAA;QACxC,aAAa,CAAC,EAAE,qBAAqB,EAAE,CAAA;KACxC,CAAC,GACF,IAAI,CACP;sBAkCuB,kBAAkB,QAAQ,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC;sBAkClF,kBAAkB,MAClB,MAAM,QACJ,aAAa,GAClB,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC;8BAa1B,kBAAkB,aACX,MAAM,GAChB,OAAO,CAAC,qBAAqB,EAAE,CAAC;0BA0B7B,kBAAkB,WACb,iBAAiB,GACzB,OAAO,CAAC,SAAS,EAAE,CAAC;2BAajB,kBAAkB,WACb,kBAAkB,GAC1B,OAAO,CAAC,gBAAgB,EAAE,CAAC;CAa/B,CAAA"}
|