00 Add currency conversion utilities and update models for GTQ handling: Introduced currency conversion functions for GTQ and centavos, updated API and database models to handle GTQ amounts directly, and modified API endpoints to streamline deposit and balance summary responses. Adjusted frontend to reflect changes in currency representation, ensuring consistency across the application.
This commit is contained in:
parent
7b40fcef21
commit
aa71321c84
5 changed files with 127 additions and 30 deletions
45
currency_utils.py
Normal file
45
currency_utils.py
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
38
models.py
38
models.py
|
|
@ -34,8 +34,30 @@ class UpdateDcaClientData(BaseModel):
|
||||||
status: Optional[str] = None
|
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):
|
class CreateDepositData(BaseModel):
|
||||||
|
"""Internal model - database stores centavos"""
|
||||||
client_id: str
|
client_id: str
|
||||||
amount: int # Amount in smallest currency unit (centavos for GTQ)
|
amount: int # Amount in smallest currency unit (centavos for GTQ)
|
||||||
currency: str = "GTQ"
|
currency: str = "GTQ"
|
||||||
|
|
@ -43,6 +65,7 @@ class CreateDepositData(BaseModel):
|
||||||
|
|
||||||
|
|
||||||
class DcaDeposit(BaseModel):
|
class DcaDeposit(BaseModel):
|
||||||
|
"""Internal model - database stores centavos"""
|
||||||
id: str
|
id: str
|
||||||
client_id: str
|
client_id: str
|
||||||
amount: int
|
amount: int
|
||||||
|
|
@ -84,8 +107,19 @@ class DcaPayment(BaseModel):
|
||||||
transaction_time: Optional[datetime] = None # Original ATM transaction time
|
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):
|
class ClientBalanceSummary(BaseModel):
|
||||||
|
"""Internal model - stores centavos"""
|
||||||
client_id: str
|
client_id: str
|
||||||
total_deposits: int # Total confirmed deposits
|
total_deposits: int # Total confirmed deposits
|
||||||
total_payments: int # Total payments made
|
total_payments: int # Total payments made
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ window.app = Vue.createApp({
|
||||||
depositsTable: {
|
depositsTable: {
|
||||||
columns: [
|
columns: [
|
||||||
{ name: 'client_id', align: 'left', label: 'Client', field: 'client_id' },
|
{ 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: 'currency', align: 'left', label: 'Currency', field: 'currency' },
|
||||||
{ name: 'status', align: 'left', label: 'Status', field: 'status' },
|
{ name: 'status', align: 'left', label: 'Status', field: 'status' },
|
||||||
{ name: 'created_at', align: 'left', label: 'Created', field: 'created_at' },
|
{ name: 'created_at', align: 'left', label: 'Created', field: 'created_at' },
|
||||||
|
|
@ -130,17 +130,15 @@ window.app = Vue.createApp({
|
||||||
///////////////////////////////////////////////////
|
///////////////////////////////////////////////////
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
// Utility Methods
|
// Utility Methods - Simplified since API handles conversion
|
||||||
formatCurrency(amount) {
|
formatCurrency(amount) {
|
||||||
if (!amount) return 'Q 0.00';
|
if (!amount) return 'Q 0.00';
|
||||||
|
|
||||||
// Convert centavos to GTQ for display
|
// Amount is already in GTQ from API
|
||||||
const gtqAmount = amount / 100;
|
|
||||||
|
|
||||||
return new Intl.NumberFormat('es-GT', {
|
return new Intl.NumberFormat('es-GT', {
|
||||||
style: 'currency',
|
style: 'currency',
|
||||||
currency: 'GTQ',
|
currency: 'GTQ',
|
||||||
}).format(gtqAmount);
|
}).format(amount);
|
||||||
},
|
},
|
||||||
|
|
||||||
formatDate(dateString) {
|
formatDate(dateString) {
|
||||||
|
|
@ -281,7 +279,7 @@ window.app = Vue.createApp({
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
...client,
|
...client,
|
||||||
remaining_balance: balance.remaining_balance
|
remaining_balance: balance.remaining_balance_gtq
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error fetching balance for client ${client.id}:`, error)
|
console.error(`Error fetching balance for client ${client.id}:`, error)
|
||||||
|
|
@ -305,7 +303,7 @@ window.app = Vue.createApp({
|
||||||
try {
|
try {
|
||||||
const data = {
|
const data = {
|
||||||
client_id: this.quickDepositForm.selectedClient?.value,
|
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',
|
currency: 'GTQ',
|
||||||
notes: this.quickDepositForm.notes
|
notes: this.quickDepositForm.notes
|
||||||
}
|
}
|
||||||
|
|
@ -378,7 +376,7 @@ window.app = Vue.createApp({
|
||||||
try {
|
try {
|
||||||
const data = {
|
const data = {
|
||||||
client_id: this.depositFormDialog.data.client_id,
|
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,
|
currency: this.depositFormDialog.data.currency,
|
||||||
notes: this.depositFormDialog.data.notes
|
notes: this.depositFormDialog.data.notes
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -173,7 +173,7 @@
|
||||||
<q-tr :props="props">
|
<q-tr :props="props">
|
||||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||||
<div v-if="col.field == 'client_id'">${ getClientUsername(col.value) }</div>
|
<div v-if="col.field == 'client_id'">${ getClientUsername(col.value) }</div>
|
||||||
<div v-else-if="col.field == 'amount'">${ formatCurrency(col.value) }</div>
|
<div v-else-if="col.field == 'amount_gtq'">${ formatCurrency(col.value) }</div>
|
||||||
<div v-else-if="col.field == 'status'">
|
<div v-else-if="col.field == 'status'">
|
||||||
<q-badge :color="col.value === 'confirmed' ? 'green' : 'orange'">
|
<q-badge :color="col.value === 'confirmed' ? 'green' : 'orange'">
|
||||||
${ col.value }
|
${ col.value }
|
||||||
|
|
@ -470,9 +470,9 @@
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-item-label caption>Balance Summary</q-item-label>
|
<q-item-label caption>Balance Summary</q-item-label>
|
||||||
<q-item-label v-if="clientDetailsDialog.balance">
|
<q-item-label v-if="clientDetailsDialog.balance">
|
||||||
Deposits: ${ formatCurrency(clientDetailsDialog.balance.total_deposits) } |
|
Deposits: ${ formatCurrency(clientDetailsDialog.balance.total_deposits_gtq) } |
|
||||||
Payments: ${ formatCurrency(clientDetailsDialog.balance.total_payments) } |
|
Payments: ${ formatCurrency(clientDetailsDialog.balance.total_payments_gtq) } |
|
||||||
Remaining: ${ formatCurrency(clientDetailsDialog.balance.remaining_balance) }
|
Remaining: ${ formatCurrency(clientDetailsDialog.balance.remaining_balance_gtq) }
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
|
|
|
||||||
50
views_api.py
50
views_api.py
|
|
@ -38,14 +38,23 @@ from .models import (
|
||||||
DcaClient,
|
DcaClient,
|
||||||
UpdateDcaClientData,
|
UpdateDcaClientData,
|
||||||
CreateDepositData,
|
CreateDepositData,
|
||||||
|
CreateDepositAPI,
|
||||||
|
DepositAPI,
|
||||||
DcaDeposit,
|
DcaDeposit,
|
||||||
UpdateDepositStatusData,
|
UpdateDepositStatusData,
|
||||||
ClientBalanceSummary,
|
ClientBalanceSummary,
|
||||||
|
ClientBalanceSummaryAPI,
|
||||||
CreateLamassuConfigData,
|
CreateLamassuConfigData,
|
||||||
LamassuConfig,
|
LamassuConfig,
|
||||||
UpdateLamassuConfigData,
|
UpdateLamassuConfigData,
|
||||||
StoredLamassuTransaction,
|
StoredLamassuTransaction,
|
||||||
)
|
)
|
||||||
|
from .currency_utils import (
|
||||||
|
gtq_to_centavos,
|
||||||
|
centavos_to_gtq,
|
||||||
|
deposit_db_to_api,
|
||||||
|
balance_summary_db_to_api,
|
||||||
|
)
|
||||||
|
|
||||||
satmachineadmin_api_router = APIRouter()
|
satmachineadmin_api_router = APIRouter()
|
||||||
|
|
||||||
|
|
@ -87,7 +96,7 @@ async def api_get_dca_client(
|
||||||
async def api_get_client_balance(
|
async def api_get_client_balance(
|
||||||
client_id: str,
|
client_id: str,
|
||||||
wallet: WalletTypeInfo = Depends(check_super_user),
|
wallet: WalletTypeInfo = Depends(check_super_user),
|
||||||
) -> ClientBalanceSummary:
|
) -> ClientBalanceSummaryAPI:
|
||||||
"""Get client balance summary"""
|
"""Get client balance summary"""
|
||||||
client = await get_dca_client(client_id)
|
client = await get_dca_client(client_id)
|
||||||
if not client:
|
if not client:
|
||||||
|
|
@ -95,7 +104,8 @@ async def api_get_client_balance(
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="DCA client not found."
|
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
|
# DCA Deposit Endpoints
|
||||||
|
|
@ -104,30 +114,31 @@ async def api_get_client_balance(
|
||||||
@satmachineadmin_api_router.get("/api/v1/dca/deposits")
|
@satmachineadmin_api_router.get("/api/v1/dca/deposits")
|
||||||
async def api_get_deposits(
|
async def api_get_deposits(
|
||||||
wallet: WalletTypeInfo = Depends(check_super_user),
|
wallet: WalletTypeInfo = Depends(check_super_user),
|
||||||
) -> list[DcaDeposit]:
|
) -> list[DepositAPI]:
|
||||||
"""Get all deposits"""
|
"""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}")
|
@satmachineadmin_api_router.get("/api/v1/dca/deposits/{deposit_id}")
|
||||||
async def api_get_deposit(
|
async def api_get_deposit(
|
||||||
deposit_id: str,
|
deposit_id: str,
|
||||||
wallet: WalletTypeInfo = Depends(check_super_user),
|
wallet: WalletTypeInfo = Depends(check_super_user),
|
||||||
) -> DcaDeposit:
|
) -> DepositAPI:
|
||||||
"""Get a specific deposit"""
|
"""Get a specific deposit"""
|
||||||
deposit = await get_deposit(deposit_id)
|
deposit_db = await get_deposit(deposit_id)
|
||||||
if not deposit:
|
if not deposit_db:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="Deposit not found."
|
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)
|
@satmachineadmin_api_router.post("/api/v1/dca/deposits", status_code=HTTPStatus.CREATED)
|
||||||
async def api_create_deposit(
|
async def api_create_deposit(
|
||||||
data: CreateDepositData,
|
data: CreateDepositAPI,
|
||||||
user: User = Depends(check_super_user),
|
user: User = Depends(check_super_user),
|
||||||
) -> DcaDeposit:
|
) -> DepositAPI:
|
||||||
"""Create a new deposit"""
|
"""Create a new deposit"""
|
||||||
# Verify client exists
|
# Verify client exists
|
||||||
client = await get_dca_client(data.client_id)
|
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."
|
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")
|
@satmachineadmin_api_router.put("/api/v1/dca/deposits/{deposit_id}/status")
|
||||||
|
|
@ -144,7 +164,7 @@ async def api_update_deposit_status(
|
||||||
deposit_id: str,
|
deposit_id: str,
|
||||||
data: UpdateDepositStatusData,
|
data: UpdateDepositStatusData,
|
||||||
user: User = Depends(check_super_user),
|
user: User = Depends(check_super_user),
|
||||||
) -> DcaDeposit:
|
) -> DepositAPI:
|
||||||
"""Update deposit status (e.g., confirm deposit)"""
|
"""Update deposit status (e.g., confirm deposit)"""
|
||||||
deposit = await get_deposit(deposit_id)
|
deposit = await get_deposit(deposit_id)
|
||||||
if not deposit:
|
if not deposit:
|
||||||
|
|
@ -152,13 +172,13 @@ async def api_update_deposit_status(
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="Deposit not found."
|
status_code=HTTPStatus.NOT_FOUND, detail="Deposit not found."
|
||||||
)
|
)
|
||||||
|
|
||||||
updated_deposit = await update_deposit_status(deposit_id, data)
|
updated_deposit_db = await update_deposit_status(deposit_id, data)
|
||||||
if not updated_deposit:
|
if not updated_deposit_db:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||||
detail="Failed to update deposit.",
|
detail="Failed to update deposit.",
|
||||||
)
|
)
|
||||||
return updated_deposit
|
return DepositAPI(**deposit_db_to_api(updated_deposit_db))
|
||||||
|
|
||||||
|
|
||||||
# Transaction Polling Endpoints
|
# Transaction Polling Endpoints
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue