Question packs removed at the root; all subscriptions 4 assistant questions a day; Bazaar is subscription-only

- subscription.0017/0018, payment.0013: pack plans deleted, QuestionCredit/HeartsPass models and Plan.kind /
  question_credits / hearts_days / Order.question_credits / hearts_days dropped. The only paid pack order was the
  company owner's test account (user 26).
- Assistant: no credit spending; quota is the daily cap only (free 2, every plan 4, gift subscription 4).
- Bazaar: verify_purchase is subscription-only (no inapp guess), start/purchase responses lose productType,
  verify refuses an order without a subscription. A paid order with no subscription now logs ERROR.
- Removed POST /api/payment/orders (internal; no caller). Its permission tests stay.
- Pack-based tests rewritten on subscriptions (refund, security, Bazaar token); pack-only test files deleted.
- docs: profit table for 4/day on both payment routes plus free users; handbook, operations, CLAUDE.md current.
949 tests green.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 20:33:38 +03:30
parent 9c3adc3e60
commit 947f946909
39 changed files with 398 additions and 1174 deletions

View File

@@ -115,8 +115,6 @@ def create_order(*, user_id: int, amount: int, currency: str = 'IRT',
user_id=user_id,
subscription_id=subscription_id,
plan_id=plan_id,
question_credits=plan.question_credits if plan else None,
hearts_days=plan.hearts_days if plan else None,
duration_days=plan.duration_days if plan else None,
amount=amount,
list_price=amount,
@@ -227,10 +225,10 @@ def apply_order_payment_result(*, order: Order, gateway: str, gateway_ref_id: st
from subscription.services import activate_subscription
activate_subscription(order.subscription_id, plan_id=order.plan_id,
duration_days=order.duration_days)
elif order.plan_id:
# بسته‌ی سوال: اعتبار و روزهای قلبِ نامحدود، بی‌دست زدن به اشتراک.
from subscription.services import grant_pack
grant_pack(order)
else:
# هر سفارشِ فروشی از PurchaseView اشتراک دارد (فقط اشتراک فروخته می‌شود، subscription.0017)؛
# سفارشِ پرداخت‌شده‌ی بی‌اشتراک یعنی چیزی داده نشد و باید دیده شود.
logger.error('سفارشِ %s پرداخت شد ولی اشتراکی ندارد؛ چیزی فعال نشد', order.id)
# پورسانتِ معرف فقط روی پرداختِ واقعی (نه ثبت‌نام، نه مهمان).
from affiliate.services import record_paid_order
record_paid_order(order)
@@ -322,7 +320,7 @@ def sync_bazaar_renewals(user_id: int) -> int:
continue
try:
result = bazaar.verify_purchase(
product_sku=last.bazaar_product_sku, purchase_token=token, is_subscription=True)
product_sku=last.bazaar_product_sku, purchase_token=token)
except bazaar.BazaarAPIError: # خودِ bazaar لاگ کرده؛ ساعتِ بعد
continue
except Exception: # noqa: BLE001 — صفحه‌ی وضعیت نباید با خطای بازار ۵۰۰ بدهد؛ ولی دیده شود
@@ -399,21 +397,18 @@ def refund_order(order: Order, *, staff: str = '') -> dict:
**پول را برنمی‌گرداند.** برگرداندنِ پول در پنلِ زرین‌پال (یا کافه‌بازار) انجام می‌شود؛ این تابع
فقط حساب‌وکتابِ خودِ اپ را با آن یکی می‌کند. چه چیزی پس گرفته می‌شود (تصمیمِ ۲۰۲۶-۱۰-۰۲):
- بسته: اعتبارِ همین سفارش، ولی فقط آن‌قدر که هنوز خرج نشده (موجودی منفی نمی‌شود)، و روزهای
قلبِ نامحدودِ همین بسته از پایانِ کارتِ قلب.
- اشتراک: روزهای همین پلن از پایانِ اشتراک؛ اگر چیزی نماند اشتراک «لغوشده» می‌شود.
- روزهای همین پلن از پایانِ اشتراک؛ اگر چیزی نماند اشتراک «لغوشده» می‌شود.
- پورسانتِ معرف برای همین سفارش به «در انتظار» برمی‌گردد.
خروجی: خلاصه برای پیامِ ادمین.
"""
from subscription.models import HeartsPass, Plan, QuestionCredit, Subscription
from subscription.services import credit_balance
from subscription.models import Plan, Subscription
with db_transaction.atomic():
order = Order.objects.select_for_update().get(pk=order.pk)
if order.status != 'paid':
raise OrderStateError(f'سفارشِ {order.id} «{order.get_status_display()}» است؛ فقط سفارشِ پرداخت‌شده بازپرداخت می‌شود.')
now = timezone.now()
summary = {'credits_granted': 0, 'credits_taken': 0, 'hearts_days': 0, 'subscription_days': 0}
summary = {'subscription_days': 0}
plan = Plan.objects.filter(pk=order.plan_id).first() if order.plan_id else None
if order.subscription_id:
@@ -426,22 +421,6 @@ def refund_order(order: Order, *, staff: str = '') -> dict:
sub.version += 1 # PurchaseView با version تشخیص می‌دهد کسی ردیف را هم‌زمان عوض کرده
sub.save(update_fields=['expires_at', 'status', 'version'])
summary['subscription_days'] = days
else:
granted = QuestionCredit.objects.filter(order_id=order.id, reason='purchase').first()
if granted is not None:
take = min(granted.delta, max(credit_balance(order.user_id), 0))
summary['credits_granted'] = granted.delta
summary['credits_taken'] = take
if take:
QuestionCredit.objects.create(user_id=order.user_id, delta=-take, reason='refund',
order_id=order.id, note=f'بازپرداختِ سفارشِ {order.id}')
hearts_days = order.hearts_days if order.hearts_days is not None else (plan.hearts_days if plan else 0)
if hearts_days:
hp = HeartsPass.objects.select_for_update().filter(user_id=order.user_id).first()
if hp is not None:
hp.expires_at = max(hp.expires_at - timezone.timedelta(days=hearts_days), now)
hp.save(update_fields=['expires_at'])
summary['hearts_days'] = hearts_days
from affiliate.models import Conversion
Conversion.objects.filter(order_id=order.id, status='paid').update(