@voyantjs/catalog-authoring 0.104.1 → 0.106.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/dist/builder.d.ts +37 -0
- package/dist/builder.d.ts.map +1 -0
- package/dist/builder.js +248 -0
- package/dist/clone-content.d.ts +38 -0
- package/dist/clone-content.d.ts.map +1 -0
- package/dist/clone-content.js +367 -0
- package/dist/clone-pricing.d.ts +9 -0
- package/dist/clone-pricing.d.ts.map +1 -0
- package/dist/clone-pricing.js +242 -0
- package/dist/clone.d.ts +45 -0
- package/dist/clone.d.ts.map +1 -0
- package/dist/clone.js +141 -0
- package/dist/errors.d.ts +21 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +13 -0
- package/dist/extension.d.ts +95 -8
- package/dist/extension.d.ts.map +1 -1
- package/dist/extension.js +92 -2
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/service.d.ts +28 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +65 -0
- package/dist/spec.d.ts +50 -32
- package/dist/spec.d.ts.map +1 -1
- package/dist/spec.js +20 -15
- package/dist/validate.d.ts +17 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +83 -0
- package/package.json +47 -50
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import { productOptionResourceTemplates } from "@voyantjs/availability";
|
|
2
|
+
import { optionExtraConfigs, productExtras } from "@voyantjs/extras";
|
|
3
|
+
import { pricingCategories, pricingCategoryDependencies } from "@voyantjs/pricing/schema";
|
|
4
|
+
import { optionUnits, optionUnitTranslations, productActivationSettings, productCapabilities, productCategoryProducts, productDayServices, productDays, productDayTranslations, productDeliveryFormats, productDestinations, productFaqs, productFeatures, productItineraries, productLocations, productMedia, productOptions, productOptionTranslations, productTagProducts, productTicketSettings, productTranslations, productVisibilitySettings, } from "@voyantjs/products/schema";
|
|
5
|
+
import { eq, inArray } from "drizzle-orm";
|
|
6
|
+
export function withoutSystemColumns(row) {
|
|
7
|
+
const { id: _id, createdAt: _createdAt, updatedAt: _updatedAt, ...values } = row;
|
|
8
|
+
return values;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Copies the product's content graph: per-product settings, options + units
|
|
12
|
+
* (with translations + resource templates), pricing categories (+ dependencies,
|
|
13
|
+
* remapped), itineraries/days/services, media, and extras. Populates the id
|
|
14
|
+
* remaps in {@link CloneContext} for the pricing/availability phase.
|
|
15
|
+
*/
|
|
16
|
+
export async function copyProductContent(ctx) {
|
|
17
|
+
const { tx, sourceId, targetId } = ctx;
|
|
18
|
+
const activationRows = await tx
|
|
19
|
+
.select()
|
|
20
|
+
.from(productActivationSettings)
|
|
21
|
+
.where(eq(productActivationSettings.productId, sourceId));
|
|
22
|
+
for (const row of activationRows) {
|
|
23
|
+
await tx.insert(productActivationSettings).values({
|
|
24
|
+
...withoutSystemColumns(row),
|
|
25
|
+
productId: targetId,
|
|
26
|
+
activationMode: "manual",
|
|
27
|
+
activateAt: null,
|
|
28
|
+
deactivateAt: null,
|
|
29
|
+
sellAt: null,
|
|
30
|
+
stopSellAt: null,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
const ticketRows = await tx
|
|
34
|
+
.select()
|
|
35
|
+
.from(productTicketSettings)
|
|
36
|
+
.where(eq(productTicketSettings.productId, sourceId));
|
|
37
|
+
for (const row of ticketRows) {
|
|
38
|
+
await tx
|
|
39
|
+
.insert(productTicketSettings)
|
|
40
|
+
.values({ ...withoutSystemColumns(row), productId: targetId });
|
|
41
|
+
}
|
|
42
|
+
const visibilityRows = await tx
|
|
43
|
+
.select()
|
|
44
|
+
.from(productVisibilitySettings)
|
|
45
|
+
.where(eq(productVisibilitySettings.productId, sourceId));
|
|
46
|
+
for (const row of visibilityRows) {
|
|
47
|
+
await tx
|
|
48
|
+
.insert(productVisibilitySettings)
|
|
49
|
+
.values({ ...withoutSystemColumns(row), productId: targetId });
|
|
50
|
+
}
|
|
51
|
+
const capabilityRows = await tx
|
|
52
|
+
.select()
|
|
53
|
+
.from(productCapabilities)
|
|
54
|
+
.where(eq(productCapabilities.productId, sourceId));
|
|
55
|
+
for (const row of capabilityRows) {
|
|
56
|
+
await tx
|
|
57
|
+
.insert(productCapabilities)
|
|
58
|
+
.values({ ...withoutSystemColumns(row), productId: targetId });
|
|
59
|
+
}
|
|
60
|
+
const deliveryFormatRows = await tx
|
|
61
|
+
.select()
|
|
62
|
+
.from(productDeliveryFormats)
|
|
63
|
+
.where(eq(productDeliveryFormats.productId, sourceId));
|
|
64
|
+
for (const row of deliveryFormatRows) {
|
|
65
|
+
await tx
|
|
66
|
+
.insert(productDeliveryFormats)
|
|
67
|
+
.values({ ...withoutSystemColumns(row), productId: targetId });
|
|
68
|
+
}
|
|
69
|
+
const featureRows = await tx
|
|
70
|
+
.select()
|
|
71
|
+
.from(productFeatures)
|
|
72
|
+
.where(eq(productFeatures.productId, sourceId));
|
|
73
|
+
for (const row of featureRows) {
|
|
74
|
+
await tx.insert(productFeatures).values({ ...withoutSystemColumns(row), productId: targetId });
|
|
75
|
+
}
|
|
76
|
+
const faqRows = await tx.select().from(productFaqs).where(eq(productFaqs.productId, sourceId));
|
|
77
|
+
for (const row of faqRows) {
|
|
78
|
+
await tx.insert(productFaqs).values({ ...withoutSystemColumns(row), productId: targetId });
|
|
79
|
+
}
|
|
80
|
+
const locationRows = await tx
|
|
81
|
+
.select()
|
|
82
|
+
.from(productLocations)
|
|
83
|
+
.where(eq(productLocations.productId, sourceId));
|
|
84
|
+
for (const row of locationRows) {
|
|
85
|
+
await tx.insert(productLocations).values({ ...withoutSystemColumns(row), productId: targetId });
|
|
86
|
+
}
|
|
87
|
+
const translationRows = await tx
|
|
88
|
+
.select()
|
|
89
|
+
.from(productTranslations)
|
|
90
|
+
.where(eq(productTranslations.productId, sourceId));
|
|
91
|
+
for (const row of translationRows) {
|
|
92
|
+
await tx
|
|
93
|
+
.insert(productTranslations)
|
|
94
|
+
.values({ ...withoutSystemColumns(row), productId: targetId });
|
|
95
|
+
}
|
|
96
|
+
const categoryRows = await tx
|
|
97
|
+
.select()
|
|
98
|
+
.from(productCategoryProducts)
|
|
99
|
+
.where(eq(productCategoryProducts.productId, sourceId));
|
|
100
|
+
for (const row of categoryRows) {
|
|
101
|
+
await tx
|
|
102
|
+
.insert(productCategoryProducts)
|
|
103
|
+
.values({ ...withoutSystemColumns(row), productId: targetId });
|
|
104
|
+
}
|
|
105
|
+
const tagRows = await tx
|
|
106
|
+
.select()
|
|
107
|
+
.from(productTagProducts)
|
|
108
|
+
.where(eq(productTagProducts.productId, sourceId));
|
|
109
|
+
for (const row of tagRows) {
|
|
110
|
+
await tx
|
|
111
|
+
.insert(productTagProducts)
|
|
112
|
+
.values({ ...withoutSystemColumns(row), productId: targetId });
|
|
113
|
+
}
|
|
114
|
+
const destinationRows = await tx
|
|
115
|
+
.select()
|
|
116
|
+
.from(productDestinations)
|
|
117
|
+
.where(eq(productDestinations.productId, sourceId));
|
|
118
|
+
for (const row of destinationRows) {
|
|
119
|
+
await tx
|
|
120
|
+
.insert(productDestinations)
|
|
121
|
+
.values({ ...withoutSystemColumns(row), productId: targetId });
|
|
122
|
+
}
|
|
123
|
+
await copyOptionsAndUnits(ctx);
|
|
124
|
+
await copyPricingCategories(ctx);
|
|
125
|
+
await copyItinerary(ctx);
|
|
126
|
+
await copyMedia(ctx);
|
|
127
|
+
await copyExtras(ctx);
|
|
128
|
+
}
|
|
129
|
+
async function copyOptionsAndUnits(ctx) {
|
|
130
|
+
const { tx, sourceId, targetId, optionIdMap, unitIdMap, unitsByNewOption } = ctx;
|
|
131
|
+
const sourceOptions = await tx
|
|
132
|
+
.select()
|
|
133
|
+
.from(productOptions)
|
|
134
|
+
.where(eq(productOptions.productId, sourceId));
|
|
135
|
+
for (const row of sourceOptions) {
|
|
136
|
+
const [copy] = await tx
|
|
137
|
+
.insert(productOptions)
|
|
138
|
+
.values({ ...withoutSystemColumns(row), productId: targetId })
|
|
139
|
+
.returning();
|
|
140
|
+
if (copy) {
|
|
141
|
+
optionIdMap.set(row.id, copy.id);
|
|
142
|
+
unitsByNewOption.set(copy.id, []);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const sourceOptionIds = sourceOptions.map((row) => row.id);
|
|
146
|
+
if (sourceOptionIds.length === 0)
|
|
147
|
+
return;
|
|
148
|
+
const optionTranslationRows = await tx
|
|
149
|
+
.select()
|
|
150
|
+
.from(productOptionTranslations)
|
|
151
|
+
.where(inArray(productOptionTranslations.optionId, sourceOptionIds));
|
|
152
|
+
for (const row of optionTranslationRows) {
|
|
153
|
+
const targetOptionId = optionIdMap.get(row.optionId);
|
|
154
|
+
if (!targetOptionId)
|
|
155
|
+
continue;
|
|
156
|
+
await tx
|
|
157
|
+
.insert(productOptionTranslations)
|
|
158
|
+
.values({ ...withoutSystemColumns(row), optionId: targetOptionId });
|
|
159
|
+
}
|
|
160
|
+
const sourceUnits = await tx
|
|
161
|
+
.select()
|
|
162
|
+
.from(optionUnits)
|
|
163
|
+
.where(inArray(optionUnits.optionId, sourceOptionIds));
|
|
164
|
+
for (const row of sourceUnits) {
|
|
165
|
+
const targetOptionId = optionIdMap.get(row.optionId);
|
|
166
|
+
if (!targetOptionId)
|
|
167
|
+
continue;
|
|
168
|
+
const [copy] = await tx
|
|
169
|
+
.insert(optionUnits)
|
|
170
|
+
.values({ ...withoutSystemColumns(row), optionId: targetOptionId })
|
|
171
|
+
.returning();
|
|
172
|
+
if (copy) {
|
|
173
|
+
unitIdMap.set(row.id, copy.id);
|
|
174
|
+
unitsByNewOption.get(targetOptionId)?.push({ id: copy.id });
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const sourceUnitIds = sourceUnits.map((row) => row.id);
|
|
178
|
+
if (sourceUnitIds.length > 0) {
|
|
179
|
+
const unitTranslationRows = await tx
|
|
180
|
+
.select()
|
|
181
|
+
.from(optionUnitTranslations)
|
|
182
|
+
.where(inArray(optionUnitTranslations.unitId, sourceUnitIds));
|
|
183
|
+
for (const row of unitTranslationRows) {
|
|
184
|
+
const targetUnitId = unitIdMap.get(row.unitId);
|
|
185
|
+
if (!targetUnitId)
|
|
186
|
+
continue;
|
|
187
|
+
await tx
|
|
188
|
+
.insert(optionUnitTranslations)
|
|
189
|
+
.values({ ...withoutSystemColumns(row), unitId: targetUnitId });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const resourceTemplateRows = await tx
|
|
193
|
+
.select()
|
|
194
|
+
.from(productOptionResourceTemplates)
|
|
195
|
+
.where(inArray(productOptionResourceTemplates.productOptionId, sourceOptionIds));
|
|
196
|
+
for (const row of resourceTemplateRows) {
|
|
197
|
+
const targetOptionId = optionIdMap.get(row.productOptionId);
|
|
198
|
+
if (!targetOptionId)
|
|
199
|
+
continue;
|
|
200
|
+
await tx
|
|
201
|
+
.insert(productOptionResourceTemplates)
|
|
202
|
+
.values({ ...withoutSystemColumns(row), productOptionId: targetOptionId });
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
async function copyPricingCategories(ctx) {
|
|
206
|
+
const { tx, sourceId, targetId, optionIdMap, unitIdMap, pricingCategoryIdMap } = ctx;
|
|
207
|
+
const pricingCategoryRows = await tx
|
|
208
|
+
.select()
|
|
209
|
+
.from(pricingCategories)
|
|
210
|
+
.where(eq(pricingCategories.productId, sourceId));
|
|
211
|
+
for (const row of pricingCategoryRows) {
|
|
212
|
+
const targetOptionId = row.optionId ? (optionIdMap.get(row.optionId) ?? null) : null;
|
|
213
|
+
const targetUnitId = row.unitId ? (unitIdMap.get(row.unitId) ?? null) : null;
|
|
214
|
+
if (row.optionId && !targetOptionId)
|
|
215
|
+
continue;
|
|
216
|
+
if (row.unitId && !targetUnitId)
|
|
217
|
+
continue;
|
|
218
|
+
const [copy] = await tx
|
|
219
|
+
.insert(pricingCategories)
|
|
220
|
+
.values({
|
|
221
|
+
...withoutSystemColumns(row),
|
|
222
|
+
productId: targetId,
|
|
223
|
+
optionId: targetOptionId,
|
|
224
|
+
unitId: targetUnitId,
|
|
225
|
+
})
|
|
226
|
+
.returning();
|
|
227
|
+
if (copy)
|
|
228
|
+
pricingCategoryIdMap.set(row.id, copy.id);
|
|
229
|
+
}
|
|
230
|
+
const sourcePricingCategoryIds = pricingCategoryRows.map((row) => row.id);
|
|
231
|
+
if (sourcePricingCategoryIds.length === 0)
|
|
232
|
+
return;
|
|
233
|
+
const dependencyRows = await tx
|
|
234
|
+
.select()
|
|
235
|
+
.from(pricingCategoryDependencies)
|
|
236
|
+
.where(inArray(pricingCategoryDependencies.pricingCategoryId, sourcePricingCategoryIds));
|
|
237
|
+
for (const row of dependencyRows) {
|
|
238
|
+
const targetPricingCategoryId = pricingCategoryIdMap.get(row.pricingCategoryId);
|
|
239
|
+
const targetMasterPricingCategoryId = pricingCategoryIdMap.get(row.masterPricingCategoryId);
|
|
240
|
+
if (!targetPricingCategoryId || !targetMasterPricingCategoryId)
|
|
241
|
+
continue;
|
|
242
|
+
await tx.insert(pricingCategoryDependencies).values({
|
|
243
|
+
...withoutSystemColumns(row),
|
|
244
|
+
pricingCategoryId: targetPricingCategoryId,
|
|
245
|
+
masterPricingCategoryId: targetMasterPricingCategoryId,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
async function copyItinerary(ctx) {
|
|
250
|
+
const { tx, sourceId, targetId, itineraryIdMap, dayIdMap } = ctx;
|
|
251
|
+
const sourceItineraries = await tx
|
|
252
|
+
.select()
|
|
253
|
+
.from(productItineraries)
|
|
254
|
+
.where(eq(productItineraries.productId, sourceId));
|
|
255
|
+
for (const row of sourceItineraries) {
|
|
256
|
+
const [copy] = await tx
|
|
257
|
+
.insert(productItineraries)
|
|
258
|
+
.values({ ...withoutSystemColumns(row), productId: targetId })
|
|
259
|
+
.returning();
|
|
260
|
+
if (copy)
|
|
261
|
+
itineraryIdMap.set(row.id, copy.id);
|
|
262
|
+
}
|
|
263
|
+
if (sourceItineraries.length === 0) {
|
|
264
|
+
await tx
|
|
265
|
+
.insert(productItineraries)
|
|
266
|
+
.values({ productId: targetId, name: "Default", isDefault: true, sortOrder: 0 });
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
const sourceItineraryIds = sourceItineraries.map((row) => row.id);
|
|
270
|
+
const sourceDays = await tx
|
|
271
|
+
.select()
|
|
272
|
+
.from(productDays)
|
|
273
|
+
.where(inArray(productDays.itineraryId, sourceItineraryIds));
|
|
274
|
+
for (const row of sourceDays) {
|
|
275
|
+
const targetItineraryId = itineraryIdMap.get(row.itineraryId);
|
|
276
|
+
if (!targetItineraryId)
|
|
277
|
+
continue;
|
|
278
|
+
const [copy] = await tx
|
|
279
|
+
.insert(productDays)
|
|
280
|
+
.values({ ...withoutSystemColumns(row), itineraryId: targetItineraryId })
|
|
281
|
+
.returning();
|
|
282
|
+
if (copy)
|
|
283
|
+
dayIdMap.set(row.id, copy.id);
|
|
284
|
+
}
|
|
285
|
+
const sourceDayIds = sourceDays.map((row) => row.id);
|
|
286
|
+
if (sourceDayIds.length === 0)
|
|
287
|
+
return;
|
|
288
|
+
const dayServiceRows = await tx
|
|
289
|
+
.select()
|
|
290
|
+
.from(productDayServices)
|
|
291
|
+
.where(inArray(productDayServices.dayId, sourceDayIds));
|
|
292
|
+
for (const row of dayServiceRows) {
|
|
293
|
+
const targetDayId = dayIdMap.get(row.dayId);
|
|
294
|
+
if (!targetDayId)
|
|
295
|
+
continue;
|
|
296
|
+
await tx.insert(productDayServices).values({ ...withoutSystemColumns(row), dayId: targetDayId });
|
|
297
|
+
}
|
|
298
|
+
const dayTranslationRows = await tx
|
|
299
|
+
.select()
|
|
300
|
+
.from(productDayTranslations)
|
|
301
|
+
.where(inArray(productDayTranslations.dayId, sourceDayIds));
|
|
302
|
+
for (const row of dayTranslationRows) {
|
|
303
|
+
const targetDayId = dayIdMap.get(row.dayId);
|
|
304
|
+
if (!targetDayId)
|
|
305
|
+
continue;
|
|
306
|
+
await tx
|
|
307
|
+
.insert(productDayTranslations)
|
|
308
|
+
.values({ ...withoutSystemColumns(row), dayId: targetDayId });
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
async function copyMedia(ctx) {
|
|
312
|
+
const { tx, sourceId, targetId, dayIdMap } = ctx;
|
|
313
|
+
const mediaRows = await tx.select().from(productMedia).where(eq(productMedia.productId, sourceId));
|
|
314
|
+
for (const row of mediaRows) {
|
|
315
|
+
if (row.isBrochure)
|
|
316
|
+
continue;
|
|
317
|
+
const targetDayId = row.dayId ? dayIdMap.get(row.dayId) : null;
|
|
318
|
+
if (row.dayId && !targetDayId)
|
|
319
|
+
continue;
|
|
320
|
+
await tx.insert(productMedia).values({
|
|
321
|
+
...withoutSystemColumns(row),
|
|
322
|
+
productId: targetId,
|
|
323
|
+
dayId: targetDayId,
|
|
324
|
+
isBrochure: false,
|
|
325
|
+
isBrochureCurrent: false,
|
|
326
|
+
brochureVersion: null,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
async function copyExtras(ctx) {
|
|
331
|
+
const { tx, sourceId, targetId, optionIdMap, productExtraIdMap, optionExtraConfigIdMap } = ctx;
|
|
332
|
+
const extraRows = await tx
|
|
333
|
+
.select()
|
|
334
|
+
.from(productExtras)
|
|
335
|
+
.where(eq(productExtras.productId, sourceId));
|
|
336
|
+
for (const row of extraRows) {
|
|
337
|
+
const [copy] = await tx
|
|
338
|
+
.insert(productExtras)
|
|
339
|
+
.values({ ...withoutSystemColumns(row), productId: targetId })
|
|
340
|
+
.returning();
|
|
341
|
+
if (copy)
|
|
342
|
+
productExtraIdMap.set(row.id, copy.id);
|
|
343
|
+
}
|
|
344
|
+
const sourceOptionIds = [...optionIdMap.keys()];
|
|
345
|
+
if (sourceOptionIds.length === 0)
|
|
346
|
+
return;
|
|
347
|
+
const extraConfigRows = await tx
|
|
348
|
+
.select()
|
|
349
|
+
.from(optionExtraConfigs)
|
|
350
|
+
.where(inArray(optionExtraConfigs.optionId, sourceOptionIds));
|
|
351
|
+
for (const row of extraConfigRows) {
|
|
352
|
+
const targetOptionId = optionIdMap.get(row.optionId);
|
|
353
|
+
const targetProductExtraId = productExtraIdMap.get(row.productExtraId);
|
|
354
|
+
if (!targetOptionId || !targetProductExtraId)
|
|
355
|
+
continue;
|
|
356
|
+
const [copy] = await tx
|
|
357
|
+
.insert(optionExtraConfigs)
|
|
358
|
+
.values({
|
|
359
|
+
...withoutSystemColumns(row),
|
|
360
|
+
optionId: targetOptionId,
|
|
361
|
+
productExtraId: targetProductExtraId,
|
|
362
|
+
})
|
|
363
|
+
.returning();
|
|
364
|
+
if (copy)
|
|
365
|
+
optionExtraConfigIdMap.set(row.id, copy.id);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type CloneContext } from "./clone-content.js";
|
|
2
|
+
/**
|
|
3
|
+
* Copies availability (when `copyDepartures`) and the full pricing-rule graph —
|
|
4
|
+
* option price rules → unit price rules (with remapped pricing categories) →
|
|
5
|
+
* tiers, plus start-time / pickup / dropoff / extra rules and departure
|
|
6
|
+
* overrides. Relies on the id remaps populated by {@link copyProductContent}.
|
|
7
|
+
*/
|
|
8
|
+
export declare function copyPricingAndAvailability(ctx: CloneContext): Promise<void>;
|
|
9
|
+
//# sourceMappingURL=clone-pricing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clone-pricing.d.ts","sourceRoot":"","sources":["../src/clone-pricing.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,KAAK,YAAY,EAAwB,MAAM,oBAAoB,CAAA;AAE5E;;;;;GAKG;AACH,wBAAsB,0BAA0B,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAQjF"}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { availabilityRules, availabilitySlots, availabilityStartTimes, } from "@voyantjs/availability";
|
|
2
|
+
import { departurePriceOverrides, dropoffPriceRules, extraPriceRules, optionPriceRules, optionStartTimeRules, optionUnitPriceRules, optionUnitTiers, pickupPriceRules, } from "@voyantjs/pricing/schema";
|
|
3
|
+
import { eq, inArray } from "drizzle-orm";
|
|
4
|
+
import { withoutSystemColumns } from "./clone-content.js";
|
|
5
|
+
/**
|
|
6
|
+
* Copies availability (when `copyDepartures`) and the full pricing-rule graph —
|
|
7
|
+
* option price rules → unit price rules (with remapped pricing categories) →
|
|
8
|
+
* tiers, plus start-time / pickup / dropoff / extra rules and departure
|
|
9
|
+
* overrides. Relies on the id remaps populated by {@link copyProductContent}.
|
|
10
|
+
*/
|
|
11
|
+
export async function copyPricingAndAvailability(ctx) {
|
|
12
|
+
if (ctx.copyDepartures) {
|
|
13
|
+
await copyAvailability(ctx);
|
|
14
|
+
}
|
|
15
|
+
await copyPriceRules(ctx);
|
|
16
|
+
if (ctx.copyDepartures && ctx.slotIdMap.size > 0) {
|
|
17
|
+
await copyDepartureOverrides(ctx);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
async function copyAvailability(ctx) {
|
|
21
|
+
const { tx, sourceId, targetId, optionIdMap, itineraryIdMap, startTimeIdMap, ruleIdMap, slotIdMap, } = ctx;
|
|
22
|
+
const startTimeRows = await tx
|
|
23
|
+
.select()
|
|
24
|
+
.from(availabilityStartTimes)
|
|
25
|
+
.where(eq(availabilityStartTimes.productId, sourceId));
|
|
26
|
+
for (const row of startTimeRows) {
|
|
27
|
+
const [copy] = await tx
|
|
28
|
+
.insert(availabilityStartTimes)
|
|
29
|
+
.values({
|
|
30
|
+
...withoutSystemColumns(row),
|
|
31
|
+
productId: targetId,
|
|
32
|
+
optionId: row.optionId ? (optionIdMap.get(row.optionId) ?? null) : null,
|
|
33
|
+
})
|
|
34
|
+
.returning();
|
|
35
|
+
if (copy)
|
|
36
|
+
startTimeIdMap.set(row.id, copy.id);
|
|
37
|
+
}
|
|
38
|
+
const availabilityRuleRows = await tx
|
|
39
|
+
.select()
|
|
40
|
+
.from(availabilityRules)
|
|
41
|
+
.where(eq(availabilityRules.productId, sourceId));
|
|
42
|
+
for (const row of availabilityRuleRows) {
|
|
43
|
+
const [copy] = await tx
|
|
44
|
+
.insert(availabilityRules)
|
|
45
|
+
.values({
|
|
46
|
+
...withoutSystemColumns(row),
|
|
47
|
+
productId: targetId,
|
|
48
|
+
optionId: row.optionId ? (optionIdMap.get(row.optionId) ?? null) : null,
|
|
49
|
+
})
|
|
50
|
+
.returning();
|
|
51
|
+
if (copy)
|
|
52
|
+
ruleIdMap.set(row.id, copy.id);
|
|
53
|
+
}
|
|
54
|
+
const slotRows = await tx
|
|
55
|
+
.select()
|
|
56
|
+
.from(availabilitySlots)
|
|
57
|
+
.where(eq(availabilitySlots.productId, sourceId));
|
|
58
|
+
for (const row of slotRows) {
|
|
59
|
+
const targetItineraryId = row.itineraryId ? (itineraryIdMap.get(row.itineraryId) ?? null) : null;
|
|
60
|
+
const targetOptionId = row.optionId ? (optionIdMap.get(row.optionId) ?? null) : null;
|
|
61
|
+
const targetRuleId = row.availabilityRuleId
|
|
62
|
+
? (ruleIdMap.get(row.availabilityRuleId) ?? null)
|
|
63
|
+
: null;
|
|
64
|
+
const targetStartTimeId = row.startTimeId ? (startTimeIdMap.get(row.startTimeId) ?? null) : null;
|
|
65
|
+
const [copy] = await tx
|
|
66
|
+
.insert(availabilitySlots)
|
|
67
|
+
.values({
|
|
68
|
+
...withoutSystemColumns(row),
|
|
69
|
+
productId: targetId,
|
|
70
|
+
itineraryId: targetItineraryId,
|
|
71
|
+
optionId: targetOptionId,
|
|
72
|
+
availabilityRuleId: targetRuleId,
|
|
73
|
+
startTimeId: targetStartTimeId,
|
|
74
|
+
remainingPax: row.initialPax,
|
|
75
|
+
remainingPickups: row.initialPickups,
|
|
76
|
+
})
|
|
77
|
+
.returning();
|
|
78
|
+
if (copy)
|
|
79
|
+
slotIdMap.set(row.id, copy.id);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async function copyPriceRules(ctx) {
|
|
83
|
+
const { tx, targetId, optionIdMap, unitIdMap, startTimeIdMap, optionPriceRuleIdMap, optionUnitPriceRuleIdMap, pricingCategoryIdMap, productExtraIdMap, optionExtraConfigIdMap, } = ctx;
|
|
84
|
+
const sourceOptionIds = [...optionIdMap.keys()];
|
|
85
|
+
if (sourceOptionIds.length === 0)
|
|
86
|
+
return;
|
|
87
|
+
const optionPriceRuleRows = await tx
|
|
88
|
+
.select()
|
|
89
|
+
.from(optionPriceRules)
|
|
90
|
+
.where(inArray(optionPriceRules.optionId, sourceOptionIds));
|
|
91
|
+
for (const row of optionPriceRuleRows) {
|
|
92
|
+
const targetOptionId = optionIdMap.get(row.optionId);
|
|
93
|
+
if (!targetOptionId)
|
|
94
|
+
continue;
|
|
95
|
+
const [copy] = await tx
|
|
96
|
+
.insert(optionPriceRules)
|
|
97
|
+
.values({ ...withoutSystemColumns(row), productId: targetId, optionId: targetOptionId })
|
|
98
|
+
.returning();
|
|
99
|
+
if (copy)
|
|
100
|
+
optionPriceRuleIdMap.set(row.id, copy.id);
|
|
101
|
+
}
|
|
102
|
+
const sourceOptionPriceRuleIds = optionPriceRuleRows.map((row) => row.id);
|
|
103
|
+
if (sourceOptionPriceRuleIds.length === 0)
|
|
104
|
+
return;
|
|
105
|
+
const optionUnitPriceRuleRows = await tx
|
|
106
|
+
.select()
|
|
107
|
+
.from(optionUnitPriceRules)
|
|
108
|
+
.where(inArray(optionUnitPriceRules.optionPriceRuleId, sourceOptionPriceRuleIds));
|
|
109
|
+
for (const row of optionUnitPriceRuleRows) {
|
|
110
|
+
const targetPriceRuleId = optionPriceRuleIdMap.get(row.optionPriceRuleId);
|
|
111
|
+
const targetOptionId = optionIdMap.get(row.optionId);
|
|
112
|
+
const targetUnitId = unitIdMap.get(row.unitId);
|
|
113
|
+
const targetPricingCategoryId = row.pricingCategoryId
|
|
114
|
+
? (pricingCategoryIdMap.get(row.pricingCategoryId) ?? null)
|
|
115
|
+
: null;
|
|
116
|
+
if (!targetPriceRuleId || !targetOptionId || !targetUnitId)
|
|
117
|
+
continue;
|
|
118
|
+
if (row.pricingCategoryId && !targetPricingCategoryId)
|
|
119
|
+
continue;
|
|
120
|
+
const [copy] = await tx
|
|
121
|
+
.insert(optionUnitPriceRules)
|
|
122
|
+
.values({
|
|
123
|
+
...withoutSystemColumns(row),
|
|
124
|
+
optionPriceRuleId: targetPriceRuleId,
|
|
125
|
+
optionId: targetOptionId,
|
|
126
|
+
unitId: targetUnitId,
|
|
127
|
+
pricingCategoryId: targetPricingCategoryId,
|
|
128
|
+
})
|
|
129
|
+
.returning();
|
|
130
|
+
if (copy)
|
|
131
|
+
optionUnitPriceRuleIdMap.set(row.id, copy.id);
|
|
132
|
+
}
|
|
133
|
+
const sourceOptionUnitPriceRuleIds = optionUnitPriceRuleRows.map((row) => row.id);
|
|
134
|
+
if (sourceOptionUnitPriceRuleIds.length > 0) {
|
|
135
|
+
const tierRows = await tx
|
|
136
|
+
.select()
|
|
137
|
+
.from(optionUnitTiers)
|
|
138
|
+
.where(inArray(optionUnitTiers.optionUnitPriceRuleId, sourceOptionUnitPriceRuleIds));
|
|
139
|
+
for (const row of tierRows) {
|
|
140
|
+
const targetUnitPriceRuleId = optionUnitPriceRuleIdMap.get(row.optionUnitPriceRuleId);
|
|
141
|
+
if (!targetUnitPriceRuleId)
|
|
142
|
+
continue;
|
|
143
|
+
await tx
|
|
144
|
+
.insert(optionUnitTiers)
|
|
145
|
+
.values({ ...withoutSystemColumns(row), optionUnitPriceRuleId: targetUnitPriceRuleId });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const startRuleRows = await tx
|
|
149
|
+
.select()
|
|
150
|
+
.from(optionStartTimeRules)
|
|
151
|
+
.where(inArray(optionStartTimeRules.optionPriceRuleId, sourceOptionPriceRuleIds));
|
|
152
|
+
for (const row of startRuleRows) {
|
|
153
|
+
const targetPriceRuleId = optionPriceRuleIdMap.get(row.optionPriceRuleId);
|
|
154
|
+
const targetOptionId = optionIdMap.get(row.optionId);
|
|
155
|
+
const targetStartTimeId = startTimeIdMap.get(row.startTimeId);
|
|
156
|
+
if (!targetPriceRuleId || !targetOptionId || !targetStartTimeId)
|
|
157
|
+
continue;
|
|
158
|
+
await tx.insert(optionStartTimeRules).values({
|
|
159
|
+
...withoutSystemColumns(row),
|
|
160
|
+
optionPriceRuleId: targetPriceRuleId,
|
|
161
|
+
optionId: targetOptionId,
|
|
162
|
+
startTimeId: targetStartTimeId,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
const pickupPriceRuleRows = await tx
|
|
166
|
+
.select()
|
|
167
|
+
.from(pickupPriceRules)
|
|
168
|
+
.where(inArray(pickupPriceRules.optionPriceRuleId, sourceOptionPriceRuleIds));
|
|
169
|
+
for (const row of pickupPriceRuleRows) {
|
|
170
|
+
const targetPriceRuleId = optionPriceRuleIdMap.get(row.optionPriceRuleId);
|
|
171
|
+
const targetOptionId = optionIdMap.get(row.optionId);
|
|
172
|
+
if (!targetPriceRuleId || !targetOptionId)
|
|
173
|
+
continue;
|
|
174
|
+
await tx.insert(pickupPriceRules).values({
|
|
175
|
+
...withoutSystemColumns(row),
|
|
176
|
+
optionPriceRuleId: targetPriceRuleId,
|
|
177
|
+
optionId: targetOptionId,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
const dropoffPriceRuleRows = await tx
|
|
181
|
+
.select()
|
|
182
|
+
.from(dropoffPriceRules)
|
|
183
|
+
.where(inArray(dropoffPriceRules.optionPriceRuleId, sourceOptionPriceRuleIds));
|
|
184
|
+
for (const row of dropoffPriceRuleRows) {
|
|
185
|
+
const targetPriceRuleId = optionPriceRuleIdMap.get(row.optionPriceRuleId);
|
|
186
|
+
const targetOptionId = optionIdMap.get(row.optionId);
|
|
187
|
+
if (!targetPriceRuleId || !targetOptionId)
|
|
188
|
+
continue;
|
|
189
|
+
await tx.insert(dropoffPriceRules).values({
|
|
190
|
+
...withoutSystemColumns(row),
|
|
191
|
+
optionPriceRuleId: targetPriceRuleId,
|
|
192
|
+
optionId: targetOptionId,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
const extraPriceRuleRows = await tx
|
|
196
|
+
.select()
|
|
197
|
+
.from(extraPriceRules)
|
|
198
|
+
.where(inArray(extraPriceRules.optionPriceRuleId, sourceOptionPriceRuleIds));
|
|
199
|
+
for (const row of extraPriceRuleRows) {
|
|
200
|
+
const targetPriceRuleId = optionPriceRuleIdMap.get(row.optionPriceRuleId);
|
|
201
|
+
const targetOptionId = optionIdMap.get(row.optionId);
|
|
202
|
+
const targetProductExtraId = row.productExtraId
|
|
203
|
+
? (productExtraIdMap.get(row.productExtraId) ?? null)
|
|
204
|
+
: null;
|
|
205
|
+
const targetOptionExtraConfigId = row.optionExtraConfigId
|
|
206
|
+
? (optionExtraConfigIdMap.get(row.optionExtraConfigId) ?? null)
|
|
207
|
+
: null;
|
|
208
|
+
if (!targetPriceRuleId || !targetOptionId)
|
|
209
|
+
continue;
|
|
210
|
+
if (row.productExtraId && !targetProductExtraId)
|
|
211
|
+
continue;
|
|
212
|
+
if (row.optionExtraConfigId && !targetOptionExtraConfigId)
|
|
213
|
+
continue;
|
|
214
|
+
await tx.insert(extraPriceRules).values({
|
|
215
|
+
...withoutSystemColumns(row),
|
|
216
|
+
optionPriceRuleId: targetPriceRuleId,
|
|
217
|
+
optionId: targetOptionId,
|
|
218
|
+
productExtraId: targetProductExtraId,
|
|
219
|
+
optionExtraConfigId: targetOptionExtraConfigId,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
async function copyDepartureOverrides(ctx) {
|
|
224
|
+
const { tx, optionIdMap, unitIdMap, slotIdMap } = ctx;
|
|
225
|
+
const overrideRows = await tx
|
|
226
|
+
.select()
|
|
227
|
+
.from(departurePriceOverrides)
|
|
228
|
+
.where(inArray(departurePriceOverrides.departureId, [...slotIdMap.keys()]));
|
|
229
|
+
for (const row of overrideRows) {
|
|
230
|
+
const targetDepartureId = slotIdMap.get(row.departureId);
|
|
231
|
+
const targetOptionId = optionIdMap.get(row.optionId);
|
|
232
|
+
const targetUnitId = unitIdMap.get(row.optionUnitId);
|
|
233
|
+
if (!targetDepartureId || !targetOptionId || !targetUnitId)
|
|
234
|
+
continue;
|
|
235
|
+
await tx.insert(departurePriceOverrides).values({
|
|
236
|
+
...withoutSystemColumns(row),
|
|
237
|
+
departureId: targetDepartureId,
|
|
238
|
+
optionId: targetOptionId,
|
|
239
|
+
optionUnitId: targetUnitId,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
package/dist/clone.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { products } from "@voyantjs/products/schema";
|
|
2
|
+
import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
|
3
|
+
type Product = typeof products.$inferSelect;
|
|
4
|
+
export interface CloneProductOptions {
|
|
5
|
+
/** New product name. Defaults to `"{source} (Copy)"` (preserves the UI's no-body call). */
|
|
6
|
+
name?: string;
|
|
7
|
+
/** New product status. Defaults to `draft`. */
|
|
8
|
+
status?: Product["status"];
|
|
9
|
+
/** New product visibility. Defaults to the source's. */
|
|
10
|
+
visibility?: Product["visibility"];
|
|
11
|
+
/**
|
|
12
|
+
* Copy availability slots/rules/start-times + departure price overrides.
|
|
13
|
+
* Defaults to `true` (the operator UI clones a full working copy). The Max AI
|
|
14
|
+
* agent passes `false` per #1493 — departures are date-specific, so the clone
|
|
15
|
+
* starts with none and the agent adds fresh ones.
|
|
16
|
+
*/
|
|
17
|
+
copyDepartures?: boolean;
|
|
18
|
+
/** Author of the initial `product_versions` snapshot. When omitted, no snapshot is taken. */
|
|
19
|
+
userId?: string;
|
|
20
|
+
/** Dedup key; a retried request with the same key returns the first clone. */
|
|
21
|
+
idempotencyKey?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface ClonedOption {
|
|
24
|
+
id: string;
|
|
25
|
+
units: {
|
|
26
|
+
id: string;
|
|
27
|
+
}[];
|
|
28
|
+
}
|
|
29
|
+
export type CloneProductOutcome = {
|
|
30
|
+
status: "ok";
|
|
31
|
+
product: Product;
|
|
32
|
+
options: ClonedOption[];
|
|
33
|
+
reused: boolean;
|
|
34
|
+
} | {
|
|
35
|
+
status: "not_found";
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Deep-clone a product (#1493). Wraps {@link cloneGraph} with idempotency and an
|
|
39
|
+
* optional `product_versions` snapshot. The operator UI calls this with no
|
|
40
|
+
* overrides (full copy, `"{X} (Copy)"`, departures included); the agent passes
|
|
41
|
+
* `name` + `copyDepartures: false` + an `Idempotency-Key`.
|
|
42
|
+
*/
|
|
43
|
+
export declare function cloneProduct(db: PostgresJsDatabase, sourceProductId: string, options?: CloneProductOptions): Promise<CloneProductOutcome>;
|
|
44
|
+
export {};
|
|
45
|
+
//# sourceMappingURL=clone.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clone.d.ts","sourceRoot":"","sources":["../src/clone.ts"],"names":[],"mappings":"AACA,OAAO,EAA+B,QAAQ,EAAE,MAAM,2BAA2B,CAAA;AAEjF,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAKjE,KAAK,OAAO,GAAG,OAAO,QAAQ,CAAC,YAAY,CAAA;AAE3C,MAAM,WAAW,mBAAmB;IAClC,2FAA2F;IAC3F,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,+CAA+C;IAC/C,MAAM,CAAC,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAA;IAC1B,wDAAwD;IACxD,UAAU,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,CAAA;IAClC;;;;;OAKG;IACH,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,6FAA6F;IAC7F,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,8EAA8E;IAC9E,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;CACxB;AAED,MAAM,MAAM,mBAAmB,GAC3B;IAAE,MAAM,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,YAAY,EAAE,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAE,GAC5E;IAAE,MAAM,EAAE,WAAW,CAAA;CAAE,CAAA;AA+G3B;;;;;GAKG;AACH,wBAAsB,YAAY,CAChC,EAAE,EAAE,kBAAkB,EACtB,eAAe,EAAE,MAAM,EACvB,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC,mBAAmB,CAAC,CAoD9B"}
|