fix: gate PayKilla by minimum payment amount
This commit is contained in:
@@ -14,6 +14,11 @@
|
||||
const iconName = String(method?.icon || "").trim();
|
||||
return iconName ? Icons[iconName] || null : null;
|
||||
}
|
||||
|
||||
function disabledTitle(method) {
|
||||
if (!method?.disabled || !method?.min_amount || !method?.min_currency) return "";
|
||||
return `Minimum ${method.min_amount} ${method.min_currency}`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
@@ -25,9 +30,12 @@
|
||||
{@const icon = methodIcon(method)}
|
||||
<button
|
||||
class:active={selectedMethod === method.id}
|
||||
class:disabled={method.disabled}
|
||||
class="method-card"
|
||||
disabled={method.disabled}
|
||||
title={disabledTitle(method)}
|
||||
type="button"
|
||||
onclick={() => onSelect(method.id)}
|
||||
onclick={() => !method.disabled && onSelect(method.id)}
|
||||
>
|
||||
<span class="method-card-main">
|
||||
{#if icon}
|
||||
|
||||
@@ -4271,6 +4271,57 @@
|
||||
"updated_at": null,
|
||||
"webhook_base_url_configured": false
|
||||
},
|
||||
{
|
||||
"key": "PAYKILLA_MIN_PAYMENT_AMOUNT",
|
||||
"type": "float",
|
||||
"section": "payments",
|
||||
"section_order": 4,
|
||||
"subsection": "PayKilla",
|
||||
"label": "Minimum payment amount",
|
||||
"description": "Minimum payment amount accepted through PayKilla. The value is interpreted in PAYKILLA_MIN_PAYMENT_CURRENCY and converted for tariff currencies.",
|
||||
"i18n_label_key": "admin_settings_field_paykilla_min_payment_amount_label",
|
||||
"i18n_description_key": "admin_settings_field_paykilla_min_payment_amount_description",
|
||||
"i18n_subsection_key": "admin_settings_subsection_paykilla",
|
||||
"i18n_placeholder_key": "admin_settings_field_paykilla_min_payment_amount_placeholder",
|
||||
"placeholder": "10.0",
|
||||
"optional": true,
|
||||
"secret": false,
|
||||
"min": 0,
|
||||
"provider_id": "paykilla",
|
||||
"provider_label": "PayKilla",
|
||||
"webhook_provider_id": "paykilla",
|
||||
"webhook_path": "/webhook/paykilla",
|
||||
"webhook_requires_base_url": false,
|
||||
"value": "",
|
||||
"overridden": false,
|
||||
"updated_at": null,
|
||||
"webhook_base_url_configured": false
|
||||
},
|
||||
{
|
||||
"key": "PAYKILLA_MIN_PAYMENT_CURRENCY",
|
||||
"type": "string",
|
||||
"section": "payments",
|
||||
"section_order": 4,
|
||||
"subsection": "PayKilla",
|
||||
"label": "Minimum payment currency",
|
||||
"description": "Currency for PAYKILLA_MIN_PAYMENT_AMOUNT. Default: USD.",
|
||||
"i18n_label_key": "admin_settings_field_paykilla_min_payment_currency_label",
|
||||
"i18n_description_key": "admin_settings_field_paykilla_min_payment_currency_description",
|
||||
"i18n_subsection_key": "admin_settings_subsection_paykilla",
|
||||
"i18n_placeholder_key": "admin_settings_field_paykilla_min_payment_currency_placeholder",
|
||||
"placeholder": "USD",
|
||||
"optional": true,
|
||||
"secret": false,
|
||||
"provider_id": "paykilla",
|
||||
"provider_label": "PayKilla",
|
||||
"webhook_provider_id": "paykilla",
|
||||
"webhook_path": "/webhook/paykilla",
|
||||
"webhook_requires_base_url": false,
|
||||
"value": "",
|
||||
"overridden": false,
|
||||
"updated_at": null,
|
||||
"webhook_base_url_configured": false
|
||||
},
|
||||
{
|
||||
"key": "PAYKILLA_VERIFY_WEBHOOK_SIGNATURE",
|
||||
"type": "bool",
|
||||
|
||||
@@ -59,6 +59,43 @@ export function priceLabel(plan, methodId = "") {
|
||||
return formatMoney(plan?.price || 0, plan?.currency);
|
||||
}
|
||||
|
||||
export function methodAmountForPlan(method, plan) {
|
||||
if (!method || !plan) return 0;
|
||||
if (
|
||||
String(method?.id || "")
|
||||
.toLowerCase()
|
||||
.includes("stars") &&
|
||||
Number(plan?.stars_price || 0) > 0
|
||||
) {
|
||||
return Number(plan.stars_price || 0);
|
||||
}
|
||||
return Number(plan?.price || 0);
|
||||
}
|
||||
|
||||
export function methodAvailableForPlan(method, plan) {
|
||||
if (!method || !plan) return true;
|
||||
const minimum = Number(method?.min_amount || 0);
|
||||
const minimumCurrency = String(method?.min_currency || "").toUpperCase();
|
||||
const planCurrency = String(plan?.currency || "").toUpperCase();
|
||||
if (!minimum || !minimumCurrency || minimumCurrency !== planCurrency) return true;
|
||||
return methodAmountForPlan(method, plan) >= minimum;
|
||||
}
|
||||
|
||||
export function methodsForPlan(methods, plan) {
|
||||
return (methods || []).map((method) => ({
|
||||
...method,
|
||||
disabled: !methodAvailableForPlan(method, plan),
|
||||
}));
|
||||
}
|
||||
|
||||
export function firstAvailableMethod(methods) {
|
||||
return (methods || []).find((method) => !method?.disabled)?.id || "";
|
||||
}
|
||||
|
||||
export function methodSelectable(methods, methodId) {
|
||||
return Boolean((methods || []).find((method) => method?.id === methodId && !method?.disabled));
|
||||
}
|
||||
|
||||
export function tariffLimitLabel(tariff, { t }) {
|
||||
if (!tariff) return "";
|
||||
if (String(tariff.billing_model || "") === "traffic") {
|
||||
|
||||
@@ -1037,6 +1037,19 @@ a {
|
||||
inset 0 1px 0 var(--inset-highlight);
|
||||
}
|
||||
|
||||
.method-card:disabled,
|
||||
.method-card.disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.46;
|
||||
}
|
||||
|
||||
.method-card:disabled.active,
|
||||
.method-card.disabled.active {
|
||||
border-color: var(--border);
|
||||
background: var(--surface-muted);
|
||||
box-shadow: inset 0 1px 0 var(--inset-highlight);
|
||||
}
|
||||
|
||||
.total-card {
|
||||
padding: 12px 14px;
|
||||
}
|
||||
|
||||
@@ -26,6 +26,9 @@
|
||||
planUnitHint as planUnitHintFn,
|
||||
tariffLimitLabel as tariffLimitLabelFn,
|
||||
priceLabel as priceLabelFn,
|
||||
firstAvailableMethod,
|
||||
methodSelectable,
|
||||
methodsForPlan,
|
||||
} from "../lib/webapp/tariffs.js";
|
||||
|
||||
export let createPayment = () => {};
|
||||
@@ -107,6 +110,15 @@
|
||||
function paymentPriceLabel(plan) {
|
||||
return priceLabelFn(planWithSelectedHwidRenewal(plan), selectedMethod);
|
||||
}
|
||||
$: selectedPlanForPayment = planWithSelectedHwidRenewal(selectedPlan);
|
||||
$: paymentMethods = methodsForPlan(methods, selectedPlanForPayment);
|
||||
$: paymentMethodSelected = methodSelectable(paymentMethods, selectedMethod);
|
||||
$: if (paymentModalOpen && paymentStep === "checkout" && selectedPlan) {
|
||||
const firstMethod = firstAvailableMethod(paymentMethods);
|
||||
if (firstMethod && !methodSelectable(paymentMethods, selectedMethod)) {
|
||||
selectedMethod = firstMethod;
|
||||
}
|
||||
}
|
||||
function hwidRenewalPriceLabel(plan = selectedPlan) {
|
||||
const renewal = hwidRenewalFor(plan);
|
||||
if (!renewal) return "";
|
||||
@@ -330,7 +342,7 @@
|
||||
<div class="payment-divider" aria-hidden="true"></div>
|
||||
{#if methods.length}
|
||||
<PaymentMethodGrid
|
||||
{methods}
|
||||
methods={paymentMethods}
|
||||
{selectedMethod}
|
||||
{t}
|
||||
onSelect={(id) => (selectedMethod = id)}
|
||||
@@ -341,7 +353,7 @@
|
||||
<Button
|
||||
class="wide bottom-action payment-submit-button"
|
||||
onclick={createPayment}
|
||||
disabled={!selectedPlan || !methods.length || payBusy}
|
||||
disabled={!selectedPlan || !paymentMethodSelected || payBusy}
|
||||
>
|
||||
{t("wa_pay")}
|
||||
{selectedPlan ? paymentPriceLabel(selectedPlan) : ""}
|
||||
@@ -421,7 +433,7 @@
|
||||
<div class="payment-divider" aria-hidden="true"></div>
|
||||
{#if methods.length}
|
||||
<PaymentMethodGrid
|
||||
{methods}
|
||||
methods={paymentMethods}
|
||||
{selectedMethod}
|
||||
{t}
|
||||
onSelect={(id) => (selectedMethod = id)}
|
||||
@@ -432,7 +444,7 @@
|
||||
<Button
|
||||
class="wide bottom-action payment-submit-button"
|
||||
onclick={createPayment}
|
||||
disabled={!selectedPlan || !methods.length || payBusy}
|
||||
disabled={!selectedPlan || !paymentMethodSelected || payBusy}
|
||||
>
|
||||
{t("wa_pay")}
|
||||
{selectedPlan ? paymentPriceLabel(selectedPlan) : ""}
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
planUnitHint as planUnitHintFn,
|
||||
priceLabel as priceLabelFn,
|
||||
actionKey as actionKeyFn,
|
||||
firstAvailableMethod,
|
||||
methodSelectable,
|
||||
methodsForPlan,
|
||||
} from "../lib/webapp/tariffs.js";
|
||||
import { premiumTitle as premiumTitleFn } from "../lib/webapp/traffic.js";
|
||||
import { formatCompactNumber } from "../lib/webapp/formatters.js";
|
||||
@@ -60,6 +63,31 @@
|
||||
return actionKeyFn(action);
|
||||
}
|
||||
|
||||
$: changePaymentMethods = methodsForPlan(methods, selectedChangeAction);
|
||||
$: topupPaymentMethods = methodsForPlan(methods, selectedTopupPlan);
|
||||
$: devicePaymentMethods = methodsForPlan(methods, selectedDeviceTopupPlan);
|
||||
$: changePaymentMethodSelected = methodSelectable(changePaymentMethods, selectedMethod);
|
||||
$: topupPaymentMethodSelected = methodSelectable(topupPaymentMethods, selectedMethod);
|
||||
$: devicePaymentMethodSelected = methodSelectable(devicePaymentMethods, selectedMethod);
|
||||
$: if (changeModalOpen && selectedChangeAction?.kind === "payment") {
|
||||
const firstMethod = firstAvailableMethod(changePaymentMethods);
|
||||
if (firstMethod && !methodSelectable(changePaymentMethods, selectedMethod)) {
|
||||
selectedMethod = firstMethod;
|
||||
}
|
||||
}
|
||||
$: if (topupModalOpen && selectedTopupPlan) {
|
||||
const firstMethod = firstAvailableMethod(topupPaymentMethods);
|
||||
if (firstMethod && !methodSelectable(topupPaymentMethods, selectedMethod)) {
|
||||
selectedMethod = firstMethod;
|
||||
}
|
||||
}
|
||||
$: if (deviceTopupModalOpen && selectedDeviceTopupPlan) {
|
||||
const firstMethod = firstAvailableMethod(devicePaymentMethods);
|
||||
if (firstMethod && !methodSelectable(devicePaymentMethods, selectedMethod)) {
|
||||
selectedMethod = firstMethod;
|
||||
}
|
||||
}
|
||||
|
||||
function changeActionTitle(action) {
|
||||
const mode = String(action?.mode || "");
|
||||
if (mode === "recalc_days") {
|
||||
@@ -253,7 +281,7 @@
|
||||
</div>
|
||||
{#if selectedChangeAction?.kind === "payment"}
|
||||
<PaymentMethodGrid
|
||||
{methods}
|
||||
methods={changePaymentMethods}
|
||||
{selectedMethod}
|
||||
{t}
|
||||
onSelect={(id) => (selectedMethod = id)}
|
||||
@@ -262,7 +290,9 @@
|
||||
<Button
|
||||
class="wide bottom-action payment-submit-button"
|
||||
onclick={openTariffChangeConfirm}
|
||||
disabled={tariffActionBusy || payBusy}
|
||||
disabled={tariffActionBusy ||
|
||||
payBusy ||
|
||||
(selectedChangeAction?.kind === "payment" && !changePaymentMethodSelected)}
|
||||
>
|
||||
{selectedChangeAction?.kind === "payment" ? t("wa_pay") : t("wa_apply")}
|
||||
<ArrowRight size={17} />
|
||||
@@ -293,7 +323,9 @@
|
||||
<Button
|
||||
class="wide bottom-action payment-submit-button"
|
||||
onclick={applyTariffChange}
|
||||
disabled={tariffActionBusy || payBusy}
|
||||
disabled={tariffActionBusy ||
|
||||
payBusy ||
|
||||
(selectedChangeAction?.kind === "payment" && !changePaymentMethodSelected)}
|
||||
>
|
||||
{selectedChangeAction?.kind === "payment"
|
||||
? t("wa_confirm_and_pay")
|
||||
@@ -354,11 +386,16 @@
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<PaymentMethodGrid {methods} {selectedMethod} {t} onSelect={(id) => (selectedMethod = id)} />
|
||||
<PaymentMethodGrid
|
||||
methods={topupPaymentMethods}
|
||||
{selectedMethod}
|
||||
{t}
|
||||
onSelect={(id) => (selectedMethod = id)}
|
||||
/>
|
||||
<Button
|
||||
class="wide bottom-action payment-submit-button"
|
||||
onclick={createTopupPayment}
|
||||
disabled={!selectedTopupPlan || !methods.length || payBusy}
|
||||
disabled={!selectedTopupPlan || !topupPaymentMethodSelected || payBusy}
|
||||
>
|
||||
{t("wa_buy_traffic")}
|
||||
{selectedTopupPlan ? priceLabel(selectedTopupPlan) : ""}
|
||||
@@ -413,11 +450,16 @@
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<PaymentMethodGrid {methods} {selectedMethod} {t} onSelect={(id) => (selectedMethod = id)} />
|
||||
<PaymentMethodGrid
|
||||
methods={devicePaymentMethods}
|
||||
{selectedMethod}
|
||||
{t}
|
||||
onSelect={(id) => (selectedMethod = id)}
|
||||
/>
|
||||
<Button
|
||||
class="wide bottom-action payment-submit-button"
|
||||
onclick={createDeviceTopupPayment}
|
||||
disabled={!selectedDeviceTopupPlan || !methods.length || payBusy}
|
||||
disabled={!selectedDeviceTopupPlan || !devicePaymentMethodSelected || payBusy}
|
||||
>
|
||||
{t("wa_pay")}
|
||||
{selectedDeviceTopupPlan ? priceLabel(selectedDeviceTopupPlan) : ""}
|
||||
|
||||
Reference in New Issue
Block a user