feat(customers): add dashboard and customer management
This commit is contained in:
parent
694b7bd09a
commit
92cb8d1286
28 changed files with 2521 additions and 13 deletions
269
backend/hermes/app/repositories/customer_repository.py
Normal file
269
backend/hermes/app/repositories/customer_repository.py
Normal file
|
|
@ -0,0 +1,269 @@
|
|||
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
|
||||
Loading…
Add table
Add a link
Reference in a new issue