from datetime import UTC, datetime, timedelta from sqlalchemy import func, select from sqlalchemy.orm import Session, selectinload from app.models.customer import Customer, CustomerAddress, CustomerContact from app.schemas.customer import ( CustomerAddressCreate, CustomerAddressUpdate, CustomerContactCreate, CustomerContactUpdate, CustomerCreate, CustomerUpdate, ) def _email(value: object) -> str: return "" if value is None else str(value) def _url(value: object) -> str: return "" if value is None else str(value) class CustomerRepository: @staticmethod def get_all(db: Session) -> list[Customer]: return list( db.scalars( select(Customer) .options( selectinload(Customer.addresses), selectinload(Customer.contacts), ) .order_by(Customer.created_at.desc()) ) ) @staticmethod def get_by_id(db: Session, customer_id: int) -> Customer | None: return db.scalar( select(Customer) .where(Customer.id == customer_id) .options( selectinload(Customer.addresses), selectinload(Customer.contacts), ) ) @staticmethod def get_by_number(db: Session, customer_number: str) -> Customer | None: return db.scalar(select(Customer).where(Customer.customer_number == customer_number)) @staticmethod def find_number_conflict( db: Session, *, customer_number: str, exclude_customer_id: int | None = None, ) -> Customer | None: query = select(Customer).where(Customer.customer_number == customer_number) if exclude_customer_id is not None: query = query.where(Customer.id != exclude_customer_id) return db.scalar(query) @staticmethod def count_all(db: Session) -> int: return db.scalar(select(func.count(Customer.id))) or 0 @staticmethod def count_active(db: Session) -> int: return db.scalar(select(func.count(Customer.id)).where(Customer.status == "active")) or 0 @staticmethod def count_recent(db: Session, days: int = 30) -> int: since = datetime.now(UTC) - timedelta(days=days) return db.scalar( select(func.count(Customer.id)) .where(Customer.created_at >= since) ) or 0 @staticmethod def get_latest(db: Session, limit: int = 5) -> list[Customer]: return list( db.scalars( select(Customer) .options(selectinload(Customer.addresses), selectinload(Customer.contacts)) .order_by(Customer.created_at.desc()) .limit(limit) ) ) @staticmethod def create(db: Session, customer: CustomerCreate) -> Customer: db_customer = Customer( customer_number=customer.customer_number, company_name=customer.company_name, legal_name=customer.legal_name, customer_type=customer.customer_type, status=customer.status, industry=customer.industry, website=_url(customer.website), email=_email(customer.email), phone=customer.phone, tax_number=customer.tax_number, vat_id=customer.vat_id, notes=customer.notes, ) db_customer.addresses = [ CustomerRepository._address_from_payload(address) for address in customer.addresses ] db_customer.contacts = [ CustomerRepository._contact_from_payload(contact) for contact in customer.contacts ] CustomerRepository._normalize_primary(db_customer.addresses) CustomerRepository._normalize_primary(db_customer.contacts) db.add(db_customer) db.commit() db.refresh(db_customer) return CustomerRepository.get_by_id(db, db_customer.id) or db_customer @staticmethod def update(db: Session, db_customer: Customer, customer: CustomerUpdate) -> Customer: db_customer.customer_number = customer.customer_number db_customer.company_name = customer.company_name db_customer.legal_name = customer.legal_name db_customer.customer_type = customer.customer_type db_customer.status = customer.status db_customer.industry = customer.industry db_customer.website = _url(customer.website) db_customer.email = _email(customer.email) db_customer.phone = customer.phone db_customer.tax_number = customer.tax_number db_customer.vat_id = customer.vat_id db_customer.notes = customer.notes db_customer.addresses = [ CustomerRepository._address_from_payload(address) for address in customer.addresses ] CustomerRepository._normalize_primary(db_customer.addresses) db.commit() db.refresh(db_customer) return CustomerRepository.get_by_id(db, db_customer.id) or db_customer @staticmethod def delete(db: Session, db_customer: Customer) -> None: db.delete(db_customer) db.commit() @staticmethod def get_contacts(db: Session, customer_id: int) -> list[CustomerContact]: return list( db.scalars( select(CustomerContact) .where(CustomerContact.customer_id == customer_id) .order_by(CustomerContact.is_primary.desc(), CustomerContact.last_name) ) ) @staticmethod def get_contact_by_id( db: Session, customer_id: int, contact_id: int, ) -> CustomerContact | None: return db.scalar( select(CustomerContact) .where(CustomerContact.customer_id == customer_id) .where(CustomerContact.id == contact_id) ) @staticmethod def create_contact( db: Session, customer_id: int, contact: CustomerContactCreate, ) -> CustomerContact: db_contact = CustomerRepository._contact_from_payload(contact) db_contact.customer_id = customer_id db.add(db_contact) db.flush() if db_contact.is_primary: CustomerRepository._clear_other_primary_contacts(db, customer_id, db_contact.id) db.commit() db.refresh(db_contact) return db_contact @staticmethod def update_contact( db: Session, db_contact: CustomerContact, contact: CustomerContactUpdate, ) -> CustomerContact: db_contact.first_name = contact.first_name db_contact.last_name = contact.last_name db_contact.position = contact.position db_contact.email = _email(contact.email) db_contact.phone = contact.phone db_contact.mobile = contact.mobile db_contact.is_primary = contact.is_primary db_contact.notes = contact.notes if db_contact.is_primary: CustomerRepository._clear_other_primary_contacts(db, db_contact.customer_id, db_contact.id) db.commit() db.refresh(db_contact) return db_contact @staticmethod def delete_contact(db: Session, db_contact: CustomerContact) -> None: db.delete(db_contact) db.commit() @staticmethod def _address_from_payload(address: CustomerAddressCreate | CustomerAddressUpdate) -> CustomerAddress: return CustomerAddress( type=address.type, street=address.street, postal_code=address.postal_code, city=address.city, state=address.state, country=address.country, is_primary=address.is_primary, ) @staticmethod def _contact_from_payload(contact: CustomerContactCreate | CustomerContactUpdate) -> CustomerContact: return CustomerContact( first_name=contact.first_name, last_name=contact.last_name, position=contact.position, email=_email(contact.email), phone=contact.phone, mobile=contact.mobile, is_primary=contact.is_primary, notes=contact.notes, ) @staticmethod def _normalize_primary(items: list[CustomerAddress] | list[CustomerContact]) -> None: primary_seen = False for item in items: if item.is_primary and not primary_seen: primary_seen = True elif item.is_primary: item.is_primary = False if items and not primary_seen: items[0].is_primary = True @staticmethod def _clear_other_primary_contacts(db: Session, customer_id: int, contact_id: int) -> None: contacts = db.scalars( select(CustomerContact) .where(CustomerContact.customer_id == customer_id) .where(CustomerContact.id != contact_id) ) for contact in contacts: contact.is_primary = False