20 KiB
Executable file
طرح محافظت از تراکنشهای کیف پول
مشکل شناسایی شده
کاربران میتوانند تراکنشهای مربوط به کیف پول را حذف کنند که منجر به خراب شدن دادهها میشود. این شامل:
- تراکنشهای واریز و برداشت کیف پول
- صورت حسابهای هوش مصنوعی
- پکیجها و استفاده از نرمافزار بابت هوش مصنوعی
- صورتحسابهای ذخیرهسازی
- سرویسهای زحل
راهحل جامع
1. ایجاد تابع محافظتی مرکزی
1.1. تابع بررسی ارتباط سند با تراکنش کیف پول
فایل: app/services/wallet_service.py
def check_document_has_wallet_transactions(db: Session, document_id: int) -> Dict[str, Any]:
"""
بررسی میکند که آیا یک سند به تراکنشهای کیف پول مرتبط است یا نه
Returns:
{
"has_wallet_transactions": bool,
"transaction_count": int,
"transaction_ids": List[int],
"transaction_types": List[str],
"message": str
}
"""
from adapters.db.models.wallet import WalletTransaction
transactions = db.query(WalletTransaction).filter(
WalletTransaction.document_id == document_id
).all()
if not transactions:
return {
"has_wallet_transactions": False,
"transaction_count": 0,
"transaction_ids": [],
"transaction_types": [],
"message": None
}
transaction_ids = [tx.id for tx in transactions]
transaction_types = list(set([tx.type for tx in transactions]))
# انواع تراکنشهای سیستمی که نباید حذف شوند
protected_types = [
"top_up", # واریز
"payout_request", # درخواست برداشت
"payout_settlement", # تسویه برداشت
"internal_invoice_payment", # پرداخت صورتحساب داخلی
"ai_subscription", # اشتراک هوش مصنوعی
"ai_usage", # استفاده از هوش مصنوعی
"customer_payment", # پرداخت مشتری (پکیجها)
"internal_service_charge", # کسر سرویس داخلی
"refund", # بازگشت وجه
"fee", # کارمزد
"chargeback", # برگشت تراکنش
"reversal" # معکوس کردن تراکنش
]
has_protected = any(tx.type in protected_types for tx in transactions)
if has_protected:
protected_tx = [tx for tx in transactions if tx.type in protected_types]
message = f"این سند به {len(protected_tx)} تراکنش کیف پول سیستمی مرتبط است و قابل حذف نمیباشد. انواع تراکنشها: {', '.join(set([tx.type for tx in protected_tx]))}"
else:
message = f"این سند به {len(transactions)} تراکنش کیف پول مرتبط است"
return {
"has_wallet_transactions": True,
"has_protected_transactions": has_protected,
"transaction_count": len(transactions),
"transaction_ids": transaction_ids,
"transaction_types": transaction_types,
"message": message
}
1.2. تابع بررسی تراکنشهای مرتبط با موجودیتهای دیگر
فایل: app/services/wallet_service.py
def check_wallet_transaction_has_dependencies(db: Session, transaction_id: int) -> Dict[str, Any]:
"""
بررسی میکند که آیا یک تراکنش کیف پول به موجودیتهای دیگر لینک شده است یا نه
Returns:
{
"has_dependencies": bool,
"dependencies": {
"ai_invoices": List[int],
"storage_invoices": List[int],
"marketplace_orders": List[int],
"zohal_services": List[int],
"ai_usage_logs": List[int]
},
"message": str
}
"""
dependencies = {
"ai_invoices": [],
"storage_invoices": [],
"marketplace_orders": [],
"zohal_services": [],
"ai_usage_logs": []
}
# بررسی AI Invoices
try:
from adapters.db.models.ai_invoice import AIInvoice
ai_invoices = db.query(AIInvoice).filter(
AIInvoice.wallet_transaction_id == transaction_id
).all()
dependencies["ai_invoices"] = [inv.id for inv in ai_invoices]
except Exception:
pass
# بررسی Storage Invoices
try:
from adapters.db.models.storage_plan import StorageInvoice
storage_invoices = db.query(StorageInvoice).filter(
StorageInvoice.wallet_transaction_id == transaction_id
).all()
dependencies["storage_invoices"] = [inv.id for inv in storage_invoices]
except Exception:
pass
# بررسی Marketplace Orders
try:
from adapters.db.models.marketplace import MarketplaceOrder
marketplace_orders = db.query(MarketplaceOrder).filter(
MarketplaceOrder.wallet_transaction_id == transaction_id
).all()
dependencies["marketplace_orders"] = [order.id for order in marketplace_orders]
except Exception:
pass
# بررسی Zohal Services
try:
from adapters.db.models.zohal import ZohalServiceUsage
zohal_services = db.query(ZohalServiceUsage).filter(
ZohalServiceUsage.wallet_transaction_id == transaction_id
).all()
dependencies["zohal_services"] = [svc.id for svc in zohal_services]
except Exception:
pass
# بررسی AI Usage Logs
try:
from adapters.db.models.ai_usage_log import AIUsageLog
ai_usage_logs = db.query(AIUsageLog).filter(
AIUsageLog.wallet_transaction_id == transaction_id
).all()
dependencies["ai_usage_logs"] = [log.id for log in ai_usage_logs]
except Exception:
pass
total_dependencies = sum(len(v) for v in dependencies.values())
has_dependencies = total_dependencies > 0
if has_dependencies:
dep_list = []
if dependencies["ai_invoices"]:
dep_list.append(f"{len(dependencies['ai_invoices'])} صورتحساب AI")
if dependencies["storage_invoices"]:
dep_list.append(f"{len(dependencies['storage_invoices'])} صورتحساب ذخیرهسازی")
if dependencies["marketplace_orders"]:
dep_list.append(f"{len(dependencies['marketplace_orders'])} سفارش مارکتپلیس")
if dependencies["zohal_services"]:
dep_list.append(f"{len(dependencies['zohal_services'])} سرویس زحل")
if dependencies["ai_usage_logs"]:
dep_list.append(f"{len(dependencies['ai_usage_logs'])} لاگ استفاده AI")
message = f"این تراکنش به {', '.join(dep_list)} مرتبط است و قابل حذف نمیباشد"
else:
message = None
return {
"has_dependencies": has_dependencies,
"dependencies": dependencies,
"message": message
}
2. تغییرات در سرویسهای حذف اسناد
2.1. تغییر در delete_document
فایل: app/services/document_service.py
قبل از حذف سند (بعد از خط 178):
# بررسی ارتباط با تراکنشهای کیف پول
from app.services.wallet_service import check_document_has_wallet_transactions
wallet_check = check_document_has_wallet_transactions(db, document_id)
if wallet_check["has_wallet_transactions"] and wallet_check.get("has_protected_transactions", False):
raise ApiError(
"DOCUMENT_HAS_WALLET_TRANSACTIONS",
wallet_check["message"],
http_status=409
)
2.2. تغییر در delete_receipt_payment
فایل: app/services/receipt_payment_service.py
بعد از بررسی چکها (بعد از خط 1429):
# 4) جلوگیری از حذف اگر سند به تراکنشهای کیف پول مرتبط باشد
try:
from app.services.wallet_service import check_document_has_wallet_transactions
wallet_check = check_document_has_wallet_transactions(db, document_id)
if wallet_check["has_wallet_transactions"] and wallet_check.get("has_protected_transactions", False):
raise ApiError(
"DOCUMENT_HAS_WALLET_TRANSACTIONS",
wallet_check["message"],
http_status=409,
)
except ApiError:
raise
except Exception:
pass
2.3. تغییر در delete_expense_income
فایل: app/services/expense_income_service.py
بعد از بررسی نوع سند (بعد از خط 1078):
# بررسی ارتباط با تراکنشهای کیف پول
from app.services.wallet_service import check_document_has_wallet_transactions
wallet_check = check_document_has_wallet_transactions(db, document_id)
if wallet_check["has_wallet_transactions"] and wallet_check.get("has_protected_transactions", False):
raise ApiError(
"DOCUMENT_HAS_WALLET_TRANSACTIONS",
wallet_check["message"],
http_status=409
)
2.4. تغییر در delete_transfer
فایل: app/services/transfer_service.py
بعد از بررسی سال مالی (بعد از خط 443):
# بررسی ارتباط با تراکنشهای کیف پول
from app.services.wallet_service import check_document_has_wallet_transactions
wallet_check = check_document_has_wallet_transactions(db, document_id)
if wallet_check["has_wallet_transactions"] and wallet_check.get("has_protected_transactions", False):
raise ApiError(
"DOCUMENT_HAS_WALLET_TRANSACTIONS",
wallet_check["message"],
http_status=409
)
2.5. تغییر در delete_invoice
فایل: app/services/invoice_service.py
بعد از بررسی کارپوشه مودیان (بعد از خط 3487):
# 3.5) جلوگیری از حذف اگر سند به تراکنشهای کیف پول مرتبط باشد
try:
from app.services.wallet_service import check_document_has_wallet_transactions
wallet_check = check_document_has_wallet_transactions(db, document_id)
if wallet_check["has_wallet_transactions"] and wallet_check.get("has_protected_transactions", False):
logger.error(f"[DELETE_INVOICE] Invoice {document_id}: Cannot delete - has wallet transactions")
raise ApiError(
"DOCUMENT_HAS_WALLET_TRANSACTIONS",
wallet_check["message"],
http_status=409,
)
except ApiError:
raise
except Exception as ex:
logger.warning(f"[DELETE_INVOICE] Invoice {document_id}: Error checking wallet transactions: {ex}")
pass
3. تغییرات در Foreign Key Constraints
3.1. Migration برای تغییر ondelete از SET NULL به RESTRICT
فایل جدید: migrations/versions/XXXXXX_protect_wallet_transactions.py
"""protect_wallet_transactions
Revision ID: XXXXXX
Revises: YYYYYY
Create Date: 2024-XX-XX XX:XX:XX.XXXXXX
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers
revision = 'XXXXXX'
down_revision = 'YYYYYY'
branch_labels = None
depends_on = None
def upgrade():
# تغییر constraint برای ai_invoices
op.drop_constraint('ai_invoices_wallet_transaction_id_fkey', 'ai_invoices', type_='foreignkey')
op.create_foreign_key(
'ai_invoices_wallet_transaction_id_fkey',
'ai_invoices', 'wallet_transactions',
['wallet_transaction_id'], ['id'],
ondelete='RESTRICT'
)
# تغییر constraint برای storage_invoices
op.drop_constraint('storage_invoices_wallet_transaction_id_fkey', 'storage_invoices', type_='foreignkey')
op.create_foreign_key(
'storage_invoices_wallet_transaction_id_fkey',
'storage_invoices', 'wallet_transactions',
['wallet_transaction_id'], ['id'],
ondelete='RESTRICT'
)
# تغییر constraint برای marketplace_orders
op.drop_constraint('marketplace_orders_wallet_transaction_id_fkey', 'marketplace_orders', type_='foreignkey')
op.create_foreign_key(
'marketplace_orders_wallet_transaction_id_fkey',
'marketplace_orders', 'wallet_transactions',
['wallet_transaction_id'], ['id'],
ondelete='RESTRICT'
)
# تغییر constraint برای zohal_service_usage
op.drop_constraint('zohal_service_usage_wallet_transaction_id_fkey', 'zohal_service_usage', type_='foreignkey')
op.create_foreign_key(
'zohal_service_usage_wallet_transaction_id_fkey',
'zohal_service_usage', 'wallet_transactions',
['wallet_transaction_id'], ['id'],
ondelete='RESTRICT'
)
# تغییر constraint برای ai_usage_logs
op.drop_constraint('ai_usage_logs_wallet_transaction_id_fkey', 'ai_usage_logs', type_='foreignkey')
op.create_foreign_key(
'ai_usage_logs_wallet_transaction_id_fkey',
'ai_usage_logs', 'wallet_transactions',
['wallet_transaction_id'], ['id'],
ondelete='RESTRICT'
)
# تغییر constraint برای document_monetization
op.drop_constraint('document_monetization_wallet_transaction_id_fkey', 'document_monetization', type_='foreignkey')
op.create_foreign_key(
'document_monetization_wallet_transaction_id_fkey',
'document_monetization', 'wallet_transactions',
['wallet_transaction_id'], ['id'],
ondelete='RESTRICT'
)
def downgrade():
# برگشت به SET NULL
op.drop_constraint('ai_invoices_wallet_transaction_id_fkey', 'ai_invoices', type_='foreignkey')
op.create_foreign_key(
'ai_invoices_wallet_transaction_id_fkey',
'ai_invoices', 'wallet_transactions',
['wallet_transaction_id'], ['id'],
ondelete='SET NULL'
)
# ... (همین کار برای بقیه جداول)
3.2. تغییر در مدلها
تغییر ondelete="SET NULL" به ondelete="RESTRICT" در:
adapters/db/models/ai_invoice.py- خط 54adapters/db/models/storage_plan.py- خط 76adapters/db/models/marketplace.py- خط 72adapters/db/models/zohal.py- خط 68adapters/db/models/ai_usage_log.py- خط 41adapters/db/models/document_monetization.py- خط 97
4. تغییرات در Endpoint ها
4.1. Endpoint حذف سند
فایل: adapters/api/v1/documents.py
تغییری لازم نیست - بررسی در سرویس انجام میشود
4.2. Endpoint حذف دریافت/پرداخت
فایل: adapters/api/v1/receipts_payments.py
تغییری لازم نیست - بررسی در سرویس انجام میشود
4.3. Endpoint حذف هزینه/درآمد
فایل: adapters/api/v1/expense_income.py
تغییری لازم نیست - بررسی در سرویس انجام میشود
4.4. Endpoint حذف انتقال
فایل: adapters/api/v1/transfers.py
تغییری لازم نیست - بررسی در سرویس انجام میشود
4.5. Endpoint حذف فاکتور
فایل: adapters/api/v1/invoices.py
تغییری لازم نیست - بررسی در سرویس انجام میشود
5. جلوگیری از حذف مستقیم تراکنشهای کیف پول
5.1. بررسی وجود Endpoint مستقیم برای حذف
بررسی شده: در adapters/api/v1/wallet.py هیچ endpoint برای حذف تراکنش وجود ندارد ✅
5.2. اگر در آینده endpoint اضافه شد
قوانین:
- فقط تراکنشهای با
typeخاص (مثل "manual") قابل حذف باشند - بررسی وابستگیها قبل از حذف
- فقط کاربران admin بتوانند حذف کنند
6. تستها و اعتبارسنجی
6.1. سناریوهای تست
تست 1: تلاش برای حذف سند مرتبط با واریز کیف پول
1. ایجاد یک سند دریافت/پرداخت
2. ایجاد تراکنش کیف پول از نوع "top_up" با document_id
3. تلاش برای حذف سند
4. انتظار: خطای DOCUMENT_HAS_WALLET_TRANSACTIONS
تست 2: تلاش برای حذف سند مرتبط با صورتحساب AI
1. پرداخت صورتحساب AI از کیف پول
2. تلاش برای حذف سند حسابداری مرتبط
3. انتظار: خطای DOCUMENT_HAS_WALLET_TRANSACTIONS
تست 3: حذف سند بدون تراکنش کیف پول
1. ایجاد یک سند دریافت/پرداخت بدون تراکنش کیف پول
2. حذف سند
3. انتظار: حذف موفق
تست 4: بررسی Foreign Key Constraint
1. تلاش برای حذف تراکنش کیف پول که به AI Invoice لینک شده
2. انتظار: خطای Foreign Key Constraint
7. مستندات و پیامهای خطا
7.1. کدهای خطا
DOCUMENT_HAS_WALLET_TRANSACTIONS: سند به تراکنشهای کیف پول سیستمی مرتبط است
7.2. پیامهای خطا
- فارسی: "این سند به {count} تراکنش کیف پول سیستمی مرتبط است و قابل حذف نمیباشد. انواع تراکنشها: {types}"
8. چکلیست پیادهسازی
- اضافه کردن توابع محافظتی در
wallet_service.py - تغییر
delete_documentدرdocument_service.py - تغییر
delete_receipt_paymentدرreceipt_payment_service.py - تغییر
delete_expense_incomeدرexpense_income_service.py - تغییر
delete_transferدرtransfer_service.py - تغییر
delete_invoiceدرinvoice_service.py - ایجاد migration برای تغییر Foreign Key Constraints
- تغییر مدلها (ondelete="RESTRICT")
- تست سناریوهای مختلف
- بهروزرسانی مستندات API
9. نکات مهم
-
تغییر Foreign Key Constraints: این تغییر ممکن است روی دادههای موجود تأثیر بگذارد. قبل از اجرا، باید دادههای موجود را بررسی کرد.
-
Backward Compatibility: اگر در آینده نیاز به حذف تراکنشهای کیف پول بود، باید از "reversal" استفاده شود نه حذف مستقیم.
-
Performance: بررسیهای اضافه شده ممکن است کمی تأخیر ایجاد کنند، اما برای حفظ یکپارچگی دادهها ضروری است.
-
لاگگذاری: تمام تلاشهای حذف ناموفق باید در لاگ ثبت شوند.
10. خلاصه تغییرات
| فایل | نوع تغییر | توضیحات |
|---|---|---|
app/services/wallet_service.py |
اضافه | توابع محافظتی |
app/services/document_service.py |
تغییر | بررسی قبل از حذف |
app/services/receipt_payment_service.py |
تغییر | بررسی قبل از حذف |
app/services/expense_income_service.py |
تغییر | بررسی قبل از حذف |
app/services/transfer_service.py |
تغییر | بررسی قبل از حذف |
app/services/invoice_service.py |
تغییر | بررسی قبل از حذف |
adapters/db/models/*.py |
تغییر | ondelete="RESTRICT" |
migrations/versions/*.py |
اضافه | Migration برای FK |
تاریخ ایجاد: 2024 وضعیت: آماده برای پیادهسازی