@voyantjs/pricing 0.3.0 → 0.4.0

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/dist/index.d.ts CHANGED
@@ -2,6 +2,9 @@ import type { Module } from "@voyantjs/core";
2
2
  import type { HonoModule } from "@voyantjs/hono/module";
3
3
  import { pricingService } from "./service.js";
4
4
  export type { PricingRoutes } from "./routes.js";
5
+ export type { PublicPricingRoutes } from "./routes-public.js";
6
+ export { publicPricingRoutes } from "./routes-public.js";
7
+ export { publicPricingService } from "./service-public.js";
5
8
  export declare const pricingModule: Module;
6
9
  export declare const pricingHonoModule: HonoModule;
7
10
  export type { CancellationPolicy, CancellationPolicyRule, DropoffPriceRule, ExtraPriceRule, NewCancellationPolicy, NewCancellationPolicyRule, NewDropoffPriceRule, NewExtraPriceRule, NewOptionPriceRule, NewOptionStartTimeRule, NewOptionUnitPriceRule, NewOptionUnitTier, NewPickupPriceRule, NewPriceCatalog, NewPriceSchedule, NewPricingCategory, NewPricingCategoryDependency, OptionPriceRule, OptionStartTimeRule, OptionUnitPriceRule, OptionUnitTier, PickupPriceRule, PriceCatalog, PriceSchedule, PricingCategory, PricingCategoryDependency, } from "./schema.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAA;AAC5C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAA;AAGvD,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAE7C,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAEhD,eAAO,MAAM,aAAa,EAAE,MAE3B,CAAA;AAED,eAAO,MAAM,iBAAiB,EAAE,UAG/B,CAAA;AAED,YAAY,EACV,kBAAkB,EAClB,sBAAsB,EACtB,gBAAgB,EAChB,cAAc,EACd,qBAAqB,EACrB,yBAAyB,EACzB,mBAAmB,EACnB,iBAAiB,EACjB,kBAAkB,EAClB,sBAAsB,EACtB,sBAAsB,EACtB,iBAAiB,EACjB,kBAAkB,EAClB,eAAe,EACf,gBAAgB,EAChB,kBAAkB,EAClB,4BAA4B,EAC5B,eAAe,EACf,mBAAmB,EACnB,mBAAmB,EACnB,cAAc,EACd,eAAe,EACf,YAAY,EACZ,aAAa,EACb,eAAe,EACf,yBAAyB,GAC1B,MAAM,aAAa,CAAA;AACpB,OAAO,EACL,oBAAoB,EACpB,0BAA0B,EAC1B,oBAAoB,EACpB,uBAAuB,EACvB,0BAA0B,EAC1B,iBAAiB,EACjB,eAAe,EACf,gBAAgB,EAChB,qBAAqB,EACrB,2BAA2B,EAC3B,oBAAoB,EACpB,oBAAoB,EACpB,yBAAyB,EACzB,eAAe,EACf,gBAAgB,EAChB,uBAAuB,EACvB,aAAa,EACb,oBAAoB,EACpB,cAAc,EACd,iBAAiB,EACjB,2BAA2B,EAC3B,uBAAuB,EACvB,yBAAyB,GAC1B,MAAM,aAAa,CAAA;AACpB,OAAO,EACL,sBAAsB,EACtB,4BAA4B,EAC5B,iCAAiC,EACjC,qCAAqC,EACrC,4BAA4B,EAC5B,+BAA+B,EAC/B,6BAA6B,EAC7B,kCAAkC,EAClC,8BAA8B,EAC9B,4BAA4B,EAC5B,0BAA0B,EAC1B,2BAA2B,EAC3B,+BAA+B,EAC/B,+BAA+B,EAC/B,0BAA0B,EAC1B,2BAA2B,EAC3B,wBAAwB,EACxB,yBAAyB,EACzB,qCAAqC,EACrC,2BAA2B,EAC3B,8BAA8B,EAC9B,uBAAuB,EACvB,kCAAkC,EAClC,6BAA6B,EAC7B,kCAAkC,EAClC,2BAA2B,EAC3B,6BAA6B,EAC7B,8BAA8B,EAC9B,yBAAyB,EACzB,2BAA2B,EAC3B,sBAAsB,EACtB,4BAA4B,EAC5B,wCAAwC,EACxC,8BAA8B,EAC9B,yBAAyB,EACzB,2BAA2B,EAC3B,kCAAkC,EAClC,8BAA8B,EAC9B,4BAA4B,EAC5B,0BAA0B,EAC1B,2BAA2B,EAC3B,+BAA+B,EAC/B,+BAA+B,EAC/B,0BAA0B,EAC1B,2BAA2B,EAC3B,wBAAwB,EACxB,yBAAyB,EACzB,qCAAqC,EACrC,2BAA2B,GAC5B,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,cAAc,EAAE,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAA;AAC5C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAA;AAGvD,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAE7C,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAChD,YAAY,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAA;AAC7D,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAA;AACxD,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAA;AAE1D,eAAO,MAAM,aAAa,EAAE,MAE3B,CAAA;AAED,eAAO,MAAM,iBAAiB,EAAE,UAK/B,CAAA;AAED,YAAY,EACV,kBAAkB,EAClB,sBAAsB,EACtB,gBAAgB,EAChB,cAAc,EACd,qBAAqB,EACrB,yBAAyB,EACzB,mBAAmB,EACnB,iBAAiB,EACjB,kBAAkB,EAClB,sBAAsB,EACtB,sBAAsB,EACtB,iBAAiB,EACjB,kBAAkB,EAClB,eAAe,EACf,gBAAgB,EAChB,kBAAkB,EAClB,4BAA4B,EAC5B,eAAe,EACf,mBAAmB,EACnB,mBAAmB,EACnB,cAAc,EACd,eAAe,EACf,YAAY,EACZ,aAAa,EACb,eAAe,EACf,yBAAyB,GAC1B,MAAM,aAAa,CAAA;AACpB,OAAO,EACL,oBAAoB,EACpB,0BAA0B,EAC1B,oBAAoB,EACpB,uBAAuB,EACvB,0BAA0B,EAC1B,iBAAiB,EACjB,eAAe,EACf,gBAAgB,EAChB,qBAAqB,EACrB,2BAA2B,EAC3B,oBAAoB,EACpB,oBAAoB,EACpB,yBAAyB,EACzB,eAAe,EACf,gBAAgB,EAChB,uBAAuB,EACvB,aAAa,EACb,oBAAoB,EACpB,cAAc,EACd,iBAAiB,EACjB,2BAA2B,EAC3B,uBAAuB,EACvB,yBAAyB,GAC1B,MAAM,aAAa,CAAA;AACpB,OAAO,EACL,sBAAsB,EACtB,4BAA4B,EAC5B,iCAAiC,EACjC,qCAAqC,EACrC,4BAA4B,EAC5B,+BAA+B,EAC/B,6BAA6B,EAC7B,kCAAkC,EAClC,8BAA8B,EAC9B,4BAA4B,EAC5B,0BAA0B,EAC1B,2BAA2B,EAC3B,+BAA+B,EAC/B,+BAA+B,EAC/B,0BAA0B,EAC1B,2BAA2B,EAC3B,wBAAwB,EACxB,yBAAyB,EACzB,qCAAqC,EACrC,2BAA2B,EAC3B,8BAA8B,EAC9B,uBAAuB,EACvB,kCAAkC,EAClC,6BAA6B,EAC7B,kCAAkC,EAClC,2BAA2B,EAC3B,6BAA6B,EAC7B,8BAA8B,EAC9B,yBAAyB,EACzB,2BAA2B,EAC3B,sBAAsB,EACtB,4BAA4B,EAC5B,wCAAwC,EACxC,8BAA8B,EAC9B,yBAAyB,EACzB,2BAA2B,EAC3B,kCAAkC,EAClC,8BAA8B,EAC9B,4BAA4B,EAC5B,0BAA0B,EAC1B,2BAA2B,EAC3B,+BAA+B,EAC/B,+BAA+B,EAC/B,0BAA0B,EAC1B,2BAA2B,EAC3B,wBAAwB,EACxB,yBAAyB,EACzB,qCAAqC,EACrC,2BAA2B,GAC5B,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,cAAc,EAAE,CAAA"}
package/dist/index.js CHANGED
@@ -1,10 +1,15 @@
1
1
  import { pricingRoutes } from "./routes.js";
2
+ import { publicPricingRoutes } from "./routes-public.js";
2
3
  import { pricingService } from "./service.js";
4
+ export { publicPricingRoutes } from "./routes-public.js";
5
+ export { publicPricingService } from "./service-public.js";
3
6
  export const pricingModule = {
4
7
  name: "pricing",
5
8
  };
6
9
  export const pricingHonoModule = {
7
10
  module: pricingModule,
11
+ adminRoutes: pricingRoutes,
12
+ publicRoutes: publicPricingRoutes,
8
13
  routes: pricingRoutes,
9
14
  };
10
15
  export { addonPricingModeEnum, cancellationChargeTypeEnum, cancellationPolicies, cancellationPolicyRules, cancellationPolicyTypeEnum, dropoffPriceRules, extraPriceRules, optionPriceRules, optionPricingModeEnum, optionStartTimeRuleModeEnum, optionStartTimeRules, optionUnitPriceRules, optionUnitPricingModeEnum, optionUnitTiers, pickupPriceRules, priceAdjustmentTypeEnum, priceCatalogs, priceCatalogTypeEnum, priceSchedules, pricingCategories, pricingCategoryDependencies, pricingCategoryTypeEnum, pricingDependencyTypeEnum, } from "./schema.js";
@@ -0,0 +1,136 @@
1
+ import { type Env } from "./routes-shared.js";
2
+ export declare const publicPricingRoutes: import("hono/hono-base").HonoBase<Env, {
3
+ "/products/:productId/pricing": {
4
+ $get: {
5
+ input: {
6
+ param: {
7
+ productId: string;
8
+ };
9
+ };
10
+ output: {
11
+ error: string;
12
+ };
13
+ outputFormat: "json";
14
+ status: 404;
15
+ } | {
16
+ input: {
17
+ param: {
18
+ productId: string;
19
+ };
20
+ };
21
+ output: {
22
+ data: {
23
+ productId: string;
24
+ catalog: {
25
+ currencyCode: string | null;
26
+ id: string;
27
+ code: string;
28
+ name: string;
29
+ };
30
+ options: {
31
+ id: string;
32
+ name: string;
33
+ description: string | null;
34
+ status: "active" | "draft" | "archived";
35
+ isDefault: boolean;
36
+ bookingMode: "other" | "date" | "open" | "date_time" | "stay" | "transfer" | "itinerary";
37
+ capacityMode: "on_request" | "free_sale" | "limited";
38
+ pricingRules: {
39
+ id: string;
40
+ name: string;
41
+ description: string | null;
42
+ pricingMode: "per_person" | "per_booking" | "starting_from" | "free" | "on_request";
43
+ baseSellAmountCents: number | null;
44
+ minPerBooking: number | null;
45
+ maxPerBooking: number | null;
46
+ isDefault: boolean;
47
+ cancellationPolicyId: string | null;
48
+ unitPrices: {
49
+ id: string;
50
+ unitId: string;
51
+ unitName: string;
52
+ unitType: "group" | "room" | "vehicle" | "service" | "other" | "person";
53
+ pricingMode: "per_person" | "per_booking" | "free" | "on_request" | "per_unit" | "included";
54
+ sellAmountCents: number | null;
55
+ minQuantity: number | null;
56
+ maxQuantity: number | null;
57
+ pricingCategoryId: string | null;
58
+ sortOrder: number;
59
+ tiers: {
60
+ id: string;
61
+ minQuantity: number;
62
+ maxQuantity: number | null;
63
+ sellAmountCents: number | null;
64
+ sortOrder: number;
65
+ }[];
66
+ }[];
67
+ startTimeAdjustments: {
68
+ id: string;
69
+ startTimeId: string;
70
+ label: string | null;
71
+ startTimeLocal: string;
72
+ ruleMode: "included" | "excluded" | "override" | "adjustment";
73
+ adjustmentType: "fixed" | "percentage" | null;
74
+ sellAdjustmentCents: number | null;
75
+ adjustmentBasisPoints: number | null;
76
+ }[];
77
+ }[];
78
+ }[];
79
+ };
80
+ };
81
+ outputFormat: "json";
82
+ status: import("hono/utils/http-status").ContentfulStatusCode;
83
+ };
84
+ };
85
+ } & {
86
+ "/products/:productId/availability": {
87
+ $get: {
88
+ input: {
89
+ param: {
90
+ productId: string;
91
+ };
92
+ };
93
+ output: {
94
+ error: string;
95
+ };
96
+ outputFormat: "json";
97
+ status: 404;
98
+ } | {
99
+ input: {
100
+ param: {
101
+ productId: string;
102
+ };
103
+ };
104
+ output: {
105
+ productId: string;
106
+ slots: {
107
+ id: string;
108
+ optionId: string | null;
109
+ dateLocal: string | null;
110
+ startsAt: string | null;
111
+ endsAt: string | null;
112
+ timezone: string;
113
+ status: "open" | "closed" | "sold_out" | "cancelled";
114
+ unlimited: boolean;
115
+ remainingPax: number | null;
116
+ remainingResources: number | null;
117
+ pastCutoff: boolean;
118
+ tooEarly: boolean;
119
+ startTime: {
120
+ id: string;
121
+ label: string | null;
122
+ startTimeLocal: string;
123
+ durationMinutes: number | null;
124
+ } | null;
125
+ }[];
126
+ total: number;
127
+ limit: number;
128
+ offset: number;
129
+ };
130
+ outputFormat: "json";
131
+ status: import("hono/utils/http-status").ContentfulStatusCode;
132
+ };
133
+ };
134
+ }, "/", "/products/:productId/availability">;
135
+ export type PublicPricingRoutes = typeof publicPricingRoutes;
136
+ //# sourceMappingURL=routes-public.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes-public.d.ts","sourceRoot":"","sources":["../src/routes-public.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,GAAG,EAAY,MAAM,oBAAoB,CAAA;AAOvD,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;4CAoB5B,CAAA;AAEJ,MAAM,MAAM,mBAAmB,GAAG,OAAO,mBAAmB,CAAA"}
@@ -0,0 +1,13 @@
1
+ import { Hono } from "hono";
2
+ import { notFound } from "./routes-shared.js";
3
+ import { publicPricingService } from "./service-public.js";
4
+ import { publicAvailabilitySnapshotQuerySchema, publicProductPricingQuerySchema, } from "./validation-public.js";
5
+ export const publicPricingRoutes = new Hono()
6
+ .get("/products/:productId/pricing", async (c) => {
7
+ const snapshot = await publicPricingService.getProductPricingSnapshot(c.get("db"), c.req.param("productId"), publicProductPricingQuerySchema.parse(Object.fromEntries(new URL(c.req.url).searchParams)));
8
+ return snapshot ? c.json({ data: snapshot }) : notFound(c, "Public pricing snapshot not found");
9
+ })
10
+ .get("/products/:productId/availability", async (c) => {
11
+ const snapshot = await publicPricingService.getAvailabilitySnapshot(c.get("db"), c.req.param("productId"), publicAvailabilitySnapshotQuerySchema.parse(Object.fromEntries(new URL(c.req.url).searchParams)));
12
+ return snapshot ? c.json(snapshot) : notFound(c, "Public availability snapshot not found");
13
+ });
package/dist/routes.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import { Hono } from "hono";
2
+ import type { publicPricingRoutes } from "./routes-public.js";
2
3
  import type { Env } from "./routes-shared.js";
3
4
  export declare const pricingRoutes: Hono<Env, import("hono/types").BlankSchema, "/">;
4
5
  export type PricingRoutes = typeof pricingRoutes;
6
+ export type PublicPricingRoutes = typeof publicPricingRoutes;
5
7
  //# sourceMappingURL=routes.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAI3B,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,oBAAoB,CAAA;AAE7C,eAAO,MAAM,aAAa,kDAAkB,CAAA;AAI5C,MAAM,MAAM,aAAa,GAAG,OAAO,aAAa,CAAA"}
1
+ {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAG3B,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAA;AAE7D,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,oBAAoB,CAAA;AAE7C,eAAO,MAAM,aAAa,kDAAkB,CAAA;AAI5C,MAAM,MAAM,aAAa,GAAG,OAAO,aAAa,CAAA;AAChD,MAAM,MAAM,mBAAmB,GAAG,OAAO,mBAAmB,CAAA"}
@@ -0,0 +1,89 @@
1
+ import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
2
+ import type { PublicAvailabilitySnapshotQuery, PublicProductPricingQuery } from "./validation-public.js";
3
+ export declare const publicPricingService: {
4
+ getProductPricingSnapshot(db: PostgresJsDatabase, productId: string, query: PublicProductPricingQuery): Promise<{
5
+ productId: string;
6
+ catalog: {
7
+ currencyCode: string | null;
8
+ id: string;
9
+ code: string;
10
+ name: string;
11
+ };
12
+ options: {
13
+ id: string;
14
+ name: string;
15
+ description: string | null;
16
+ status: "active" | "draft" | "archived";
17
+ isDefault: boolean;
18
+ bookingMode: "other" | "date" | "open" | "date_time" | "stay" | "transfer" | "itinerary";
19
+ capacityMode: "on_request" | "free_sale" | "limited";
20
+ pricingRules: {
21
+ id: string;
22
+ name: string;
23
+ description: string | null;
24
+ pricingMode: "per_person" | "per_booking" | "starting_from" | "free" | "on_request";
25
+ baseSellAmountCents: number | null;
26
+ minPerBooking: number | null;
27
+ maxPerBooking: number | null;
28
+ isDefault: boolean;
29
+ cancellationPolicyId: string | null;
30
+ unitPrices: {
31
+ id: string;
32
+ unitId: string;
33
+ unitName: string;
34
+ unitType: "group" | "room" | "vehicle" | "service" | "other" | "person";
35
+ pricingMode: "per_person" | "per_booking" | "free" | "on_request" | "per_unit" | "included";
36
+ sellAmountCents: number | null;
37
+ minQuantity: number | null;
38
+ maxQuantity: number | null;
39
+ pricingCategoryId: string | null;
40
+ sortOrder: number;
41
+ tiers: {
42
+ id: string;
43
+ minQuantity: number;
44
+ maxQuantity: number | null;
45
+ sellAmountCents: number | null;
46
+ sortOrder: number;
47
+ }[];
48
+ }[];
49
+ startTimeAdjustments: {
50
+ id: string;
51
+ startTimeId: string;
52
+ label: string | null;
53
+ startTimeLocal: string;
54
+ ruleMode: "included" | "excluded" | "override" | "adjustment";
55
+ adjustmentType: "fixed" | "percentage" | null;
56
+ sellAdjustmentCents: number | null;
57
+ adjustmentBasisPoints: number | null;
58
+ }[];
59
+ }[];
60
+ }[];
61
+ } | null>;
62
+ getAvailabilitySnapshot(db: PostgresJsDatabase, productId: string, query: PublicAvailabilitySnapshotQuery): Promise<{
63
+ productId: string;
64
+ slots: {
65
+ id: string;
66
+ optionId: string | null;
67
+ dateLocal: string | null;
68
+ startsAt: string | null;
69
+ endsAt: string | null;
70
+ timezone: string;
71
+ status: "open" | "closed" | "sold_out" | "cancelled";
72
+ unlimited: boolean;
73
+ remainingPax: number | null;
74
+ remainingResources: number | null;
75
+ pastCutoff: boolean;
76
+ tooEarly: boolean;
77
+ startTime: {
78
+ id: string;
79
+ label: string | null;
80
+ startTimeLocal: string;
81
+ durationMinutes: number | null;
82
+ } | null;
83
+ }[];
84
+ total: number;
85
+ limit: number;
86
+ offset: number;
87
+ } | null>;
88
+ };
89
+ //# sourceMappingURL=service-public.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-public.d.ts","sourceRoot":"","sources":["../src/service-public.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AASjE,OAAO,KAAK,EACV,+BAA+B,EAC/B,yBAAyB,EAC1B,MAAM,wBAAwB,CAAA;AAuE/B,eAAO,MAAM,oBAAoB;kCAEzB,kBAAkB,aACX,MAAM,SACV,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gCA0R5B,kBAAkB,aACX,MAAM,SACV,+BAA+B;;;;;;;;;;;;;;;;;;;;;;;;;;CAgGzC,CAAA"}
@@ -0,0 +1,355 @@
1
+ import { availabilitySlots, availabilityStartTimes } from "@voyantjs/availability/schema";
2
+ import { optionUnits, productOptions, products } from "@voyantjs/products/schema";
3
+ import { and, asc, desc, eq, gte, inArray, lte, ne, or, sql } from "drizzle-orm";
4
+ import { optionPriceRules, optionStartTimeRules, optionUnitPriceRules, optionUnitTiers, priceCatalogs, } from "./schema.js";
5
+ function normalizeDate(value) {
6
+ if (!value) {
7
+ return null;
8
+ }
9
+ return value instanceof Date ? value.toISOString() : value;
10
+ }
11
+ async function ensurePublicProduct(db, productId) {
12
+ const [product] = await db
13
+ .select({
14
+ id: products.id,
15
+ bookingMode: products.bookingMode,
16
+ capacityMode: products.capacityMode,
17
+ })
18
+ .from(products)
19
+ .where(and(eq(products.id, productId), eq(products.status, "active"), eq(products.activated, true), eq(products.visibility, "public")))
20
+ .limit(1);
21
+ return product ?? null;
22
+ }
23
+ async function resolvePublicCatalog(db, input) {
24
+ if (input.catalogId) {
25
+ const [catalog] = await db
26
+ .select({
27
+ id: priceCatalogs.id,
28
+ code: priceCatalogs.code,
29
+ name: priceCatalogs.name,
30
+ currencyCode: priceCatalogs.currencyCode,
31
+ })
32
+ .from(priceCatalogs)
33
+ .where(and(eq(priceCatalogs.id, input.catalogId), eq(priceCatalogs.catalogType, "public"), eq(priceCatalogs.active, true)))
34
+ .limit(1);
35
+ return catalog ?? null;
36
+ }
37
+ const [catalog] = await db
38
+ .select({
39
+ id: priceCatalogs.id,
40
+ code: priceCatalogs.code,
41
+ name: priceCatalogs.name,
42
+ currencyCode: priceCatalogs.currencyCode,
43
+ })
44
+ .from(priceCatalogs)
45
+ .where(and(eq(priceCatalogs.catalogType, "public"), eq(priceCatalogs.active, true)))
46
+ .orderBy(desc(priceCatalogs.isDefault), asc(priceCatalogs.name))
47
+ .limit(1);
48
+ return catalog ?? null;
49
+ }
50
+ export const publicPricingService = {
51
+ async getProductPricingSnapshot(db, productId, query) {
52
+ const product = await ensurePublicProduct(db, productId);
53
+ if (!product) {
54
+ return null;
55
+ }
56
+ const catalog = await resolvePublicCatalog(db, query);
57
+ if (!catalog) {
58
+ return null;
59
+ }
60
+ const optionConditions = [
61
+ eq(productOptions.productId, productId),
62
+ eq(productOptions.status, "active"),
63
+ ];
64
+ if (query.optionId) {
65
+ optionConditions.push(eq(productOptions.id, query.optionId));
66
+ }
67
+ const options = await db
68
+ .select({
69
+ id: productOptions.id,
70
+ name: productOptions.name,
71
+ description: productOptions.description,
72
+ status: productOptions.status,
73
+ isDefault: productOptions.isDefault,
74
+ })
75
+ .from(productOptions)
76
+ .where(and(...optionConditions))
77
+ .orderBy(desc(productOptions.isDefault), asc(productOptions.sortOrder), asc(productOptions.name));
78
+ if (options.length === 0) {
79
+ return {
80
+ productId,
81
+ catalog: {
82
+ ...catalog,
83
+ currencyCode: catalog.currencyCode ?? null,
84
+ },
85
+ options: [],
86
+ };
87
+ }
88
+ const optionIds = options.map((option) => option.id);
89
+ const [units, rules] = await Promise.all([
90
+ db
91
+ .select({
92
+ id: optionUnits.id,
93
+ optionId: optionUnits.optionId,
94
+ name: optionUnits.name,
95
+ unitType: optionUnits.unitType,
96
+ isHidden: optionUnits.isHidden,
97
+ sortOrder: optionUnits.sortOrder,
98
+ })
99
+ .from(optionUnits)
100
+ .where(and(inArray(optionUnits.optionId, optionIds), eq(optionUnits.isHidden, false)))
101
+ .orderBy(asc(optionUnits.sortOrder), asc(optionUnits.name)),
102
+ db
103
+ .select({
104
+ id: optionPriceRules.id,
105
+ optionId: optionPriceRules.optionId,
106
+ name: optionPriceRules.name,
107
+ description: optionPriceRules.description,
108
+ pricingMode: optionPriceRules.pricingMode,
109
+ baseSellAmountCents: optionPriceRules.baseSellAmountCents,
110
+ minPerBooking: optionPriceRules.minPerBooking,
111
+ maxPerBooking: optionPriceRules.maxPerBooking,
112
+ isDefault: optionPriceRules.isDefault,
113
+ cancellationPolicyId: optionPriceRules.cancellationPolicyId,
114
+ })
115
+ .from(optionPriceRules)
116
+ .where(and(eq(optionPriceRules.productId, productId), inArray(optionPriceRules.optionId, optionIds), eq(optionPriceRules.priceCatalogId, catalog.id), eq(optionPriceRules.active, true)))
117
+ .orderBy(desc(optionPriceRules.isDefault), asc(optionPriceRules.name)),
118
+ ]);
119
+ const ruleIds = rules.map((rule) => rule.id);
120
+ const [unitPrices, startTimeAdjustments] = await Promise.all([
121
+ ruleIds.length > 0
122
+ ? db
123
+ .select({
124
+ id: optionUnitPriceRules.id,
125
+ optionPriceRuleId: optionUnitPriceRules.optionPriceRuleId,
126
+ unitId: optionUnitPriceRules.unitId,
127
+ pricingMode: optionUnitPriceRules.pricingMode,
128
+ sellAmountCents: optionUnitPriceRules.sellAmountCents,
129
+ minQuantity: optionUnitPriceRules.minQuantity,
130
+ maxQuantity: optionUnitPriceRules.maxQuantity,
131
+ pricingCategoryId: optionUnitPriceRules.pricingCategoryId,
132
+ sortOrder: optionUnitPriceRules.sortOrder,
133
+ })
134
+ .from(optionUnitPriceRules)
135
+ .where(and(inArray(optionUnitPriceRules.optionPriceRuleId, ruleIds), eq(optionUnitPriceRules.active, true)))
136
+ .orderBy(asc(optionUnitPriceRules.sortOrder), asc(optionUnitPriceRules.createdAt))
137
+ : Promise.resolve([]),
138
+ ruleIds.length > 0
139
+ ? db
140
+ .select({
141
+ id: optionStartTimeRules.id,
142
+ optionPriceRuleId: optionStartTimeRules.optionPriceRuleId,
143
+ startTimeId: optionStartTimeRules.startTimeId,
144
+ label: availabilityStartTimes.label,
145
+ startTimeLocal: availabilityStartTimes.startTimeLocal,
146
+ durationMinutes: availabilityStartTimes.durationMinutes,
147
+ ruleMode: optionStartTimeRules.ruleMode,
148
+ adjustmentType: optionStartTimeRules.adjustmentType,
149
+ sellAdjustmentCents: optionStartTimeRules.sellAdjustmentCents,
150
+ adjustmentBasisPoints: optionStartTimeRules.adjustmentBasisPoints,
151
+ })
152
+ .from(optionStartTimeRules)
153
+ .innerJoin(availabilityStartTimes, eq(availabilityStartTimes.id, optionStartTimeRules.startTimeId))
154
+ .where(and(inArray(optionStartTimeRules.optionPriceRuleId, ruleIds), eq(optionStartTimeRules.active, true), eq(availabilityStartTimes.active, true)))
155
+ .orderBy(asc(availabilityStartTimes.sortOrder), asc(availabilityStartTimes.startTimeLocal))
156
+ : Promise.resolve([]),
157
+ ]);
158
+ const unitPriceIds = unitPrices.map((unitPrice) => unitPrice.id);
159
+ const tiers = unitPriceIds.length > 0
160
+ ? await db
161
+ .select({
162
+ id: optionUnitTiers.id,
163
+ optionUnitPriceRuleId: optionUnitTiers.optionUnitPriceRuleId,
164
+ minQuantity: optionUnitTiers.minQuantity,
165
+ maxQuantity: optionUnitTiers.maxQuantity,
166
+ sellAmountCents: optionUnitTiers.sellAmountCents,
167
+ sortOrder: optionUnitTiers.sortOrder,
168
+ })
169
+ .from(optionUnitTiers)
170
+ .where(and(inArray(optionUnitTiers.optionUnitPriceRuleId, unitPriceIds), eq(optionUnitTiers.active, true)))
171
+ .orderBy(asc(optionUnitTiers.sortOrder), asc(optionUnitTiers.minQuantity))
172
+ : [];
173
+ const unitById = new Map(units.map((unit) => [
174
+ unit.id,
175
+ {
176
+ id: unit.id,
177
+ unitId: unit.id,
178
+ unitName: unit.name,
179
+ unitType: unit.unitType,
180
+ sortOrder: unit.sortOrder,
181
+ },
182
+ ]));
183
+ const tiersByUnitPriceRule = new Map();
184
+ for (const tier of tiers) {
185
+ const existing = tiersByUnitPriceRule.get(tier.optionUnitPriceRuleId) ?? [];
186
+ existing.push(tier);
187
+ tiersByUnitPriceRule.set(tier.optionUnitPriceRuleId, existing);
188
+ }
189
+ const unitPricesByRule = new Map();
190
+ for (const unitPrice of unitPrices) {
191
+ const existing = unitPricesByRule.get(unitPrice.optionPriceRuleId) ?? [];
192
+ existing.push(unitPrice);
193
+ unitPricesByRule.set(unitPrice.optionPriceRuleId, existing);
194
+ }
195
+ const startTimeAdjustmentsByRule = new Map();
196
+ for (const adjustment of startTimeAdjustments) {
197
+ const existing = startTimeAdjustmentsByRule.get(adjustment.optionPriceRuleId) ?? [];
198
+ existing.push(adjustment);
199
+ startTimeAdjustmentsByRule.set(adjustment.optionPriceRuleId, existing);
200
+ }
201
+ const rulesByOption = new Map();
202
+ for (const rule of rules) {
203
+ const existing = rulesByOption.get(rule.optionId) ?? [];
204
+ existing.push(rule);
205
+ rulesByOption.set(rule.optionId, existing);
206
+ }
207
+ return {
208
+ productId,
209
+ catalog: {
210
+ ...catalog,
211
+ currencyCode: catalog.currencyCode ?? null,
212
+ },
213
+ options: options.map((option) => ({
214
+ id: option.id,
215
+ name: option.name,
216
+ description: option.description ?? null,
217
+ status: option.status,
218
+ isDefault: option.isDefault,
219
+ bookingMode: product.bookingMode,
220
+ capacityMode: product.capacityMode,
221
+ pricingRules: (rulesByOption.get(option.id) ?? []).map((rule) => ({
222
+ id: rule.id,
223
+ name: rule.name,
224
+ description: rule.description ?? null,
225
+ pricingMode: rule.pricingMode,
226
+ baseSellAmountCents: rule.baseSellAmountCents ?? null,
227
+ minPerBooking: rule.minPerBooking ?? null,
228
+ maxPerBooking: rule.maxPerBooking ?? null,
229
+ isDefault: rule.isDefault,
230
+ cancellationPolicyId: rule.cancellationPolicyId ?? null,
231
+ unitPrices: (unitPricesByRule.get(rule.id) ?? [])
232
+ .map((unitPrice) => {
233
+ const unit = unitById.get(unitPrice.unitId);
234
+ if (!unit) {
235
+ return null;
236
+ }
237
+ return {
238
+ id: unitPrice.id,
239
+ unitId: unit.unitId,
240
+ unitName: unit.unitName,
241
+ unitType: unit.unitType,
242
+ pricingMode: unitPrice.pricingMode,
243
+ sellAmountCents: unitPrice.sellAmountCents ?? null,
244
+ minQuantity: unitPrice.minQuantity ?? null,
245
+ maxQuantity: unitPrice.maxQuantity ?? null,
246
+ pricingCategoryId: unitPrice.pricingCategoryId ?? null,
247
+ sortOrder: unitPrice.sortOrder,
248
+ tiers: (tiersByUnitPriceRule.get(unitPrice.id) ?? []).map((tier) => ({
249
+ id: tier.id,
250
+ minQuantity: tier.minQuantity,
251
+ maxQuantity: tier.maxQuantity ?? null,
252
+ sellAmountCents: tier.sellAmountCents ?? null,
253
+ sortOrder: tier.sortOrder,
254
+ })),
255
+ };
256
+ })
257
+ .filter((value) => value !== null),
258
+ startTimeAdjustments: (startTimeAdjustmentsByRule.get(rule.id) ?? []).map((adjustment) => ({
259
+ id: adjustment.id,
260
+ startTimeId: adjustment.startTimeId,
261
+ label: adjustment.label ?? null,
262
+ startTimeLocal: adjustment.startTimeLocal,
263
+ ruleMode: adjustment.ruleMode,
264
+ adjustmentType: adjustment.adjustmentType ?? null,
265
+ sellAdjustmentCents: adjustment.sellAdjustmentCents ?? null,
266
+ adjustmentBasisPoints: adjustment.adjustmentBasisPoints ?? null,
267
+ })),
268
+ })),
269
+ })),
270
+ };
271
+ },
272
+ async getAvailabilitySnapshot(db, productId, query) {
273
+ const product = await ensurePublicProduct(db, productId);
274
+ if (!product) {
275
+ return null;
276
+ }
277
+ const conditions = [
278
+ eq(availabilitySlots.productId, productId),
279
+ ne(availabilitySlots.status, "cancelled"),
280
+ ];
281
+ if (query.optionId) {
282
+ conditions.push(eq(availabilitySlots.optionId, query.optionId));
283
+ }
284
+ if (query.dateFrom) {
285
+ conditions.push(gte(availabilitySlots.dateLocal, query.dateFrom));
286
+ }
287
+ if (query.dateTo) {
288
+ conditions.push(lte(availabilitySlots.dateLocal, query.dateTo));
289
+ }
290
+ if (query.status) {
291
+ conditions.push(eq(availabilitySlots.status, query.status));
292
+ }
293
+ else {
294
+ conditions.push(or(eq(availabilitySlots.status, "open"), eq(availabilitySlots.status, "sold_out")) ??
295
+ sql `1 = 1`);
296
+ }
297
+ const where = and(...conditions);
298
+ const [rows, countResult] = await Promise.all([
299
+ db
300
+ .select({
301
+ id: availabilitySlots.id,
302
+ optionId: availabilitySlots.optionId,
303
+ dateLocal: availabilitySlots.dateLocal,
304
+ startsAt: availabilitySlots.startsAt,
305
+ endsAt: availabilitySlots.endsAt,
306
+ timezone: availabilitySlots.timezone,
307
+ status: availabilitySlots.status,
308
+ unlimited: availabilitySlots.unlimited,
309
+ remainingPax: availabilitySlots.remainingPax,
310
+ remainingResources: availabilitySlots.remainingResources,
311
+ pastCutoff: availabilitySlots.pastCutoff,
312
+ tooEarly: availabilitySlots.tooEarly,
313
+ startTimeId: availabilityStartTimes.id,
314
+ startTimeLabel: availabilityStartTimes.label,
315
+ startTimeLocal: availabilityStartTimes.startTimeLocal,
316
+ durationMinutes: availabilityStartTimes.durationMinutes,
317
+ })
318
+ .from(availabilitySlots)
319
+ .leftJoin(availabilityStartTimes, eq(availabilityStartTimes.id, availabilitySlots.startTimeId))
320
+ .where(where)
321
+ .orderBy(asc(availabilitySlots.startsAt))
322
+ .limit(query.limit)
323
+ .offset(query.offset),
324
+ db.select({ count: sql `count(*)::int` }).from(availabilitySlots).where(where),
325
+ ]);
326
+ return {
327
+ productId,
328
+ slots: rows.map((row) => ({
329
+ id: row.id,
330
+ optionId: row.optionId ?? null,
331
+ dateLocal: normalizeDate(row.dateLocal),
332
+ startsAt: normalizeDate(row.startsAt),
333
+ endsAt: normalizeDate(row.endsAt),
334
+ timezone: row.timezone,
335
+ status: row.status,
336
+ unlimited: row.unlimited,
337
+ remainingPax: row.remainingPax ?? null,
338
+ remainingResources: row.remainingResources ?? null,
339
+ pastCutoff: row.pastCutoff,
340
+ tooEarly: row.tooEarly,
341
+ startTime: row.startTimeId
342
+ ? {
343
+ id: row.startTimeId,
344
+ label: row.startTimeLabel ?? null,
345
+ startTimeLocal: row.startTimeLocal ?? "",
346
+ durationMinutes: row.durationMinutes ?? null,
347
+ }
348
+ : null,
349
+ })),
350
+ total: countResult[0]?.count ?? 0,
351
+ limit: query.limit,
352
+ offset: query.offset,
353
+ };
354
+ },
355
+ };