fix: expire Wata payment links sooner
This commit is contained in:
@@ -63,6 +63,34 @@ router = Router(name="user_subscription_payments_wata_router")
|
|||||||
_LOG = "wata"
|
_LOG = "wata"
|
||||||
_WATA_IN_PROGRESS_STATUSES = {"created", "pending"}
|
_WATA_IN_PROGRESS_STATUSES = {"created", "pending"}
|
||||||
_WATA_LINK_OPENED_STATUSES = {"opened", "open"}
|
_WATA_LINK_OPENED_STATUSES = {"opened", "open"}
|
||||||
|
_WATA_LINK_DEFAULT_TTL_MINUTES = 15
|
||||||
|
_WATA_LINK_MIN_TTL_MINUTES = 15
|
||||||
|
_WATA_LINK_MAX_TTL_MINUTES = 30 * 24 * 60
|
||||||
|
|
||||||
|
|
||||||
|
def _clamp_wata_link_ttl_minutes(value: Any, *, default: int) -> int:
|
||||||
|
if isinstance(value, str):
|
||||||
|
value = value.strip()
|
||||||
|
try:
|
||||||
|
minutes = int(value)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return default
|
||||||
|
return min(_WATA_LINK_MAX_TTL_MINUTES, max(_WATA_LINK_MIN_TTL_MINUTES, minutes))
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_wata_datetime(raw: Any) -> Optional[datetime]:
|
||||||
|
if not raw:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
iso_value = str(raw).strip()
|
||||||
|
if iso_value.endswith("Z"):
|
||||||
|
iso_value = iso_value[:-1] + "+00:00"
|
||||||
|
parsed = datetime.fromisoformat(iso_value)
|
||||||
|
if parsed.tzinfo is None:
|
||||||
|
return parsed.replace(tzinfo=timezone.utc)
|
||||||
|
return parsed.astimezone(timezone.utc)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _wata_success_status(status: int, _body: Any) -> bool:
|
def _wata_success_status(status: int, _body: Any) -> bool:
|
||||||
@@ -105,21 +133,15 @@ class WataConfig(ProviderEnvConfig):
|
|||||||
BASE_URL: str = Field(default="https://api.wata.pro/api/h2h")
|
BASE_URL: str = Field(default="https://api.wata.pro/api/h2h")
|
||||||
RETURN_URL: Optional[str] = None
|
RETURN_URL: Optional[str] = None
|
||||||
FAILED_URL: Optional[str] = None
|
FAILED_URL: Optional[str] = None
|
||||||
PAYMENT_LINK_TTL_DAYS: int = Field(default=3)
|
LINK_TTL_MINUTES: int = Field(default=_WATA_LINK_DEFAULT_TTL_MINUTES)
|
||||||
WEBHOOK_VERIFY_SIGNATURE: bool = Field(default=True)
|
WEBHOOK_VERIFY_SIGNATURE: bool = Field(default=True)
|
||||||
PUBLIC_KEY: Optional[str] = None
|
PUBLIC_KEY: Optional[str] = None
|
||||||
TRUSTED_IPS: str = Field(default="62.84.126.140,51.250.106.150")
|
TRUSTED_IPS: str = Field(default="62.84.126.140,51.250.106.150")
|
||||||
|
|
||||||
@field_validator("PAYMENT_LINK_TTL_DAYS", mode="before")
|
@field_validator("LINK_TTL_MINUTES", mode="before")
|
||||||
@classmethod
|
@classmethod
|
||||||
def _clamp_ttl(cls, v):
|
def _clamp_link_ttl_minutes(cls, v):
|
||||||
if isinstance(v, str):
|
return _clamp_wata_link_ttl_minutes(v, default=_WATA_LINK_DEFAULT_TTL_MINUTES)
|
||||||
v = v.strip()
|
|
||||||
try:
|
|
||||||
value = int(v)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
return 3
|
|
||||||
return min(30, max(1, value))
|
|
||||||
|
|
||||||
@field_validator("API_TOKEN", "RETURN_URL", "FAILED_URL", "PUBLIC_KEY", mode="before")
|
@field_validator("API_TOKEN", "RETURN_URL", "FAILED_URL", "PUBLIC_KEY", mode="before")
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -201,8 +223,8 @@ class WataService(HttpClientMixin):
|
|||||||
return self.config.FAILED_URL or self.return_url
|
return self.config.FAILED_URL or self.return_url
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def payment_link_ttl_days(self) -> int:
|
def payment_link_ttl_minutes(self) -> int:
|
||||||
return self.config.PAYMENT_LINK_TTL_DAYS
|
return self.config.LINK_TTL_MINUTES
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def verify_webhook_signature(self) -> bool:
|
def verify_webhook_signature(self) -> bool:
|
||||||
@@ -236,7 +258,7 @@ class WataService(HttpClientMixin):
|
|||||||
|
|
||||||
session = await self._get_session()
|
session = await self._get_session()
|
||||||
expires_at = (
|
expires_at = (
|
||||||
datetime.now(timezone.utc) + timedelta(days=self.payment_link_ttl_days)
|
datetime.now(timezone.utc) + timedelta(minutes=self.payment_link_ttl_minutes)
|
||||||
).replace(microsecond=0)
|
).replace(microsecond=0)
|
||||||
body: Dict[str, Any] = {
|
body: Dict[str, Any] = {
|
||||||
"amount": float(format_decimal_amount(amount)),
|
"amount": float(format_decimal_amount(amount)),
|
||||||
@@ -327,20 +349,15 @@ class WataService(HttpClientMixin):
|
|||||||
|
|
||||||
expiration_raw = data.get("expirationDateTime") or data.get("expiration_date_time")
|
expiration_raw = data.get("expirationDateTime") or data.get("expiration_date_time")
|
||||||
if expiration_raw:
|
if expiration_raw:
|
||||||
try:
|
exp_dt = _parse_wata_datetime(expiration_raw)
|
||||||
iso_value = str(expiration_raw).strip()
|
if exp_dt is None:
|
||||||
if iso_value.endswith("Z"):
|
|
||||||
iso_value = iso_value[:-1] + "+00:00"
|
|
||||||
exp_dt = datetime.fromisoformat(iso_value)
|
|
||||||
if exp_dt.tzinfo is None:
|
|
||||||
exp_dt = exp_dt.replace(tzinfo=timezone.utc)
|
|
||||||
if exp_dt <= datetime.now(timezone.utc):
|
|
||||||
return None
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
logging.warning(
|
logging.warning(
|
||||||
"Wata try_reuse_pending_link: unparseable expirationDateTime %r",
|
"Wata try_reuse_pending_link: unparseable expirationDateTime %r",
|
||||||
expiration_raw,
|
expiration_raw,
|
||||||
)
|
)
|
||||||
|
return None
|
||||||
|
if exp_dt <= datetime.now(timezone.utc):
|
||||||
|
return None
|
||||||
|
|
||||||
return first_value(data, "url", "paymentUrl", "payment_url")
|
return first_value(data, "url", "paymentUrl", "payment_url")
|
||||||
|
|
||||||
@@ -579,6 +596,72 @@ class WataService(HttpClientMixin):
|
|||||||
)
|
)
|
||||||
return await payment_dal.get_payment_by_db_id(session, payment.payment_id) or payment
|
return await payment_dal.get_payment_by_db_id(session, payment.payment_id) or payment
|
||||||
|
|
||||||
|
def _local_payment_link_ttl_expired(self, payment: Any) -> bool:
|
||||||
|
created_at = getattr(payment, "created_at", None)
|
||||||
|
if isinstance(created_at, datetime):
|
||||||
|
created_dt = (
|
||||||
|
created_at.replace(tzinfo=timezone.utc)
|
||||||
|
if created_at.tzinfo is None
|
||||||
|
else created_at.astimezone(timezone.utc)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
created_dt = _parse_wata_datetime(created_at)
|
||||||
|
if created_dt is None:
|
||||||
|
return False
|
||||||
|
expires_at = created_dt + timedelta(minutes=self.payment_link_ttl_minutes)
|
||||||
|
return expires_at <= datetime.now(timezone.utc)
|
||||||
|
|
||||||
|
async def _expired_link_payload_for_payment(self, payment: Any) -> Optional[Mapping[str, Any]]:
|
||||||
|
provider_payment_id = str(getattr(payment, "provider_payment_id", "") or "").strip()
|
||||||
|
if not provider_payment_id:
|
||||||
|
return None
|
||||||
|
|
||||||
|
success, data = await self.get_payment_link(provider_payment_id)
|
||||||
|
if not success or not isinstance(data, dict):
|
||||||
|
status_code = data.get("status") if isinstance(data, dict) else None
|
||||||
|
if status_code == 404 and self._local_payment_link_ttl_expired(payment):
|
||||||
|
return {"id": provider_payment_id}
|
||||||
|
return None
|
||||||
|
|
||||||
|
expiration_raw = data.get("expirationDateTime") or data.get("expiration_date_time")
|
||||||
|
expiration_dt = _parse_wata_datetime(expiration_raw)
|
||||||
|
if expiration_dt is None:
|
||||||
|
return None
|
||||||
|
if expiration_dt > datetime.now(timezone.utc):
|
||||||
|
return None
|
||||||
|
return data
|
||||||
|
|
||||||
|
async def _mark_expired_link(
|
||||||
|
self,
|
||||||
|
session: AsyncSession,
|
||||||
|
payment: Any,
|
||||||
|
payload: Mapping[str, Any],
|
||||||
|
*,
|
||||||
|
log_prefix: str,
|
||||||
|
) -> Optional[Any]:
|
||||||
|
provider_payment_id = (
|
||||||
|
first_value(payload, "id", "paymentLinkId", "payment_link_id")
|
||||||
|
or getattr(payment, "provider_payment_id", None)
|
||||||
|
or str(payment.payment_id)
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
await payment_dal.update_provider_payment_and_status(
|
||||||
|
session,
|
||||||
|
payment.payment_id,
|
||||||
|
str(provider_payment_id),
|
||||||
|
"canceled",
|
||||||
|
)
|
||||||
|
await session.commit()
|
||||||
|
except Exception:
|
||||||
|
await session.rollback()
|
||||||
|
logging.exception(
|
||||||
|
"%s: failed to mark expired payment link %s as canceled.",
|
||||||
|
log_prefix,
|
||||||
|
provider_payment_id,
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
return await payment_dal.get_payment_by_db_id(session, payment.payment_id) or payment
|
||||||
|
|
||||||
async def refresh_payment_status(self, session: AsyncSession, payment: Any) -> Any:
|
async def refresh_payment_status(self, session: AsyncSession, payment: Any) -> Any:
|
||||||
if str(getattr(payment, "provider", "") or "").lower() != "wata":
|
if str(getattr(payment, "provider", "") or "").lower() != "wata":
|
||||||
return payment
|
return payment
|
||||||
@@ -615,6 +698,16 @@ class WataService(HttpClientMixin):
|
|||||||
)
|
)
|
||||||
return refreshed or payment
|
return refreshed or payment
|
||||||
|
|
||||||
|
expired_link_payload = await self._expired_link_payload_for_payment(payment)
|
||||||
|
if expired_link_payload:
|
||||||
|
refreshed = await self._mark_expired_link(
|
||||||
|
session,
|
||||||
|
payment,
|
||||||
|
expired_link_payload,
|
||||||
|
log_prefix="Wata status refresh",
|
||||||
|
)
|
||||||
|
return refreshed or payment
|
||||||
|
|
||||||
return payment
|
return payment
|
||||||
|
|
||||||
async def webhook_route(self, request: web.Request) -> web.Response:
|
async def webhook_route(self, request: web.Request) -> web.Response:
|
||||||
@@ -769,9 +862,7 @@ async def pay_wata_callback_handler(
|
|||||||
|
|
||||||
reuse_amounts = payment_record_amounts(months=parts.months, sale_mode=parts.sale_mode)
|
reuse_amounts = payment_record_amounts(months=parts.months, sale_mode=parts.sale_mode)
|
||||||
months_for_lookup = (
|
months_for_lookup = (
|
||||||
reuse_amounts.months
|
reuse_amounts.months if sale_mode_base(parts.sale_mode) == "subscription" else None
|
||||||
if sale_mode_base(parts.sale_mode) == "subscription"
|
|
||||||
else None
|
|
||||||
)
|
)
|
||||||
reusable_payment = await payment_dal.find_recent_pending_provider_payment(
|
reusable_payment = await payment_dal.find_recent_pending_provider_payment(
|
||||||
session,
|
session,
|
||||||
@@ -784,7 +875,7 @@ async def pay_wata_callback_handler(
|
|||||||
purchased_gb=reuse_amounts.purchased_gb,
|
purchased_gb=reuse_amounts.purchased_gb,
|
||||||
purchased_hwid_devices=reuse_amounts.purchased_hwid_devices,
|
purchased_hwid_devices=reuse_amounts.purchased_hwid_devices,
|
||||||
tariff_key=reuse_amounts.tariff_key,
|
tariff_key=reuse_amounts.tariff_key,
|
||||||
since_minutes=wata_service.payment_link_ttl_days * 24 * 60,
|
since_minutes=wata_service.payment_link_ttl_minutes,
|
||||||
)
|
)
|
||||||
if reusable_payment is not None:
|
if reusable_payment is not None:
|
||||||
reusable_url = await wata_service.try_reuse_pending_link(reusable_payment)
|
reusable_url = await wata_service.try_reuse_pending_link(reusable_payment)
|
||||||
@@ -860,9 +951,7 @@ async def create_webapp_payment(ctx: WebAppPaymentContext) -> web.Response:
|
|||||||
traffic_gb=ctx.traffic_gb,
|
traffic_gb=ctx.traffic_gb,
|
||||||
)
|
)
|
||||||
months_for_lookup = (
|
months_for_lookup = (
|
||||||
reuse_amounts.months
|
reuse_amounts.months if sale_mode_base(ctx.sale_mode) == "subscription" else None
|
||||||
if sale_mode_base(ctx.sale_mode) == "subscription"
|
|
||||||
else None
|
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
reusable_payment = await payment_dal.find_recent_pending_provider_payment(
|
reusable_payment = await payment_dal.find_recent_pending_provider_payment(
|
||||||
@@ -876,7 +965,7 @@ async def create_webapp_payment(ctx: WebAppPaymentContext) -> web.Response:
|
|||||||
purchased_gb=reuse_amounts.purchased_gb,
|
purchased_gb=reuse_amounts.purchased_gb,
|
||||||
purchased_hwid_devices=reuse_amounts.purchased_hwid_devices,
|
purchased_hwid_devices=reuse_amounts.purchased_hwid_devices,
|
||||||
tariff_key=reuse_amounts.tariff_key,
|
tariff_key=reuse_amounts.tariff_key,
|
||||||
since_minutes=service.payment_link_ttl_days * 24 * 60,
|
since_minutes=service.payment_link_ttl_minutes,
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
logging.exception("Wata WebApp: lookup of reusable payment failed")
|
logging.exception("Wata WebApp: lookup of reusable payment failed")
|
||||||
@@ -1028,14 +1117,17 @@ _CONFIG_MANIFEST = (
|
|||||||
"WATA_FAILED_URL", "url", "Failed URL", subsection="Wata", attr="FAILED_URL"
|
"WATA_FAILED_URL", "url", "Failed URL", subsection="Wata", attr="FAILED_URL"
|
||||||
),
|
),
|
||||||
ProviderManifestField(
|
ProviderManifestField(
|
||||||
"WATA_PAYMENT_LINK_TTL_DAYS",
|
"WATA_LINK_TTL_MINUTES",
|
||||||
"int",
|
"int",
|
||||||
"Payment link lifetime (days)",
|
"Payment link lifetime (minutes)",
|
||||||
description="1..30; Wata defaults to 3 days and allows up to 30 days.",
|
description=(
|
||||||
|
"15..43200; default 15 minutes. Wata requires more than 10 minutes "
|
||||||
|
"and allows up to 30 days."
|
||||||
|
),
|
||||||
subsection="Wata",
|
subsection="Wata",
|
||||||
min=1,
|
min=_WATA_LINK_MIN_TTL_MINUTES,
|
||||||
max=30,
|
max=_WATA_LINK_MAX_TTL_MINUTES,
|
||||||
attr="PAYMENT_LINK_TTL_DAYS",
|
attr="LINK_TTL_MINUTES",
|
||||||
),
|
),
|
||||||
ProviderManifestField(
|
ProviderManifestField(
|
||||||
"WATA_WEBHOOK_VERIFY_SIGNATURE",
|
"WATA_WEBHOOK_VERIFY_SIGNATURE",
|
||||||
|
|||||||
+1
-1
@@ -298,7 +298,7 @@ PAYMENT_HELEKET_TELEGRAM_EMOJI
|
|||||||
| `WATA_API_TOKEN` | Bearer token. |
|
| `WATA_API_TOKEN` | Bearer token. |
|
||||||
| `WATA_RETURN_URL` | URL успешного возврата. |
|
| `WATA_RETURN_URL` | URL успешного возврата. |
|
||||||
| `WATA_FAILED_URL` | URL неуспешного возврата. |
|
| `WATA_FAILED_URL` | URL неуспешного возврата. |
|
||||||
| `WATA_PAYMENT_LINK_TTL_DAYS` | TTL платежной ссылки в днях. |
|
| `WATA_LINK_TTL_MINUTES` | TTL платежной ссылки в минутах (по умолчанию 15, минимум 15, максимум 43200). |
|
||||||
| `WATA_WEBHOOK_VERIFY_SIGNATURE` | Проверять `X-Signature`. |
|
| `WATA_WEBHOOK_VERIFY_SIGNATURE` | Проверять `X-Signature`. |
|
||||||
| `WATA_PUBLIC_KEY` | Cached public key; если пусто, загружается из API. |
|
| `WATA_PUBLIC_KEY` | Cached public key; если пусто, загружается из API. |
|
||||||
| `WATA_TRUSTED_IPS` | IP-allowlist webhook-источников. |
|
| `WATA_TRUSTED_IPS` | IP-allowlist webhook-источников. |
|
||||||
|
|||||||
+2
-2
@@ -1523,8 +1523,8 @@
|
|||||||
"admin_settings_field_wata_base_url_label": "Base URL",
|
"admin_settings_field_wata_base_url_label": "Base URL",
|
||||||
"admin_settings_field_wata_return_url_label": "Return URL",
|
"admin_settings_field_wata_return_url_label": "Return URL",
|
||||||
"admin_settings_field_wata_failed_url_label": "Failed URL",
|
"admin_settings_field_wata_failed_url_label": "Failed URL",
|
||||||
"admin_settings_field_wata_payment_link_ttl_days_label": "Payment link lifetime (days)",
|
"admin_settings_field_wata_link_ttl_minutes_label": "Payment link lifetime (minutes)",
|
||||||
"admin_settings_field_wata_payment_link_ttl_days_description": "1..30; Wata defaults to 3 days and allows up to 30 days.",
|
"admin_settings_field_wata_link_ttl_minutes_description": "15..43200; default 15 minutes. Wata requires more than 10 minutes and allows up to 30 days.",
|
||||||
"admin_settings_field_wata_webhook_verify_signature_label": "Verify webhook signature",
|
"admin_settings_field_wata_webhook_verify_signature_label": "Verify webhook signature",
|
||||||
"admin_settings_field_wata_public_key_label": "Webhook public key",
|
"admin_settings_field_wata_public_key_label": "Webhook public key",
|
||||||
"admin_settings_field_wata_public_key_description": "Optional. If empty, the backend fetches it from Wata.",
|
"admin_settings_field_wata_public_key_description": "Optional. If empty, the backend fetches it from Wata.",
|
||||||
|
|||||||
+2
-2
@@ -1523,8 +1523,8 @@
|
|||||||
"admin_settings_field_wata_base_url_label": "Base URL",
|
"admin_settings_field_wata_base_url_label": "Base URL",
|
||||||
"admin_settings_field_wata_return_url_label": "Return URL",
|
"admin_settings_field_wata_return_url_label": "Return URL",
|
||||||
"admin_settings_field_wata_failed_url_label": "Failed URL",
|
"admin_settings_field_wata_failed_url_label": "Failed URL",
|
||||||
"admin_settings_field_wata_payment_link_ttl_days_label": "Срок жизни ссылки (дней)",
|
"admin_settings_field_wata_link_ttl_minutes_label": "Срок жизни ссылки (мин)",
|
||||||
"admin_settings_field_wata_payment_link_ttl_days_description": "1..30; по умолчанию Wata использует 3 дня.",
|
"admin_settings_field_wata_link_ttl_minutes_description": "15..43200; по умолчанию 15 минут. Wata требует больше 10 минут и допускает до 30 дней.",
|
||||||
"admin_settings_field_wata_webhook_verify_signature_label": "Проверять подпись вебхука",
|
"admin_settings_field_wata_webhook_verify_signature_label": "Проверять подпись вебхука",
|
||||||
"admin_settings_field_wata_public_key_label": "Публичный ключ вебхука",
|
"admin_settings_field_wata_public_key_label": "Публичный ключ вебхука",
|
||||||
"admin_settings_field_wata_public_key_description": "Необязательно. Если пусто, бэкенд получит ключ из Wata.",
|
"admin_settings_field_wata_public_key_description": "Необязательно. Если пусто, бэкенд получит ключ из Wata.",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
|
|
||||||
from bot.payment_providers import wata
|
from bot.payment_providers import wata
|
||||||
@@ -48,27 +49,30 @@ def _payment(**overrides):
|
|||||||
"subscription_duration_months": 1,
|
"subscription_duration_months": 1,
|
||||||
"sale_mode": "subscription",
|
"sale_mode": "subscription",
|
||||||
"user": None,
|
"user": None,
|
||||||
|
"created_at": datetime.now(timezone.utc),
|
||||||
}
|
}
|
||||||
values.update(overrides)
|
values.update(overrides)
|
||||||
return SimpleNamespace(**values)
|
return SimpleNamespace(**values)
|
||||||
|
|
||||||
|
|
||||||
def _service(session):
|
def _service(session, **config_overrides):
|
||||||
settings = SimpleNamespace(
|
settings = SimpleNamespace(
|
||||||
DEFAULT_CURRENCY_SYMBOL="RUB",
|
DEFAULT_CURRENCY_SYMBOL="RUB",
|
||||||
DEFAULT_LANGUAGE="ru",
|
DEFAULT_LANGUAGE="ru",
|
||||||
traffic_sale_mode=False,
|
traffic_sale_mode=False,
|
||||||
trusted_proxies=[],
|
trusted_proxies=[],
|
||||||
)
|
)
|
||||||
|
config_values = {
|
||||||
|
"ENABLED": True,
|
||||||
|
"API_TOKEN": "token",
|
||||||
|
"WEBHOOK_VERIFY_SIGNATURE": False,
|
||||||
|
"TRUSTED_IPS": "",
|
||||||
|
}
|
||||||
|
config_values.update(config_overrides)
|
||||||
return WataService(
|
return WataService(
|
||||||
bot=SimpleNamespace(),
|
bot=SimpleNamespace(),
|
||||||
settings=settings,
|
settings=settings,
|
||||||
config=WataConfig(
|
config=WataConfig(**config_values),
|
||||||
ENABLED=True,
|
|
||||||
API_TOKEN="token",
|
|
||||||
WEBHOOK_VERIFY_SIGNATURE=False,
|
|
||||||
TRUSTED_IPS="",
|
|
||||||
),
|
|
||||||
i18n=SimpleNamespace(),
|
i18n=SimpleNamespace(),
|
||||||
async_session_factory=session,
|
async_session_factory=session,
|
||||||
subscription_service=SimpleNamespace(),
|
subscription_service=SimpleNamespace(),
|
||||||
@@ -378,3 +382,69 @@ def test_create_payment_link_uses_clean_iso_expiration_without_microseconds(monk
|
|||||||
assert expiration.endswith("Z"), expiration
|
assert expiration.endswith("Z"), expiration
|
||||||
assert "." not in expiration, expiration
|
assert "." not in expiration, expiration
|
||||||
assert "+" not in expiration, expiration
|
assert "+" not in expiration, expiration
|
||||||
|
expiration_dt = wata._parse_wata_datetime(expiration)
|
||||||
|
assert expiration_dt is not None
|
||||||
|
ttl_delta = expiration_dt - datetime.now(timezone.utc)
|
||||||
|
assert timedelta(minutes=14, seconds=30) <= ttl_delta <= timedelta(minutes=15, seconds=30)
|
||||||
|
|
||||||
|
|
||||||
|
def test_wata_link_ttl_uses_minutes_with_default_and_minimum():
|
||||||
|
default_config = WataConfig(ENABLED=True, API_TOKEN="token")
|
||||||
|
assert default_config.LINK_TTL_MINUTES == 15
|
||||||
|
|
||||||
|
too_short_config = WataConfig(ENABLED=True, API_TOKEN="token", LINK_TTL_MINUTES=10)
|
||||||
|
assert too_short_config.LINK_TTL_MINUTES == 15
|
||||||
|
|
||||||
|
custom_config = WataConfig(ENABLED=True, API_TOKEN="token", LINK_TTL_MINUTES=45)
|
||||||
|
assert custom_config.LINK_TTL_MINUTES == 45
|
||||||
|
|
||||||
|
|
||||||
|
def test_refresh_marks_expired_wata_link_as_canceled(monkeypatch):
|
||||||
|
session = _FakeSession()
|
||||||
|
service = _service(session)
|
||||||
|
payment = _payment(
|
||||||
|
provider="wata",
|
||||||
|
provider_payment_id="link-id",
|
||||||
|
created_at=datetime.now(timezone.utc) - timedelta(minutes=16),
|
||||||
|
)
|
||||||
|
updates = []
|
||||||
|
|
||||||
|
async def find_transaction_for_payment(_payment, *, status):
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def get_payment_link(payment_link_id):
|
||||||
|
assert payment_link_id == "link-id"
|
||||||
|
return True, {
|
||||||
|
"id": "link-id",
|
||||||
|
"status": "Opened",
|
||||||
|
"expirationDateTime": "2000-01-01T00:00:00Z",
|
||||||
|
}
|
||||||
|
|
||||||
|
async def update_provider_payment_and_status(
|
||||||
|
_session,
|
||||||
|
payment_id,
|
||||||
|
provider_payment_id,
|
||||||
|
status,
|
||||||
|
):
|
||||||
|
updates.append((payment_id, provider_payment_id, status))
|
||||||
|
payment.provider_payment_id = provider_payment_id
|
||||||
|
payment.status = status
|
||||||
|
|
||||||
|
async def get_payment_by_db_id(_session, payment_id):
|
||||||
|
assert payment_id == payment.payment_id
|
||||||
|
return payment
|
||||||
|
|
||||||
|
service._find_transaction_for_payment = find_transaction_for_payment
|
||||||
|
service.get_payment_link = get_payment_link
|
||||||
|
monkeypatch.setattr(
|
||||||
|
wata.payment_dal,
|
||||||
|
"update_provider_payment_and_status",
|
||||||
|
update_provider_payment_and_status,
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(wata.payment_dal, "get_payment_by_db_id", get_payment_by_db_id)
|
||||||
|
|
||||||
|
result = asyncio.run(service.refresh_payment_status(session, payment))
|
||||||
|
|
||||||
|
assert result is payment
|
||||||
|
assert updates == [(465, "link-id", "canceled")]
|
||||||
|
assert session.commits == 1
|
||||||
|
|||||||
Reference in New Issue
Block a user