first commit
This commit is contained in:
commit
34ea15a54d
246 changed files with 18660 additions and 0 deletions
0
backend/app/services/admin/__init__.py
Normal file
0
backend/app/services/admin/__init__.py
Normal file
126
backend/app/services/admin/catalog.py
Normal file
126
backend/app/services/admin/catalog.py
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.errors import AppError
|
||||
from app.models.catalog import Event, TicketType, Venue
|
||||
from app.models.enums import TicketTypeStatus
|
||||
from app.schemas.admin import (
|
||||
EventCreate,
|
||||
EventUpdate,
|
||||
TicketTypeCreate,
|
||||
TicketTypeUpdate,
|
||||
VenueCreate,
|
||||
VenueUpdate,
|
||||
)
|
||||
|
||||
|
||||
def list_venues_admin(db: Session) -> list[Venue]:
|
||||
return list(db.scalars(select(Venue).order_by(Venue.name.asc())).all())
|
||||
|
||||
|
||||
def create_venue(db: Session, body: VenueCreate) -> Venue:
|
||||
if db.scalar(select(Venue.id).where(Venue.slug == body.slug)):
|
||||
raise AppError("Venue slug already exists", status_code=409)
|
||||
venue = Venue(**body.model_dump())
|
||||
db.add(venue)
|
||||
db.commit()
|
||||
db.refresh(venue)
|
||||
return venue
|
||||
|
||||
|
||||
def update_venue(db: Session, venue_id: int, body: VenueUpdate) -> Venue:
|
||||
venue = db.get(Venue, venue_id)
|
||||
if not venue:
|
||||
raise AppError("Venue not found", status_code=404)
|
||||
data = body.model_dump(exclude_unset=True)
|
||||
if "slug" in data and data["slug"] != venue.slug:
|
||||
if db.scalar(select(Venue.id).where(Venue.slug == data["slug"])):
|
||||
raise AppError("Venue slug already exists", status_code=409)
|
||||
for key, value in data.items():
|
||||
setattr(venue, key, value)
|
||||
db.commit()
|
||||
db.refresh(venue)
|
||||
return venue
|
||||
|
||||
|
||||
def list_events_admin(db: Session) -> list[Event]:
|
||||
return list(db.scalars(select(Event).order_by(Event.starts_at.desc())).all())
|
||||
|
||||
|
||||
def create_event(db: Session, body: EventCreate) -> Event:
|
||||
if not db.get(Venue, body.venue_id):
|
||||
raise AppError("Venue not found", status_code=400)
|
||||
if db.scalar(select(Event.id).where(Event.slug == body.slug)):
|
||||
raise AppError("Event slug already exists", status_code=409)
|
||||
event = Event(**body.model_dump())
|
||||
db.add(event)
|
||||
db.commit()
|
||||
db.refresh(event)
|
||||
return event
|
||||
|
||||
|
||||
def update_event(db: Session, event_id: int, body: EventUpdate) -> Event:
|
||||
event = db.get(Event, event_id)
|
||||
if not event:
|
||||
raise AppError("Event not found", status_code=404)
|
||||
data = body.model_dump(exclude_unset=True)
|
||||
if "venue_id" in data and not db.get(Venue, data["venue_id"]):
|
||||
raise AppError("Venue not found", status_code=400)
|
||||
if "slug" in data and data["slug"] != event.slug:
|
||||
if db.scalar(select(Event.id).where(Event.slug == data["slug"])):
|
||||
raise AppError("Event slug already exists", status_code=409)
|
||||
for key, value in data.items():
|
||||
setattr(event, key, value)
|
||||
db.commit()
|
||||
db.refresh(event)
|
||||
return event
|
||||
|
||||
|
||||
def ticket_type_to_admin(tier: TicketType) -> dict:
|
||||
return {
|
||||
"id": tier.id,
|
||||
"event_id": tier.event_id,
|
||||
"name": tier.name,
|
||||
"description": tier.description,
|
||||
"price_paise": tier.price_paise,
|
||||
"quantity_total": tier.quantity_total,
|
||||
"quantity_sold": tier.quantity_sold,
|
||||
"remaining": max(tier.quantity_total - tier.quantity_sold, 0),
|
||||
"status": tier.status,
|
||||
"sort_order": tier.sort_order,
|
||||
}
|
||||
|
||||
|
||||
def list_ticket_types(db: Session, event_id: int | None = None) -> list[dict]:
|
||||
query = select(TicketType).order_by(TicketType.event_id.asc(), TicketType.sort_order.asc())
|
||||
if event_id is not None:
|
||||
query = query.where(TicketType.event_id == event_id)
|
||||
return [ticket_type_to_admin(row) for row in db.scalars(query).all()]
|
||||
|
||||
|
||||
def create_ticket_type(db: Session, body: TicketTypeCreate) -> TicketType:
|
||||
if not db.get(Event, body.event_id):
|
||||
raise AppError("Event not found", status_code=400)
|
||||
tier = TicketType(**body.model_dump())
|
||||
db.add(tier)
|
||||
db.commit()
|
||||
db.refresh(tier)
|
||||
return tier
|
||||
|
||||
|
||||
def update_ticket_type(db: Session, tier_id: int, body: TicketTypeUpdate) -> TicketType:
|
||||
tier = db.get(TicketType, tier_id)
|
||||
if not tier:
|
||||
raise AppError("Ticket type not found", status_code=404)
|
||||
data = body.model_dump(exclude_unset=True)
|
||||
if "quantity_total" in data and data["quantity_total"] < tier.quantity_sold:
|
||||
raise AppError("Total quantity cannot be less than sold count", status_code=400)
|
||||
for key, value in data.items():
|
||||
setattr(tier, key, value)
|
||||
if tier.quantity_sold >= tier.quantity_total:
|
||||
tier.status = TicketTypeStatus.sold_out
|
||||
elif tier.status == TicketTypeStatus.sold_out and tier.quantity_sold < tier.quantity_total:
|
||||
tier.status = TicketTypeStatus.active
|
||||
db.commit()
|
||||
db.refresh(tier)
|
||||
return tier
|
||||
82
backend/app/services/admin/dashboard.py
Normal file
82
backend/app/services/admin/dashboard.py
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.models.catalog import Event, TicketType
|
||||
from app.models.enums import EventStatus, OrderStatus, TicketStatus
|
||||
from app.models.orders import Order, Ticket
|
||||
from app.schemas.admin import DashboardMetrics, EventStatsRow
|
||||
|
||||
|
||||
def get_dashboard_metrics(db: Session) -> DashboardMetrics:
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
revenue = db.scalar(
|
||||
select(func.coalesce(func.sum(Order.total_paise), 0)).where(Order.status == OrderStatus.paid)
|
||||
) or 0
|
||||
|
||||
tickets_sold = db.scalar(select(func.coalesce(func.sum(TicketType.quantity_sold), 0))) or 0
|
||||
tickets_total = db.scalar(select(func.coalesce(func.sum(TicketType.quantity_total), 0))) or 0
|
||||
tickets_remaining = max(int(tickets_total) - int(tickets_sold), 0)
|
||||
|
||||
orders_total = db.scalar(select(func.count(Order.id))) or 0
|
||||
orders_paid = db.scalar(select(func.count(Order.id)).where(Order.status == OrderStatus.paid)) or 0
|
||||
orders_pending = db.scalar(select(func.count(Order.id)).where(Order.status == OrderStatus.pending)) or 0
|
||||
|
||||
attendance = db.scalar(select(func.count(Ticket.id)).where(Ticket.status == TicketStatus.used)) or 0
|
||||
|
||||
upcoming_events = (
|
||||
db.scalar(
|
||||
select(func.count(Event.id)).where(
|
||||
Event.status == EventStatus.published,
|
||||
Event.starts_at >= now,
|
||||
)
|
||||
)
|
||||
or 0
|
||||
)
|
||||
|
||||
return DashboardMetrics(
|
||||
total_revenue_paise=int(revenue),
|
||||
tickets_sold=int(tickets_sold),
|
||||
tickets_remaining=tickets_remaining,
|
||||
orders_total=int(orders_total),
|
||||
orders_paid=int(orders_paid),
|
||||
orders_pending=int(orders_pending),
|
||||
attendance=int(attendance),
|
||||
upcoming_events=int(upcoming_events),
|
||||
)
|
||||
|
||||
|
||||
def get_event_statistics(db: Session) -> list[EventStatsRow]:
|
||||
events = db.scalars(select(Event).order_by(Event.starts_at.desc())).all()
|
||||
rows: list[EventStatsRow] = []
|
||||
|
||||
for event in events:
|
||||
tiers = db.scalars(select(TicketType).where(TicketType.event_id == event.id)).all()
|
||||
sold = sum(t.quantity_sold for t in tiers)
|
||||
total = sum(t.quantity_total for t in tiers)
|
||||
revenue = db.scalar(
|
||||
select(func.coalesce(func.sum(Order.total_paise), 0)).where(
|
||||
Order.event_id == event.id,
|
||||
Order.status == OrderStatus.paid,
|
||||
)
|
||||
) or 0
|
||||
attendance = db.scalar(
|
||||
select(func.count(Ticket.id)).where(
|
||||
Ticket.event_id == event.id,
|
||||
Ticket.status == TicketStatus.used,
|
||||
)
|
||||
) or 0
|
||||
rows.append(
|
||||
EventStatsRow(
|
||||
event_id=event.id,
|
||||
event_title=event.title,
|
||||
starts_at=event.starts_at,
|
||||
tickets_sold=sold,
|
||||
tickets_remaining=max(total - sold, 0),
|
||||
revenue_paise=int(revenue),
|
||||
attendance=int(attendance),
|
||||
)
|
||||
)
|
||||
return rows
|
||||
40
backend/app/services/admin/orders.py
Normal file
40
backend/app/services/admin/orders.py
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.errors import AppError
|
||||
from app.models.enums import OrderStatus
|
||||
from app.models.orders import Order
|
||||
|
||||
|
||||
def list_orders_admin(db: Session, status: OrderStatus | None = None) -> list[Order]:
|
||||
query = select(Order).order_by(Order.created_at.desc())
|
||||
if status is not None:
|
||||
query = query.where(Order.status == status)
|
||||
return list(db.scalars(query).all())
|
||||
|
||||
|
||||
def get_order_admin(db: Session, order_id: int) -> Order:
|
||||
order = db.get(Order, order_id)
|
||||
if not order:
|
||||
raise AppError("Order not found", status_code=404)
|
||||
return order
|
||||
|
||||
|
||||
def list_payments_admin(db: Session) -> list[dict]:
|
||||
orders = db.scalars(
|
||||
select(Order)
|
||||
.where(Order.status.in_([OrderStatus.paid, OrderStatus.pending, OrderStatus.failed]))
|
||||
.order_by(Order.created_at.desc())
|
||||
).all()
|
||||
return [
|
||||
{
|
||||
"order_id": order.id,
|
||||
"event_id": order.event_id,
|
||||
"status": order.status,
|
||||
"total_paise": order.total_paise,
|
||||
"provider_payment_id": order.provider_payment_id,
|
||||
"payment_reference": order.payment_reference,
|
||||
"paid_at": order.paid_at,
|
||||
}
|
||||
for order in orders
|
||||
]
|
||||
Loading…
Add table
Add a link
Reference in a new issue