2026-04-03 19:04:46 +08:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
2026-04-28 17:19:08 +08:00
|
|
|
from datetime import datetime
|
2026-04-03 19:04:46 +08:00
|
|
|
from typing import Annotated
|
|
|
|
|
|
2026-04-28 17:19:08 +08:00
|
|
|
from fastapi import APIRouter, Depends, Query
|
2026-04-03 19:04:46 +08:00
|
|
|
|
|
|
|
|
from core.auth.models import CurrentUser
|
2026-04-28 17:19:08 +08:00
|
|
|
from core.http.errors import ApiProblemError, problem_payload
|
2026-04-03 19:04:46 +08:00
|
|
|
from v1.points.dependencies import get_points_service
|
2026-04-28 17:19:08 +08:00
|
|
|
from v1.points.schemas import (
|
|
|
|
|
PackagesResponse,
|
|
|
|
|
PackageInfo,
|
|
|
|
|
PointsBalanceResponse,
|
|
|
|
|
LedgerListResponse,
|
|
|
|
|
LedgerItem,
|
|
|
|
|
)
|
2026-04-03 19:04:46 +08:00
|
|
|
from v1.points.service import PointsService
|
|
|
|
|
from v1.users.dependencies import get_current_user
|
|
|
|
|
|
|
|
|
|
router = APIRouter(prefix="/points", tags=["points"])
|
|
|
|
|
|
|
|
|
|
|
2026-04-28 17:19:08 +08:00
|
|
|
def _parse_cursor(cursor: str | None) -> datetime | None:
|
|
|
|
|
if cursor is None:
|
|
|
|
|
return None
|
|
|
|
|
try:
|
|
|
|
|
return datetime.fromisoformat(cursor.replace("Z", "+00:00"))
|
|
|
|
|
except ValueError as exc:
|
|
|
|
|
raise ApiProblemError(
|
|
|
|
|
status_code=422,
|
|
|
|
|
detail=problem_payload(
|
|
|
|
|
code="POINTS_INVALID_CURSOR",
|
|
|
|
|
detail="Points ledger cursor must be an ISO 8601 datetime",
|
|
|
|
|
params={"cursor": cursor},
|
|
|
|
|
),
|
|
|
|
|
) from exc
|
|
|
|
|
|
|
|
|
|
|
2026-04-03 19:04:46 +08:00
|
|
|
@router.get("/balance", response_model=PointsBalanceResponse)
|
|
|
|
|
async def get_points_balance(
|
|
|
|
|
service: Annotated[PointsService, Depends(get_points_service)],
|
|
|
|
|
current_user: Annotated[CurrentUser, Depends(get_current_user)],
|
|
|
|
|
) -> PointsBalanceResponse:
|
|
|
|
|
result = await service.get_points_balance(user_id=current_user.id)
|
|
|
|
|
return PointsBalanceResponse(
|
|
|
|
|
balance=result.balance,
|
|
|
|
|
frozenBalance=result.frozen_balance,
|
|
|
|
|
availableBalance=result.available_balance,
|
|
|
|
|
runCost=result.run_cost,
|
|
|
|
|
canRun=result.can_run,
|
|
|
|
|
)
|
2026-04-16 16:11:09 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/packages", response_model=PackagesResponse)
|
|
|
|
|
async def get_available_packages(
|
|
|
|
|
service: Annotated[PointsService, Depends(get_points_service)],
|
|
|
|
|
current_user: Annotated[CurrentUser, Depends(get_current_user)],
|
|
|
|
|
) -> PackagesResponse:
|
|
|
|
|
result = await service.get_available_packages(
|
|
|
|
|
user_id=current_user.id,
|
|
|
|
|
user_email=current_user.email or "",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return PackagesResponse(
|
|
|
|
|
packages=[
|
|
|
|
|
PackageInfo(
|
|
|
|
|
productCode=pkg.product_code,
|
2026-04-28 10:45:29 +08:00
|
|
|
appStoreProductId=pkg.app_store_product_id,
|
2026-04-28 17:31:24 +08:00
|
|
|
type=pkg.type,
|
2026-04-16 16:11:09 +08:00
|
|
|
credits=pkg.credits,
|
|
|
|
|
isStarter=pkg.is_starter,
|
|
|
|
|
starterEligible=pkg.starter_eligible,
|
|
|
|
|
sortOrder=pkg.sort_order,
|
|
|
|
|
)
|
|
|
|
|
for pkg in result.packages
|
|
|
|
|
],
|
|
|
|
|
)
|
2026-04-28 17:19:08 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/ledger", response_model=LedgerListResponse)
|
|
|
|
|
async def get_points_ledger(
|
|
|
|
|
service: Annotated[PointsService, Depends(get_points_service)],
|
|
|
|
|
current_user: Annotated[CurrentUser, Depends(get_current_user)],
|
|
|
|
|
limit: Annotated[int, Query(ge=1, le=100)] = 20,
|
|
|
|
|
cursor: str | None = None,
|
|
|
|
|
) -> LedgerListResponse:
|
|
|
|
|
items, next_cursor, has_more = await service.get_ledger_list(
|
|
|
|
|
user_id=current_user.id,
|
|
|
|
|
limit=limit,
|
|
|
|
|
cursor=_parse_cursor(cursor),
|
|
|
|
|
)
|
|
|
|
|
return LedgerListResponse(
|
|
|
|
|
items=[
|
|
|
|
|
LedgerItem(
|
|
|
|
|
id=item.id,
|
|
|
|
|
direction=item.direction,
|
|
|
|
|
amount=item.amount,
|
|
|
|
|
balanceAfter=item.balance_after,
|
|
|
|
|
changeType=item.change_type,
|
|
|
|
|
createdAt=item.created_at,
|
|
|
|
|
)
|
|
|
|
|
for item in items
|
|
|
|
|
],
|
|
|
|
|
nextCursor=next_cursor,
|
|
|
|
|
hasMore=has_more,
|
|
|
|
|
)
|