forked from UKSOURCE/cms.hailearning.edu.vn
567 lines
21 KiB
JavaScript
567 lines
21 KiB
JavaScript
const fs = require('fs');
|
|
const path = require('path');
|
|
const { addBaseUrlToImages } = require("../utils/imageHelper");
|
|
const Booking = require("../models/booking");
|
|
const {
|
|
validateLengthRules,
|
|
summarizeLengthErrors,
|
|
} = require("../utils/lengthValidation");
|
|
const {
|
|
BOOKING_LENGTH_RULES,
|
|
} = require("../constants/contentLengthRules");
|
|
|
|
const getBookingLengthMessage = (validation) =>
|
|
summarizeLengthErrors(validation, 3) ||
|
|
"One or more fields exceed the allowed length.";
|
|
|
|
// -------------------- Public helpers --------------------
|
|
const getBookingData = async () => {
|
|
const booking = await Booking.findOne().sort({ updatedAt: -1 });
|
|
return booking ? (booking.toObject ? booking.toObject() : booking) : null;
|
|
};
|
|
|
|
// Load static booking JSON from `data/booking.json` (if present)
|
|
const loadStaticBooking = () => {
|
|
try {
|
|
const p = path.join(__dirname, '..', 'data', 'booking.json');
|
|
if (!fs.existsSync(p)) return null;
|
|
const raw = fs.readFileSync(p, 'utf8');
|
|
return JSON.parse(raw);
|
|
} catch (e) {
|
|
console.error('booking.loadStaticBooking error:', e && e.message);
|
|
return null;
|
|
}
|
|
};
|
|
|
|
// Normalize booking shape: ensure configuration exists with discounts/vouchers
|
|
const normalizeBookingShape = (booking) => {
|
|
if (!booking || typeof booking !== 'object') return booking;
|
|
const b = JSON.parse(JSON.stringify(booking));
|
|
|
|
if (!b.configuration || typeof b.configuration !== 'object') {
|
|
b.configuration = { currency: 'USD', discounts: [], vouchers: [] };
|
|
}
|
|
|
|
// Ensure configuration.discounts and configuration.vouchers exist
|
|
if (!Array.isArray(b.configuration.discounts)) {
|
|
b.configuration.discounts = [];
|
|
}
|
|
if (!Array.isArray(b.configuration.vouchers)) {
|
|
b.configuration.vouchers = [];
|
|
}
|
|
|
|
return b;
|
|
};
|
|
|
|
// Deep merge: properties from `overrides` replace / merge into `base`.
|
|
const deepMerge = (base, overrides) => {
|
|
if (overrides === undefined) return base;
|
|
if (base === undefined || base === null) return overrides;
|
|
if (Array.isArray(overrides)) return overrides;
|
|
if (typeof overrides !== 'object' || overrides === null) return overrides;
|
|
const out = Object.assign({}, base);
|
|
Object.keys(overrides).forEach((k) => {
|
|
if (Array.isArray(overrides[k]) || typeof overrides[k] !== 'object' || overrides[k] === null) {
|
|
out[k] = overrides[k];
|
|
} else {
|
|
out[k] = deepMerge(base[k], overrides[k]);
|
|
}
|
|
});
|
|
return out;
|
|
};
|
|
|
|
// Ensure booking data fields have the expected shapes to avoid runtime errors
|
|
const sanitizeBookingData = (raw) => {
|
|
const defaults = {
|
|
hero: { title: '', backgroundImage: '' },
|
|
searchBar: { locationLabel: '', holidaySeasonLabel: '', searchButtonText: '' },
|
|
filterPanel: {
|
|
title: '',
|
|
priceTitle: '',
|
|
priceLabel: '',
|
|
pricePlaceholder: '',
|
|
priceMin: 0,
|
|
priceMax: 0,
|
|
ageTitle: '',
|
|
ageMin: 0,
|
|
ageMax: 0,
|
|
ageSelectPlaceholder: '',
|
|
activitiesTitle: '',
|
|
ratingTitle: '',
|
|
ratingOptions: [],
|
|
resetButtonText: ''
|
|
},
|
|
programs: [],
|
|
holidays: [],
|
|
locations: [],
|
|
camps: [],
|
|
configuration: { currency: 'USD', discounts: [], vouchers: [] },
|
|
formSteps: [],
|
|
validation: {}
|
|
};
|
|
|
|
if (!raw || typeof raw !== 'object') return defaults;
|
|
|
|
// Use raw data first, then fill in missing fields with defaults
|
|
const safe = Object.assign({}, raw);
|
|
|
|
// Ensure nested objects/arrays have correct types (use raw data if valid, otherwise defaults)
|
|
safe.hero = (safe.hero && typeof safe.hero === 'object') ? safe.hero : defaults.hero;
|
|
safe.searchBar = (safe.searchBar && typeof safe.searchBar === 'object') ? safe.searchBar : defaults.searchBar;
|
|
safe.filterPanel = (safe.filterPanel && typeof safe.filterPanel === 'object') ? safe.filterPanel : defaults.filterPanel;
|
|
|
|
if (!Array.isArray(safe.filterPanel.ratingOptions)) safe.filterPanel.ratingOptions = defaults.filterPanel.ratingOptions;
|
|
|
|
safe.programs = Array.isArray(safe.programs) ? safe.programs : defaults.programs;
|
|
safe.holidays = Array.isArray(safe.holidays) ? safe.holidays : defaults.holidays;
|
|
safe.locations = Array.isArray(safe.locations) ? safe.locations : defaults.locations;
|
|
safe.camps = Array.isArray(safe.camps) ? safe.camps : defaults.camps;
|
|
|
|
// Ensure configuration has proper structure
|
|
if (!safe.configuration || typeof safe.configuration !== 'object') {
|
|
safe.configuration = defaults.configuration;
|
|
}
|
|
if (!Array.isArray(safe.configuration.discounts)) {
|
|
safe.configuration.discounts = defaults.configuration.discounts;
|
|
}
|
|
if (!Array.isArray(safe.configuration.vouchers)) {
|
|
safe.configuration.vouchers = defaults.configuration.vouchers;
|
|
}
|
|
|
|
// Ensure formSteps and validation have correct types
|
|
safe.formSteps = Array.isArray(safe.formSteps) ? safe.formSteps : defaults.formSteps;
|
|
safe.validation = (safe.validation && typeof safe.validation === 'object' && !Array.isArray(safe.validation)) ? safe.validation : defaults.validation;
|
|
|
|
return safe;
|
|
};
|
|
|
|
// Safe JSON parse with better error handling - handles double-encoded JSON and JS object notation
|
|
const safeParse = (value, fieldName = 'unknown') => {
|
|
// If already an object or array, return as-is
|
|
if (typeof value === 'object' && value !== null) {
|
|
return value;
|
|
}
|
|
|
|
// If string, try to parse
|
|
if (typeof value === 'string') {
|
|
try {
|
|
let cleaned = value.trim();
|
|
|
|
// Check if it looks like JavaScript object notation (has single quotes or unquoted keys)
|
|
if (cleaned.includes("'") || /\{\s*\w+:/.test(cleaned)) {
|
|
console.warn(`safeParse: Converting JS notation to JSON for "${fieldName}"`);
|
|
|
|
// Aggressive conversion approach
|
|
cleaned = cleaned
|
|
.replace(/'/g, '"') // Replace ALL single quotes with double quotes
|
|
.replace(/\r?\n|\r/g, ' ') // Remove all newlines
|
|
.replace(/\s+/g, ' ') // Normalize multiple spaces to single space
|
|
.replace(/,(\s*[}\]])/g, '$1') // Remove trailing commas before } or ]
|
|
.replace(/([{,]\s*)(\w+):/g, '$1"$2":'); // Quote unquoted object keys
|
|
}
|
|
|
|
// Try parsing
|
|
let parsed = JSON.parse(cleaned);
|
|
|
|
// If result is still a string, try parsing again (double-encoded)
|
|
if (typeof parsed === 'string') {
|
|
console.warn(`safeParse: Double-encoded JSON detected for "${fieldName}"`);
|
|
parsed = JSON.parse(parsed);
|
|
}
|
|
|
|
return parsed;
|
|
} catch (e) {
|
|
console.error(`safeParse: Failed to parse field "${fieldName}"`, {
|
|
error: e.message,
|
|
valuePreview: value.substring(0, 200)
|
|
});
|
|
|
|
throw new Error(`Invalid JSON format for field: ${fieldName}. Error: ${e.message}`);
|
|
}
|
|
}
|
|
|
|
// For other types, return empty array or object
|
|
console.warn(`safeParse: Unexpected type for field "${fieldName}": ${typeof value}`);
|
|
return Array.isArray(value) ? [] : {};
|
|
};
|
|
|
|
// Validate booking data structure
|
|
const validateBookingData = (data) => {
|
|
const errors = [];
|
|
|
|
// Check required fields
|
|
if (!data.hero || typeof data.hero !== 'object') {
|
|
errors.push('Hero data is required and must be an object');
|
|
}
|
|
|
|
if (!data.searchBar || typeof data.searchBar !== 'object') {
|
|
errors.push('SearchBar data is required and must be an object');
|
|
}
|
|
|
|
if (!data.filterPanel || typeof data.filterPanel !== 'object') {
|
|
errors.push('FilterPanel data is required and must be an object');
|
|
}
|
|
|
|
// Validate arrays
|
|
if (data.programs && !Array.isArray(data.programs)) {
|
|
errors.push('Programs must be an array');
|
|
}
|
|
|
|
if (data.holidays && !Array.isArray(data.holidays)) {
|
|
errors.push('Holidays must be an array');
|
|
}
|
|
|
|
if (data.locations && !Array.isArray(data.locations)) {
|
|
errors.push('Locations must be an array');
|
|
}
|
|
|
|
if (data.camps && !Array.isArray(data.camps)) {
|
|
errors.push('Camps must be an array');
|
|
}
|
|
|
|
// Validate configuration structure
|
|
if (data.configuration) {
|
|
if (typeof data.configuration !== 'object') {
|
|
errors.push('Configuration must be an object');
|
|
} else {
|
|
if (data.configuration.discounts && !Array.isArray(data.configuration.discounts)) {
|
|
errors.push('Configuration.discounts must be an array');
|
|
}
|
|
if (data.configuration.vouchers && !Array.isArray(data.configuration.vouchers)) {
|
|
errors.push('Configuration.vouchers must be an array');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Validate formSteps and validation structure if provided
|
|
if (data.formSteps && !Array.isArray(data.formSteps)) {
|
|
errors.push('formSteps must be an array');
|
|
}
|
|
|
|
if (data.validation && (typeof data.validation !== 'object' || Array.isArray(data.validation))) {
|
|
errors.push('validation must be an object');
|
|
}
|
|
|
|
return {
|
|
isValid: errors.length === 0,
|
|
errors
|
|
};
|
|
};
|
|
|
|
// -------------------- Public endpoints --------------------
|
|
// Public endpoint: return Booking JSON
|
|
exports.page = async (req, res) => {
|
|
try {
|
|
const dbBooking = await getBookingData();
|
|
const staticBooking = loadStaticBooking();
|
|
|
|
// Normalize shapes so `configuration.discounts`/`configuration.vouchers` exist
|
|
const normStatic = normalizeBookingShape(staticBooking);
|
|
const normDb = normalizeBookingShape(dbBooking);
|
|
|
|
// Build final payload according to BOOKING_MODE env var
|
|
const finalBooking = getFinalBooking(normStatic, normDb);
|
|
|
|
if (!finalBooking) {
|
|
return res.status(404).json({
|
|
error: "No booking data found",
|
|
message: "Please configure booking data in admin panel"
|
|
});
|
|
}
|
|
|
|
const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get('host')}`;
|
|
const processed = addBaseUrlToImages(finalBooking, baseUrl);
|
|
|
|
return res.json(processed);
|
|
} catch (err) {
|
|
console.error("booking.page error:", err);
|
|
return res.status(500).json({
|
|
error: "Error loading booking data",
|
|
message: process.env.NODE_ENV === 'development' ? err.message : undefined
|
|
});
|
|
}
|
|
};
|
|
|
|
|
|
// API endpoint to return booking JSON
|
|
exports.api = async (req, res) => {
|
|
try {
|
|
const dbBooking = await getBookingData();
|
|
const staticBooking = loadStaticBooking();
|
|
|
|
// Normalize shapes so `configuration.discounts`/`configuration.vouchers` exist
|
|
const normStatic = normalizeBookingShape(staticBooking);
|
|
const normDb = normalizeBookingShape(dbBooking);
|
|
|
|
const finalBooking = getFinalBooking(normStatic, normDb);
|
|
|
|
if (!finalBooking) {
|
|
return res.status(404).json({
|
|
error: "No booking data found",
|
|
message: "Please configure booking data in admin panel"
|
|
});
|
|
}
|
|
|
|
const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get('host')}`;
|
|
const processed = addBaseUrlToImages(finalBooking, baseUrl);
|
|
|
|
return res.json(processed);
|
|
} catch (err) {
|
|
console.error("booking.api error:", err);
|
|
return res.status(500).json({
|
|
error: "Error loading booking data",
|
|
message: process.env.NODE_ENV === 'development' ? err.message : undefined
|
|
});
|
|
}
|
|
};
|
|
|
|
// -------------------- Admin endpoints --------------------
|
|
// Display Booking management page
|
|
exports.index = async (req, res) => {
|
|
try {
|
|
const dbBooking = await getBookingData();
|
|
const staticBooking = loadStaticBooking();
|
|
|
|
// Merge static booking with DB data (use same merge logic as public endpoints)
|
|
const normStatic = normalizeBookingShape(staticBooking);
|
|
const normDb = normalizeBookingShape(dbBooking);
|
|
const mergedData = getFinalBooking(normStatic, normDb);
|
|
|
|
// Normalize again after merge to ensure discounts/vouchers are synced to top-level
|
|
const data = normalizeBookingShape(mergedData);
|
|
|
|
// Sanitize data to ensure nested fields are objects/arrays (fixes malformed DB entries)
|
|
const safeData = sanitizeBookingData(data);
|
|
|
|
res.render("admin/booking/index", {
|
|
layout: "layouts/main",
|
|
title: "Booking Management",
|
|
data: safeData,
|
|
frontendUrl: process.env.FRONTEND_URL || req.protocol + "://" + req.get("host"),
|
|
currentPath: req.path,
|
|
user: req.session.user,
|
|
});
|
|
} catch (err) {
|
|
console.error("booking.index error:", err);
|
|
req.flash("error_msg", "Error loading booking page");
|
|
res.redirect("/admin/dashboard");
|
|
}
|
|
};
|
|
|
|
// Update booking data
|
|
exports.update = async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
// ADD THIS DEBUG LOG
|
|
console.log('=== RAW REQUEST BODY ===');
|
|
console.log('Discounts type:', typeof req.body.discounts);
|
|
console.log('Discounts first 500 chars:', req.body.discounts?.substring(0, 500));
|
|
console.log('Vouchers type:', typeof req.body.vouchers);
|
|
console.log('Vouchers first 500 chars:', req.body.vouchers?.substring(0, 500));
|
|
console.log('========================');
|
|
const {
|
|
hero,
|
|
searchBar,
|
|
filterPanel,
|
|
programs,
|
|
holidays,
|
|
locations,
|
|
camps,
|
|
discounts,
|
|
vouchers,
|
|
formSteps,
|
|
validation: validationRaw
|
|
} = req.body;
|
|
|
|
// Parse JSON strings
|
|
const errors = [];
|
|
let updateData = {};
|
|
|
|
try {
|
|
console.log('Raw discounts from req.body:', typeof discounts, discounts);
|
|
console.log('Raw vouchers from req.body:', typeof vouchers, vouchers);
|
|
|
|
const parsedDiscounts = safeParse(discounts, 'discounts');
|
|
const parsedVouchers = safeParse(vouchers, 'vouchers');
|
|
|
|
console.log('Parsed discounts:', typeof parsedDiscounts, Array.isArray(parsedDiscounts), parsedDiscounts);
|
|
console.log('Parsed vouchers:', typeof parsedVouchers, Array.isArray(parsedVouchers), parsedVouchers);
|
|
|
|
updateData = {
|
|
hero: safeParse(hero, 'hero'),
|
|
searchBar: safeParse(searchBar, 'searchBar'),
|
|
filterPanel: safeParse(filterPanel, 'filterPanel'),
|
|
programs: safeParse(programs, 'programs'),
|
|
holidays: safeParse(holidays, 'holidays'),
|
|
locations: safeParse(locations, 'locations'),
|
|
camps: safeParse(camps, 'camps'),
|
|
formSteps: safeParse(formSteps, 'formSteps'),
|
|
validation: safeParse(validationRaw, 'validation'),
|
|
configuration: {
|
|
currency: 'USD',
|
|
discounts: parsedDiscounts,
|
|
vouchers: parsedVouchers
|
|
}
|
|
};
|
|
} catch (parseError) {
|
|
console.error('booking.update: Parse error', parseError);
|
|
req.flash("error_msg", `Data processing error: ${parseError.message}`);
|
|
return req.session.save(() => res.redirect("/admin/booking"));
|
|
}
|
|
|
|
const lengthValidation = validateLengthRules(updateData, BOOKING_LENGTH_RULES);
|
|
if (!lengthValidation.valid) {
|
|
req.flash("error_msg", getBookingLengthMessage(lengthValidation));
|
|
return req.session.save(() => res.redirect("/admin/booking"));
|
|
}
|
|
|
|
// Validate data structure
|
|
const validation = validateBookingData(updateData);
|
|
if (!validation.isValid) {
|
|
console.error('booking.update: Validation failed', validation.errors);
|
|
req.flash("error_msg", `Validation failed: ${validation.errors[0]}`);
|
|
return req.session.save(() => res.redirect("/admin/booking"));
|
|
}
|
|
|
|
console.log('Final updateData keys:', Object.keys(updateData));
|
|
console.log('updateData.discounts:', updateData.discounts);
|
|
console.log('updateData.configuration:', updateData.configuration);
|
|
|
|
// CRITICAL: Remove any top-level discounts/vouchers to prevent schema conflicts
|
|
// These should ONLY exist in configuration object
|
|
delete updateData.discounts;
|
|
delete updateData.vouchers;
|
|
|
|
// Update or create booking document
|
|
let result;
|
|
try {
|
|
if (id && id !== 'undefined') {
|
|
result = await Booking.findByIdAndUpdate(
|
|
id,
|
|
{
|
|
...updateData,
|
|
$unset: { discounts: "", vouchers: "" } // Remove old top-level fields
|
|
},
|
|
{
|
|
new: true,
|
|
runValidators: false, // TẮT validator để tránh lỗi cast
|
|
strict: false // TẮT strict mode
|
|
}
|
|
);
|
|
|
|
if (!result) {
|
|
req.flash("error_msg", "Booking document not found");
|
|
return req.session.save(() => res.redirect("/admin/booking"));
|
|
}
|
|
} else {
|
|
// Upsert: update existing or create new
|
|
result = await Booking.findOneAndUpdate(
|
|
{},
|
|
{
|
|
...updateData,
|
|
$unset: { discounts: "", vouchers: "" } // Remove old top-level fields
|
|
},
|
|
{
|
|
upsert: true,
|
|
new: true,
|
|
runValidators: false, // TẮT validator
|
|
strict: false // TẮT strict mode
|
|
}
|
|
);
|
|
}
|
|
|
|
req.flash("success_msg", "Booking data updated successfully");
|
|
return req.session.save(() => res.redirect("/admin/booking"));
|
|
} catch (dbError) {
|
|
console.error("booking.update: Database error", dbError);
|
|
req.flash("error_msg", `Database error: ${dbError.message || "Unknown"}`);
|
|
return req.session.save(() => res.redirect("/admin/booking"));
|
|
}
|
|
} catch (err) {
|
|
console.error("booking.update error:", err);
|
|
req.flash("error_msg", `Update error: ${err.message || "Unknown"}`);
|
|
return req.session.save(() => res.redirect("/admin/booking"));
|
|
}
|
|
};
|
|
|
|
// Booking selection mode: 'merge' (default) = static base, DB overrides;
|
|
// 'static' = use `data/booking.json` only; 'db' = use DB only.
|
|
const getFinalBooking = (staticBooking, dbBooking) => {
|
|
const mode = (process.env.BOOKING_MODE || 'merge').toLowerCase();
|
|
if (mode === 'static') return staticBooking || dbBooking || null;
|
|
if (mode === 'db') return dbBooking || staticBooking || null;
|
|
// default: merge static (base) with DB overrides
|
|
// If both static and db present, attempt to map DB primitive lists (e.g. ["915"]) to
|
|
// full objects from the static file (e.g. {id:"915", name:..., type:...}).
|
|
const mapDbPrimitivesToObjects = (db, stat) => {
|
|
if (!db || !stat) return db;
|
|
const dbCfg = db.configuration || {};
|
|
const statCfg = stat.configuration || {};
|
|
|
|
console.log('DB discounts/vouchers:', db.discounts, db.vouchers);
|
|
console.log('DB config:', dbCfg.discounts, dbCfg.vouchers);
|
|
console.log('Static config:', statCfg.discounts, statCfg.vouchers);
|
|
|
|
// Handle legacy: if top-level discounts/vouchers exist as strings, migrate to configuration
|
|
if (Array.isArray(db.discounts) && db.discounts.length > 0 && (!dbCfg.discounts || dbCfg.discounts.length === 0)) {
|
|
const statDiscountById = {};
|
|
if (statCfg.discounts) {
|
|
statCfg.discounts.forEach(d => { if (d && d.id) statDiscountById[String(d.id)] = d; });
|
|
}
|
|
if (typeof db.discounts[0] === 'string') {
|
|
dbCfg.discounts = db.discounts.map(s => statDiscountById[String(s)] || { id: s, name: '', type: 'percentage', value: 0 });
|
|
} else {
|
|
dbCfg.discounts = db.discounts;
|
|
}
|
|
}
|
|
|
|
if (Array.isArray(db.vouchers) && db.vouchers.length > 0 && (!dbCfg.vouchers || dbCfg.vouchers.length === 0)) {
|
|
const statVouchByCode = {};
|
|
if (statCfg.vouchers) {
|
|
statCfg.vouchers.forEach(v => { if (v && v.validCodes) statVouchByCode[String(v.validCodes)] = v; });
|
|
}
|
|
if (typeof db.vouchers[0] === 'string') {
|
|
dbCfg.vouchers = db.vouchers.map(s => statVouchByCode[String(s)] || { validCodes: s, type: 'percentage', value: 0 });
|
|
} else {
|
|
dbCfg.vouchers = db.vouchers;
|
|
}
|
|
}
|
|
|
|
// If DB configuration still empty, use static data
|
|
if (Array.isArray(dbCfg.discounts) && dbCfg.discounts.length === 0 && statCfg.discounts && statCfg.discounts.length > 0) {
|
|
dbCfg.discounts = statCfg.discounts;
|
|
} else if (Array.isArray(dbCfg.discounts) && dbCfg.discounts.length > 0 && typeof dbCfg.discounts[0] === 'string') {
|
|
// Map string IDs to full objects from static
|
|
const statDiscountById = {};
|
|
if (statCfg.discounts) {
|
|
statCfg.discounts.forEach(d => { if (d && d.id) statDiscountById[String(d.id)] = d; });
|
|
}
|
|
dbCfg.discounts = dbCfg.discounts.map(s => statDiscountById[String(s)] || { id: s, name: '', type: 'percentage', value: 0 });
|
|
}
|
|
|
|
if (Array.isArray(dbCfg.vouchers) && dbCfg.vouchers.length === 0 && statCfg.vouchers && statCfg.vouchers.length > 0) {
|
|
dbCfg.vouchers = statCfg.vouchers;
|
|
} else if (Array.isArray(dbCfg.vouchers) && dbCfg.vouchers.length > 0 && typeof dbCfg.vouchers[0] === 'string') {
|
|
// Map string codes to full objects from static
|
|
const statVouchByCode = {};
|
|
if (statCfg.vouchers) {
|
|
statCfg.vouchers.forEach(v => { if (v && v.validCodes) statVouchByCode[String(v.validCodes)] = v; });
|
|
}
|
|
dbCfg.vouchers = dbCfg.vouchers.map(s => statVouchByCode[String(s)] || { validCodes: s, type: 'percentage', value: 0 });
|
|
}
|
|
|
|
return Object.assign({}, db, { configuration: dbCfg });
|
|
};
|
|
|
|
const mappedDb = mapDbPrimitivesToObjects(dbBooking, staticBooking);
|
|
const merged = staticBooking ? deepMerge(staticBooking, mappedDb || {}) : (mappedDb || null);
|
|
|
|
// Clean up: remove top-level discounts/vouchers after migrating to configuration
|
|
if (merged) {
|
|
delete merged.discounts;
|
|
delete merged.vouchers;
|
|
}
|
|
|
|
return merged;
|
|
};
|