feat: add multicurrency tariff payments
This commit is contained in:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user