hesabixArc/hesabixAPI/app/services/bank_account_service.py

256 lines
8.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from __future__ import annotations
from typing import Any, Dict, List, Optional
from sqlalchemy.orm import Session
from sqlalchemy import and_, func
from adapters.db.models.bank_account import BankAccount
from app.core.responses import ApiError
def create_bank_account(
db: Session,
business_id: int,
data: Dict[str, Any],
) -> Dict[str, Any]:
# مدیریت کد یکتا در هر کسب‌وکار (در صورت ارسال)
code = data.get("code")
if code is not None and str(code).strip() != "":
# اعتبارسنجی عددی بودن کد
if not str(code).isdigit():
raise ApiError("INVALID_BANK_ACCOUNT_CODE", "Bank account code must be numeric", http_status=400)
# اعتبارسنجی حداقل طول کد
if len(str(code)) < 3:
raise ApiError("INVALID_BANK_ACCOUNT_CODE", "Bank account code must be at least 3 digits", http_status=400)
exists = db.query(BankAccount).filter(and_(BankAccount.business_id == business_id, BankAccount.code == str(code))).first()
if exists:
raise ApiError("DUPLICATE_BANK_ACCOUNT_CODE", "Duplicate bank account code", http_status=400)
else:
# تولید خودکار کد: max + 1 به صورت رشته (حداقل ۳ رقم)
max_code = db.query(func.max(BankAccount.code)).filter(BankAccount.business_id == business_id).scalar()
try:
if max_code is not None and str(max_code).isdigit():
next_code_int = int(max_code) + 1
else:
next_code_int = 100 # شروع از ۱۰۰ برای حداقل ۳ رقم
# اگر کد کمتر از ۳ رقم است، آن را به ۳ رقم تبدیل کن
if next_code_int < 100:
next_code_int = 100
code = str(next_code_int)
except Exception:
code = "100" # در صورت خطا، حداقل کد ۳ رقمی
obj = BankAccount(
business_id=business_id,
code=code,
name=data.get("name"),
branch=data.get("branch"),
account_number=data.get("account_number"),
sheba_number=data.get("sheba_number"),
card_number=data.get("card_number"),
owner_name=data.get("owner_name"),
pos_number=data.get("pos_number"),
payment_id=data.get("payment_id"),
description=data.get("description"),
currency_id=int(data.get("currency_id")),
is_active=bool(data.get("is_active", True)),
is_default=bool(data.get("is_default", False)),
)
# اگر پیش فرض شد، بقیه را غیر پیش فرض کن
if obj.is_default:
db.query(BankAccount).filter(BankAccount.business_id == business_id, BankAccount.id != obj.id).update({BankAccount.is_default: False})
db.add(obj)
db.commit()
db.refresh(obj)
return bank_account_to_dict(obj)
def get_bank_account_by_id(db: Session, account_id: int) -> Optional[Dict[str, Any]]:
obj = db.query(BankAccount).filter(BankAccount.id == account_id).first()
return bank_account_to_dict(obj) if obj else None
def update_bank_account(db: Session, account_id: int, data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
obj = db.query(BankAccount).filter(BankAccount.id == account_id).first()
if obj is None:
return None
if "code" in data and data["code"] is not None and str(data["code"]).strip() != "":
if not str(data["code"]).isdigit():
raise ApiError("INVALID_BANK_ACCOUNT_CODE", "Bank account code must be numeric", http_status=400)
if len(str(data["code"])) < 3:
raise ApiError("INVALID_BANK_ACCOUNT_CODE", "Bank account code must be at least 3 digits", http_status=400)
exists = db.query(BankAccount).filter(and_(BankAccount.business_id == obj.business_id, BankAccount.code == str(data["code"]), BankAccount.id != obj.id)).first()
if exists:
raise ApiError("DUPLICATE_BANK_ACCOUNT_CODE", "Duplicate bank account code", http_status=400)
obj.code = str(data["code"])
for field in [
"name","branch","account_number","sheba_number","card_number",
"owner_name","pos_number","payment_id","description",
]:
if field in data:
setattr(obj, field, data.get(field))
if "currency_id" in data and data["currency_id"] is not None:
obj.currency_id = int(data["currency_id"]) # TODO: اعتبارسنجی وجود ارز
if "is_active" in data and data["is_active"] is not None:
obj.is_active = bool(data["is_active"])
if "is_default" in data and data["is_default"] is not None:
obj.is_default = bool(data["is_default"])
if obj.is_default:
# تنها یک حساب پیش‌فرض در هر بیزنس
db.query(BankAccount).filter(BankAccount.business_id == obj.business_id, BankAccount.id != obj.id).update({BankAccount.is_default: False})
db.commit()
db.refresh(obj)
return bank_account_to_dict(obj)
def delete_bank_account(db: Session, account_id: int) -> bool:
obj = db.query(BankAccount).filter(BankAccount.id == account_id).first()
if obj is None:
return False
db.delete(obj)
db.commit()
return True
def list_bank_accounts(
db: Session,
business_id: int,
query: Dict[str, Any],
) -> Dict[str, Any]:
q = db.query(BankAccount).filter(BankAccount.business_id == business_id)
# جستجو
if query.get("search") and query.get("search_fields"):
term = f"%{query['search']}%"
from sqlalchemy import or_
conditions = []
for f in query["search_fields"]:
if f == "code":
conditions.append(BankAccount.code.ilike(term))
elif f == "name":
conditions.append(BankAccount.name.ilike(term))
elif f == "branch":
conditions.append(BankAccount.branch.ilike(term))
elif f == "account_number":
conditions.append(BankAccount.account_number.ilike(term))
elif f == "sheba_number":
conditions.append(BankAccount.sheba_number.ilike(term))
elif f == "card_number":
conditions.append(BankAccount.card_number.ilike(term))
elif f == "owner_name":
conditions.append(BankAccount.owner_name.ilike(term))
elif f == "pos_number":
conditions.append(BankAccount.pos_number.ilike(term))
elif f == "payment_id":
conditions.append(BankAccount.payment_id.ilike(term))
if conditions:
q = q.filter(or_(*conditions))
# فیلترها
if query.get("filters"):
for flt in query["filters"]:
prop = getattr(flt, 'property', None) if not isinstance(flt, dict) else flt.get('property')
op = getattr(flt, 'operator', None) if not isinstance(flt, dict) else flt.get('operator')
val = getattr(flt, 'value', None) if not isinstance(flt, dict) else flt.get('value')
if not prop or not op:
continue
if prop in {"is_active", "is_default"} and op == "=":
q = q.filter(getattr(BankAccount, prop) == val)
elif prop == "currency_id" and op == "=":
q = q.filter(BankAccount.currency_id == val)
# مرتب سازی
sort_by = query.get("sort_by") or "created_at"
sort_desc = bool(query.get("sort_desc", True))
col = getattr(BankAccount, sort_by, BankAccount.created_at)
q = q.order_by(col.desc() if sort_desc else col.asc())
# صفحه‌بندی
skip = int(query.get("skip", 0))
take = int(query.get("take", 20))
total = q.count()
items = q.offset(skip).limit(take).all()
return {
"items": [bank_account_to_dict(i) for i in items],
"pagination": {
"total": total,
"page": (skip // take) + 1,
"per_page": take,
"total_pages": (total + take - 1) // take,
"has_next": skip + take < total,
"has_prev": skip > 0,
},
"query_info": query,
}
def bulk_delete_bank_accounts(db: Session, business_id: int, account_ids: List[int]) -> Dict[str, Any]:
"""
حذف گروهی حساب‌های بانکی
"""
if not account_ids:
return {"deleted": 0, "skipped": 0}
# بررسی وجود حساب‌ها و دسترسی به کسب‌وکار
accounts = db.query(BankAccount).filter(
BankAccount.id.in_(account_ids),
BankAccount.business_id == business_id
).all()
deleted_count = 0
skipped_count = 0
for account in accounts:
try:
db.delete(account)
deleted_count += 1
except Exception:
skipped_count += 1
# commit تغییرات
try:
db.commit()
except Exception:
db.rollback()
raise ApiError("BULK_DELETE_FAILED", "Bulk delete failed for bank accounts", http_status=500)
return {
"deleted": deleted_count,
"skipped": skipped_count,
"total_requested": len(account_ids)
}
def bank_account_to_dict(obj: BankAccount) -> Dict[str, Any]:
return {
"id": obj.id,
"business_id": obj.business_id,
"code": obj.code,
"name": obj.name,
"branch": obj.branch,
"account_number": obj.account_number,
"sheba_number": obj.sheba_number,
"card_number": obj.card_number,
"owner_name": obj.owner_name,
"pos_number": obj.pos_number,
"payment_id": obj.payment_id,
"description": obj.description,
"currency_id": obj.currency_id,
"is_active": bool(obj.is_active),
"is_default": bool(obj.is_default),
"created_at": obj.created_at.isoformat(),
"updated_at": obj.updated_at.isoformat(),
}