Merge branch 'main' into fork-new
# Conflicts: # docker-compose.yml
This commit is contained in:
@@ -18,6 +18,7 @@ from bot.services.subscription_service import SubscriptionService
|
||||
from bot.services.referral_service import ReferralService
|
||||
from bot.services.panel_api_service import PanelApiService
|
||||
from bot.services.yookassa_service import YooKassaService
|
||||
from bot.services.lknpd_service import LknpdService
|
||||
from bot.middlewares.i18n import JsonI18n
|
||||
from config.settings import Settings
|
||||
from bot.services.notification_service import NotificationService
|
||||
@@ -37,7 +38,8 @@ async def process_successful_payment(session: AsyncSession, bot: Bot,
|
||||
i18n: JsonI18n, settings: Settings,
|
||||
panel_service: PanelApiService,
|
||||
subscription_service: SubscriptionService,
|
||||
referral_service: ReferralService):
|
||||
referral_service: ReferralService,
|
||||
lknpd_service: Optional[LknpdService] = None):
|
||||
metadata = payment_info_from_webhook.get("metadata", {})
|
||||
user_id_str = metadata.get("user_id")
|
||||
subscription_months_str = metadata.get("subscription_months")
|
||||
@@ -75,44 +77,53 @@ async def process_successful_payment(session: AsyncSession, bot: Bot,
|
||||
amount_data = payment_info_from_webhook.get("amount", {})
|
||||
months_for_record = int(subscription_months) if sale_mode != "traffic" else 0
|
||||
payment_value = float(amount_data.get("value", 0.0))
|
||||
yk_payment_id_from_hook = payment_info_from_webhook.get("id")
|
||||
|
||||
payment_record = None
|
||||
# If this is an auto-renewal (no payment_db_id in metadata), ensure a payment record exists
|
||||
if payment_db_id is None and auto_renew_subscription_id_str:
|
||||
try:
|
||||
# Create/ensure provider payment by YooKassa payment id for idempotency
|
||||
yk_payment_id_from_hook = payment_info_from_webhook.get("id")
|
||||
if not yk_payment_id_from_hook:
|
||||
logging.error(
|
||||
"Auto-renew webhook missing YooKassa payment id; cannot ensure payment record."
|
||||
)
|
||||
return
|
||||
from db.dal import payment_dal as _payment_dal
|
||||
ensured_payment = await _payment_dal.ensure_payment_with_provider_id(
|
||||
session,
|
||||
user_id=user_id,
|
||||
amount=payment_value,
|
||||
currency=amount_data.get("currency", settings.DEFAULT_CURRENCY_SYMBOL),
|
||||
months=months_for_record or 1,
|
||||
description=payment_info_from_webhook.get(
|
||||
"description") or f"Auto-renewal for {months_for_record or subscription_months} months",
|
||||
provider="yookassa",
|
||||
provider_payment_id=yk_payment_id_from_hook,
|
||||
payment_record = await _payment_dal.get_payment_by_provider_payment_id(
|
||||
session, yk_payment_id_from_hook
|
||||
)
|
||||
payment_db_id = ensured_payment.payment_id
|
||||
# Also persist yookassa_payment_id field if not set yet
|
||||
try:
|
||||
await _payment_dal.update_payment_status_by_db_id(
|
||||
if not payment_record:
|
||||
payment_record = await _payment_dal.ensure_payment_with_provider_id(
|
||||
session,
|
||||
payment_db_id,
|
||||
payment_info_from_webhook.get("status", "succeeded"),
|
||||
yk_payment_id_from_hook,
|
||||
)
|
||||
except Exception:
|
||||
# Non-fatal; continue processing
|
||||
logging.exception(
|
||||
"Failed to backfill yookassa_payment_id for ensured auto-renew payment"
|
||||
user_id=user_id,
|
||||
amount=payment_value,
|
||||
currency=amount_data.get("currency", settings.DEFAULT_CURRENCY_SYMBOL),
|
||||
months=months_for_record or 1,
|
||||
description=payment_info_from_webhook.get(
|
||||
"description") or f"Auto-renewal for {months_for_record or subscription_months} months",
|
||||
provider="yookassa",
|
||||
provider_payment_id=yk_payment_id_from_hook,
|
||||
)
|
||||
payment_db_id = payment_record.payment_id
|
||||
except Exception as e_ensure:
|
||||
logging.error(
|
||||
f"Failed to ensure payment record for auto-renew webhook (YK {payment_info_from_webhook.get('id')}): {e_ensure}",
|
||||
exc_info=True,
|
||||
)
|
||||
return
|
||||
elif payment_db_id is not None:
|
||||
payment_record = await payment_dal.get_payment_by_db_id(session, payment_db_id)
|
||||
if not payment_record:
|
||||
logging.error(
|
||||
f"Payment record {payment_db_id} not found for YK ID {yk_payment_id_from_hook}."
|
||||
)
|
||||
return
|
||||
|
||||
if payment_record and payment_record.status == "succeeded":
|
||||
logging.info(
|
||||
f"Skipping duplicate YooKassa webhook for payment {payment_db_id} (YK: {yk_payment_id_from_hook})."
|
||||
)
|
||||
return
|
||||
|
||||
db_user = await user_dal.get_user_by_id(session, user_id)
|
||||
if not db_user:
|
||||
@@ -144,6 +155,20 @@ async def process_successful_payment(session: AsyncSession, bot: Bot,
|
||||
|
||||
try:
|
||||
yk_payment_id_from_hook = payment_info_from_webhook.get("id")
|
||||
payment_before_update = None
|
||||
if payment_db_id is not None:
|
||||
payment_before_update = await payment_dal.get_payment_by_db_id(
|
||||
session,
|
||||
payment_db_id,
|
||||
)
|
||||
should_send_lknpd_receipt = bool(
|
||||
lknpd_service
|
||||
and lknpd_service.configured
|
||||
and payment_info_from_webhook.get("paid") is True
|
||||
and payment_info_from_webhook.get("status") == "succeeded"
|
||||
and payment_before_update
|
||||
and payment_before_update.status != "succeeded"
|
||||
)
|
||||
# Try to capture and save payment method for future charges if available
|
||||
try:
|
||||
payment_method = payment_info_from_webhook.get("payment_method")
|
||||
@@ -192,18 +217,6 @@ async def process_successful_payment(session: AsyncSession, bot: Bot,
|
||||
logging.exception("Failed to persist multi-card YooKassa method from webhook")
|
||||
except Exception:
|
||||
logging.exception("Failed to persist YooKassa payment method from webhook")
|
||||
updated_payment_record = await payment_dal.update_payment_status_by_db_id(
|
||||
session,
|
||||
payment_db_id=payment_db_id,
|
||||
new_status=payment_info_from_webhook.get("status", "succeeded"),
|
||||
yk_payment_id=yk_payment_id_from_hook)
|
||||
if not updated_payment_record:
|
||||
logging.error(
|
||||
f"Failed to update payment record {payment_db_id} for yk_id {yk_payment_id_from_hook}"
|
||||
)
|
||||
raise Exception(
|
||||
f"DB Error: Could not update payment record {payment_db_id}")
|
||||
|
||||
months_for_activation = int(subscription_months) if sale_mode != "traffic" else 0
|
||||
activation_details = await subscription_service.activate_subscription(
|
||||
session,
|
||||
@@ -224,6 +237,18 @@ async def process_successful_payment(session: AsyncSession, bot: Bot,
|
||||
raise Exception(
|
||||
f"Subscription Error: Failed to activate for user {user_id}")
|
||||
|
||||
updated_payment_record = await payment_dal.update_payment_status_by_db_id(
|
||||
session,
|
||||
payment_db_id=payment_db_id,
|
||||
new_status=payment_info_from_webhook.get("status", "succeeded"),
|
||||
yk_payment_id=yk_payment_id_from_hook)
|
||||
if not updated_payment_record:
|
||||
logging.error(
|
||||
f"Failed to update payment record {payment_db_id} for yk_id {yk_payment_id_from_hook}"
|
||||
)
|
||||
raise Exception(
|
||||
f"DB Error: Could not update payment record {payment_db_id}")
|
||||
|
||||
base_subscription_end_date = activation_details['end_date']
|
||||
final_end_date_for_user = base_subscription_end_date
|
||||
applied_promo_bonus_days = activation_details.get(
|
||||
@@ -253,6 +278,25 @@ async def process_successful_payment(session: AsyncSession, bot: Bot,
|
||||
traffic_label = (
|
||||
str(int(traffic_amount_gb)) if float(traffic_amount_gb).is_integer() else f"{traffic_amount_gb:g}"
|
||||
)
|
||||
if should_send_lknpd_receipt:
|
||||
receipt_item_name = payment_info_from_webhook.get("description")
|
||||
if not receipt_item_name:
|
||||
if sale_mode == "traffic":
|
||||
receipt_item_name = settings.LKNPD_RECEIPT_NAME_TRAFFIC.format(gb=traffic_label)
|
||||
else:
|
||||
receipt_item_name = settings.LKNPD_RECEIPT_NAME_SUBSCRIPTION.format(months=int(subscription_months))
|
||||
try:
|
||||
await lknpd_service.create_income_receipt(
|
||||
item_name=receipt_item_name,
|
||||
amount=payment_value,
|
||||
quantity=1.0,
|
||||
operation_time=datetime.now(timezone.utc),
|
||||
)
|
||||
except Exception:
|
||||
logging.exception(
|
||||
"Failed to send LKNPD receipt for payment %s",
|
||||
yk_payment_id_from_hook,
|
||||
)
|
||||
config_link_display, connect_button_url = await prepare_config_links(
|
||||
settings, activation_details.get("subscription_url") if activation_details else None
|
||||
)
|
||||
@@ -429,6 +473,7 @@ async def yookassa_webhook_route(request: web.Request):
|
||||
subscription_service: SubscriptionService = request.app[
|
||||
'subscription_service']
|
||||
referral_service: ReferralService = request.app['referral_service']
|
||||
lknpd_service: Optional[LknpdService] = request.app.get('lknpd_service')
|
||||
async_session_factory: sessionmaker = request.app[
|
||||
'async_session_factory']
|
||||
except KeyError as e_app_ctx:
|
||||
@@ -521,7 +566,8 @@ async def yookassa_webhook_route(request: web.Request):
|
||||
await process_successful_payment(
|
||||
session, bot, payment_dict_for_processing,
|
||||
i18n_instance, settings, panel_service,
|
||||
subscription_service, referral_service)
|
||||
subscription_service, referral_service,
|
||||
lknpd_service)
|
||||
await session.commit()
|
||||
else:
|
||||
logging.warning(
|
||||
|
||||
Reference in New Issue
Block a user