Payment providers as classes (Strategy + factory); 10342 = monthly; health logs a failed Bazaar check

- payment/providers.py: ZarinpalProvider / BazaarProvider with one interface (check_plan, sku_for, payment_url,
  purchase_extras, discount_blocker, reconcile_user); provider_for / provider_for_method pick one; unknown fails.
  create_order, purchase view, discount redeem and reconcile_user_orders use it instead of scattered ifs.
- subscription.0015: Bazaar SKU 10342 on the one-month plan (owner: "probably the monthly one").
- config/health: the Bazaar check no longer swallows its exception without a log.
982 tests green.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 18:21:45 +03:30
parent 76b0ef976e
commit c0fa46be44
11 changed files with 246 additions and 66 deletions

View File

@@ -18,6 +18,7 @@ from rest_framework.exceptions import APIException
from statuslog.signals import status_changed
from .models import Order, Transaction
from .providers import PROVIDERS, provider_for
logger = logging.getLogger(__name__)
@@ -102,7 +103,7 @@ def create_order(*, user_id: int, amount: int, currency: str = 'IRT',
existing, user_id=user_id, amount=amount, currency=currency,
subscription_id=subscription_id, plan_id=plan_id, provider=provider,
)
payment_url = None if existing.provider == 'bazaar' else call_payment_gateway(existing)
payment_url = provider_for(existing.provider).payment_url(existing)
return existing, payment_url
from subscription.models import Plan
@@ -136,9 +137,8 @@ def create_order(*, user_id: int, amount: int, currency: str = 'IRT',
subscription_id=subscription_id, plan_id=plan_id, provider=provider,
)
# برای provider='bazaar' هیچ paymentUrlی وجود ندارد: کلاینت اندروید مستقیماً
# با SDK بازار خرید را انجام می‌دهد و purchaseToken را به verify می‌فرستد.
payment_url = None if order.provider == 'bazaar' else call_payment_gateway(order)
# بازار paymentUrl ندارد (اپ با Poolakey می‌خرد)؛ زرین‌پال آدرسِ PaymentRedirectView را دارد (providers.py).
payment_url = provider_for(order.provider).payment_url(order)
return order, payment_url
@@ -282,13 +282,9 @@ def reconcile_pending_order(order: Order) -> bool:
def reconcile_user_orders(user_id: int) -> None:
"""سفارش‌های pendingِ اخیرِ یک کاربر و تمدیدِ اشتراکِ بازار؛ از صفحه‌ی وضعیتِ اشتراک صدا زده می‌شود."""
since = timezone.now() - timezone.timedelta(days=2)
for order in Order.objects.filter(
user_id=user_id, status='pending', provider='gateway', created_at__gte=since,
).exclude(gateway_authority='')[:3]:
reconcile_pending_order(order)
sync_bazaar_renewals(user_id)
"""هر روشِ پرداخت آنچه از این کاربر جا مانده را از درگاهِ خودش می‌پرسد (providers.py)؛ از صفحه‌ی وضعیتِ اشتراک."""
for provider in PROVIDERS:
provider.reconcile_user(user_id)
# تا این فاصله پیش از پایانِ دوره، از بازار پرسیده می‌شود که تمدید شده یا نه. بازار روزِ آخر تمدید می‌کند.