diff --git a/currency_utils.py b/currency_utils.py new file mode 100644 index 0000000..3241134 --- /dev/null +++ b/currency_utils.py @@ -0,0 +1,45 @@ +# Currency conversion utilities for API boundary +from decimal import Decimal +from typing import Union + + +def gtq_to_centavos(gtq_amount: Union[float, int, str]) -> int: + """Convert GTQ to centavos for database storage""" + return int(Decimal(str(gtq_amount)) * 100) + + +def centavos_to_gtq(centavos: int) -> float: + """Convert centavos to GTQ for API responses""" + return float(centavos) / 100 + + +def format_gtq_currency(centavos: int) -> str: + """Format centavos as GTQ currency string""" + gtq_amount = centavos_to_gtq(centavos) + return f"Q{gtq_amount:.2f}" + + +# Conversion helpers for API responses +def deposit_db_to_api(deposit_db) -> dict: + """Convert database deposit model to API response""" + return { + "id": deposit_db.id, + "client_id": deposit_db.client_id, + "amount_gtq": centavos_to_gtq(deposit_db.amount), + "currency": deposit_db.currency, + "status": deposit_db.status, + "notes": deposit_db.notes, + "created_at": deposit_db.created_at, + "confirmed_at": deposit_db.confirmed_at + } + + +def balance_summary_db_to_api(balance_db) -> dict: + """Convert database balance summary to API response""" + return { + "client_id": balance_db.client_id, + "total_deposits_gtq": centavos_to_gtq(balance_db.total_deposits), + "total_payments_gtq": centavos_to_gtq(balance_db.total_payments), + "remaining_balance_gtq": centavos_to_gtq(balance_db.remaining_balance), + "currency": balance_db.currency + } \ No newline at end of file diff --git a/models.py b/models.py index 9d0e8e7..7727bee 100644 --- a/models.py +++ b/models.py @@ -34,8 +34,30 @@ class UpdateDcaClientData(BaseModel): status: Optional[str] = None -# Deposit Models +# API Models for Deposits (Frontend <-> Backend communication) +class CreateDepositAPI(BaseModel): + """API model - frontend sends GTQ amounts""" + client_id: str + amount_gtq: float # Amount in GTQ (e.g., 150.75) + currency: str = "GTQ" + notes: Optional[str] = None + + +class DepositAPI(BaseModel): + """API model - backend returns GTQ amounts""" + id: str + client_id: str + amount_gtq: float # Amount in GTQ (e.g., 150.75) + currency: str + status: str # 'pending' or 'confirmed' + notes: Optional[str] + created_at: datetime + confirmed_at: Optional[datetime] + + +# Database Models for Deposits (Internal storage in centavos) class CreateDepositData(BaseModel): + """Internal model - database stores centavos""" client_id: str amount: int # Amount in smallest currency unit (centavos for GTQ) currency: str = "GTQ" @@ -43,6 +65,7 @@ class CreateDepositData(BaseModel): class DcaDeposit(BaseModel): + """Internal model - database stores centavos""" id: str client_id: str amount: int @@ -84,8 +107,19 @@ class DcaPayment(BaseModel): transaction_time: Optional[datetime] = None # Original ATM transaction time -# Client Balance Summary +# API Models for Client Balance Summary +class ClientBalanceSummaryAPI(BaseModel): + """API model - returns GTQ amounts""" + client_id: str + total_deposits_gtq: float # Total confirmed deposits in GTQ + total_payments_gtq: float # Total payments made in GTQ + remaining_balance_gtq: float # Available balance for DCA in GTQ + currency: str + + +# Internal Models for Client Balance Summary class ClientBalanceSummary(BaseModel): + """Internal model - stores centavos""" client_id: str total_deposits: int # Total confirmed deposits total_payments: int # Total payments made diff --git a/static/js/index.js b/static/js/index.js index cb3e281..6178a02 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -27,7 +27,7 @@ window.app = Vue.createApp({ depositsTable: { columns: [ { name: 'client_id', align: 'left', label: 'Client', field: 'client_id' }, - { name: 'amount', align: 'left', label: 'Amount', field: 'amount' }, + { name: 'amount_gtq', align: 'left', label: 'Amount', field: 'amount_gtq' }, { name: 'currency', align: 'left', label: 'Currency', field: 'currency' }, { name: 'status', align: 'left', label: 'Status', field: 'status' }, { name: 'created_at', align: 'left', label: 'Created', field: 'created_at' }, @@ -130,17 +130,15 @@ window.app = Vue.createApp({ /////////////////////////////////////////////////// methods: { - // Utility Methods + // Utility Methods - Simplified since API handles conversion formatCurrency(amount) { if (!amount) return 'Q 0.00'; - // Convert centavos to GTQ for display - const gtqAmount = amount / 100; - + // Amount is already in GTQ from API return new Intl.NumberFormat('es-GT', { style: 'currency', currency: 'GTQ', - }).format(gtqAmount); + }).format(amount); }, formatDate(dateString) { @@ -281,7 +279,7 @@ window.app = Vue.createApp({ ) return { ...client, - remaining_balance: balance.remaining_balance + remaining_balance: balance.remaining_balance_gtq } } catch (error) { console.error(`Error fetching balance for client ${client.id}:`, error) @@ -305,7 +303,7 @@ window.app = Vue.createApp({ try { const data = { client_id: this.quickDepositForm.selectedClient?.value, - amount: Math.round(this.quickDepositForm.amount * 100), // Convert GTQ to centavos + amount_gtq: this.quickDepositForm.amount, // Send GTQ directly - API handles conversion currency: 'GTQ', notes: this.quickDepositForm.notes } @@ -378,7 +376,7 @@ window.app = Vue.createApp({ try { const data = { client_id: this.depositFormDialog.data.client_id, - amount: Math.round(this.depositFormDialog.data.amount * 100), // Convert GTQ to centavos + amount_gtq: this.depositFormDialog.data.amount, // Send GTQ directly - API handles conversion currency: this.depositFormDialog.data.currency, notes: this.depositFormDialog.data.notes } diff --git a/templates/satmachineadmin/index.html b/templates/satmachineadmin/index.html index 61099ce..8901460 100644 --- a/templates/satmachineadmin/index.html +++ b/templates/satmachineadmin/index.html @@ -173,7 +173,7 @@
${ getClientUsername(col.value) }
-
${ formatCurrency(col.value) }
+
${ formatCurrency(col.value) }
${ col.value } @@ -470,9 +470,9 @@ Balance Summary - Deposits: ${ formatCurrency(clientDetailsDialog.balance.total_deposits) } | - Payments: ${ formatCurrency(clientDetailsDialog.balance.total_payments) } | - Remaining: ${ formatCurrency(clientDetailsDialog.balance.remaining_balance) } + Deposits: ${ formatCurrency(clientDetailsDialog.balance.total_deposits_gtq) } | + Payments: ${ formatCurrency(clientDetailsDialog.balance.total_payments_gtq) } | + Remaining: ${ formatCurrency(clientDetailsDialog.balance.remaining_balance_gtq) } diff --git a/views_api.py b/views_api.py index 3700497..9d3c41a 100644 --- a/views_api.py +++ b/views_api.py @@ -38,14 +38,23 @@ from .models import ( DcaClient, UpdateDcaClientData, CreateDepositData, + CreateDepositAPI, + DepositAPI, DcaDeposit, UpdateDepositStatusData, ClientBalanceSummary, + ClientBalanceSummaryAPI, CreateLamassuConfigData, LamassuConfig, UpdateLamassuConfigData, StoredLamassuTransaction, ) +from .currency_utils import ( + gtq_to_centavos, + centavos_to_gtq, + deposit_db_to_api, + balance_summary_db_to_api, +) satmachineadmin_api_router = APIRouter() @@ -87,7 +96,7 @@ async def api_get_dca_client( async def api_get_client_balance( client_id: str, wallet: WalletTypeInfo = Depends(check_super_user), -) -> ClientBalanceSummary: +) -> ClientBalanceSummaryAPI: """Get client balance summary""" client = await get_dca_client(client_id) if not client: @@ -95,7 +104,8 @@ async def api_get_client_balance( status_code=HTTPStatus.NOT_FOUND, detail="DCA client not found." ) - return await get_client_balance_summary(client_id) + balance_db = await get_client_balance_summary(client_id) + return ClientBalanceSummaryAPI(**balance_summary_db_to_api(balance_db)) # DCA Deposit Endpoints @@ -104,30 +114,31 @@ async def api_get_client_balance( @satmachineadmin_api_router.get("/api/v1/dca/deposits") async def api_get_deposits( wallet: WalletTypeInfo = Depends(check_super_user), -) -> list[DcaDeposit]: +) -> list[DepositAPI]: """Get all deposits""" - return await get_all_deposits() + deposits_db = await get_all_deposits() + return [DepositAPI(**deposit_db_to_api(deposit)) for deposit in deposits_db] @satmachineadmin_api_router.get("/api/v1/dca/deposits/{deposit_id}") async def api_get_deposit( deposit_id: str, wallet: WalletTypeInfo = Depends(check_super_user), -) -> DcaDeposit: +) -> DepositAPI: """Get a specific deposit""" - deposit = await get_deposit(deposit_id) - if not deposit: + deposit_db = await get_deposit(deposit_id) + if not deposit_db: raise HTTPException( status_code=HTTPStatus.NOT_FOUND, detail="Deposit not found." ) - return deposit + return DepositAPI(**deposit_db_to_api(deposit_db)) @satmachineadmin_api_router.post("/api/v1/dca/deposits", status_code=HTTPStatus.CREATED) async def api_create_deposit( - data: CreateDepositData, + data: CreateDepositAPI, user: User = Depends(check_super_user), -) -> DcaDeposit: +) -> DepositAPI: """Create a new deposit""" # Verify client exists client = await get_dca_client(data.client_id) @@ -136,7 +147,16 @@ async def api_create_deposit( status_code=HTTPStatus.NOT_FOUND, detail="DCA client not found." ) - return await create_deposit(data) + # Convert GTQ to centavos at API boundary + deposit_data = CreateDepositData( + client_id=data.client_id, + amount=gtq_to_centavos(data.amount_gtq), + currency=data.currency, + notes=data.notes + ) + + deposit_db = await create_deposit(deposit_data) + return DepositAPI(**deposit_db_to_api(deposit_db)) @satmachineadmin_api_router.put("/api/v1/dca/deposits/{deposit_id}/status") @@ -144,7 +164,7 @@ async def api_update_deposit_status( deposit_id: str, data: UpdateDepositStatusData, user: User = Depends(check_super_user), -) -> DcaDeposit: +) -> DepositAPI: """Update deposit status (e.g., confirm deposit)""" deposit = await get_deposit(deposit_id) if not deposit: @@ -152,13 +172,13 @@ async def api_update_deposit_status( status_code=HTTPStatus.NOT_FOUND, detail="Deposit not found." ) - updated_deposit = await update_deposit_status(deposit_id, data) - if not updated_deposit: + updated_deposit_db = await update_deposit_status(deposit_id, data) + if not updated_deposit_db: raise HTTPException( status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Failed to update deposit.", ) - return updated_deposit + return DepositAPI(**deposit_db_to_api(updated_deposit_db)) # Transaction Polling Endpoints