@voyant-travel/commerce 0.1.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/LICENSE +201 -0
- package/README.md +145 -0
- package/dist/accepted-quote-version-reservation-golden-flow.test.d.ts +2 -0
- package/dist/accepted-quote-version-reservation-golden-flow.test.d.ts.map +1 -0
- package/dist/accepted-quote-version-reservation-golden-flow.test.js +398 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/interface.d.ts +18 -0
- package/dist/interface.d.ts.map +1 -0
- package/dist/interface.js +246 -0
- package/dist/interface.test.d.ts +2 -0
- package/dist/interface.test.d.ts.map +1 -0
- package/dist/interface.test.js +357 -0
- package/dist/markets/index.d.ts +11 -0
- package/dist/markets/index.d.ts.map +1 -0
- package/dist/markets/index.js +12 -0
- package/dist/markets/routes.d.ts +1182 -0
- package/dist/markets/routes.d.ts.map +1 -0
- package/dist/markets/routes.js +209 -0
- package/dist/markets/schema.d.ts +1527 -0
- package/dist/markets/schema.d.ts.map +1 -0
- package/dist/markets/schema.js +240 -0
- package/dist/markets/service-core.d.ts +253 -0
- package/dist/markets/service-core.d.ts.map +1 -0
- package/dist/markets/service-core.js +242 -0
- package/dist/markets/service-rules.d.ts +191 -0
- package/dist/markets/service-rules.d.ts.map +1 -0
- package/dist/markets/service-rules.js +155 -0
- package/dist/markets/service-shared.d.ts +36 -0
- package/dist/markets/service-shared.d.ts.map +1 -0
- package/dist/markets/service-shared.js +7 -0
- package/dist/markets/service.d.ts +43 -0
- package/dist/markets/service.d.ts.map +1 -0
- package/dist/markets/service.js +42 -0
- package/dist/markets/validation.d.ts +451 -0
- package/dist/markets/validation.d.ts.map +1 -0
- package/dist/markets/validation.js +160 -0
- package/dist/pricing/events.d.ts +53 -0
- package/dist/pricing/events.d.ts.map +1 -0
- package/dist/pricing/events.js +28 -0
- package/dist/pricing/index.d.ts +15 -0
- package/dist/pricing/index.d.ts.map +1 -0
- package/dist/pricing/index.js +18 -0
- package/dist/pricing/routes-core.d.ts +981 -0
- package/dist/pricing/routes-core.d.ts.map +1 -0
- package/dist/pricing/routes-core.js +102 -0
- package/dist/pricing/routes-public.d.ts +136 -0
- package/dist/pricing/routes-public.d.ts.map +1 -0
- package/dist/pricing/routes-public.js +14 -0
- package/dist/pricing/routes-rules.d.ts +1339 -0
- package/dist/pricing/routes-rules.d.ts.map +1 -0
- package/dist/pricing/routes-rules.js +138 -0
- package/dist/pricing/routes-shared.d.ts +14 -0
- package/dist/pricing/routes-shared.d.ts.map +1 -0
- package/dist/pricing/routes-shared.js +3 -0
- package/dist/pricing/routes.d.ts +7 -0
- package/dist/pricing/routes.d.ts.map +1 -0
- package/dist/pricing/routes.js +6 -0
- package/dist/pricing/schema-catalogs.d.ts +467 -0
- package/dist/pricing/schema-catalogs.d.ts.map +1 -0
- package/dist/pricing/schema-catalogs.js +47 -0
- package/dist/pricing/schema-categories.d.ts +497 -0
- package/dist/pricing/schema-categories.d.ts.map +1 -0
- package/dist/pricing/schema-categories.js +54 -0
- package/dist/pricing/schema-departure-overrides.d.ts +228 -0
- package/dist/pricing/schema-departure-overrides.d.ts.map +1 -0
- package/dist/pricing/schema-departure-overrides.js +36 -0
- package/dist/pricing/schema-option-rules.d.ts +1770 -0
- package/dist/pricing/schema-option-rules.d.ts.map +1 -0
- package/dist/pricing/schema-option-rules.js +181 -0
- package/dist/pricing/schema-policies.d.ts +395 -0
- package/dist/pricing/schema-policies.d.ts.map +1 -0
- package/dist/pricing/schema-policies.js +41 -0
- package/dist/pricing/schema-relations.d.ts +59 -0
- package/dist/pricing/schema-relations.d.ts.map +1 -0
- package/dist/pricing/schema-relations.js +111 -0
- package/dist/pricing/schema-shared.d.ts +11 -0
- package/dist/pricing/schema-shared.d.ts.map +1 -0
- package/dist/pricing/schema-shared.js +67 -0
- package/dist/pricing/schema.d.ts +8 -0
- package/dist/pricing/schema.d.ts.map +1 -0
- package/dist/pricing/schema.js +7 -0
- package/dist/pricing/service-catalog-plane-pricing.d.ts +95 -0
- package/dist/pricing/service-catalog-plane-pricing.d.ts.map +1 -0
- package/dist/pricing/service-catalog-plane-pricing.js +382 -0
- package/dist/pricing/service-catalogs.d.ts +139 -0
- package/dist/pricing/service-catalogs.d.ts.map +1 -0
- package/dist/pricing/service-catalogs.js +89 -0
- package/dist/pricing/service-categories.d.ts +147 -0
- package/dist/pricing/service-categories.d.ts.map +1 -0
- package/dist/pricing/service-categories.js +105 -0
- package/dist/pricing/service-departure-overrides.d.ts +67 -0
- package/dist/pricing/service-departure-overrides.d.ts.map +1 -0
- package/dist/pricing/service-departure-overrides.js +54 -0
- package/dist/pricing/service-option-rules.d.ts +321 -0
- package/dist/pricing/service-option-rules.d.ts.map +1 -0
- package/dist/pricing/service-option-rules.js +340 -0
- package/dist/pricing/service-policies.d.ts +123 -0
- package/dist/pricing/service-policies.d.ts.map +1 -0
- package/dist/pricing/service-policies.js +95 -0
- package/dist/pricing/service-public.d.ts +89 -0
- package/dist/pricing/service-public.d.ts.map +1 -0
- package/dist/pricing/service-public.js +473 -0
- package/dist/pricing/service-rule-resolver.d.ts +67 -0
- package/dist/pricing/service-rule-resolver.d.ts.map +1 -0
- package/dist/pricing/service-rule-resolver.js +204 -0
- package/dist/pricing/service-shared.d.ts +53 -0
- package/dist/pricing/service-shared.d.ts.map +1 -0
- package/dist/pricing/service-shared.js +4 -0
- package/dist/pricing/service-transfer-rules.d.ts +211 -0
- package/dist/pricing/service-transfer-rules.d.ts.map +1 -0
- package/dist/pricing/service-transfer-rules.js +139 -0
- package/dist/pricing/service.d.ts +79 -0
- package/dist/pricing/service.d.ts.map +1 -0
- package/dist/pricing/service.js +78 -0
- package/dist/pricing/validation-public.d.ts +412 -0
- package/dist/pricing/validation-public.d.ts.map +1 -0
- package/dist/pricing/validation-public.js +111 -0
- package/dist/pricing/validation-shared.d.ts +71 -0
- package/dist/pricing/validation-shared.d.ts.map +1 -0
- package/dist/pricing/validation-shared.js +63 -0
- package/dist/pricing/validation.d.ts +987 -0
- package/dist/pricing/validation.d.ts.map +1 -0
- package/dist/pricing/validation.js +307 -0
- package/dist/promotions/events.d.ts +38 -0
- package/dist/promotions/events.d.ts.map +1 -0
- package/dist/promotions/events.js +25 -0
- package/dist/promotions/index.d.ts +12 -0
- package/dist/promotions/index.d.ts.map +1 -0
- package/dist/promotions/index.js +17 -0
- package/dist/promotions/routes-shared.d.ts +14 -0
- package/dist/promotions/routes-shared.d.ts.map +1 -0
- package/dist/promotions/routes-shared.js +3 -0
- package/dist/promotions/routes.d.ts +395 -0
- package/dist/promotions/routes.d.ts.map +1 -0
- package/dist/promotions/routes.js +55 -0
- package/dist/promotions/schema.d.ts +675 -0
- package/dist/promotions/schema.d.ts.map +1 -0
- package/dist/promotions/schema.js +126 -0
- package/dist/promotions/service-booking-confirmed.d.ts +77 -0
- package/dist/promotions/service-booking-confirmed.d.ts.map +1 -0
- package/dist/promotions/service-booking-confirmed.js +134 -0
- package/dist/promotions/service-boundary-scheduler.d.ts +85 -0
- package/dist/promotions/service-boundary-scheduler.d.ts.map +1 -0
- package/dist/promotions/service-boundary-scheduler.js +141 -0
- package/dist/promotions/service-catalog-evaluator.d.ts +22 -0
- package/dist/promotions/service-catalog-evaluator.d.ts.map +1 -0
- package/dist/promotions/service-catalog-evaluator.js +33 -0
- package/dist/promotions/service-catalog-plane-promotions.d.ts +73 -0
- package/dist/promotions/service-catalog-plane-promotions.d.ts.map +1 -0
- package/dist/promotions/service-catalog-plane-promotions.js +118 -0
- package/dist/promotions/service-evaluator.d.ts +134 -0
- package/dist/promotions/service-evaluator.d.ts.map +1 -0
- package/dist/promotions/service-evaluator.js +302 -0
- package/dist/promotions/service-storefront.d.ts +147 -0
- package/dist/promotions/service-storefront.d.ts.map +1 -0
- package/dist/promotions/service-storefront.js +326 -0
- package/dist/promotions/service.d.ts +143 -0
- package/dist/promotions/service.d.ts.map +1 -0
- package/dist/promotions/service.js +359 -0
- package/dist/promotions/validation.d.ts +195 -0
- package/dist/promotions/validation.d.ts.map +1 -0
- package/dist/promotions/validation.js +167 -0
- package/dist/promotions/workflow-bulk-reindex.d.ts +36 -0
- package/dist/promotions/workflow-bulk-reindex.d.ts.map +1 -0
- package/dist/promotions/workflow-bulk-reindex.js +53 -0
- package/dist/promotions/workflow-runtime.d.ts +17 -0
- package/dist/promotions/workflow-runtime.d.ts.map +1 -0
- package/dist/promotions/workflow-runtime.js +9 -0
- package/dist/runtime.d.ts +18 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +27 -0
- package/dist/runtime.test.d.ts +2 -0
- package/dist/runtime.test.d.ts.map +1 -0
- package/dist/runtime.test.js +25 -0
- package/dist/schema.d.ts +5 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +4 -0
- package/dist/sellability/index.d.ts +13 -0
- package/dist/sellability/index.d.ts.map +1 -0
- package/dist/sellability/index.js +17 -0
- package/dist/sellability/routes.d.ts +2332 -0
- package/dist/sellability/routes.d.ts.map +1 -0
- package/dist/sellability/routes.js +166 -0
- package/dist/sellability/schema.d.ts +1716 -0
- package/dist/sellability/schema.d.ts.map +1 -0
- package/dist/sellability/schema.js +278 -0
- package/dist/sellability/service-records.d.ts +316 -0
- package/dist/sellability/service-records.d.ts.map +1 -0
- package/dist/sellability/service-records.js +253 -0
- package/dist/sellability/service-resolve.d.ts +72 -0
- package/dist/sellability/service-resolve.d.ts.map +1 -0
- package/dist/sellability/service-resolve.js +580 -0
- package/dist/sellability/service-shared.d.ts +124 -0
- package/dist/sellability/service-shared.d.ts.map +1 -0
- package/dist/sellability/service-shared.js +96 -0
- package/dist/sellability/service-snapshots.d.ts +191 -0
- package/dist/sellability/service-snapshots.d.ts.map +1 -0
- package/dist/sellability/service-snapshots.js +153 -0
- package/dist/sellability/service.d.ts +1038 -0
- package/dist/sellability/service.d.ts.map +1 -0
- package/dist/sellability/service.js +17 -0
- package/dist/sellability/validation.d.ts +477 -0
- package/dist/sellability/validation.d.ts.map +1 -0
- package/dist/sellability/validation.js +192 -0
- package/dist/types.d.ts +239 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/package.json +62 -0
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Projection extension that aggregates a "price from" amount across the
|
|
3
|
+
* product's future bookable rate-plan prices and contributes
|
|
4
|
+
* `priceFromAmountCents`, `priceFromCurrency`, and `hasPricing` to the
|
|
5
|
+
* product search document.
|
|
6
|
+
*
|
|
7
|
+
* Lives in `@voyant-travel/commerce` because:
|
|
8
|
+
* - The data lives here (`option_price_rules`, `option_unit_price_rules`,
|
|
9
|
+
* `price_catalogs`).
|
|
10
|
+
* - Product owns the document-builder implementation, while this package
|
|
11
|
+
* exposes a structural extension that satisfies that builder contract.
|
|
12
|
+
*
|
|
13
|
+
* Wire via `createProductDocumentBuilder({ extensions: [pricingExtension] })`
|
|
14
|
+
* after composing `productPricingCatalogPolicy` into the registry.
|
|
15
|
+
*
|
|
16
|
+
* Scope intentionally narrow:
|
|
17
|
+
* - **No schedule-aware rule resolution.** Only `is_default = true`
|
|
18
|
+
* rules contribute. Seasonal / promo rules with schedules don't
|
|
19
|
+
* surface here; they require per-slice rule evaluation beyond the
|
|
20
|
+
* future-departure presence check below.
|
|
21
|
+
* - **No per-departure overrides.** Same reason.
|
|
22
|
+
* - **Currency consistency.** Only rules whose catalog currency matches
|
|
23
|
+
* the product's `sellCurrency` (or whose catalog currency is null
|
|
24
|
+
* and therefore inherits the product's) are MIN'd together.
|
|
25
|
+
*
|
|
26
|
+
* Document churn: this projection is `now()`-dependent because it only
|
|
27
|
+
* considers future bookable departures. A product can move to "unpriced"
|
|
28
|
+
* once its final departure starts unless a row-level fallback remains.
|
|
29
|
+
*/
|
|
30
|
+
import { sql } from "drizzle-orm";
|
|
31
|
+
const EMPTY_AGGREGATE = {
|
|
32
|
+
priceFromAmountCents: null,
|
|
33
|
+
priceFromCurrency: null,
|
|
34
|
+
hasPricing: false,
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Pure aggregation kernel. Room prices take precedence over base/unit
|
|
38
|
+
* prices, which take precedence over the product-row fallback. Non-
|
|
39
|
+
* positive values are treated as absent so stale `0` caches don't block
|
|
40
|
+
* nullish fallbacks in catalog consumers.
|
|
41
|
+
*/
|
|
42
|
+
function aggregatePricing(productPrice, currency, roomPrices, basePrices) {
|
|
43
|
+
const min = firstPositiveMin(roomPrices) ?? firstPositiveMin(basePrices) ?? positive(productPrice);
|
|
44
|
+
if (min === null) {
|
|
45
|
+
return { ...EMPTY_AGGREGATE, priceFromCurrency: currency };
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
priceFromAmountCents: min,
|
|
49
|
+
priceFromCurrency: currency,
|
|
50
|
+
hasPricing: true,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Construct the pricing projection extension.
|
|
55
|
+
*
|
|
56
|
+
* Pass loaders in tests to stub DB reads; production uses raw SQL against
|
|
57
|
+
* the deployed schema.
|
|
58
|
+
*/
|
|
59
|
+
export function createProductPricingProjectionExtension(options = {}) {
|
|
60
|
+
const loadProductPricing = options.loadProductPricing ?? defaultLoadProductPricing;
|
|
61
|
+
const loadRatePlanPricing = options.loadRatePlanPricing ?? defaultLoadRatePlanPricing;
|
|
62
|
+
return {
|
|
63
|
+
name: "products:pricing",
|
|
64
|
+
async project(db, productId, _slice) {
|
|
65
|
+
const product = await loadProductPricing(db, productId);
|
|
66
|
+
const currency = product.sellCurrency;
|
|
67
|
+
// Without a product row currency we can't safely filter rules by
|
|
68
|
+
// matching currency. Emit only the positive row-level fallback.
|
|
69
|
+
if (!currency) {
|
|
70
|
+
const out = aggregatePricing(product.sellAmountCents, null, [], []);
|
|
71
|
+
return toProjectionMap(out);
|
|
72
|
+
}
|
|
73
|
+
const ratePlans = await loadRatePlanPricing(db, productId, currency);
|
|
74
|
+
const out = aggregatePricing(product.sellAmountCents, currency, ratePlans.roomPrices, ratePlans.basePrices);
|
|
75
|
+
return toProjectionMap(out);
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Resolve the same "from price" value emitted by the pricing projection.
|
|
81
|
+
* Promotion projection wiring uses this so strikethrough base prices
|
|
82
|
+
* follow the same rate-plan-first fallback chain.
|
|
83
|
+
*/
|
|
84
|
+
export async function loadProductPriceFrom(db, productId) {
|
|
85
|
+
const product = await defaultLoadProductPricing(db, productId);
|
|
86
|
+
const currency = product.sellCurrency;
|
|
87
|
+
if (!currency) {
|
|
88
|
+
return { amountCents: positive(product.sellAmountCents), currency: null };
|
|
89
|
+
}
|
|
90
|
+
const ratePlans = await defaultLoadRatePlanPricing(db, productId, currency);
|
|
91
|
+
const amountCents = firstPositiveMin(ratePlans.roomPrices) ??
|
|
92
|
+
firstPositiveMin(ratePlans.basePrices) ??
|
|
93
|
+
positive(product.sellAmountCents);
|
|
94
|
+
return { amountCents, currency };
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Read positive prices from active default rules that have at least one
|
|
98
|
+
* future bookable departure. Room prices are separated from base/unit
|
|
99
|
+
* fallbacks so per-room pricing wins even when the product row contains
|
|
100
|
+
* a stale zero or stale manual price.
|
|
101
|
+
*/
|
|
102
|
+
async function defaultLoadRatePlanPricing(db, productId, productCurrency) {
|
|
103
|
+
try {
|
|
104
|
+
const [roomPrice, basePrice] = await Promise.all([
|
|
105
|
+
fetchBookableRoomPrice(db, productId, productCurrency),
|
|
106
|
+
fetchBookableBasePrice(db, productId, productCurrency),
|
|
107
|
+
]);
|
|
108
|
+
return {
|
|
109
|
+
roomPrices: roomPrice == null ? [] : [roomPrice],
|
|
110
|
+
basePrices: basePrice == null ? [] : [basePrice],
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
// Slim test fixtures may omit availability_slots/product_options/
|
|
115
|
+
// option_units. Keep reindex failure-isolated and fall back to the
|
|
116
|
+
// product row only for those expected schema gaps.
|
|
117
|
+
if (isMissingCatalogPricingDependencyError(error)) {
|
|
118
|
+
return { roomPrices: [], basePrices: [] };
|
|
119
|
+
}
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
async function fetchBookableRoomPrice(db, productId, productCurrency) {
|
|
124
|
+
const rows = await executeRows(db, sql `
|
|
125
|
+
WITH active_rules AS (
|
|
126
|
+
SELECT opr.id
|
|
127
|
+
FROM option_price_rules opr
|
|
128
|
+
INNER JOIN price_catalogs pc ON pc.id = opr.price_catalog_id
|
|
129
|
+
WHERE opr.product_id = ${productId}
|
|
130
|
+
AND opr.active = true
|
|
131
|
+
AND opr.is_default = true
|
|
132
|
+
AND pc.active = true
|
|
133
|
+
AND (pc.currency_code = ${productCurrency} OR pc.currency_code IS NULL)
|
|
134
|
+
AND EXISTS (
|
|
135
|
+
SELECT 1
|
|
136
|
+
FROM product_options po
|
|
137
|
+
WHERE po.id = opr.option_id
|
|
138
|
+
AND po.product_id = opr.product_id
|
|
139
|
+
AND po.status = 'active'
|
|
140
|
+
)
|
|
141
|
+
AND EXISTS (
|
|
142
|
+
SELECT 1
|
|
143
|
+
FROM availability_slots slot
|
|
144
|
+
WHERE slot.product_id = opr.product_id
|
|
145
|
+
AND slot.starts_at >= NOW()
|
|
146
|
+
AND slot.status::text IN ('open', 'planned', 'confirmed')
|
|
147
|
+
AND (slot.option_id IS NULL OR slot.option_id = opr.option_id)
|
|
148
|
+
)
|
|
149
|
+
),
|
|
150
|
+
candidates AS (
|
|
151
|
+
SELECT
|
|
152
|
+
unit_rule.sell_amount_cents AS price,
|
|
153
|
+
(
|
|
154
|
+
(
|
|
155
|
+
category.id IS NULL
|
|
156
|
+
OR category.category_type = 'adult'
|
|
157
|
+
OR (
|
|
158
|
+
category.category_type NOT IN ('child', 'infant', 'senior')
|
|
159
|
+
AND category.min_age IS NULL
|
|
160
|
+
AND category.max_age IS NULL
|
|
161
|
+
)
|
|
162
|
+
)
|
|
163
|
+
AND COALESCE(unit_rule.min_quantity, 0) <= 1
|
|
164
|
+
AND COALESCE(unit_rule.max_quantity, 0) = 0
|
|
165
|
+
) AS standard_price
|
|
166
|
+
FROM active_rules rule
|
|
167
|
+
INNER JOIN option_unit_price_rules unit_rule
|
|
168
|
+
ON unit_rule.option_price_rule_id = rule.id
|
|
169
|
+
INNER JOIN option_units unit
|
|
170
|
+
ON unit.id = unit_rule.unit_id
|
|
171
|
+
LEFT JOIN pricing_categories category
|
|
172
|
+
ON category.id = unit_rule.pricing_category_id
|
|
173
|
+
WHERE unit_rule.active = true
|
|
174
|
+
AND unit.unit_type = 'room'
|
|
175
|
+
UNION ALL
|
|
176
|
+
SELECT
|
|
177
|
+
tier.sell_amount_cents AS price,
|
|
178
|
+
(
|
|
179
|
+
(
|
|
180
|
+
category.id IS NULL
|
|
181
|
+
OR category.category_type = 'adult'
|
|
182
|
+
OR (
|
|
183
|
+
category.category_type NOT IN ('child', 'infant', 'senior')
|
|
184
|
+
AND category.min_age IS NULL
|
|
185
|
+
AND category.max_age IS NULL
|
|
186
|
+
)
|
|
187
|
+
)
|
|
188
|
+
AND COALESCE(unit_rule.min_quantity, 0) <= 1
|
|
189
|
+
AND COALESCE(unit_rule.max_quantity, 0) = 0
|
|
190
|
+
AND tier.min_quantity <= 1
|
|
191
|
+
AND COALESCE(tier.max_quantity, 0) = 0
|
|
192
|
+
) AS standard_price
|
|
193
|
+
FROM active_rules rule
|
|
194
|
+
INNER JOIN option_unit_price_rules unit_rule
|
|
195
|
+
ON unit_rule.option_price_rule_id = rule.id
|
|
196
|
+
INNER JOIN option_units unit
|
|
197
|
+
ON unit.id = unit_rule.unit_id
|
|
198
|
+
LEFT JOIN pricing_categories category
|
|
199
|
+
ON category.id = unit_rule.pricing_category_id
|
|
200
|
+
INNER JOIN option_unit_tiers tier
|
|
201
|
+
ON tier.option_unit_price_rule_id = unit_rule.id
|
|
202
|
+
AND tier.active = true
|
|
203
|
+
WHERE unit_rule.active = true
|
|
204
|
+
AND unit.unit_type = 'room'
|
|
205
|
+
)
|
|
206
|
+
SELECT COALESCE(
|
|
207
|
+
MIN(price) FILTER (WHERE standard_price),
|
|
208
|
+
MIN(price) FILTER (WHERE NOT standard_price)
|
|
209
|
+
)::int AS price
|
|
210
|
+
FROM candidates
|
|
211
|
+
WHERE price > 0
|
|
212
|
+
`);
|
|
213
|
+
return readNullableInt(rows[0], "price");
|
|
214
|
+
}
|
|
215
|
+
async function fetchBookableBasePrice(db, productId, productCurrency) {
|
|
216
|
+
const rows = await executeRows(db, sql `
|
|
217
|
+
WITH active_rules AS (
|
|
218
|
+
SELECT opr.id, opr.base_sell_amount_cents
|
|
219
|
+
FROM option_price_rules opr
|
|
220
|
+
INNER JOIN price_catalogs pc ON pc.id = opr.price_catalog_id
|
|
221
|
+
WHERE opr.product_id = ${productId}
|
|
222
|
+
AND opr.active = true
|
|
223
|
+
AND opr.is_default = true
|
|
224
|
+
AND pc.active = true
|
|
225
|
+
AND (pc.currency_code = ${productCurrency} OR pc.currency_code IS NULL)
|
|
226
|
+
AND EXISTS (
|
|
227
|
+
SELECT 1
|
|
228
|
+
FROM product_options po
|
|
229
|
+
WHERE po.id = opr.option_id
|
|
230
|
+
AND po.product_id = opr.product_id
|
|
231
|
+
AND po.status = 'active'
|
|
232
|
+
)
|
|
233
|
+
AND EXISTS (
|
|
234
|
+
SELECT 1
|
|
235
|
+
FROM availability_slots slot
|
|
236
|
+
WHERE slot.product_id = opr.product_id
|
|
237
|
+
AND slot.starts_at >= NOW()
|
|
238
|
+
AND slot.status::text IN ('open', 'planned', 'confirmed')
|
|
239
|
+
AND (slot.option_id IS NULL OR slot.option_id = opr.option_id)
|
|
240
|
+
)
|
|
241
|
+
),
|
|
242
|
+
candidates AS (
|
|
243
|
+
SELECT base_sell_amount_cents AS price, true AS standard_price
|
|
244
|
+
FROM active_rules
|
|
245
|
+
UNION ALL
|
|
246
|
+
SELECT
|
|
247
|
+
unit_rule.sell_amount_cents AS price,
|
|
248
|
+
(
|
|
249
|
+
(
|
|
250
|
+
category.id IS NULL
|
|
251
|
+
OR category.category_type = 'adult'
|
|
252
|
+
OR (
|
|
253
|
+
category.category_type NOT IN ('child', 'infant', 'senior')
|
|
254
|
+
AND category.min_age IS NULL
|
|
255
|
+
AND category.max_age IS NULL
|
|
256
|
+
)
|
|
257
|
+
)
|
|
258
|
+
AND COALESCE(unit_rule.min_quantity, 0) <= 1
|
|
259
|
+
AND COALESCE(unit_rule.max_quantity, 0) = 0
|
|
260
|
+
) AS standard_price
|
|
261
|
+
FROM active_rules rule
|
|
262
|
+
INNER JOIN option_unit_price_rules unit_rule
|
|
263
|
+
ON unit_rule.option_price_rule_id = rule.id
|
|
264
|
+
INNER JOIN option_units unit
|
|
265
|
+
ON unit.id = unit_rule.unit_id
|
|
266
|
+
LEFT JOIN pricing_categories category
|
|
267
|
+
ON category.id = unit_rule.pricing_category_id
|
|
268
|
+
WHERE unit_rule.active = true
|
|
269
|
+
AND unit.unit_type <> 'room'
|
|
270
|
+
UNION ALL
|
|
271
|
+
SELECT
|
|
272
|
+
tier.sell_amount_cents AS price,
|
|
273
|
+
(
|
|
274
|
+
(
|
|
275
|
+
category.id IS NULL
|
|
276
|
+
OR category.category_type = 'adult'
|
|
277
|
+
OR (
|
|
278
|
+
category.category_type NOT IN ('child', 'infant', 'senior')
|
|
279
|
+
AND category.min_age IS NULL
|
|
280
|
+
AND category.max_age IS NULL
|
|
281
|
+
)
|
|
282
|
+
)
|
|
283
|
+
AND COALESCE(unit_rule.min_quantity, 0) <= 1
|
|
284
|
+
AND COALESCE(unit_rule.max_quantity, 0) = 0
|
|
285
|
+
AND tier.min_quantity <= 1
|
|
286
|
+
AND COALESCE(tier.max_quantity, 0) = 0
|
|
287
|
+
) AS standard_price
|
|
288
|
+
FROM active_rules rule
|
|
289
|
+
INNER JOIN option_unit_price_rules unit_rule
|
|
290
|
+
ON unit_rule.option_price_rule_id = rule.id
|
|
291
|
+
INNER JOIN option_units unit
|
|
292
|
+
ON unit.id = unit_rule.unit_id
|
|
293
|
+
LEFT JOIN pricing_categories category
|
|
294
|
+
ON category.id = unit_rule.pricing_category_id
|
|
295
|
+
INNER JOIN option_unit_tiers tier
|
|
296
|
+
ON tier.option_unit_price_rule_id = unit_rule.id
|
|
297
|
+
AND tier.active = true
|
|
298
|
+
WHERE unit_rule.active = true
|
|
299
|
+
AND unit.unit_type <> 'room'
|
|
300
|
+
)
|
|
301
|
+
SELECT COALESCE(
|
|
302
|
+
MIN(price) FILTER (WHERE standard_price),
|
|
303
|
+
MIN(price) FILTER (WHERE NOT standard_price)
|
|
304
|
+
)::int AS price
|
|
305
|
+
FROM candidates
|
|
306
|
+
WHERE price > 0
|
|
307
|
+
`);
|
|
308
|
+
return readNullableInt(rows[0], "price");
|
|
309
|
+
}
|
|
310
|
+
async function executeRows(db, query) {
|
|
311
|
+
// biome-ignore lint/suspicious/noExplicitAny: #1141 supports multiple drizzle driver result shapes
|
|
312
|
+
const result = await db.execute(query);
|
|
313
|
+
return Array.isArray(result) ? result : (result?.rows ?? []);
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Default loader reads the products row via raw SQL so we don't pull
|
|
317
|
+
* the products schema into this file. The columns we read are stable
|
|
318
|
+
* enough that a rename would break far more than this.
|
|
319
|
+
*/
|
|
320
|
+
async function defaultLoadProductPricing(db, productId) {
|
|
321
|
+
// biome-ignore lint/suspicious/noExplicitAny: #1141 keeps cross-package product lookup driver-agnostic
|
|
322
|
+
const dbAny = db;
|
|
323
|
+
const result = await dbAny.execute(
|
|
324
|
+
// agent-quality: raw-sql reviewed -- owner: pricing; dynamic SQL interpolation uses Drizzle parameter binding or vetted SQL identifiers.
|
|
325
|
+
sql `SELECT sell_amount_cents, sell_currency FROM products WHERE id = ${productId} LIMIT 1`);
|
|
326
|
+
// postgres-js returns rows as an array-like; node-postgres returns `{ rows: [...] }`.
|
|
327
|
+
const rows = Array.isArray(result) ? result : (result?.rows ?? []);
|
|
328
|
+
const first = rows[0];
|
|
329
|
+
if (!first)
|
|
330
|
+
return { sellAmountCents: null, sellCurrency: null };
|
|
331
|
+
return {
|
|
332
|
+
sellAmountCents: first.sell_amount_cents,
|
|
333
|
+
sellCurrency: first.sell_currency,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
function positive(value) {
|
|
337
|
+
return typeof value === "number" && value > 0 ? value : null;
|
|
338
|
+
}
|
|
339
|
+
function firstPositiveMin(values) {
|
|
340
|
+
let min = null;
|
|
341
|
+
for (const value of values) {
|
|
342
|
+
if (value <= 0)
|
|
343
|
+
continue;
|
|
344
|
+
if (min === null || value < min)
|
|
345
|
+
min = value;
|
|
346
|
+
}
|
|
347
|
+
return min;
|
|
348
|
+
}
|
|
349
|
+
function readNullableInt(row, key) {
|
|
350
|
+
const value = row?.[key];
|
|
351
|
+
if (typeof value === "number")
|
|
352
|
+
return Number.isFinite(value) ? value : null;
|
|
353
|
+
if (typeof value === "string") {
|
|
354
|
+
const parsed = Number(value);
|
|
355
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
356
|
+
}
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
function isMissingCatalogPricingDependencyError(error) {
|
|
360
|
+
const err = error;
|
|
361
|
+
const code = typeof err?.code === "string" ? err.code : null;
|
|
362
|
+
if (code === "42P01" || code === "42703")
|
|
363
|
+
return true;
|
|
364
|
+
const message = typeof err?.message === "string" ? err.message.toLowerCase() : "";
|
|
365
|
+
return ((message.includes("relation") && message.includes("does not exist")) ||
|
|
366
|
+
message.includes("no such table") ||
|
|
367
|
+
message.includes("no such column"));
|
|
368
|
+
}
|
|
369
|
+
function toProjectionMap(a) {
|
|
370
|
+
return new Map([
|
|
371
|
+
["priceFromAmountCents", a.priceFromAmountCents],
|
|
372
|
+
["priceFromCurrency", a.priceFromCurrency],
|
|
373
|
+
["hasPricing", a.hasPricing],
|
|
374
|
+
]);
|
|
375
|
+
}
|
|
376
|
+
// Internal exports for unit tests - kept off the public surface.
|
|
377
|
+
export const __test__ = {
|
|
378
|
+
aggregatePricing,
|
|
379
|
+
EMPTY_AGGREGATE,
|
|
380
|
+
firstPositiveMin,
|
|
381
|
+
isMissingCatalogPricingDependencyError,
|
|
382
|
+
};
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
|
2
|
+
import type { CreatePriceCatalogInput, CreatePriceScheduleInput, PriceCatalogListQuery, PriceScheduleListQuery, UpdatePriceCatalogInput, UpdatePriceScheduleInput } from "./service-shared.js";
|
|
3
|
+
export declare function listPriceCatalogs(db: PostgresJsDatabase, query: PriceCatalogListQuery): Promise<{
|
|
4
|
+
data: {
|
|
5
|
+
id: string;
|
|
6
|
+
code: string;
|
|
7
|
+
name: string;
|
|
8
|
+
currencyCode: string | null;
|
|
9
|
+
catalogType: "internal" | "other" | "public" | "contract" | "net" | "gross" | "promo";
|
|
10
|
+
isDefault: boolean;
|
|
11
|
+
active: boolean;
|
|
12
|
+
notes: string | null;
|
|
13
|
+
metadata: Record<string, unknown> | null;
|
|
14
|
+
createdAt: Date;
|
|
15
|
+
updatedAt: Date;
|
|
16
|
+
}[];
|
|
17
|
+
total: number;
|
|
18
|
+
limit: number;
|
|
19
|
+
offset: number;
|
|
20
|
+
}>;
|
|
21
|
+
export declare function getPriceCatalogById(db: PostgresJsDatabase, id: string): Promise<{
|
|
22
|
+
id: string;
|
|
23
|
+
code: string;
|
|
24
|
+
name: string;
|
|
25
|
+
currencyCode: string | null;
|
|
26
|
+
catalogType: "internal" | "other" | "public" | "contract" | "net" | "gross" | "promo";
|
|
27
|
+
isDefault: boolean;
|
|
28
|
+
active: boolean;
|
|
29
|
+
notes: string | null;
|
|
30
|
+
metadata: Record<string, unknown> | null;
|
|
31
|
+
createdAt: Date;
|
|
32
|
+
updatedAt: Date;
|
|
33
|
+
} | null>;
|
|
34
|
+
export declare function createPriceCatalog(db: PostgresJsDatabase, data: CreatePriceCatalogInput): Promise<{
|
|
35
|
+
metadata: Record<string, unknown> | null;
|
|
36
|
+
id: string;
|
|
37
|
+
name: string;
|
|
38
|
+
createdAt: Date;
|
|
39
|
+
code: string;
|
|
40
|
+
notes: string | null;
|
|
41
|
+
updatedAt: Date;
|
|
42
|
+
active: boolean;
|
|
43
|
+
isDefault: boolean;
|
|
44
|
+
currencyCode: string | null;
|
|
45
|
+
catalogType: "internal" | "other" | "public" | "contract" | "net" | "gross" | "promo";
|
|
46
|
+
} | null>;
|
|
47
|
+
export declare function updatePriceCatalog(db: PostgresJsDatabase, id: string, data: UpdatePriceCatalogInput): Promise<{
|
|
48
|
+
id: string;
|
|
49
|
+
code: string;
|
|
50
|
+
name: string;
|
|
51
|
+
currencyCode: string | null;
|
|
52
|
+
catalogType: "internal" | "other" | "public" | "contract" | "net" | "gross" | "promo";
|
|
53
|
+
isDefault: boolean;
|
|
54
|
+
active: boolean;
|
|
55
|
+
notes: string | null;
|
|
56
|
+
metadata: Record<string, unknown> | null;
|
|
57
|
+
createdAt: Date;
|
|
58
|
+
updatedAt: Date;
|
|
59
|
+
} | null>;
|
|
60
|
+
export declare function deletePriceCatalog(db: PostgresJsDatabase, id: string): Promise<{
|
|
61
|
+
id: string;
|
|
62
|
+
} | null>;
|
|
63
|
+
export declare function listPriceSchedules(db: PostgresJsDatabase, query: PriceScheduleListQuery): Promise<{
|
|
64
|
+
data: {
|
|
65
|
+
id: string;
|
|
66
|
+
priceCatalogId: string;
|
|
67
|
+
code: string | null;
|
|
68
|
+
name: string;
|
|
69
|
+
recurrenceRule: string;
|
|
70
|
+
timezone: string | null;
|
|
71
|
+
validFrom: string | null;
|
|
72
|
+
validTo: string | null;
|
|
73
|
+
weekdays: string[] | null;
|
|
74
|
+
priority: number;
|
|
75
|
+
active: boolean;
|
|
76
|
+
notes: string | null;
|
|
77
|
+
metadata: Record<string, unknown> | null;
|
|
78
|
+
createdAt: Date;
|
|
79
|
+
updatedAt: Date;
|
|
80
|
+
}[];
|
|
81
|
+
total: number;
|
|
82
|
+
limit: number;
|
|
83
|
+
offset: number;
|
|
84
|
+
}>;
|
|
85
|
+
export declare function getPriceScheduleById(db: PostgresJsDatabase, id: string): Promise<{
|
|
86
|
+
id: string;
|
|
87
|
+
priceCatalogId: string;
|
|
88
|
+
code: string | null;
|
|
89
|
+
name: string;
|
|
90
|
+
recurrenceRule: string;
|
|
91
|
+
timezone: string | null;
|
|
92
|
+
validFrom: string | null;
|
|
93
|
+
validTo: string | null;
|
|
94
|
+
weekdays: string[] | null;
|
|
95
|
+
priority: number;
|
|
96
|
+
active: boolean;
|
|
97
|
+
notes: string | null;
|
|
98
|
+
metadata: Record<string, unknown> | null;
|
|
99
|
+
createdAt: Date;
|
|
100
|
+
updatedAt: Date;
|
|
101
|
+
} | null>;
|
|
102
|
+
export declare function createPriceSchedule(db: PostgresJsDatabase, data: CreatePriceScheduleInput): Promise<{
|
|
103
|
+
metadata: Record<string, unknown> | null;
|
|
104
|
+
id: string;
|
|
105
|
+
name: string;
|
|
106
|
+
createdAt: Date;
|
|
107
|
+
code: string | null;
|
|
108
|
+
priority: number;
|
|
109
|
+
notes: string | null;
|
|
110
|
+
updatedAt: Date;
|
|
111
|
+
active: boolean;
|
|
112
|
+
timezone: string | null;
|
|
113
|
+
priceCatalogId: string;
|
|
114
|
+
recurrenceRule: string;
|
|
115
|
+
validFrom: string | null;
|
|
116
|
+
validTo: string | null;
|
|
117
|
+
weekdays: string[] | null;
|
|
118
|
+
} | null>;
|
|
119
|
+
export declare function updatePriceSchedule(db: PostgresJsDatabase, id: string, data: UpdatePriceScheduleInput): Promise<{
|
|
120
|
+
id: string;
|
|
121
|
+
priceCatalogId: string;
|
|
122
|
+
code: string | null;
|
|
123
|
+
name: string;
|
|
124
|
+
recurrenceRule: string;
|
|
125
|
+
timezone: string | null;
|
|
126
|
+
validFrom: string | null;
|
|
127
|
+
validTo: string | null;
|
|
128
|
+
weekdays: string[] | null;
|
|
129
|
+
priority: number;
|
|
130
|
+
active: boolean;
|
|
131
|
+
notes: string | null;
|
|
132
|
+
metadata: Record<string, unknown> | null;
|
|
133
|
+
createdAt: Date;
|
|
134
|
+
updatedAt: Date;
|
|
135
|
+
} | null>;
|
|
136
|
+
export declare function deletePriceSchedule(db: PostgresJsDatabase, id: string): Promise<{
|
|
137
|
+
id: string;
|
|
138
|
+
} | null>;
|
|
139
|
+
//# sourceMappingURL=service-catalogs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-catalogs.d.ts","sourceRoot":"","sources":["../../src/pricing/service-catalogs.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAGjE,OAAO,KAAK,EACV,uBAAuB,EACvB,wBAAwB,EACxB,qBAAqB,EACrB,sBAAsB,EACtB,uBAAuB,EACvB,wBAAwB,EACzB,MAAM,qBAAqB,CAAA;AAG5B,wBAAsB,iBAAiB,CAAC,EAAE,EAAE,kBAAkB,EAAE,KAAK,EAAE,qBAAqB;;;;;;;;;;;;;;;;;GAuB3F;AAED,wBAAsB,mBAAmB,CAAC,EAAE,EAAE,kBAAkB,EAAE,EAAE,EAAE,MAAM;;;;;;;;;;;;UAG3E;AAED,wBAAsB,kBAAkB,CAAC,EAAE,EAAE,kBAAkB,EAAE,IAAI,EAAE,uBAAuB;;;;;;;;;;;;UAG7F;AAED,wBAAsB,kBAAkB,CACtC,EAAE,EAAE,kBAAkB,EACtB,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,uBAAuB;;;;;;;;;;;;UAQ9B;AAED,wBAAsB,kBAAkB,CAAC,EAAE,EAAE,kBAAkB,EAAE,EAAE,EAAE,MAAM;;UAM1E;AAED,wBAAsB,kBAAkB,CAAC,EAAE,EAAE,kBAAkB,EAAE,KAAK,EAAE,sBAAsB;;;;;;;;;;;;;;;;;;;;;GAsB7F;AAED,wBAAsB,oBAAoB,CAAC,EAAE,EAAE,kBAAkB,EAAE,EAAE,EAAE,MAAM;;;;;;;;;;;;;;;;UAG5E;AAED,wBAAsB,mBAAmB,CAAC,EAAE,EAAE,kBAAkB,EAAE,IAAI,EAAE,wBAAwB;;;;;;;;;;;;;;;;UAG/F;AAED,wBAAsB,mBAAmB,CACvC,EAAE,EAAE,kBAAkB,EACtB,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,wBAAwB;;;;;;;;;;;;;;;;UAQ/B;AAED,wBAAsB,mBAAmB,CAAC,EAAE,EAAE,kBAAkB,EAAE,EAAE,EAAE,MAAM;;UAM3E"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { and, asc, desc, eq, ilike, or, sql } from "drizzle-orm";
|
|
2
|
+
import { priceCatalogs, priceSchedules } from "./schema.js";
|
|
3
|
+
import { paginate } from "./service-shared.js";
|
|
4
|
+
export async function listPriceCatalogs(db, query) {
|
|
5
|
+
const conditions = [];
|
|
6
|
+
if (query.currencyCode)
|
|
7
|
+
conditions.push(eq(priceCatalogs.currencyCode, query.currencyCode));
|
|
8
|
+
if (query.catalogType)
|
|
9
|
+
conditions.push(eq(priceCatalogs.catalogType, query.catalogType));
|
|
10
|
+
if (query.active !== undefined)
|
|
11
|
+
conditions.push(eq(priceCatalogs.active, query.active));
|
|
12
|
+
if (query.search) {
|
|
13
|
+
const term = `%${query.search}%`;
|
|
14
|
+
conditions.push(or(ilike(priceCatalogs.name, term), ilike(priceCatalogs.code, term)));
|
|
15
|
+
}
|
|
16
|
+
const where = conditions.length ? and(...conditions) : undefined;
|
|
17
|
+
return paginate(db
|
|
18
|
+
.select()
|
|
19
|
+
.from(priceCatalogs)
|
|
20
|
+
.where(where)
|
|
21
|
+
.limit(query.limit)
|
|
22
|
+
.offset(query.offset)
|
|
23
|
+
.orderBy(asc(priceCatalogs.name)), db.select({ count: sql `count(*)::int` }).from(priceCatalogs).where(where), query.limit, query.offset);
|
|
24
|
+
}
|
|
25
|
+
export async function getPriceCatalogById(db, id) {
|
|
26
|
+
const [row] = await db.select().from(priceCatalogs).where(eq(priceCatalogs.id, id)).limit(1);
|
|
27
|
+
return row ?? null;
|
|
28
|
+
}
|
|
29
|
+
export async function createPriceCatalog(db, data) {
|
|
30
|
+
const [row] = await db.insert(priceCatalogs).values(data).returning();
|
|
31
|
+
return row ?? null;
|
|
32
|
+
}
|
|
33
|
+
export async function updatePriceCatalog(db, id, data) {
|
|
34
|
+
const [row] = await db
|
|
35
|
+
.update(priceCatalogs)
|
|
36
|
+
.set({ ...data, updatedAt: new Date() })
|
|
37
|
+
.where(eq(priceCatalogs.id, id))
|
|
38
|
+
.returning();
|
|
39
|
+
return row ?? null;
|
|
40
|
+
}
|
|
41
|
+
export async function deletePriceCatalog(db, id) {
|
|
42
|
+
const [row] = await db
|
|
43
|
+
.delete(priceCatalogs)
|
|
44
|
+
.where(eq(priceCatalogs.id, id))
|
|
45
|
+
.returning({ id: priceCatalogs.id });
|
|
46
|
+
return row ?? null;
|
|
47
|
+
}
|
|
48
|
+
export async function listPriceSchedules(db, query) {
|
|
49
|
+
const conditions = [];
|
|
50
|
+
if (query.priceCatalogId)
|
|
51
|
+
conditions.push(eq(priceSchedules.priceCatalogId, query.priceCatalogId));
|
|
52
|
+
if (query.active !== undefined)
|
|
53
|
+
conditions.push(eq(priceSchedules.active, query.active));
|
|
54
|
+
if (query.search) {
|
|
55
|
+
const term = `%${query.search}%`;
|
|
56
|
+
conditions.push(or(ilike(priceSchedules.name, term), ilike(priceSchedules.code, term)));
|
|
57
|
+
}
|
|
58
|
+
const where = conditions.length ? and(...conditions) : undefined;
|
|
59
|
+
return paginate(db
|
|
60
|
+
.select()
|
|
61
|
+
.from(priceSchedules)
|
|
62
|
+
.where(where)
|
|
63
|
+
.limit(query.limit)
|
|
64
|
+
.offset(query.offset)
|
|
65
|
+
.orderBy(desc(priceSchedules.priority), asc(priceSchedules.name)), db.select({ count: sql `count(*)::int` }).from(priceSchedules).where(where), query.limit, query.offset);
|
|
66
|
+
}
|
|
67
|
+
export async function getPriceScheduleById(db, id) {
|
|
68
|
+
const [row] = await db.select().from(priceSchedules).where(eq(priceSchedules.id, id)).limit(1);
|
|
69
|
+
return row ?? null;
|
|
70
|
+
}
|
|
71
|
+
export async function createPriceSchedule(db, data) {
|
|
72
|
+
const [row] = await db.insert(priceSchedules).values(data).returning();
|
|
73
|
+
return row ?? null;
|
|
74
|
+
}
|
|
75
|
+
export async function updatePriceSchedule(db, id, data) {
|
|
76
|
+
const [row] = await db
|
|
77
|
+
.update(priceSchedules)
|
|
78
|
+
.set({ ...data, updatedAt: new Date() })
|
|
79
|
+
.where(eq(priceSchedules.id, id))
|
|
80
|
+
.returning();
|
|
81
|
+
return row ?? null;
|
|
82
|
+
}
|
|
83
|
+
export async function deletePriceSchedule(db, id) {
|
|
84
|
+
const [row] = await db
|
|
85
|
+
.delete(priceSchedules)
|
|
86
|
+
.where(eq(priceSchedules.id, id))
|
|
87
|
+
.returning({ id: priceSchedules.id });
|
|
88
|
+
return row ?? null;
|
|
89
|
+
}
|