first commit

This commit is contained in:
debu-2 2026-10-02 11:22:40 +05:30
commit efa5080897
127 changed files with 18610 additions and 0 deletions

View file

@ -0,0 +1,359 @@
from __future__ import annotations
from datetime import datetime, timedelta, timezone
from typing import Literal
from sqlalchemy import case, func, select
from sqlalchemy.orm import Session
from .models import OtpMessageLog, PhoneNumber, SmsMessageLog, UserAccount, VoiceCall, VoiceSocketEndpoint
Period = Literal["weekly", "monthly", "quarterly"]
Service = Literal["sms", "inbound", "outbound", "otp"]
GroupBy = Literal["dlt_sender", "dlt_template", "did", "socket", "project", "otp_type"]
PERIOD_BUCKETS = {
"weekly": ("week", 12),
"monthly": ("month", 12),
"quarterly": ("quarter", 4),
}
SERVICE_GROUP_BY: dict[str, frozenset[str]] = {
"sms": frozenset({"dlt_sender", "dlt_template"}),
"inbound": frozenset({"did"}),
"outbound": frozenset({"socket", "did"}),
"otp": frozenset({"project", "otp_type"}),
}
DEFAULT_GROUP_BY: dict[str, str] = {
"sms": "dlt_sender",
"inbound": "did",
"outbound": "socket",
"otp": "project",
}
def _truncate_start(value: datetime, unit: str) -> datetime:
value = value.astimezone(timezone.utc)
if unit == "week":
start = value - timedelta(days=value.weekday())
return start.replace(hour=0, minute=0, second=0, microsecond=0)
if unit == "month":
return value.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
quarter_month = ((value.month - 1) // 3) * 3 + 1
return value.replace(month=quarter_month, day=1, hour=0, minute=0, second=0, microsecond=0)
def _shift_bucket(start: datetime, unit: str, offset: int) -> datetime:
if unit == "week":
return start + timedelta(weeks=offset)
if unit == "month":
month = start.month + offset
year = start.year + (month - 1) // 12
month = ((month - 1) % 12) + 1
return start.replace(year=year, month=month, day=1)
month = start.month + (offset * 3)
year = start.year + (month - 1) // 12
month = ((month - 1) % 12) + 1
return start.replace(year=year, month=month, day=1)
def _format_bucket_label(start: datetime, unit: str) -> str:
if unit == "week":
return start.strftime("%d %b")
if unit == "month":
return start.strftime("%b %Y")
quarter = ((start.month - 1) // 3) + 1
return f"Q{quarter} {start.year}"
def _build_bucket_keys(end: datetime, unit: str, count: int) -> list[tuple[datetime, str]]:
current = _truncate_start(end, unit)
keys: list[tuple[datetime, str]] = []
for offset in range(-(count - 1), 1):
start = _shift_bucket(current, unit, offset)
keys.append((start, _format_bucket_label(start, unit)))
return keys
def _bucket_key(value: datetime, unit: str) -> str:
return _truncate_start(value, unit).isoformat()
def _bucket_map(rows: list[tuple[datetime, int]], unit: str) -> dict[str, int]:
mapped: dict[str, int] = {}
for bucket, count in rows:
if bucket.tzinfo is None:
bucket = bucket.replace(tzinfo=timezone.utc)
mapped[_bucket_key(bucket.astimezone(timezone.utc), unit)] = int(count)
return mapped
def _breakdown_item(label: str, detail: str, count: int) -> dict:
return {"label": label, "detail": detail, "count": int(count)}
def _count_by_bucket(
db: Session,
*,
model,
user_id,
start: datetime,
end: datetime,
unit: str,
extra_filters=(),
) -> dict[str, int]:
bucket = func.date_trunc(unit, model.created_at).label("bucket")
rows = db.execute(
select(bucket, func.count())
.where(
model.user_id == user_id,
model.created_at >= start,
model.created_at <= end,
*extra_filters,
)
.group_by(bucket)
.order_by(bucket)
).all()
return _bucket_map(rows, unit)
def _voice_bucket_map(
db: Session,
*,
user_id,
direction: str,
start: datetime,
end: datetime,
unit: str,
) -> dict[str, int]:
bucket = func.date_trunc(unit, VoiceCall.created_at).label("bucket")
rows = db.execute(
select(bucket, func.count())
.where(
VoiceCall.user_id == user_id,
VoiceCall.direction == direction,
VoiceCall.created_at >= start,
VoiceCall.created_at <= end,
)
.group_by(bucket)
.order_by(bucket)
).all()
return _bucket_map(rows, unit)
def _breakdown_sms_sender(db: Session, user_id, start: datetime, end: datetime) -> list[dict]:
rows = db.execute(
select(SmsMessageLog.sender_id, func.count())
.where(
SmsMessageLog.user_id == user_id,
SmsMessageLog.created_at >= start,
SmsMessageLog.created_at <= end,
)
.group_by(SmsMessageLog.sender_id)
.order_by(func.count().desc())
.limit(10)
).all()
return [_breakdown_item(sender_id, "DLT sender header", count) for sender_id, count in rows]
def _breakdown_sms_template(db: Session, user_id, start: datetime, end: datetime) -> list[dict]:
rows = db.execute(
select(SmsMessageLog.template_name, func.count())
.where(
SmsMessageLog.user_id == user_id,
SmsMessageLog.created_at >= start,
SmsMessageLog.created_at <= end,
)
.group_by(SmsMessageLog.template_name)
.order_by(func.count().desc())
.limit(10)
).all()
return [_breakdown_item(template_name, "DLT template", count) for template_name, count in rows]
def _breakdown_inbound_did(db: Session, user_id, start: datetime, end: datetime) -> list[dict]:
rows = db.execute(
select(VoiceCall.from_did, func.count())
.where(
VoiceCall.user_id == user_id,
VoiceCall.direction == "inbound",
VoiceCall.created_at >= start,
VoiceCall.created_at <= end,
)
.group_by(VoiceCall.from_did)
.order_by(func.count().desc())
.limit(10)
).all()
return [_breakdown_item(did, "Your Urmi number", count) for did, count in rows]
def _breakdown_outbound_did(db: Session, user_id, start: datetime, end: datetime) -> list[dict]:
rows = db.execute(
select(VoiceCall.from_did, func.count())
.where(
VoiceCall.user_id == user_id,
VoiceCall.direction == "outbound",
VoiceCall.created_at >= start,
VoiceCall.created_at <= end,
)
.group_by(VoiceCall.from_did)
.order_by(func.count().desc())
.limit(10)
).all()
return [_breakdown_item(did, "Caller ID used", count) for did, count in rows]
def _breakdown_outbound_socket(db: Session, user_id, start: datetime, end: datetime) -> list[dict]:
socket_name = VoiceSocketEndpoint.name
phone_number = PhoneNumber.phone_number
label = case(
(socket_name.isnot(None), socket_name),
else_="No voice socket linked",
).label("label")
detail = phone_number.label("detail")
rows = db.execute(
select(label, detail, func.count())
.select_from(VoiceCall)
.join(PhoneNumber, VoiceCall.phone_number_id == PhoneNumber.id)
.outerjoin(
VoiceSocketEndpoint,
(VoiceSocketEndpoint.phone_number_id == VoiceCall.phone_number_id)
& (VoiceSocketEndpoint.user_id == VoiceCall.user_id),
)
.where(
VoiceCall.user_id == user_id,
VoiceCall.direction == "outbound",
VoiceCall.created_at >= start,
VoiceCall.created_at <= end,
)
.group_by(label, detail)
.order_by(func.count().desc())
.limit(10)
).all()
return [
_breakdown_item(
name,
f"Linked to {number}" if name != "No voice socket linked" else f"Number {number}",
count,
)
for name, number, count in rows
]
def _breakdown_otp_project(db: Session, user_id, start: datetime, end: datetime) -> list[dict]:
rows = db.execute(
select(OtpMessageLog.project_name, func.count())
.where(
OtpMessageLog.user_id == user_id,
OtpMessageLog.created_at >= start,
OtpMessageLog.created_at <= end,
)
.group_by(OtpMessageLog.project_name)
.order_by(func.count().desc())
.limit(10)
).all()
return [_breakdown_item(project, "UrmiOTP project", count) for project, count in rows]
def _breakdown_otp_type(db: Session, user_id, start: datetime, end: datetime) -> list[dict]:
rows = db.execute(
select(OtpMessageLog.template_type, func.count())
.where(
OtpMessageLog.user_id == user_id,
OtpMessageLog.created_at >= start,
OtpMessageLog.created_at <= end,
)
.group_by(OtpMessageLog.template_type)
.order_by(func.count().desc())
.limit(10)
).all()
return [_breakdown_item(template_type.replace("_", " ").title(), "OTP template type", count) for template_type, count in rows]
def _build_breakdown(
db: Session,
user_id,
*,
service: Service,
group_by: GroupBy,
start: datetime,
end: datetime,
) -> list[dict]:
builders = {
("sms", "dlt_sender"): _breakdown_sms_sender,
("sms", "dlt_template"): _breakdown_sms_template,
("inbound", "did"): _breakdown_inbound_did,
("outbound", "did"): _breakdown_outbound_did,
("outbound", "socket"): _breakdown_outbound_socket,
("otp", "project"): _breakdown_otp_project,
("otp", "otp_type"): _breakdown_otp_type,
}
builder = builders.get((service, group_by))
if not builder:
return []
return builder(db, user_id, start, end)
def build_usage_analytics(
db: Session,
user: UserAccount,
*,
period: Period,
service: Service,
group_by: GroupBy,
) -> dict:
if group_by not in SERVICE_GROUP_BY[service]:
group_by = DEFAULT_GROUP_BY[service] # type: ignore[assignment]
unit, bucket_count = PERIOD_BUCKETS[period]
end = datetime.now(timezone.utc)
first_bucket_start = _build_bucket_keys(end, unit, bucket_count)[0][0]
start = first_bucket_start
sms_map = _count_by_bucket(db, model=SmsMessageLog, user_id=user.id, start=start, end=end, unit=unit)
otp_map = _count_by_bucket(db, model=OtpMessageLog, user_id=user.id, start=start, end=end, unit=unit)
inbound_map = _voice_bucket_map(db, user_id=user.id, direction="inbound", start=start, end=end, unit=unit)
outbound_map = _voice_bucket_map(db, user_id=user.id, direction="outbound", start=start, end=end, unit=unit)
timeline = []
for bucket_start, label in _build_bucket_keys(end, unit, bucket_count):
key = _bucket_key(bucket_start, unit)
timeline.append(
{
"label": label,
"period_start": bucket_start,
"sms": sms_map.get(key, 0),
"inbound": inbound_map.get(key, 0),
"outbound": outbound_map.get(key, 0),
"otp": otp_map.get(key, 0),
}
)
totals = {
"sms": sum(item["sms"] for item in timeline),
"inbound": sum(item["inbound"] for item in timeline),
"outbound": sum(item["outbound"] for item in timeline),
"otp": sum(item["otp"] for item in timeline),
}
breakdown = _build_breakdown(
db,
user.id,
service=service,
group_by=group_by,
start=start,
end=end,
)
return {
"period": period,
"service": service,
"group_by": group_by,
"from": start,
"to": end,
"totals": totals,
"timeline": timeline,
"breakdown": breakdown,
}