230 lines
8.8 KiB
Python
230 lines
8.8 KiB
Python
import logging
|
|
from azbankgateways import (
|
|
bankfactories,
|
|
models as bank_models,
|
|
default_settings as settings,
|
|
)
|
|
from azbankgateways.models.enum import PaymentStatus
|
|
from .models import OrderModel
|
|
from account.models import PushSubscription
|
|
import ghasedak_sms
|
|
from product.models import ProductImageModel
|
|
from celery import shared_task
|
|
from django.db import transaction
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
@shared_task
|
|
def udpate_bank_status():
|
|
factory = bankfactories.BankFactory()
|
|
|
|
# ۱. بروزرسانی رکوردهای منقضی در یک تراکنش جداگانه
|
|
try:
|
|
with transaction.atomic():
|
|
bank_models.Bank.objects.update_expire_records()
|
|
except Exception as e:
|
|
logger.error(f"Error in update_expire_records: {e}")
|
|
|
|
# ۲. پردازش رکوردهایی که از بانک برگشتهاند
|
|
for item in bank_models.Bank.objects.filter_return_from_bank():
|
|
try:
|
|
with transaction.atomic():
|
|
bank = factory.create(
|
|
bank_type=item.bank_type, identifier=item.bank_choose_identifier
|
|
)
|
|
bank.verify(item.tracking_code)
|
|
# استفاده از select_for_update برای جلوگیری از Race Condition
|
|
bank_record = bank_models.Bank.objects.select_related('order').select_for_update().get(tracking_code=item.tracking_code)
|
|
|
|
if bank_record.is_success and bank_record.order:
|
|
bank_record.order.cart.clear_cart()
|
|
bank_record.order.is_paid = True
|
|
bank_record.order.save(update_fields=['is_paid'])
|
|
elif bank_record.order:
|
|
bank_record.order.rollback_stock()
|
|
except Exception as e:
|
|
logger.error(f"Failed to verify bank record {item.tracking_code}: {e}")
|
|
|
|
failed_statuses = [
|
|
PaymentStatus.CANCEL_BY_USER,
|
|
PaymentStatus.EXPIRE_GATEWAY_TOKEN,
|
|
PaymentStatus.EXPIRE_VERIFY_PAYMENT,
|
|
PaymentStatus.ERROR,
|
|
]
|
|
|
|
# 1. ابتدا رکوردهای بانکی را بدون select_related پیدا کنید
|
|
# از select_for_update اینجا استفاده نکنید چون به Order وصل است
|
|
failed_records = bank_models.Bank.objects.filter(
|
|
status__in=failed_statuses,
|
|
order__isnull=False,
|
|
order__is_paid=False,
|
|
order__is_stock_rolled_back=False,
|
|
)
|
|
|
|
logger.info(f"Found {failed_records.count()} failed records for rollback.")
|
|
|
|
for bank_record in failed_records:
|
|
try:
|
|
# استفاده از تراکنش اتمیک برای هر رکورد
|
|
with transaction.atomic():
|
|
# 2. حالا خودِ Order مربوطه را با select_for_update قفل کنید
|
|
# این کار مانع از تداخل با بقیه تسکها میشود
|
|
order = bank_record.order
|
|
|
|
# بررسی مجدد شرط برای اطمینان در لحظه قفل شدن
|
|
if order and not order.is_paid and not order.is_stock_rolled_back:
|
|
order.rollback_stock()
|
|
logger.info(f"Successfully rolled back stock for bank record {bank_record.id}")
|
|
else:
|
|
logger.info(f"Order {order.id} already processed or paid, skipping.")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to rollback stock for bank record {bank_record.id}: {str(e)}")
|
|
|
|
return "update bank record is done"
|
|
|
|
|
|
@shared_task
|
|
def send_change_status_notif(instance_pk, new_status):
|
|
instance = OrderModel.objects.get(pk=instance_pk)
|
|
user_subs = PushSubscription.objects.filter(user=instance.user)
|
|
for user_sub in user_subs:
|
|
try:
|
|
user_sub.send_notif(f'سفارش شما به {new_status} تغییر کرد', f'سفارش شما به {new_status} تغییر کرد', ProductImageModel.objects.all().first().image.url)
|
|
except Exception as e:
|
|
logger.error('Error sending status notification: ' + str(e))
|
|
|
|
@shared_task
|
|
def send_change_status_sms(instance_pk, new_status):
|
|
instance = OrderModel.objects.get(pk=instance_pk)
|
|
sms_api = ghasedak_sms.Ghasedak(api_key="8f7396f1e3c39e3a4621009c558d955336eea6d21cf257dd74ae262d6f22a458XdoDjH6egJsiZsy8")
|
|
|
|
|
|
response = sms_api.send_single_sms(
|
|
ghasedak_sms.SendSingleSmsInput(
|
|
message=f'سفارش شما به {new_status} تغییر کرد',
|
|
receptor=instance.user.phone,
|
|
line_number='30005006004095',
|
|
client_reference_id=str(instance.user.pk)
|
|
)
|
|
)
|
|
if response['statusCode'] == 200:
|
|
return 'done log later'
|
|
else:
|
|
return f'error: {response}'
|
|
|
|
|
|
@shared_task
|
|
def generate_daily_shop_reports():
|
|
"""Generate daily shop reports for the previous day.
|
|
|
|
This task aggregates ShopOrderModel entries from yesterday
|
|
and creates/updates ShopDailyReport records for each shop.
|
|
Scheduled to run daily at midnight.
|
|
"""
|
|
from django.utils import timezone
|
|
from datetime import timedelta
|
|
from django.db.models import Sum
|
|
from .models import ShopOrderModel, ShopDailyReport
|
|
|
|
target_date = (timezone.now() - timedelta(days=1)).date()
|
|
logging.info(f'Generating shop reports for {target_date}')
|
|
|
|
shop_orders = ShopOrderModel.objects.filter(created_at__date=target_date)
|
|
if not shop_orders.exists():
|
|
logging.warning(f'No shop orders found for {target_date}')
|
|
return f'No shop orders for {target_date}'
|
|
|
|
shops = shop_orders.values('shop').distinct()
|
|
reports_created = 0
|
|
reports_updated = 0
|
|
|
|
for s in shops:
|
|
shop_id = s['shop']
|
|
aggr = shop_orders.filter(shop_id=shop_id).aggregate(
|
|
total_sales=Sum('subtotal'),
|
|
total_commission=Sum('commission_amount'),
|
|
total_payable=Sum('payable_amount')
|
|
)
|
|
|
|
total_sales = aggr['total_sales'] or 0
|
|
total_commission = aggr['total_commission'] or 0
|
|
total_payable = aggr['total_payable'] or 0
|
|
|
|
report, created = ShopDailyReport.objects.update_or_create(
|
|
shop_id=shop_id,
|
|
date=target_date,
|
|
defaults={
|
|
'total_sales': total_sales,
|
|
'total_commission': total_commission,
|
|
'total_payable': total_payable,
|
|
}
|
|
)
|
|
|
|
# Link all shop orders for this shop and date to the daily report
|
|
shop_orders.filter(shop_id=shop_id).update(daily_report=report)
|
|
|
|
if created:
|
|
reports_created += 1
|
|
else:
|
|
reports_updated += 1
|
|
|
|
logging.info(f"Shop {shop_id}: sales={total_sales}, commission={total_commission}, payable={total_payable}")
|
|
|
|
result = f'Generated reports for {target_date}: {reports_created} created, {reports_updated} updated'
|
|
logging.info(result)
|
|
return result
|
|
|
|
|
|
@shared_task
|
|
def send_shop_order_invoice_telegram_task(shop_order_id, chat_id, bot_token):
|
|
"""Send shop order invoice PDF to Telegram chat.
|
|
|
|
Args:
|
|
shop_order_id: ID of the ShopOrderModel
|
|
chat_id: Telegram chat ID to send invoice to
|
|
bot_token: Telegram bot token for authentication
|
|
|
|
Returns:
|
|
Success or error message
|
|
"""
|
|
import telebot
|
|
from .invoice_generator import generate_shop_order_invoice
|
|
from .models import ShopOrderModel
|
|
|
|
try:
|
|
# Get the shop order
|
|
shop_order = ShopOrderModel.objects.get(pk=shop_order_id)
|
|
|
|
# Generate invoice PDF (should return a file-like object)
|
|
pdf_buffer = generate_shop_order_invoice(shop_order_id)
|
|
if hasattr(pdf_buffer, 'seek'):
|
|
pdf_buffer.seek(0)
|
|
|
|
caption = f'فاکتور سفارش #{shop_order_id}\n{shop_order.shop.shop_name}'
|
|
|
|
# Initialize bot and send document
|
|
bot = telebot.TeleBot(bot_token)
|
|
bot.send_document(
|
|
chat_id=chat_id,
|
|
document=pdf_buffer,
|
|
caption=caption,
|
|
visible_file_name=f'invoice_shop_order_{shop_order_id}.pdf'
|
|
)
|
|
|
|
logging.info(f'Successfully sent shop order invoice {shop_order_id} to Telegram chat {chat_id}')
|
|
return f'Invoice sent successfully to chat {chat_id}'
|
|
|
|
except ShopOrderModel.DoesNotExist:
|
|
error_msg = f'ShopOrderModel with id {shop_order_id} does not exist'
|
|
logging.error(error_msg)
|
|
return error_msg
|
|
except telebot.apihelper.ApiException as e:
|
|
error_msg = f'Telegram API error sending invoice {shop_order_id}: {str(e)}'
|
|
logging.error(error_msg)
|
|
return error_msg
|
|
except Exception as e:
|
|
error_msg = f'Error sending invoice {shop_order_id} to Telegram: {str(e)}'
|
|
logging.error(error_msg)
|
|
return error_msg
|