feat: add multicurrency tariff payments

This commit is contained in:
3252a8
2026-05-31 22:17:28 +03:00
parent df8f2636d2
commit 80e5f0c80d
35 changed files with 1242 additions and 227 deletions
@@ -5,7 +5,7 @@
import { Plus, Save, Trash2, X } from "$components/ui/icons.js";
import { AdminButton, AdminSelect } from "$components/patterns/admin/index.js";
import { getContext } from "svelte";
import { normalizeUuidList } from "../../lib/admin/tariffDraft.js";
import { normalizeCurrencyKey, normalizeUuidList } from "../../lib/admin/tariffDraft.js";
export let at;
const tariffsStore = getContext("tariffsStore");
@@ -19,6 +19,7 @@
tariffDeleteTarget,
panelSquadsLoading,
panelSquads,
tariffsCatalog,
} = $tariffsStore);
$: billingModelOptions = [
@@ -29,6 +30,33 @@
value: squad.uuid,
label: squad.name,
}));
$: defaultCurrencyKey = normalizeCurrencyKey(tariffsCatalog?.default_currency || "rub");
$: defaultCurrencyCode = defaultCurrencyKey.toUpperCase();
$: currencyPackageLabel = at(
"tariff_btn_package_currency",
{ currency: defaultCurrencyCode },
`Пакет ${defaultCurrencyCode}`
);
$: currencyPaymentLabel = at(
"payment_default_currency",
{ currency: defaultCurrencyCode },
`Оплата ${defaultCurrencyCode}`
);
$: currencyPriceColumnLabel = at(
"tariff_col_price_currency",
{ currency: defaultCurrencyCode },
`Цена, ${defaultCurrencyCode}`
);
$: currencyPriceAriaLabel = at(
"tariff_label_price_currency",
{ currency: defaultCurrencyCode },
`Цена в ${defaultCurrencyCode}`
);
$: conversionCurrencyLabel = at(
"tariff_label_conversion_currency",
{ currency: defaultCurrencyCode },
`Курс конвертации, ${defaultCurrencyCode} за 1 GB`
);
</script>
<Dialog
@@ -241,7 +269,7 @@
</Label.Root>
{:else}
<Label.Root class="admin-field-label">
<span>{at("tariff_label_conversion", {}, "Курс конвертации, ₽ за 1 GB")}</span>
<span>{conversionCurrencyLabel}</span>
<small
>{at(
"tariff_hint_conversion",
@@ -388,7 +416,7 @@
<AdminButton
size="sm"
onclick={() => tariffsStore.addDraftRow("premiumTopupRubRows", { gb: 10, price: "" })}
><Plus size={12} /> {at("tariff_btn_package_rub", {}, "Пакет ₽")}</AdminButton
><Plus size={12} /> {currencyPackageLabel}</AdminButton
>
<AdminButton
size="sm"
@@ -400,11 +428,11 @@
</header>
<div class="admin-package-columns">
<div class="admin-row-editor">
<span class="admin-row-editor-caption">{at("payment_rub", {}, "Оплата рублями")}</span>
<span class="admin-row-editor-caption">{currencyPaymentLabel}</span>
{#if tariffDraft.premiumTopupRubRows.length}
<div class="admin-row-editor-line admin-row-editor-header">
<span>{at("tariff_col_volume_gb", {}, "Объём, GB")}</span>
<span>{at("tariff_col_price_rub", {}, "Цена, ₽")}</span>
<span>{currencyPriceColumnLabel}</span>
<span></span>
</div>
{/if}
@@ -426,7 +454,7 @@
step="0.01"
placeholder="199"
bind:value={row.price}
aria-label={at("tariff_label_price_rub", {}, "Цена premium-пакета в рублях")}
aria-label={currencyPriceAriaLabel}
/>
<AdminButton
size="sm"
@@ -526,7 +554,7 @@
<div class="admin-row-editor">
<div class="admin-row-editor-line admin-row-editor-6 admin-row-editor-header">
<span>{at("tariff_col_period_months", {}, "Срок, мес.")}</span>
<span>{at("tariff_col_price_rub", {}, "Цена, ₽")}</span>
<span>{currencyPriceColumnLabel}</span>
<span>{at("tariff_col_price_stars_full", {}, "Цена, ⭐ Stars")}</span>
<span>{at("tariff_col_ref_inviter", {}, "Бонус приглашающему")}</span>
<span>{at("tariff_col_ref_referee", {}, "Бонус приглашённому")}</span>
@@ -549,7 +577,7 @@
step="0.01"
placeholder="299"
bind:value={row.rub}
aria-label={at("tariff_label_price_rub", {}, "Цена в рублях")}
aria-label={currencyPriceAriaLabel}
/>
<Input
class="input"
@@ -608,7 +636,7 @@
<AdminButton
size="sm"
onclick={() => tariffsStore.addDraftRow("trafficRubRows", { gb: 10, price: "" })}
><Plus size={12} /> {at("tariff_btn_package_rub", {}, "Пакет ₽")}</AdminButton
><Plus size={12} /> {currencyPackageLabel}</AdminButton
>
<AdminButton
size="sm"
@@ -619,12 +647,11 @@
</header>
<div class="admin-package-columns">
<div class="admin-row-editor">
<span class="admin-row-editor-caption">{at("payment_rub", {}, "Оплата рублями")}</span
>
<span class="admin-row-editor-caption">{currencyPaymentLabel}</span>
{#if tariffDraft.trafficRubRows.length}
<div class="admin-row-editor-line admin-row-editor-header">
<span>{at("tariff_col_volume_gb", {}, "Объём, GB")}</span>
<span>{at("tariff_col_price_rub", {}, "Цена, ₽")}</span>
<span>{currencyPriceColumnLabel}</span>
<span></span>
</div>
{/if}
@@ -646,7 +673,7 @@
step="0.01"
placeholder="299"
bind:value={row.price}
aria-label={at("tariff_label_price_rub", {}, "Цена пакета в рублях")}
aria-label={currencyPriceAriaLabel}
/>
<AdminButton
size="sm"
@@ -722,7 +749,7 @@
<AdminButton
size="sm"
onclick={() => tariffsStore.addDraftRow("topupRubRows", { gb: 10, price: "" })}
><Plus size={12} /> {at("tariff_btn_package_rub", {}, "Пакет ₽")}</AdminButton
><Plus size={12} /> {currencyPackageLabel}</AdminButton
>
<AdminButton
size="sm"
@@ -733,12 +760,11 @@
</header>
<div class="admin-package-columns">
<div class="admin-row-editor">
<span class="admin-row-editor-caption">{at("payment_rub", {}, "Оплата рублями")}</span
>
<span class="admin-row-editor-caption">{currencyPaymentLabel}</span>
{#if tariffDraft.topupRubRows.length}
<div class="admin-row-editor-line admin-row-editor-header">
<span>{at("tariff_col_volume_gb", {}, "Объём, GB")}</span>
<span>{at("tariff_col_price_rub", {}, "Цена, ₽")}</span>
<span>{currencyPriceColumnLabel}</span>
<span></span>
</div>
{/if}
@@ -760,7 +786,7 @@
step="0.01"
placeholder="149"
bind:value={row.price}
aria-label={at("tariff_label_price_rub", {}, "Цена пакета в рублях")}
aria-label={currencyPriceAriaLabel}
/>
<AdminButton
size="sm"
@@ -847,7 +873,7 @@
<AdminButton
size="sm"
onclick={() => tariffsStore.addDraftRow("hwidRubRows", { count: 1, price: "" })}
><Plus size={12} /> {at("tariff_btn_package_rub", {}, "Пакет ₽")}</AdminButton
><Plus size={12} /> {currencyPackageLabel}</AdminButton
>
<AdminButton
size="sm"
@@ -858,11 +884,11 @@
</header>
<div class="admin-package-columns">
<div class="admin-row-editor">
<span class="admin-row-editor-caption">{at("payment_rub", {}, "Оплата рублями")}</span>
<span class="admin-row-editor-caption">{currencyPaymentLabel}</span>
{#if tariffDraft.hwidRubRows.length}
<div class="admin-row-editor-line admin-row-editor-header">
<span>{at("tariff_col_hwid_count", {}, "+ устройств")}</span>
<span>{at("tariff_col_price_rub", {}, "Цена, ₽")}</span>
<span>{currencyPriceColumnLabel}</span>
<span></span>
</div>
{/if}
@@ -888,7 +914,7 @@
step="0.01"
placeholder="99"
bind:value={row.price}
aria-label={at("tariff_label_price_rub", {}, "Цена пакета в рублях")}
aria-label={currencyPriceAriaLabel}
/>
<AdminButton
size="sm"
@@ -17,6 +17,7 @@
AdminSelect,
} from "$components/patterns/admin/index.js";
import { Accordion, Switch } from "$components/ui/primitives.js";
import { normalizeCurrencyKey } from "$lib/admin/tariffDraft.js";
export let at;
export let fmtMoney;
@@ -88,6 +89,7 @@
tariffsPath,
tariffsSaving,
panelSquads,
providerCurrencySupport,
panelSquadsLoading,
} = $tariffsStore);
$: ({ settingsSections, settingsDirty, settingsSaving } = $settingsStore);
@@ -111,25 +113,30 @@
let selectedTrialSquad = "";
let trialSquadSelectKey = 0;
let tariffSettingsOpen = [];
let defaultCurrencyDraft = "RUB";
function tariffName(tariff) {
return tariff?.names?.ru || tariff?.names?.en || tariff?.key || "—";
}
function tariffPriceSummary(tariff) {
const currency = normalizeCurrencyKey(tariffsCatalog.default_currency || "rub");
const currencyCode = currency.toUpperCase();
if (tariff.billing_model === "traffic") {
const rub = tariff.traffic_packages?.rub || [];
const first = rub[0];
const packages = tariff.traffic_packages?.[currency] || [];
const first = packages[0];
return first
? `${first.gb} GB ${at("at", {}, "за")} ${fmtMoney(first.price, "RUB")}`
? `${first.gb} GB ${at("at", {}, "за")} ${fmtMoney(first.price, currencyCode)}`
: at("tariff_traffic_packages", {}, "Пакеты трафика");
}
const months = [...(tariff.enabled_periods || [])].sort((a, b) => a - b);
return months
.map((month) => {
const rub = tariff.prices_rub?.[String(month)];
const rub =
(currency === "rub" ? tariff.prices_rub?.[String(month)] : undefined) ??
tariff.prices?.[currency]?.[String(month)];
const stars = tariff.prices_stars?.[String(month)];
if (rub) return `${month} ${at("months_short", {}, "мес.")} ${fmtMoney(rub, "RUB")}`;
if (rub) return `${month} ${at("months_short", {}, "мес.")} ${fmtMoney(rub, currencyCode)}`;
if (stars) return `${month} ${at("months_short", {}, "мес.")} ${stars} ⭐`;
return `${month} ${at("months_short", {}, "мес.")}`;
})
@@ -203,6 +210,27 @@
trialSquadSelectKey += 1;
}
$: catalogCurrencyKey = normalizeCurrencyKey(tariffsCatalog.default_currency || "rub");
$: defaultCurrencyDraft = catalogCurrencyKey.toUpperCase();
$: defaultCurrencyDraftKey = normalizeCurrencyKey(defaultCurrencyDraft || "rub");
$: defaultCurrencyDirty = defaultCurrencyDraftKey !== catalogCurrencyKey;
async function saveDefaultCurrency() {
await tariffsStore.setDefaultCurrency(defaultCurrencyDraft);
}
function providerCurrencyLabel(provider) {
if (provider.accepts_any_currency) return at("tariff_provider_any_currency", {}, "Любая");
return (
(provider.currencies || []).join(", ") || at("tariff_provider_not_declared", {}, "Не задано")
);
}
function providerCurrencyVariant(provider) {
if (!provider.enabled || !provider.configured) return "muted";
return provider.supports_default_currency ? "success" : "warning";
}
function removeTrialSquad(uuid) {
setCsvList(
"TRIAL_SQUAD_UUIDS",
@@ -653,6 +681,62 @@
</div>
</header>
<div class="admin-card-body">
<div class="admin-tariff-catalog-bar">
<label class="admin-field-label-compact">
<span>{at("tariff_default_currency", {}, "Валюта оплаты")}</span>
<Input
class="input admin-currency-input"
type="text"
maxlength="12"
value={defaultCurrencyDraft}
oninput={(event) => (defaultCurrencyDraft = event.currentTarget.value.toUpperCase())}
onkeydown={(event) => {
if (event.key === "Enter" && defaultCurrencyDirty) saveDefaultCurrency();
}}
/>
</label>
<AdminBadge variant="muted">{catalogCurrencyKey.toUpperCase()}</AdminBadge>
{#if defaultCurrencyDirty}
<AdminButton
size="sm"
variant="primary"
onclick={saveDefaultCurrency}
disabled={tariffsSaving}
>
<Save size={13} />
{tariffsSaving
? at("btn_saving", {}, "Сохранение...")
: at("btn_save", {}, "Сохранить")}
</AdminButton>
{/if}
</div>
{#if providerCurrencySupport?.length}
<div class="admin-provider-currency-grid">
{#each providerCurrencySupport as provider}
<div
class="admin-provider-currency"
class:is-supported={provider.supports_default_currency && provider.enabled}
class:is-unavailable={!provider.supports_default_currency || !provider.enabled}
>
<div>
<strong>{provider.label}</strong>
<small>{providerCurrencyLabel(provider)}</small>
</div>
<AdminBadge variant={providerCurrencyVariant(provider)}>
{#if !provider.enabled}
{at("disabled", {}, "Отключен")}
{:else if !provider.configured}
{at("status_not_configured", {}, "Не настроен")}
{:else if provider.supports_default_currency}
{at("tariff_currency_supported", {}, "Доступен")}
{:else}
{at("tariff_currency_unsupported", {}, "Заблокирован")}
{/if}
</AdminBadge>
</div>
{/each}
</div>
{/if}
{#if !tariffsCatalog.tariffs.length}
<AdminEmptyState>
{at(
+32 -4
View File
@@ -4,6 +4,7 @@ import {
cloneCatalog,
draftFromTariff,
tariffFromDraft as tariffFromDraftFn,
normalizeCurrencyKey,
normalizeUuidList,
} from "../tariffDraft.js";
@@ -11,6 +12,7 @@ export function createTariffsStore({ api, onTariffsSaved, flash, at }) {
const state = writable({
tariffsCatalog: {
default_tariff: "",
default_currency: "rub",
topup_packages_default: { rub: [], stars: [] },
tariffs: [],
},
@@ -23,13 +25,15 @@ export function createTariffsStore({ api, onTariffsSaved, flash, at }) {
tariffDeleteTarget: null,
tariffDraft: emptyTariffDraft(),
panelSquads: [],
providerCurrencySupport: [],
panelSquadsLoading: false,
selectedBaseSquad: "",
selectedPremiumSquad: "",
tariffEditorTab: "general",
});
const tariffFromDraft = (draft) => tariffFromDraftFn(draft);
const tariffFromDraft = (draft, defaultCurrency = "rub") =>
tariffFromDraftFn(draft, defaultCurrency);
async function loadTariffs() {
state.update((s) => ({ ...s, tariffsLoading: true }));
@@ -41,6 +45,7 @@ export function createTariffsStore({ api, onTariffsSaved, flash, at }) {
...s,
tariffsCatalog: cloneCatalog(data.catalog),
tariffsPath: data.path || "",
providerCurrencySupport: data.provider_currency_support || [],
}));
} else {
flash(data?.message || data?.error || at("load_failed", {}, "Не удалось загрузить тарифы"));
@@ -119,6 +124,7 @@ export function createTariffsStore({ api, onTariffsSaved, flash, at }) {
...s,
tariffsCatalog: cloneCatalog(res.catalog),
tariffsPath: res.path || currentPath,
providerCurrencySupport: res.provider_currency_support || s.providerCurrencySupport || [],
tariffEditorOpen: false,
tariffDeleteOpen: false,
tariffDeleteTarget: null,
@@ -139,7 +145,10 @@ export function createTariffsStore({ api, onTariffsSaved, flash, at }) {
state.update((s) => ({
...s,
tariffEditingKey: "",
tariffDraft: emptyTariffDraft(),
tariffDraft: {
...emptyTariffDraft(),
defaultCurrency: s.tariffsCatalog.default_currency || "rub",
},
tariffEditorTab: "general",
selectedBaseSquad: "",
selectedPremiumSquad: "",
@@ -151,7 +160,7 @@ export function createTariffsStore({ api, onTariffsSaved, flash, at }) {
state.update((s) => ({
...s,
tariffEditingKey: tariff.key,
tariffDraft: draftFromTariff(tariff),
tariffDraft: draftFromTariff(tariff, s.tariffsCatalog.default_currency || "rub"),
tariffEditorTab: "general",
selectedBaseSquad: "",
selectedPremiumSquad: "",
@@ -165,7 +174,7 @@ export function createTariffsStore({ api, onTariffsSaved, flash, at }) {
s = st;
return st;
});
const tariff = tariffFromDraft(s.tariffDraft);
const tariff = tariffFromDraft(s.tariffDraft, s.tariffsCatalog.default_currency || "rub");
if (!tariff.key) {
flash(at("tariff_error_key_required", {}, "Укажите ключ тарифа"));
return;
@@ -233,6 +242,24 @@ export function createTariffsStore({ api, onTariffsSaved, flash, at }) {
);
}
async function setDefaultCurrency(value) {
const currency = normalizeCurrencyKey(value || "rub");
if (!currency || currency === "stars") {
flash(at("tariff_currency_invalid", {}, "Укажите фиатную или криптовалюту, но не Stars"));
return;
}
let s;
state.update((st) => {
s = st;
return st;
});
if (currency === normalizeCurrencyKey(s.tariffsCatalog.default_currency || "rub")) return;
await persistTariffs(
{ ...cloneCatalog(s.tariffsCatalog), default_currency: currency },
at("tariff_currency_updated", {}, "Валюта оплаты обновлена")
);
}
async function deleteTariff() {
let s;
state.update((st) => {
@@ -295,6 +322,7 @@ export function createTariffsStore({ api, onTariffsSaved, flash, at }) {
saveTariffDraft,
toggleTariffEnabled,
setDefaultTariff,
setDefaultCurrency,
deleteTariff,
addDraftRow,
removeDraftRow,
+62 -16
View File
@@ -2,6 +2,7 @@ import { structuredCloneSafe } from "./format.js";
export function emptyTariffDraft() {
return {
defaultCurrency: "rub",
key: "",
nameRu: "",
nameEn: "",
@@ -40,11 +41,22 @@ export function emptyTariffDraft() {
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],
@@ -54,10 +66,13 @@ export function rowsFromPackages(packageSet, currency, valueKey) {
}));
}
export function draftFromTariff(tariff) {
export function draftFromTariff(tariff, defaultCurrency = "rub") {
const currency = normalizeCurrencyKey(defaultCurrency);
const defaultPrices = tariff.prices?.[currency] || {};
const months = new Set([
...(tariff.enabled_periods || []),
...Object.keys(tariff.prices_rub || {}).map(Number),
...Object.keys(defaultPrices).map(Number),
...(currency === "rub" ? Object.keys(tariff.prices_rub || {}).map(Number) : []),
...Object.keys(tariff.prices_stars || {}).map(Number),
]);
const periodRows = [...months]
@@ -65,7 +80,10 @@ export function draftFromTariff(tariff) {
.sort((a, b) => a - b)
.map((month) => ({
months: month,
rub: tariff.prices_rub?.[String(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)] ?? "",
@@ -73,6 +91,7 @@ export function draftFromTariff(tariff) {
return {
...emptyTariffDraft(),
defaultCurrency: currency,
key: tariff.key || "",
nameRu: tariff.names?.ru || "",
nameEn: tariff.names?.en || "",
@@ -89,13 +108,13 @@ export function draftFromTariff(tariff) {
hwid_device_limit: tariff.hwid_device_limit ?? "",
conversion_rate_rub_per_gb: tariff.conversion_rate_rub_per_gb ?? "",
periodRows: periodRows.length ? periodRows : emptyTariffDraft().periodRows,
topupRubRows: rowsFromPackages(tariff.topup_packages, "rub", "gb"),
topupRubRows: rowsFromPackages(tariff.topup_packages, currency, "gb"),
topupStarsRows: rowsFromPackages(tariff.topup_packages, "stars", "gb"),
premiumTopupRubRows: rowsFromPackages(tariff.premium_topup_packages, "rub", "gb"),
premiumTopupRubRows: rowsFromPackages(tariff.premium_topup_packages, currency, "gb"),
premiumTopupStarsRows: rowsFromPackages(tariff.premium_topup_packages, "stars", "gb"),
trafficRubRows: rowsFromPackages(tariff.traffic_packages, "rub", "gb"),
trafficRubRows: rowsFromPackages(tariff.traffic_packages, currency, "gb"),
trafficStarsRows: rowsFromPackages(tariff.traffic_packages, "stars", "gb"),
hwidRubRows: rowsFromPackages(tariff.hwid_device_packages, "rub", "count"),
hwidRubRows: rowsFromPackages(tariff.hwid_device_packages, currency, "count"),
hwidStarsRows: rowsFromPackages(tariff.hwid_device_packages, "stars", "count"),
};
}
@@ -136,10 +155,15 @@ export function packagesFromRows(rows, valueKey) {
.filter((row) => row[valueKey] > 0 && row.price !== null && row.price >= 0);
}
export function packageSetFromRows(rubRows, starsRows, valueKey) {
const rub = packagesFromRows(rubRows, valueKey);
export function packageSetFromRows(rubRows, starsRows, valueKey, defaultCurrency = "rub") {
const currency = normalizeCurrencyKey(defaultCurrency);
const defaultCurrencyPackages = packagesFromRows(rubRows, valueKey);
const stars = packagesFromRows(starsRows, valueKey);
return rub.length || stars.length ? { rub, stars } : null;
if (!defaultCurrencyPackages.length && !stars.length) return null;
return {
...(defaultCurrencyPackages.length ? { [currency]: defaultCurrencyPackages } : {}),
...(stars.length ? { stars } : {}),
};
}
export function normalizeUuidList(value) {
@@ -150,7 +174,8 @@ export function normalizeUuidList(value) {
.filter(Boolean);
}
export function tariffFromDraft(draft) {
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({
@@ -174,14 +199,20 @@ export function tariffFromDraft(draft) {
const hwidLimit = parseIntNumber(draft.hwid_device_limit);
if (hwidLimit !== null) tariff.hwid_device_limit = hwidLimit;
const hwidPackages = packageSetFromRows(draft.hwidRubRows, draft.hwidStarsRows, "count");
const hwidPackages = packageSetFromRows(
draft.hwidRubRows,
draft.hwidStarsRows,
"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.premiumTopupRubRows,
draft.premiumTopupStarsRows,
"gb"
"gb",
defaultCurrency
);
if (premiumTopupPackages) tariff.premium_topup_packages = premiumTopupPackages;
@@ -204,7 +235,12 @@ export function tariffFromDraft(draft) {
.sort((a, b) => a.months - b.months);
tariff.monthly_gb = parseNumber(draft.monthly_gb, 0);
tariff.enabled_periods = rows.map((row) => row.months);
tariff.prices_rub = Object.fromEntries(rows.map((row) => [String(row.months), row.rub || 0]));
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])
);
@@ -218,10 +254,20 @@ export function tariffFromDraft(draft) {
.filter((row) => row.referral_referee !== null)
.map((row) => [String(row.months), row.referral_referee])
);
const topupPackages = packageSetFromRows(draft.topupRubRows, draft.topupStarsRows, "gb");
const topupPackages = packageSetFromRows(
draft.topupRubRows,
draft.topupStarsRows,
"gb",
defaultCurrency
);
if (topupPackages) tariff.topup_packages = topupPackages;
} else {
const trafficPackages = packageSetFromRows(draft.trafficRubRows, draft.trafficStarsRows, "gb");
const trafficPackages = packageSetFromRows(
draft.trafficRubRows,
draft.trafficStarsRows,
"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;
+64
View File
@@ -3222,6 +3222,70 @@
flex-wrap: wrap;
}
.admin-tariff-catalog-bar {
display: flex;
align-items: end;
gap: 10px;
flex-wrap: wrap;
margin-bottom: 12px;
}
.admin-currency-input {
width: 120px;
text-transform: uppercase;
}
.admin-provider-currency-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 8px;
margin-bottom: 14px;
}
.admin-provider-currency {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
min-width: 0;
padding: 9px 10px;
border: 1px solid var(--admin-border);
border-radius: 8px;
background: var(--admin-surface-2);
}
.admin-provider-currency.is-supported {
border-color: color-mix(in srgb, var(--success-border) 70%, var(--admin-border));
}
.admin-provider-currency.is-unavailable {
opacity: 0.72;
}
.admin-provider-currency div {
display: grid;
gap: 2px;
min-width: 0;
}
.admin-provider-currency strong,
.admin-provider-currency small {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.admin-provider-currency strong {
color: var(--admin-text);
font-size: 12px;
font-weight: 700;
}
.admin-provider-currency small {
color: var(--admin-muted);
font-size: 11px;
}
.admin-tariff-settings-card {
margin-top: 14px;
}