@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.
@@ -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
+ }
@@ -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"}