fix: refresh YooKassa webapp payments
This commit is contained in:
@@ -586,6 +586,126 @@ async def device_topup_options_route(request: web.Request) -> web.Response:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _yookassa_payment_payload_for_processing(payload: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
normalized = dict(payload or {})
|
||||||
|
if not isinstance(normalized.get("amount"), dict):
|
||||||
|
amount_value = normalized.get("amount_value")
|
||||||
|
amount_currency = normalized.get("amount_currency")
|
||||||
|
if amount_value is not None or amount_currency:
|
||||||
|
normalized["amount"] = {
|
||||||
|
"value": str(amount_value if amount_value is not None else 0),
|
||||||
|
"currency": amount_currency or "RUB",
|
||||||
|
}
|
||||||
|
return normalized
|
||||||
|
|
||||||
|
|
||||||
|
def _payment_status_can_be_refreshed(payment: Payment) -> bool:
|
||||||
|
normalized = str(getattr(payment, "status", "") or "").lower()
|
||||||
|
if normalized == "succeeded":
|
||||||
|
return False
|
||||||
|
if normalized in {"failed", "canceled", "cancelled", "failed_creation"}:
|
||||||
|
return False
|
||||||
|
return normalized.startswith("pending") or normalized in {"waiting_for_capture", "created"}
|
||||||
|
|
||||||
|
|
||||||
|
async def _refresh_yookassa_payment_status(
|
||||||
|
request: web.Request,
|
||||||
|
session: AsyncSession,
|
||||||
|
payment: Payment,
|
||||||
|
) -> Payment:
|
||||||
|
if str(getattr(payment, "provider", "") or "").lower() != "yookassa":
|
||||||
|
return payment
|
||||||
|
if not _payment_status_can_be_refreshed(payment):
|
||||||
|
return payment
|
||||||
|
|
||||||
|
yookassa_payment_id = payment.yookassa_payment_id or payment.provider_payment_id
|
||||||
|
yookassa_service = request.app.get("yookassa_service")
|
||||||
|
if (
|
||||||
|
not yookassa_payment_id
|
||||||
|
or not yookassa_service
|
||||||
|
or not getattr(yookassa_service, "configured", False)
|
||||||
|
or not hasattr(yookassa_service, "get_payment_info")
|
||||||
|
):
|
||||||
|
return payment
|
||||||
|
|
||||||
|
try:
|
||||||
|
provider_payload = await yookassa_service.get_payment_info(yookassa_payment_id)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Failed to refresh YooKassa payment %s status", payment.payment_id)
|
||||||
|
return payment
|
||||||
|
|
||||||
|
if not provider_payload:
|
||||||
|
return payment
|
||||||
|
|
||||||
|
provider_payload = _yookassa_payment_payload_for_processing(provider_payload)
|
||||||
|
provider_status = str(provider_payload.get("status") or "").lower()
|
||||||
|
if provider_status == "succeeded" and provider_payload.get("paid") is True:
|
||||||
|
from bot.payment_providers.yookassa import (
|
||||||
|
payment_processing_lock,
|
||||||
|
process_successful_payment,
|
||||||
|
)
|
||||||
|
|
||||||
|
async with payment_processing_lock:
|
||||||
|
current = await payment_dal.get_payment_by_db_id(session, payment.payment_id)
|
||||||
|
if not current:
|
||||||
|
return payment
|
||||||
|
if current.status == "succeeded":
|
||||||
|
return current
|
||||||
|
try:
|
||||||
|
await process_successful_payment(
|
||||||
|
session,
|
||||||
|
request.app["bot"],
|
||||||
|
provider_payload,
|
||||||
|
request.app["i18n"],
|
||||||
|
request.app["settings"],
|
||||||
|
request.app["panel_service"],
|
||||||
|
request.app["subscription_service"],
|
||||||
|
request.app["referral_service"],
|
||||||
|
request.app.get("lknpd_service"),
|
||||||
|
)
|
||||||
|
await session.commit()
|
||||||
|
except Exception:
|
||||||
|
await session.rollback()
|
||||||
|
logger.exception(
|
||||||
|
"Failed to process refreshed YooKassa payment %s",
|
||||||
|
payment.payment_id,
|
||||||
|
)
|
||||||
|
return current
|
||||||
|
return await payment_dal.get_payment_by_db_id(session, payment.payment_id) or current
|
||||||
|
|
||||||
|
if provider_status in {"canceled", "cancelled"}:
|
||||||
|
from bot.payment_providers.yookassa import (
|
||||||
|
payment_processing_lock,
|
||||||
|
process_cancelled_payment,
|
||||||
|
)
|
||||||
|
|
||||||
|
async with payment_processing_lock:
|
||||||
|
current = await payment_dal.get_payment_by_db_id(session, payment.payment_id)
|
||||||
|
if not current:
|
||||||
|
return payment
|
||||||
|
if not _payment_status_can_be_refreshed(current):
|
||||||
|
return current
|
||||||
|
try:
|
||||||
|
await process_cancelled_payment(
|
||||||
|
session,
|
||||||
|
request.app["bot"],
|
||||||
|
provider_payload,
|
||||||
|
request.app["i18n"],
|
||||||
|
request.app["settings"],
|
||||||
|
)
|
||||||
|
await session.commit()
|
||||||
|
except Exception:
|
||||||
|
await session.rollback()
|
||||||
|
logger.exception(
|
||||||
|
"Failed to process refreshed cancelled YooKassa payment %s",
|
||||||
|
payment.payment_id,
|
||||||
|
)
|
||||||
|
return current
|
||||||
|
return await payment_dal.get_payment_by_db_id(session, payment.payment_id) or current
|
||||||
|
|
||||||
|
return payment
|
||||||
|
|
||||||
|
|
||||||
async def payment_status_route(request: web.Request) -> web.Response:
|
async def payment_status_route(request: web.Request) -> web.Response:
|
||||||
user_id = _require_user_id(request)
|
user_id = _require_user_id(request)
|
||||||
try:
|
try:
|
||||||
@@ -598,6 +718,7 @@ async def payment_status_route(request: web.Request) -> web.Response:
|
|||||||
payment = await payment_dal.get_payment_by_db_id(session, payment_id)
|
payment = await payment_dal.get_payment_by_db_id(session, payment_id)
|
||||||
if not payment or payment.user_id != user_id:
|
if not payment or payment.user_id != user_id:
|
||||||
return _json_error(404, "not_found", "Payment not found")
|
return _json_error(404, "not_found", "Payment not found")
|
||||||
|
payment = await _refresh_yookassa_payment_status(request, session, payment)
|
||||||
return web.json_response(
|
return web.json_response(
|
||||||
{
|
{
|
||||||
"ok": True,
|
"ok": True,
|
||||||
|
|||||||
@@ -258,6 +258,9 @@ class YooKassaService:
|
|||||||
if save_payment_method:
|
if save_payment_method:
|
||||||
# Ask YooKassa to save method for off-session charges
|
# Ask YooKassa to save method for off-session charges
|
||||||
builder.set_save_payment_method(True)
|
builder.set_save_payment_method(True)
|
||||||
|
elif not payment_method_id:
|
||||||
|
# Keep the Smart Payment form unrestricted for one-off payments.
|
||||||
|
builder.set_save_payment_method(False)
|
||||||
if payment_method_id:
|
if payment_method_id:
|
||||||
# Use a previously saved payment method for merchant-initiated payments
|
# Use a previously saved payment method for merchant-initiated payments
|
||||||
builder.set_payment_method_id(payment_method_id)
|
builder.set_payment_method_id(payment_method_id)
|
||||||
@@ -2659,10 +2662,7 @@ async def create_webapp_payment(ctx: WebAppPaymentContext) -> web.Response:
|
|||||||
description=ctx.description,
|
description=ctx.description,
|
||||||
metadata=metadata,
|
metadata=metadata,
|
||||||
receipt_email=service.config.DEFAULT_RECEIPT_EMAIL,
|
receipt_email=service.config.DEFAULT_RECEIPT_EMAIL,
|
||||||
save_payment_method=bool(
|
save_payment_method=False,
|
||||||
service.config.autopayments_active
|
|
||||||
and service.config.AUTOPAYMENTS_REQUIRE_CARD_BINDING
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
payment_url = response.get("confirmation_url") if response else None
|
payment_url = response.get("confirmation_url") if response else None
|
||||||
if not payment_url:
|
if not payment_url:
|
||||||
@@ -2802,8 +2802,8 @@ SPEC = PaymentProviderSpec(
|
|||||||
id="yookassa",
|
id="yookassa",
|
||||||
provider_key="yookassa",
|
provider_key="yookassa",
|
||||||
label="YooKassa",
|
label="YooKassa",
|
||||||
webapp_label="Банковская карта",
|
webapp_label="ЮKassa",
|
||||||
webapp_labels={"ru": "Банковская карта", "en": "Bank card"},
|
webapp_labels={"ru": "ЮKassa", "en": "YooKassa"},
|
||||||
webapp_icon="CreditCard",
|
webapp_icon="CreditCard",
|
||||||
telegram_labels={"ru": "ЮKassa", "en": "YooKassa"},
|
telegram_labels={"ru": "ЮKassa", "en": "YooKassa"},
|
||||||
telegram_emoji="💳",
|
telegram_emoji="💳",
|
||||||
|
|||||||
@@ -1080,7 +1080,7 @@
|
|||||||
function openExternalLink(url) {
|
function openExternalLink(url) {
|
||||||
if (!url) return;
|
if (!url) return;
|
||||||
if (tg?.openLink) {
|
if (tg?.openLink) {
|
||||||
tg.openLink(url);
|
tg.openLink(url, { try_instant_view: false });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
window.location.assign(url);
|
window.location.assign(url);
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ export function createBillingActions({ api }) {
|
|||||||
return api("/payments", { method: "POST", body: JSON.stringify(body) });
|
return api("/payments", { method: "POST", body: JSON.stringify(body) });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fetchPaymentStatus(paymentId) {
|
||||||
|
return api(`/payments/${encodeURIComponent(paymentId)}`);
|
||||||
|
}
|
||||||
|
|
||||||
async function postTariffChange(body) {
|
async function postTariffChange(body) {
|
||||||
return api("/tariffs/change", { method: "POST", body: JSON.stringify(body) });
|
return api("/tariffs/change", { method: "POST", body: JSON.stringify(body) });
|
||||||
}
|
}
|
||||||
@@ -79,6 +83,7 @@ export function createBillingActions({ api }) {
|
|||||||
fetchDeviceTopupOptions,
|
fetchDeviceTopupOptions,
|
||||||
fetchTariffChangeOptions,
|
fetchTariffChangeOptions,
|
||||||
postPayment,
|
postPayment,
|
||||||
|
fetchPaymentStatus,
|
||||||
postTariffChange,
|
postTariffChange,
|
||||||
postTariffChangePayment,
|
postTariffChangePayment,
|
||||||
planPaymentBody,
|
planPaymentBody,
|
||||||
|
|||||||
@@ -767,6 +767,14 @@ export async function mockApi(path, options = {}, context = {}) {
|
|||||||
payment_id: 10001,
|
payment_id: 10001,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (/^\/payments\/\d+$/.test(path) && String(options.method || "GET").toUpperCase() === "GET") {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
payment_id: Number(path.split("/").pop()),
|
||||||
|
status: "pending_yookassa",
|
||||||
|
paid: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
if (path === "/tariffs/change" && String(options.method || "").toUpperCase() === "POST") {
|
if (path === "/tariffs/change" && String(options.method || "").toUpperCase() === "POST") {
|
||||||
return { ok: true, tariff_key: "business" };
|
return { ok: true, tariff_key: "business" };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,11 @@ export function createBillingStore({ billing, loadData, t, showToast, openExtern
|
|||||||
});
|
});
|
||||||
|
|
||||||
let topupOptionsRequestId = 0;
|
let topupOptionsRequestId = 0;
|
||||||
|
let paymentPollToken = 0;
|
||||||
|
|
||||||
|
function sleep(ms) {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
function openPaymentModal(
|
function openPaymentModal(
|
||||||
tariffMode,
|
tariffMode,
|
||||||
@@ -175,6 +180,38 @@ export function createBillingStore({ billing, loadData, t, showToast, openExtern
|
|||||||
openExternalLink(url);
|
openExternalLink(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function startPaymentStatusPolling(paymentId) {
|
||||||
|
if (!paymentId || !billing.fetchPaymentStatus) return;
|
||||||
|
const token = ++paymentPollToken;
|
||||||
|
void (async () => {
|
||||||
|
for (let attempt = 0; attempt < 45 && token === paymentPollToken; attempt += 1) {
|
||||||
|
await sleep(attempt === 0 ? 1500 : 2000);
|
||||||
|
if (token !== paymentPollToken) return;
|
||||||
|
try {
|
||||||
|
const status = await billing.fetchPaymentStatus(paymentId);
|
||||||
|
if (!status?.ok) continue;
|
||||||
|
if (status.paid || status.status === "succeeded") {
|
||||||
|
showToast(t("wa_payment_success", {}, "Payment successful"));
|
||||||
|
await loadData();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const normalized = String(status.status || "").toLowerCase();
|
||||||
|
if (
|
||||||
|
normalized === "failed" ||
|
||||||
|
normalized === "canceled" ||
|
||||||
|
normalized === "cancelled" ||
|
||||||
|
normalized.startsWith("failed_")
|
||||||
|
) {
|
||||||
|
showToast(t("wa_payment_create_failed"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (_error) {
|
||||||
|
void _error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
async function createPayment() {
|
async function createPayment() {
|
||||||
const s = get(state);
|
const s = get(state);
|
||||||
if (!s.selectedPlan || !s.selectedMethod || s.payBusy) return;
|
if (!s.selectedPlan || !s.selectedMethod || s.payBusy) return;
|
||||||
@@ -195,6 +232,7 @@ export function createBillingStore({ billing, loadData, t, showToast, openExtern
|
|||||||
if (!response.payment_url) throw response;
|
if (!response.payment_url) throw response;
|
||||||
openExternalLink(response.payment_url);
|
openExternalLink(response.payment_url);
|
||||||
}
|
}
|
||||||
|
startPaymentStatusPolling(response.payment_id);
|
||||||
state.update((s) => ({ ...s, paymentModalOpen: false }));
|
state.update((s) => ({ ...s, paymentModalOpen: false }));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showToast(error?.message || t("wa_payment_create_failed"));
|
showToast(error?.message || t("wa_payment_create_failed"));
|
||||||
@@ -244,6 +282,7 @@ export function createBillingStore({ billing, loadData, t, showToast, openExtern
|
|||||||
if (!response.ok || !response.payment_url) throw response;
|
if (!response.ok || !response.payment_url) throw response;
|
||||||
showToast(t("wa_payment_created"));
|
showToast(t("wa_payment_created"));
|
||||||
openExternalLink(response.payment_url);
|
openExternalLink(response.payment_url);
|
||||||
|
startPaymentStatusPolling(response.payment_id);
|
||||||
state.update((s) => ({ ...s, topupModalOpen: false }));
|
state.update((s) => ({ ...s, topupModalOpen: false }));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showToast(error?.message || t("wa_payment_create_failed"));
|
showToast(error?.message || t("wa_payment_create_failed"));
|
||||||
@@ -321,6 +360,7 @@ export function createBillingStore({ billing, loadData, t, showToast, openExtern
|
|||||||
if (!response.ok || !response.payment_url) throw response;
|
if (!response.ok || !response.payment_url) throw response;
|
||||||
showToast(t("wa_payment_created"));
|
showToast(t("wa_payment_created"));
|
||||||
openExternalLink(response.payment_url);
|
openExternalLink(response.payment_url);
|
||||||
|
startPaymentStatusPolling(response.payment_id);
|
||||||
state.update((s) => ({ ...s, changeConfirmOpen: false, changeModalOpen: false }));
|
state.update((s) => ({ ...s, changeConfirmOpen: false, changeModalOpen: false }));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showToast(error?.message || t("wa_payment_create_failed"));
|
showToast(error?.message || t("wa_payment_create_failed"));
|
||||||
@@ -364,6 +404,7 @@ export function createBillingStore({ billing, loadData, t, showToast, openExtern
|
|||||||
if (!response.ok || !response.payment_url) throw response;
|
if (!response.ok || !response.payment_url) throw response;
|
||||||
showToast(t("wa_payment_created"));
|
showToast(t("wa_payment_created"));
|
||||||
openExternalLink(response.payment_url);
|
openExternalLink(response.payment_url);
|
||||||
|
startPaymentStatusPolling(response.payment_id);
|
||||||
state.update((s) => ({ ...s, deviceTopupModalOpen: false }));
|
state.update((s) => ({ ...s, deviceTopupModalOpen: false }));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showToast(error?.message || t("wa_payment_create_failed"));
|
showToast(error?.message || t("wa_payment_create_failed"));
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ def test_provider_presentation_ignores_cross_language_override():
|
|||||||
|
|
||||||
settings = SimpleNamespace(PAYMENT_YOOKASSA_WEBAPP_LABEL_RU="Карта")
|
settings = SimpleNamespace(PAYMENT_YOOKASSA_WEBAPP_LABEL_RU="Карта")
|
||||||
|
|
||||||
assert resolve_provider_presentation(spec, settings, language="en").webapp_label == "Bank card"
|
assert resolve_provider_presentation(spec, settings, language="en").webapp_label == "YooKassa"
|
||||||
|
|
||||||
|
|
||||||
def test_payment_method_keyboard_uses_custom_telegram_text_without_changing_callback(monkeypatch):
|
def test_payment_method_keyboard_uses_custom_telegram_text_without_changing_callback(monkeypatch):
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
from types import SimpleNamespace
|
||||||
|
from unittest import IsolatedAsyncioTestCase
|
||||||
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
|
from bot.app.web.webapp import billing as billing_module
|
||||||
|
from bot.payment_providers.base import WebAppPaymentContext
|
||||||
|
from bot.payment_providers.yookassa import create_webapp_payment
|
||||||
|
|
||||||
|
|
||||||
|
class WebAppPaymentStatusTests(IsolatedAsyncioTestCase):
|
||||||
|
async def test_yookassa_pending_payment_refresh_processes_succeeded_provider_status(self):
|
||||||
|
payment = SimpleNamespace(
|
||||||
|
payment_id=42,
|
||||||
|
user_id=1001,
|
||||||
|
provider="yookassa",
|
||||||
|
status="pending_yookassa",
|
||||||
|
yookassa_payment_id="yk_42",
|
||||||
|
provider_payment_id=None,
|
||||||
|
)
|
||||||
|
refreshed_payment = SimpleNamespace(
|
||||||
|
payment_id=42,
|
||||||
|
user_id=1001,
|
||||||
|
provider="yookassa",
|
||||||
|
status="succeeded",
|
||||||
|
yookassa_payment_id="yk_42",
|
||||||
|
provider_payment_id=None,
|
||||||
|
)
|
||||||
|
yookassa_service = SimpleNamespace(
|
||||||
|
configured=True,
|
||||||
|
get_payment_info=AsyncMock(
|
||||||
|
return_value={
|
||||||
|
"id": "yk_42",
|
||||||
|
"status": "succeeded",
|
||||||
|
"paid": True,
|
||||||
|
"amount_value": 100.0,
|
||||||
|
"amount_currency": "RUB",
|
||||||
|
"metadata": {"user_id": "1001", "payment_db_id": "42"},
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
request = SimpleNamespace(
|
||||||
|
app={
|
||||||
|
"bot": SimpleNamespace(),
|
||||||
|
"i18n": SimpleNamespace(),
|
||||||
|
"settings": SimpleNamespace(),
|
||||||
|
"panel_service": SimpleNamespace(),
|
||||||
|
"subscription_service": SimpleNamespace(),
|
||||||
|
"referral_service": SimpleNamespace(),
|
||||||
|
"lknpd_service": None,
|
||||||
|
"yookassa_service": yookassa_service,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
session = AsyncMock()
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch.object(
|
||||||
|
billing_module.payment_dal,
|
||||||
|
"get_payment_by_db_id",
|
||||||
|
AsyncMock(side_effect=[payment, refreshed_payment]),
|
||||||
|
),
|
||||||
|
patch(
|
||||||
|
"bot.payment_providers.yookassa.process_successful_payment",
|
||||||
|
AsyncMock(),
|
||||||
|
) as process_success,
|
||||||
|
):
|
||||||
|
result = await billing_module._refresh_yookassa_payment_status(
|
||||||
|
request,
|
||||||
|
session,
|
||||||
|
payment,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIs(result, refreshed_payment)
|
||||||
|
session.commit.assert_awaited_once()
|
||||||
|
process_success.assert_awaited_once()
|
||||||
|
provider_payload = process_success.await_args.args[2]
|
||||||
|
self.assertEqual(provider_payload["amount"], {"value": "100.0", "currency": "RUB"})
|
||||||
|
|
||||||
|
async def test_yookassa_webapp_payment_uses_unrestricted_checkout_form(self):
|
||||||
|
payment_record = SimpleNamespace(payment_id=77)
|
||||||
|
yookassa_service = SimpleNamespace(
|
||||||
|
configured=True,
|
||||||
|
config=SimpleNamespace(
|
||||||
|
DEFAULT_RECEIPT_EMAIL="receipt@example.test",
|
||||||
|
autopayments_active=True,
|
||||||
|
AUTOPAYMENTS_REQUIRE_CARD_BINDING=True,
|
||||||
|
),
|
||||||
|
create_payment=AsyncMock(
|
||||||
|
return_value={
|
||||||
|
"id": "yk_77",
|
||||||
|
"status": "pending",
|
||||||
|
"confirmation_url": "https://yookassa.example/pay",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
session = AsyncMock()
|
||||||
|
ctx = WebAppPaymentContext(
|
||||||
|
request=SimpleNamespace(app={"yookassa_service": yookassa_service}),
|
||||||
|
session=session,
|
||||||
|
user_id=1001,
|
||||||
|
method="yookassa",
|
||||||
|
months=1,
|
||||||
|
price=100.0,
|
||||||
|
stars_price=None,
|
||||||
|
description="Subscription",
|
||||||
|
sale_mode="subscription",
|
||||||
|
)
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch(
|
||||||
|
"bot.payment_providers.yookassa.create_webapp_payment_record",
|
||||||
|
AsyncMock(return_value=payment_record),
|
||||||
|
),
|
||||||
|
patch(
|
||||||
|
"bot.payment_providers.yookassa.payment_dal.update_payment_status_by_db_id",
|
||||||
|
AsyncMock(),
|
||||||
|
),
|
||||||
|
):
|
||||||
|
response = await create_webapp_payment(ctx)
|
||||||
|
|
||||||
|
self.assertEqual(response.status, 200)
|
||||||
|
yookassa_service.create_payment.assert_awaited_once()
|
||||||
|
self.assertIs(
|
||||||
|
yookassa_service.create_payment.await_args.kwargs["save_payment_method"],
|
||||||
|
False,
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user