256 lines
8.5 KiB
Python
256 lines
8.5 KiB
Python
|
|
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(),
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|