318 lines
11 KiB
JavaScript
318 lines
11 KiB
JavaScript
import { structuredCloneSafe } from "./format.js";
|
|
|
|
export function emptyTariffDraft() {
|
|
return {
|
|
defaultCurrency: "rub",
|
|
key: "",
|
|
nameRu: "",
|
|
nameEn: "",
|
|
descriptionRu: "",
|
|
descriptionEn: "",
|
|
premiumNameRu: "",
|
|
premiumNameEn: "",
|
|
squadUuids: [],
|
|
premiumSquadUuids: [],
|
|
billing_model: "period",
|
|
enabled: true,
|
|
monthly_gb: 500,
|
|
premium_monthly_gb: "",
|
|
hwid_device_limit: "",
|
|
conversion_rate_rub_per_gb: "",
|
|
periodRows: [
|
|
{ months: 1, rub: 200, stars: "", referral_inviter: 3, referral_referee: 1 },
|
|
{ months: 3, rub: 600, stars: "", referral_inviter: 7, referral_referee: 3 },
|
|
{ months: 6, rub: 1200, stars: "", referral_inviter: 15, referral_referee: 7 },
|
|
{ months: 12, rub: 2400, stars: "", referral_inviter: 30, referral_referee: 15 },
|
|
],
|
|
topupRows: [],
|
|
premiumTopupRows: [],
|
|
trafficRows: [
|
|
{ gb: 10, price: 199, stars: "" },
|
|
{ gb: 50, price: 799, stars: "" },
|
|
],
|
|
hwidRows: [],
|
|
};
|
|
}
|
|
|
|
export function cloneCatalog(catalog) {
|
|
return structuredCloneSafe({
|
|
default_tariff: catalog?.default_tariff || "",
|
|
default_currency: normalizeCurrencyKey(catalog?.default_currency || "rub"),
|
|
topup_packages_default: catalog?.topup_packages_default || { rub: [], stars: [] },
|
|
tariffs: catalog?.tariffs || [],
|
|
});
|
|
}
|
|
|
|
export function normalizeCurrencyKey(value, fallback = "rub") {
|
|
const text = String(value || "")
|
|
.trim()
|
|
.toLowerCase();
|
|
if (!text) return fallback;
|
|
if (text === "rur") return "rub";
|
|
if (["xtr", "star", "stars"].includes(text)) return "stars";
|
|
return text.replace(/[^a-z0-9_-]/g, "") || fallback;
|
|
}
|
|
|
|
export function rowsFromPackages(packageSet, currency, valueKey) {
|
|
return (packageSet?.[currency] || []).map((pkg) => ({
|
|
[valueKey]: pkg[valueKey],
|
|
price: pkg.price,
|
|
prices: pkg.prices ? structuredCloneSafe(pkg.prices) : undefined,
|
|
min_price: pkg.min_price ?? "",
|
|
}));
|
|
}
|
|
|
|
function packageValueSignature(value) {
|
|
const num = Number(value);
|
|
return Number.isFinite(num) ? String(num) : String(value || "");
|
|
}
|
|
|
|
export function packageRowsFromPackageSet(packageSet, currency, valueKey) {
|
|
const currencyRows = rowsFromPackages(packageSet, currency, valueKey);
|
|
const starsRows = rowsFromPackages(packageSet, "stars", valueKey);
|
|
const usedStars = new Set();
|
|
|
|
const rows = currencyRows.map((row) => {
|
|
const rowSignature = packageValueSignature(row[valueKey]);
|
|
const starsIndex = starsRows.findIndex(
|
|
(starsRow, index) =>
|
|
!usedStars.has(index) && packageValueSignature(starsRow[valueKey]) === rowSignature
|
|
);
|
|
const starsRow = starsIndex >= 0 ? starsRows[starsIndex] : null;
|
|
if (starsIndex >= 0) usedStars.add(starsIndex);
|
|
|
|
return {
|
|
[valueKey]: row[valueKey],
|
|
price: row.price,
|
|
stars: starsRow?.price ?? "",
|
|
prices: row.prices,
|
|
min_price: row.min_price,
|
|
stars_prices: starsRow?.prices,
|
|
stars_min_price: starsRow?.min_price ?? "",
|
|
};
|
|
});
|
|
|
|
starsRows.forEach((starsRow, index) => {
|
|
if (usedStars.has(index)) return;
|
|
rows.push({
|
|
[valueKey]: starsRow[valueKey],
|
|
price: "",
|
|
stars: starsRow.price,
|
|
stars_prices: starsRow.prices,
|
|
stars_min_price: starsRow.min_price ?? "",
|
|
});
|
|
});
|
|
|
|
return rows;
|
|
}
|
|
|
|
export function draftFromTariff(tariff, defaultCurrency = "rub") {
|
|
const currency = normalizeCurrencyKey(defaultCurrency);
|
|
const defaultPrices = tariff.prices?.[currency] || {};
|
|
// enabled_periods comes first so its order (the configured purchase order)
|
|
// is preserved; any extra price-only months are appended afterwards.
|
|
const months = new Set([
|
|
...(tariff.enabled_periods || []),
|
|
...Object.keys(defaultPrices).map(Number),
|
|
...(currency === "rub" ? Object.keys(tariff.prices_rub || {}).map(Number) : []),
|
|
...Object.keys(tariff.prices_stars || {}).map(Number),
|
|
]);
|
|
const periodRows = [...months]
|
|
.filter((month) => Number.isFinite(month) && month > 0)
|
|
.map((month) => ({
|
|
months: month,
|
|
rub:
|
|
(currency === "rub" ? tariff.prices_rub?.[String(month)] : undefined) ??
|
|
defaultPrices?.[String(month)] ??
|
|
"",
|
|
stars: tariff.prices_stars?.[String(month)] ?? "",
|
|
referral_inviter: tariff.referral_bonus_days_inviter?.[String(month)] ?? "",
|
|
referral_referee: tariff.referral_bonus_days_referee?.[String(month)] ?? "",
|
|
}));
|
|
|
|
return {
|
|
...emptyTariffDraft(),
|
|
defaultCurrency: currency,
|
|
key: tariff.key || "",
|
|
nameRu: tariff.names?.ru || "",
|
|
nameEn: tariff.names?.en || "",
|
|
descriptionRu: tariff.descriptions?.ru || "",
|
|
descriptionEn: tariff.descriptions?.en || "",
|
|
premiumNameRu: tariff.premium_names?.ru || "",
|
|
premiumNameEn: tariff.premium_names?.en || "",
|
|
squadUuids: tariff.squad_uuids || [],
|
|
premiumSquadUuids: tariff.premium_squad_uuids || [],
|
|
billing_model: tariff.billing_model || "period",
|
|
enabled: tariff.enabled !== false,
|
|
monthly_gb: tariff.monthly_gb ?? "",
|
|
premium_monthly_gb: tariff.premium_monthly_gb ?? "",
|
|
hwid_device_limit: tariff.hwid_device_limit ?? "",
|
|
conversion_rate_rub_per_gb: tariff.conversion_rate_rub_per_gb ?? "",
|
|
periodRows: periodRows.length ? periodRows : emptyTariffDraft().periodRows,
|
|
topupRows: packageRowsFromPackageSet(tariff.topup_packages, currency, "gb"),
|
|
premiumTopupRows: packageRowsFromPackageSet(tariff.premium_topup_packages, currency, "gb"),
|
|
trafficRows: packageRowsFromPackageSet(tariff.traffic_packages, currency, "gb"),
|
|
hwidRows: packageRowsFromPackageSet(tariff.hwid_device_packages, currency, "count"),
|
|
};
|
|
}
|
|
|
|
export function parseNumber(value, fallback = null) {
|
|
if (value === "" || value === null || value === undefined) return fallback;
|
|
const num = Number(value);
|
|
return Number.isFinite(num) ? num : fallback;
|
|
}
|
|
|
|
export function parseIntNumber(value, fallback = null) {
|
|
const num = parseNumber(value, fallback);
|
|
return num === null ? fallback : Math.trunc(num);
|
|
}
|
|
|
|
export function compactMap(obj) {
|
|
return Object.fromEntries(
|
|
Object.entries(obj).filter(([, value]) => value !== "" && value !== null && value !== undefined)
|
|
);
|
|
}
|
|
|
|
export function packagesFromRows(rows, valueKey) {
|
|
return (rows || [])
|
|
.map((row) => {
|
|
const pkg = {
|
|
[valueKey]: parseNumber(row[valueKey]),
|
|
price: parseNumber(row.price),
|
|
};
|
|
if (row.prices && typeof row.prices === "object") {
|
|
pkg.prices = structuredCloneSafe(row.prices);
|
|
}
|
|
const minPrice = parseNumber(row.min_price);
|
|
if (minPrice !== null) {
|
|
pkg.min_price = minPrice;
|
|
}
|
|
return pkg;
|
|
})
|
|
.filter((row) => row[valueKey] > 0 && row.price !== null && row.price >= 0);
|
|
}
|
|
|
|
export function packagesFromPackageRows(rows, valueKey, priceKey, options = {}) {
|
|
const pricesKey = options.pricesKey || "prices";
|
|
const minPriceKey = options.minPriceKey || "min_price";
|
|
return (rows || [])
|
|
.map((row) => {
|
|
const pkg = {
|
|
[valueKey]: parseNumber(row[valueKey]),
|
|
price: parseNumber(row[priceKey]),
|
|
};
|
|
if (row[pricesKey] && typeof row[pricesKey] === "object") {
|
|
pkg.prices = structuredCloneSafe(row[pricesKey]);
|
|
}
|
|
const minPrice = parseNumber(row[minPriceKey]);
|
|
if (minPrice !== null) {
|
|
pkg.min_price = minPrice;
|
|
}
|
|
return pkg;
|
|
})
|
|
.filter((row) => row[valueKey] > 0 && row.price !== null && row.price >= 0);
|
|
}
|
|
|
|
export function packageSetFromRows(rows, valueKey, defaultCurrency = "rub") {
|
|
const currency = normalizeCurrencyKey(defaultCurrency);
|
|
const defaultCurrencyPackages = packagesFromPackageRows(rows, valueKey, "price");
|
|
const stars = packagesFromPackageRows(rows, valueKey, "stars", {
|
|
pricesKey: "stars_prices",
|
|
minPriceKey: "stars_min_price",
|
|
});
|
|
if (!defaultCurrencyPackages.length && !stars.length) return null;
|
|
return {
|
|
...(defaultCurrencyPackages.length ? { [currency]: defaultCurrencyPackages } : {}),
|
|
...(stars.length ? { stars } : {}),
|
|
};
|
|
}
|
|
|
|
export function normalizeUuidList(value) {
|
|
if (Array.isArray(value)) return value.map((item) => String(item).trim()).filter(Boolean);
|
|
return String(value || "")
|
|
.split(/[\n,]+/)
|
|
.map((item) => item.trim())
|
|
.filter(Boolean);
|
|
}
|
|
|
|
export function tariffFromDraft(draft, fallbackCurrency = "rub") {
|
|
const defaultCurrency = normalizeCurrencyKey(draft.defaultCurrency || fallbackCurrency);
|
|
const key = draft.key.trim();
|
|
const names = compactMap({ ru: draft.nameRu.trim(), en: draft.nameEn.trim() });
|
|
const descriptions = compactMap({
|
|
ru: draft.descriptionRu.trim(),
|
|
en: draft.descriptionEn.trim(),
|
|
});
|
|
const premiumNames = compactMap({
|
|
ru: draft.premiumNameRu.trim(),
|
|
en: draft.premiumNameEn.trim(),
|
|
});
|
|
const tariff = {
|
|
key,
|
|
names,
|
|
descriptions,
|
|
premium_names: premiumNames,
|
|
squad_uuids: normalizeUuidList(draft.squadUuids),
|
|
premium_squad_uuids: normalizeUuidList(draft.premiumSquadUuids),
|
|
billing_model: draft.billing_model,
|
|
enabled: Boolean(draft.enabled),
|
|
};
|
|
|
|
const hwidLimit = parseIntNumber(draft.hwid_device_limit);
|
|
if (hwidLimit !== null) tariff.hwid_device_limit = hwidLimit;
|
|
const hwidPackages = packageSetFromRows(draft.hwidRows, "count", defaultCurrency);
|
|
if (hwidPackages) tariff.hwid_device_packages = hwidPackages;
|
|
const premiumMonthlyGb = parseNumber(draft.premium_monthly_gb);
|
|
if (premiumMonthlyGb !== null) tariff.premium_monthly_gb = premiumMonthlyGb;
|
|
const premiumTopupPackages = packageSetFromRows(draft.premiumTopupRows, "gb", defaultCurrency);
|
|
if (premiumTopupPackages) tariff.premium_topup_packages = premiumTopupPackages;
|
|
|
|
if (tariff.billing_model === "period") {
|
|
const seenMonths = new Set();
|
|
const rows = (draft.periodRows || [])
|
|
.map((row) => ({
|
|
months: parseIntNumber(row.months),
|
|
rub: parseNumber(row.rub, 0),
|
|
stars: parseNumber(row.stars, 0),
|
|
referral_inviter: parseIntNumber(row.referral_inviter),
|
|
referral_referee: parseIntNumber(row.referral_referee),
|
|
}))
|
|
.filter((row) => row.months > 0)
|
|
.filter((row) => {
|
|
if (seenMonths.has(row.months)) return false;
|
|
seenMonths.add(row.months);
|
|
return true;
|
|
});
|
|
tariff.monthly_gb = parseNumber(draft.monthly_gb, 0);
|
|
tariff.enabled_periods = rows.map((row) => row.months);
|
|
const defaultPrices = Object.fromEntries(rows.map((row) => [String(row.months), row.rub || 0]));
|
|
if (defaultCurrency === "rub") {
|
|
tariff.prices_rub = defaultPrices;
|
|
} else {
|
|
tariff.prices = { [defaultCurrency]: defaultPrices };
|
|
}
|
|
tariff.prices_stars = Object.fromEntries(
|
|
rows.map((row) => [String(row.months), row.stars || 0])
|
|
);
|
|
tariff.referral_bonus_days_inviter = Object.fromEntries(
|
|
rows
|
|
.filter((row) => row.referral_inviter !== null)
|
|
.map((row) => [String(row.months), row.referral_inviter])
|
|
);
|
|
tariff.referral_bonus_days_referee = Object.fromEntries(
|
|
rows
|
|
.filter((row) => row.referral_referee !== null)
|
|
.map((row) => [String(row.months), row.referral_referee])
|
|
);
|
|
const topupPackages = packageSetFromRows(draft.topupRows, "gb", defaultCurrency);
|
|
if (topupPackages) tariff.topup_packages = topupPackages;
|
|
} else {
|
|
const trafficPackages = packageSetFromRows(draft.trafficRows, "gb", defaultCurrency);
|
|
if (trafficPackages) tariff.traffic_packages = trafficPackages;
|
|
const conversion = parseNumber(draft.conversion_rate_rub_per_gb);
|
|
if (conversion !== null) tariff.conversion_rate_rub_per_gb = conversion;
|
|
}
|
|
|
|
return tariff;
|
|
}
|