From d701b7c770855558a8286bf7d17aceee61c51998 Mon Sep 17 00:00:00 2001 From: padreug Date: Sat, 5 Jul 2025 18:31:20 +0200 Subject: [PATCH 01/10] Implement final balance verification in distribution process: Added checks to ensure clients have positive balances before finalizing distributions and making payments. Enhanced logging for rejected clients and balance sufficiency, improving transaction reliability and transparency. --- transaction_processor.py | 42 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/transaction_processor.py b/transaction_processor.py index 7e161f7..8982423 100644 --- a/transaction_processor.py +++ b/transaction_processor.py @@ -773,8 +773,32 @@ class LamassuTransactionProcessor: logger.error(f"Distribution mismatch! Expected: {base_crypto_atoms} sats, Distributed: {total_distributed} sats") raise ValueError(f"Satoshi distribution calculation error: {base_crypto_atoms} != {total_distributed}") - logger.info(f"Distribution verified: {total_distributed} sats distributed across {len(distributions)} clients") - return distributions + # Safety check: Re-verify all clients still have positive balances before finalizing distributions + # This prevents race conditions where balances changed during calculation + final_distributions = {} + for client_id, distribution in distributions.items(): + # Re-check current balance (without temporal filtering to get most recent state) + current_balance = await get_client_balance_summary(client_id) + if current_balance.remaining_balance > 0: + final_distributions[client_id] = distribution + logger.info(f"Client {client_id[:8]}... final balance check: {current_balance.remaining_balance} centavos - APPROVED for {distribution['sats_amount']} sats") + else: + logger.warning(f"Client {client_id[:8]}... final balance check: {current_balance.remaining_balance} centavos - REJECTED (negative balance)") + + if len(final_distributions) != len(distributions): + logger.warning(f"Rejected {len(distributions) - len(final_distributions)} clients due to negative balances during final check") + + # Recalculate proportions if some clients were rejected + if len(final_distributions) == 0: + logger.info("All clients rejected due to negative balances - no distributions") + return {} + + # For simplicity, we'll still return the original distributions but log the warning + # In a production system, you might want to recalculate the entire distribution + logger.warning("Proceeding with original distribution despite balance warnings - manual review recommended") + + logger.info(f"Distribution verified: {total_distributed} sats distributed across {len(final_distributions)} clients") + return final_distributions except Exception as e: logger.error(f"Error calculating distribution amounts: {e}") @@ -803,6 +827,20 @@ class LamassuTransactionProcessor: logger.error(f"Client {client_id} not found") continue + # Final safety check: Verify client still has positive balance before payment + current_balance = await get_client_balance_summary(client_id) + if current_balance.remaining_balance <= 0: + logger.error(f"CRITICAL: Client {client_id[:8]}... has negative balance ({current_balance.remaining_balance} centavos) - REFUSING payment of {distribution['sats_amount']} sats") + continue + + # Verify balance is sufficient for this distribution + fiat_equivalent = distribution["fiat_amount"] # Already in centavos + if current_balance.remaining_balance < fiat_equivalent: + logger.error(f"CRITICAL: Client {client_id[:8]}... insufficient balance ({current_balance.remaining_balance} < {fiat_equivalent} centavos) - REFUSING payment") + continue + + logger.info(f"Client {client_id[:8]}... pre-payment balance check: {current_balance.remaining_balance} centavos - SUFFICIENT for {fiat_equivalent} centavos payment") + # Create DCA payment record payment_data = CreateDcaPaymentData( client_id=client_id, From 7b40fcef215c57d313f227da33f786dd414b7717 Mon Sep 17 00:00:00 2001 From: padreug Date: Sat, 5 Jul 2025 18:31:59 +0200 Subject: [PATCH 02/10] Change order of transaction confirmation sorting: Updated the SQL query in the LamassuTransactionProcessor to sort confirmed transactions in ascending order by confirmation date, improving the retrieval of older transactions. --- transaction_processor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/transaction_processor.py b/transaction_processor.py index 8982423..c9c4f84 100644 --- a/transaction_processor.py +++ b/transaction_processor.py @@ -591,7 +591,7 @@ class LamassuTransactionProcessor: AND co.status IN ('confirmed', 'authorized') AND co.dispense = 't' AND co.dispense_confirmed = 't' - ORDER BY co.confirmed_at DESC + ORDER BY co.confirmed_at ASC """ all_transactions = await self.execute_ssh_query(db_config, lamassu_query) From aa71321c84d6d88779c63becb25144780db0ac1b Mon Sep 17 00:00:00 2001 From: padreug Date: Sat, 5 Jul 2025 23:38:23 +0200 Subject: [PATCH 03/10] 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. --- currency_utils.py | 45 +++++++++++++++++++++++++ models.py | 38 +++++++++++++++++++-- static/js/index.js | 16 ++++----- templates/satmachineadmin/index.html | 8 ++--- views_api.py | 50 +++++++++++++++++++--------- 5 files changed, 127 insertions(+), 30 deletions(-) create mode 100644 currency_utils.py 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 From c83ebf43ab676b068ed6714e0762ed3347916c65 Mon Sep 17 00:00:00 2001 From: padreug Date: Sun, 6 Jul 2025 00:00:30 +0200 Subject: [PATCH 04/10] 01 Refactor currency handling to store amounts in GTQ: Removed currency conversion utilities, updated models and API endpoints to directly handle GTQ amounts, and modified transaction processing logic for consistency. Enhanced frontend to reflect these changes, ensuring accurate display and submission of GTQ values across the application. Refactor GTQ storage migration: Moved the conversion logic for centavo amounts to GTQ into a new migration function, m004_convert_to_gtq_storage, ensuring proper data type changes and updates across relevant tables. This enhances clarity and maintains the integrity of the migration process. --- crud.py | 2 +- currency_utils.py | 45 --------------- migrations.py | 84 ++++++++++++++++++++++++++++ models.py | 82 ++++++++++----------------- static/js/index.js | 12 ++-- templates/satmachineadmin/index.html | 8 +-- transaction_processor.py | 36 ++++++------ views_api.py | 50 +++++------------ 8 files changed, 157 insertions(+), 162 deletions(-) delete mode 100644 currency_utils.py diff --git a/crud.py b/crud.py index 60bad39..94c1d20 100644 --- a/crud.py +++ b/crud.py @@ -267,7 +267,7 @@ async def get_client_balance_summary(client_id: str, as_of_time: Optional[dateti from loguru import logger # Verify timezone consistency for temporal filtering tz_info = "UTC" if as_of_time.tzinfo == timezone.utc else f"TZ: {as_of_time.tzinfo}" - logger.info(f"Client {client_id[:8]}... balance as of {as_of_time} ({tz_info}): deposits.confirmed_at <= cutoff, payments.transaction_time <= cutoff → {total_deposits - total_payments} centavos remaining") + logger.info(f"Client {client_id[:8]}... balance as of {as_of_time} ({tz_info}): deposits.confirmed_at <= cutoff, payments.transaction_time <= cutoff → {total_deposits - total_payments:.2f} GTQ remaining") return ClientBalanceSummary( client_id=client_id, diff --git a/currency_utils.py b/currency_utils.py deleted file mode 100644 index 3241134..0000000 --- a/currency_utils.py +++ /dev/null @@ -1,45 +0,0 @@ -# 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/migrations.py b/migrations.py index 0456f43..6654c71 100644 --- a/migrations.py +++ b/migrations.py @@ -134,4 +134,88 @@ async def m003_add_max_daily_limit_config(db): ALTER TABLE satoshimachine.lamassu_config ADD COLUMN max_daily_limit_gtq INTEGER NOT NULL DEFAULT 2000 """ + ) + + +async def m004_convert_to_gtq_storage(db): + """ + Convert centavo storage to GTQ storage by changing data types and converting existing data. + """ + # Convert dca_deposits amounts from centavos to GTQ + await db.execute( + """ + UPDATE satoshimachine.dca_deposits + SET amount = CAST(amount AS DECIMAL(10,2)) / 100.0 + WHERE currency = 'GTQ' + """ + ) + + # Convert dca_payments amounts from centavos to GTQ + await db.execute( + """ + UPDATE satoshimachine.dca_payments + SET amount_fiat = CAST(amount_fiat AS DECIMAL(10,2)) / 100.0 + """ + ) + + # Convert lamassu_transactions amounts from centavos to GTQ + await db.execute( + """ + UPDATE satoshimachine.lamassu_transactions + SET fiat_amount = CAST(fiat_amount AS DECIMAL(10,2)) / 100.0 + """ + ) + + # Convert fixed_mode_daily_limit from centavos to GTQ + await db.execute( + """ + UPDATE satoshimachine.dca_clients + SET fixed_mode_daily_limit = CAST(fixed_mode_daily_limit AS DECIMAL(10,2)) / 100.0 + WHERE fixed_mode_daily_limit IS NOT NULL + """ + ) + + # Convert max_daily_limit_gtq in config (if already in centavos) + await db.execute( + """ + UPDATE satoshimachine.lamassu_config + SET max_daily_limit_gtq = CAST(max_daily_limit_gtq AS DECIMAL(10,2)) / 100.0 + WHERE max_daily_limit_gtq > 1000 + """ + ) + + # Change column types to DECIMAL + await db.execute( + """ + ALTER TABLE satoshimachine.dca_deposits + ALTER COLUMN amount TYPE DECIMAL(10,2) + """ + ) + + await db.execute( + """ + ALTER TABLE satoshimachine.dca_payments + ALTER COLUMN amount_fiat TYPE DECIMAL(10,2) + """ + ) + + await db.execute( + """ + ALTER TABLE satoshimachine.lamassu_transactions + ALTER COLUMN fiat_amount TYPE DECIMAL(10,2) + """ + ) + + await db.execute( + """ + ALTER TABLE satoshimachine.dca_clients + ALTER COLUMN fixed_mode_daily_limit TYPE DECIMAL(10,2) + """ + ) + + await db.execute( + """ + ALTER TABLE satoshimachine.lamassu_config + ALTER COLUMN max_daily_limit_gtq TYPE DECIMAL(10,2) + """ ) \ No newline at end of file diff --git a/models.py b/models.py index 7727bee..e4bf64d 100644 --- a/models.py +++ b/models.py @@ -3,7 +3,7 @@ from datetime import datetime from typing import Optional -from pydantic import BaseModel +from pydantic import BaseModel, validator # DCA Client Models @@ -12,7 +12,7 @@ class CreateDcaClientData(BaseModel): wallet_id: str username: str dca_mode: str = "flow" # 'flow' or 'fixed' - fixed_mode_daily_limit: Optional[int] = None + fixed_mode_daily_limit: Optional[float] = None class DcaClient(BaseModel): @@ -30,45 +30,29 @@ class DcaClient(BaseModel): class UpdateDcaClientData(BaseModel): username: Optional[str] = None dca_mode: Optional[str] = None - fixed_mode_daily_limit: Optional[int] = None + fixed_mode_daily_limit: Optional[float] = None status: Optional[str] = None -# 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) +# Deposit Models (Now storing GTQ directly) class CreateDepositData(BaseModel): - """Internal model - database stores centavos""" client_id: str - amount: int # Amount in smallest currency unit (centavos for GTQ) + amount: float # Amount in GTQ (e.g., 150.75) currency: str = "GTQ" notes: Optional[str] = None + + @validator('amount') + def round_amount_to_cents(cls, v): + """Ensure amount is rounded to 2 decimal places for DECIMAL(10,2) storage""" + if v is not None: + return round(float(v), 2) + return v class DcaDeposit(BaseModel): - """Internal model - database stores centavos""" id: str client_id: str - amount: int + amount: float # Amount in GTQ (e.g., 150.75) currency: str status: str # 'pending' or 'confirmed' notes: Optional[str] @@ -85,7 +69,7 @@ class UpdateDepositStatusData(BaseModel): class CreateDcaPaymentData(BaseModel): client_id: str amount_sats: int - amount_fiat: int # Stored in centavos (GTQ * 100) for precision + amount_fiat: float # Amount in GTQ (e.g., 150.75) exchange_rate: float transaction_type: str # 'flow', 'fixed', 'manual', 'commission' lamassu_transaction_id: Optional[str] = None @@ -97,7 +81,7 @@ class DcaPayment(BaseModel): id: str client_id: str amount_sats: int - amount_fiat: int # Stored in centavos (GTQ * 100) for precision + amount_fiat: float # Amount in GTQ (e.g., 150.75) exchange_rate: float transaction_type: str lamassu_transaction_id: Optional[str] @@ -107,30 +91,19 @@ class DcaPayment(BaseModel): transaction_time: Optional[datetime] = None # Original ATM transaction time -# 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 +# Client Balance Summary (Now storing GTQ directly) class ClientBalanceSummary(BaseModel): - """Internal model - stores centavos""" client_id: str - total_deposits: int # Total confirmed deposits - total_payments: int # Total payments made - remaining_balance: int # Available balance for DCA + total_deposits: float # Total confirmed deposits in GTQ + total_payments: float # Total payments made in GTQ + remaining_balance: float # Available balance for DCA in GTQ currency: str # Transaction Processing Models class LamassuTransaction(BaseModel): transaction_id: str - amount_fiat: int # Stored in centavos (GTQ * 100) for precision + amount_fiat: float # Amount in GTQ (e.g., 150.75) amount_crypto: int exchange_rate: float transaction_type: str # 'cash_in' or 'cash_out' @@ -141,7 +114,7 @@ class LamassuTransaction(BaseModel): # Lamassu Transaction Storage Models class CreateLamassuTransactionData(BaseModel): lamassu_transaction_id: str - fiat_amount: int # Stored in centavos (GTQ * 100) for precision + fiat_amount: float # Amount in GTQ (e.g., 150.75) crypto_amount: int commission_percentage: float discount: float = 0.0 @@ -158,7 +131,7 @@ class CreateLamassuTransactionData(BaseModel): class StoredLamassuTransaction(BaseModel): id: str lamassu_transaction_id: str - fiat_amount: int + fiat_amount: float # Amount in GTQ (e.g., 150.75) crypto_amount: int commission_percentage: float discount: float @@ -194,7 +167,14 @@ class CreateLamassuConfigData(BaseModel): ssh_password: Optional[str] = None ssh_private_key: Optional[str] = None # Path to private key file or key content # DCA Client Limits - max_daily_limit_gtq: int = 2000 # Maximum daily limit for Fixed mode clients + max_daily_limit_gtq: float = 2000.0 # Maximum daily limit for Fixed mode clients + + @validator('max_daily_limit_gtq') + def round_max_daily_limit(cls, v): + """Ensure max daily limit is rounded to 2 decimal places""" + if v is not None: + return round(float(v), 2) + return v class LamassuConfig(BaseModel): @@ -224,7 +204,7 @@ class LamassuConfig(BaseModel): last_poll_time: Optional[datetime] = None last_successful_poll: Optional[datetime] = None # DCA Client Limits - max_daily_limit_gtq: int = 2000 # Maximum daily limit for Fixed mode clients + max_daily_limit_gtq: float = 2000.0 # Maximum daily limit for Fixed mode clients class UpdateLamassuConfigData(BaseModel): diff --git a/static/js/index.js b/static/js/index.js index 6178a02..1989e9b 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_gtq', align: 'left', label: 'Amount', field: 'amount_gtq' }, + { name: 'amount', align: 'left', label: 'Amount', field: 'amount' }, { 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,11 +130,11 @@ window.app = Vue.createApp({ /////////////////////////////////////////////////// methods: { - // Utility Methods - Simplified since API handles conversion + // Utility Methods formatCurrency(amount) { if (!amount) return 'Q 0.00'; - // Amount is already in GTQ from API + // Amount is now stored as GTQ directly in database return new Intl.NumberFormat('es-GT', { style: 'currency', currency: 'GTQ', @@ -279,7 +279,7 @@ window.app = Vue.createApp({ ) return { ...client, - remaining_balance: balance.remaining_balance_gtq + remaining_balance: balance.remaining_balance } } catch (error) { console.error(`Error fetching balance for client ${client.id}:`, error) @@ -303,7 +303,7 @@ window.app = Vue.createApp({ try { const data = { client_id: this.quickDepositForm.selectedClient?.value, - amount_gtq: this.quickDepositForm.amount, // Send GTQ directly - API handles conversion + amount: this.quickDepositForm.amount, // Send GTQ directly - now stored as GTQ currency: 'GTQ', notes: this.quickDepositForm.notes } @@ -376,7 +376,7 @@ window.app = Vue.createApp({ try { const data = { client_id: this.depositFormDialog.data.client_id, - amount_gtq: this.depositFormDialog.data.amount, // Send GTQ directly - API handles conversion + amount: this.depositFormDialog.data.amount, // Send GTQ directly - now stored as GTQ currency: this.depositFormDialog.data.currency, notes: this.depositFormDialog.data.notes } diff --git a/templates/satmachineadmin/index.html b/templates/satmachineadmin/index.html index 8901460..61099ce 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_gtq) } | - Payments: ${ formatCurrency(clientDetailsDialog.balance.total_payments_gtq) } | - Remaining: ${ formatCurrency(clientDetailsDialog.balance.remaining_balance_gtq) } + Deposits: ${ formatCurrency(clientDetailsDialog.balance.total_deposits) } | + Payments: ${ formatCurrency(clientDetailsDialog.balance.total_payments) } | + Remaining: ${ formatCurrency(clientDetailsDialog.balance.remaining_balance) } diff --git a/transaction_processor.py b/transaction_processor.py index c9c4f84..895f8d7 100644 --- a/transaction_processor.py +++ b/transaction_processor.py @@ -671,7 +671,7 @@ class LamassuTransactionProcessor: # Since crypto_atoms already includes commission, we need to extract the base amount # Formula: crypto_atoms = base_amount * (1 + effective_commission) # Therefore: base_amount = crypto_atoms / (1 + effective_commission) - base_crypto_atoms = int(crypto_atoms / (1 + effective_commission)) + base_crypto_atoms = round(crypto_atoms / (1 + effective_commission)) commission_amount_sats = crypto_atoms - base_crypto_atoms else: effective_commission = 0.0 @@ -755,9 +755,8 @@ class LamassuTransactionProcessor: client_sats_amount = calc['allocated_sats'] proportion = calc['proportion'] - # Calculate equivalent fiat value in centavos for tracking purposes (industry standard) - # Store as centavos to maintain precision and avoid floating-point errors - client_fiat_amount = round(client_sats_amount * 100 / exchange_rate) if exchange_rate > 0 else 0 + # Calculate equivalent fiat value in GTQ for tracking purposes + client_fiat_amount = round(client_sats_amount / exchange_rate, 2) if exchange_rate > 0 else 0.0 distributions[client_id] = { "fiat_amount": client_fiat_amount, @@ -765,7 +764,7 @@ class LamassuTransactionProcessor: "exchange_rate": exchange_rate } - logger.info(f"Client {client_id[:8]}... gets {client_sats_amount} sats (≈{client_fiat_amount/100:.2f} GTQ, {proportion:.2%} share)") + logger.info(f"Client {client_id[:8]}... gets {client_sats_amount} sats (≈{client_fiat_amount:.2f} GTQ, {proportion:.2%} share)") # Verification: ensure total distribution equals base amount total_distributed = sum(dist["sats_amount"] for dist in distributions.values()) @@ -781,9 +780,9 @@ class LamassuTransactionProcessor: current_balance = await get_client_balance_summary(client_id) if current_balance.remaining_balance > 0: final_distributions[client_id] = distribution - logger.info(f"Client {client_id[:8]}... final balance check: {current_balance.remaining_balance} centavos - APPROVED for {distribution['sats_amount']} sats") + logger.info(f"Client {client_id[:8]}... final balance check: {current_balance.remaining_balance:.2f} GTQ - APPROVED for {distribution['sats_amount']} sats") else: - logger.warning(f"Client {client_id[:8]}... final balance check: {current_balance.remaining_balance} centavos - REJECTED (negative balance)") + logger.warning(f"Client {client_id[:8]}... final balance check: {current_balance.remaining_balance:.2f} GTQ - REJECTED (negative balance)") if len(final_distributions) != len(distributions): logger.warning(f"Rejected {len(distributions) - len(final_distributions)} clients due to negative balances during final check") @@ -830,22 +829,22 @@ class LamassuTransactionProcessor: # Final safety check: Verify client still has positive balance before payment current_balance = await get_client_balance_summary(client_id) if current_balance.remaining_balance <= 0: - logger.error(f"CRITICAL: Client {client_id[:8]}... has negative balance ({current_balance.remaining_balance} centavos) - REFUSING payment of {distribution['sats_amount']} sats") + logger.error(f"CRITICAL: Client {client_id[:8]}... has negative balance ({current_balance.remaining_balance:.2f} GTQ) - REFUSING payment of {distribution['sats_amount']} sats") continue # Verify balance is sufficient for this distribution - fiat_equivalent = distribution["fiat_amount"] # Already in centavos + fiat_equivalent = distribution["fiat_amount"] # Amount in GTQ if current_balance.remaining_balance < fiat_equivalent: - logger.error(f"CRITICAL: Client {client_id[:8]}... insufficient balance ({current_balance.remaining_balance} < {fiat_equivalent} centavos) - REFUSING payment") + logger.error(f"CRITICAL: Client {client_id[:8]}... insufficient balance ({current_balance.remaining_balance:.2f} < {fiat_equivalent:.2f} GTQ) - REFUSING payment") continue - logger.info(f"Client {client_id[:8]}... pre-payment balance check: {current_balance.remaining_balance} centavos - SUFFICIENT for {fiat_equivalent} centavos payment") + logger.info(f"Client {client_id[:8]}... pre-payment balance check: {current_balance.remaining_balance:.2f} GTQ - SUFFICIENT for {fiat_equivalent:.2f} GTQ payment") # Create DCA payment record payment_data = CreateDcaPaymentData( client_id=client_id, amount_sats=distribution["sats_amount"], - amount_fiat=distribution["fiat_amount"], # Still store centavos in DB + amount_fiat=distribution["fiat_amount"], # Amount in GTQ exchange_rate=distribution["exchange_rate"], transaction_type="flow", lamassu_transaction_id=transaction_id, @@ -888,12 +887,9 @@ class LamassuTransactionProcessor: return False # Create descriptive memo with DCA metrics - fiat_amount_centavos = distribution.get("fiat_amount", 0) + fiat_amount_gtq = distribution.get("fiat_amount", 0.0) exchange_rate = distribution.get("exchange_rate", 0) - # Convert centavos to GTQ for display - fiat_amount_gtq = fiat_amount_centavos / 100 - # Calculate cost basis (fiat per BTC) if exchange_rate > 0: # exchange_rate is sats per fiat unit, so convert to fiat per BTC @@ -1011,7 +1007,7 @@ class LamassuTransactionProcessor: # Calculate commission metrics if commission_percentage > 0: effective_commission = commission_percentage * (100 - discount) / 100 - base_crypto_atoms = int(crypto_atoms / (1 + effective_commission)) + base_crypto_atoms = round(crypto_atoms / (1 + effective_commission)) commission_amount_sats = crypto_atoms - base_crypto_atoms else: effective_commission = 0.0 @@ -1021,10 +1017,10 @@ class LamassuTransactionProcessor: # Calculate exchange rate exchange_rate = base_crypto_atoms / fiat_amount if fiat_amount > 0 else 0 - # Create transaction data (store fiat_amount in centavos for consistency) + # Create transaction data with GTQ amounts transaction_data = CreateLamassuTransactionData( lamassu_transaction_id=transaction["transaction_id"], - fiat_amount=int(fiat_amount * 100), # Convert GTQ to centavos + fiat_amount=round(fiat_amount, 2), # Store GTQ with 2 decimal places crypto_amount=crypto_atoms, commission_percentage=commission_percentage, discount=discount, @@ -1137,7 +1133,7 @@ class LamassuTransactionProcessor: if commission_percentage and commission_percentage > 0: effective_commission = commission_percentage * (100 - discount) / 100 - base_crypto_atoms = int(crypto_atoms / (1 + effective_commission)) + base_crypto_atoms = round(crypto_atoms / (1 + effective_commission)) commission_amount_sats = crypto_atoms - base_crypto_atoms else: commission_amount_sats = 0 diff --git a/views_api.py b/views_api.py index 9d3c41a..3700497 100644 --- a/views_api.py +++ b/views_api.py @@ -38,23 +38,14 @@ 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() @@ -96,7 +87,7 @@ async def api_get_dca_client( async def api_get_client_balance( client_id: str, wallet: WalletTypeInfo = Depends(check_super_user), -) -> ClientBalanceSummaryAPI: +) -> ClientBalanceSummary: """Get client balance summary""" client = await get_dca_client(client_id) if not client: @@ -104,8 +95,7 @@ async def api_get_client_balance( status_code=HTTPStatus.NOT_FOUND, detail="DCA client not found." ) - balance_db = await get_client_balance_summary(client_id) - return ClientBalanceSummaryAPI(**balance_summary_db_to_api(balance_db)) + return await get_client_balance_summary(client_id) # DCA Deposit Endpoints @@ -114,31 +104,30 @@ 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[DepositAPI]: +) -> list[DcaDeposit]: """Get all deposits""" - deposits_db = await get_all_deposits() - return [DepositAPI(**deposit_db_to_api(deposit)) for deposit in deposits_db] + return await get_all_deposits() @satmachineadmin_api_router.get("/api/v1/dca/deposits/{deposit_id}") async def api_get_deposit( deposit_id: str, wallet: WalletTypeInfo = Depends(check_super_user), -) -> DepositAPI: +) -> DcaDeposit: """Get a specific deposit""" - deposit_db = await get_deposit(deposit_id) - if not deposit_db: + deposit = await get_deposit(deposit_id) + if not deposit: raise HTTPException( status_code=HTTPStatus.NOT_FOUND, detail="Deposit not found." ) - return DepositAPI(**deposit_db_to_api(deposit_db)) + return deposit @satmachineadmin_api_router.post("/api/v1/dca/deposits", status_code=HTTPStatus.CREATED) async def api_create_deposit( - data: CreateDepositAPI, + data: CreateDepositData, user: User = Depends(check_super_user), -) -> DepositAPI: +) -> DcaDeposit: """Create a new deposit""" # Verify client exists client = await get_dca_client(data.client_id) @@ -147,16 +136,7 @@ async def api_create_deposit( status_code=HTTPStatus.NOT_FOUND, detail="DCA client not found." ) - # 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)) + return await create_deposit(data) @satmachineadmin_api_router.put("/api/v1/dca/deposits/{deposit_id}/status") @@ -164,7 +144,7 @@ async def api_update_deposit_status( deposit_id: str, data: UpdateDepositStatusData, user: User = Depends(check_super_user), -) -> DepositAPI: +) -> DcaDeposit: """Update deposit status (e.g., confirm deposit)""" deposit = await get_deposit(deposit_id) if not deposit: @@ -172,13 +152,13 @@ async def api_update_deposit_status( status_code=HTTPStatus.NOT_FOUND, detail="Deposit not found." ) - updated_deposit_db = await update_deposit_status(deposit_id, data) - if not updated_deposit_db: + updated_deposit = await update_deposit_status(deposit_id, data) + if not updated_deposit: raise HTTPException( status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Failed to update deposit.", ) - return DepositAPI(**deposit_db_to_api(updated_deposit_db)) + return updated_deposit # Transaction Polling Endpoints From a864f285e42dc1648f771cccf88058a4505059bc Mon Sep 17 00:00:00 2001 From: padreug Date: Sun, 6 Jul 2025 00:24:49 +0200 Subject: [PATCH 05/10] Refactor m004_convert_to_gtq_storage migration: Streamlined the conversion of centavo amounts to GTQ by detecting the database type (PostgreSQL or SQLite) and applying appropriate data type changes and updates. This enhances clarity and ensures proper handling of data conversions across relevant tables. --- migrations.py | 105 ++++++++++++++------------------------------------ 1 file changed, 28 insertions(+), 77 deletions(-) diff --git a/migrations.py b/migrations.py index 6654c71..c8147f5 100644 --- a/migrations.py +++ b/migrations.py @@ -140,82 +140,33 @@ async def m003_add_max_daily_limit_config(db): async def m004_convert_to_gtq_storage(db): """ Convert centavo storage to GTQ storage by changing data types and converting existing data. + Handles both SQLite (data conversion only) and PostgreSQL (data + schema changes). """ - # Convert dca_deposits amounts from centavos to GTQ - await db.execute( - """ - UPDATE satoshimachine.dca_deposits - SET amount = CAST(amount AS DECIMAL(10,2)) / 100.0 - WHERE currency = 'GTQ' - """ - ) + # Detect database type + db_type = str(type(db)).lower() + is_postgres = 'postgres' in db_type or 'asyncpg' in db_type - # Convert dca_payments amounts from centavos to GTQ - await db.execute( - """ - UPDATE satoshimachine.dca_payments - SET amount_fiat = CAST(amount_fiat AS DECIMAL(10,2)) / 100.0 - """ - ) - - # Convert lamassu_transactions amounts from centavos to GTQ - await db.execute( - """ - UPDATE satoshimachine.lamassu_transactions - SET fiat_amount = CAST(fiat_amount AS DECIMAL(10,2)) / 100.0 - """ - ) - - # Convert fixed_mode_daily_limit from centavos to GTQ - await db.execute( - """ - UPDATE satoshimachine.dca_clients - SET fixed_mode_daily_limit = CAST(fixed_mode_daily_limit AS DECIMAL(10,2)) / 100.0 - WHERE fixed_mode_daily_limit IS NOT NULL - """ - ) - - # Convert max_daily_limit_gtq in config (if already in centavos) - await db.execute( - """ - UPDATE satoshimachine.lamassu_config - SET max_daily_limit_gtq = CAST(max_daily_limit_gtq AS DECIMAL(10,2)) / 100.0 - WHERE max_daily_limit_gtq > 1000 - """ - ) - - # Change column types to DECIMAL - await db.execute( - """ - ALTER TABLE satoshimachine.dca_deposits - ALTER COLUMN amount TYPE DECIMAL(10,2) - """ - ) - - await db.execute( - """ - ALTER TABLE satoshimachine.dca_payments - ALTER COLUMN amount_fiat TYPE DECIMAL(10,2) - """ - ) - - await db.execute( - """ - ALTER TABLE satoshimachine.lamassu_transactions - ALTER COLUMN fiat_amount TYPE DECIMAL(10,2) - """ - ) - - await db.execute( - """ - ALTER TABLE satoshimachine.dca_clients - ALTER COLUMN fixed_mode_daily_limit TYPE DECIMAL(10,2) - """ - ) - - await db.execute( - """ - ALTER TABLE satoshimachine.lamassu_config - ALTER COLUMN max_daily_limit_gtq TYPE DECIMAL(10,2) - """ - ) \ No newline at end of file + if is_postgres: + # PostgreSQL: Need to change column types first, then convert data + + # Change column types to DECIMAL(10,2) + await db.execute("ALTER TABLE satoshimachine.dca_deposits ALTER COLUMN amount TYPE DECIMAL(10,2)") + await db.execute("ALTER TABLE satoshimachine.dca_payments ALTER COLUMN amount_fiat TYPE DECIMAL(10,2)") + await db.execute("ALTER TABLE satoshimachine.lamassu_transactions ALTER COLUMN fiat_amount TYPE DECIMAL(10,2)") + await db.execute("ALTER TABLE satoshimachine.dca_clients ALTER COLUMN fixed_mode_daily_limit TYPE DECIMAL(10,2)") + await db.execute("ALTER TABLE satoshimachine.lamassu_config ALTER COLUMN max_daily_limit_gtq TYPE DECIMAL(10,2)") + + # Convert data from centavos to GTQ + await db.execute("UPDATE satoshimachine.dca_deposits SET amount = amount / 100.0 WHERE currency = 'GTQ'") + await db.execute("UPDATE satoshimachine.dca_payments SET amount_fiat = amount_fiat / 100.0") + await db.execute("UPDATE satoshimachine.lamassu_transactions SET fiat_amount = fiat_amount / 100.0") + await db.execute("UPDATE satoshimachine.dca_clients SET fixed_mode_daily_limit = fixed_mode_daily_limit / 100.0 WHERE fixed_mode_daily_limit IS NOT NULL") + await db.execute("UPDATE satoshimachine.lamassu_config SET max_daily_limit_gtq = max_daily_limit_gtq / 100.0 WHERE max_daily_limit_gtq > 1000") + + else: + # SQLite: Data conversion only (dynamic typing handles the rest) + await db.execute("UPDATE satoshimachine.dca_deposits SET amount = CAST(amount AS REAL) / 100.0 WHERE currency = 'GTQ'") + await db.execute("UPDATE satoshimachine.dca_payments SET amount_fiat = CAST(amount_fiat AS REAL) / 100.0") + await db.execute("UPDATE satoshimachine.lamassu_transactions SET fiat_amount = CAST(fiat_amount AS REAL) / 100.0") + await db.execute("UPDATE satoshimachine.dca_clients SET fixed_mode_daily_limit = CAST(fixed_mode_daily_limit AS REAL) / 100.0 WHERE fixed_mode_daily_limit IS NOT NULL") + await db.execute("UPDATE satoshimachine.lamassu_config SET max_daily_limit_gtq = CAST(max_daily_limit_gtq AS REAL) / 100.0 WHERE max_daily_limit_gtq > 1000") \ No newline at end of file From bca39b91cd13eb1d76fe37dc8d371cf8eab38083 Mon Sep 17 00:00:00 2001 From: padreug Date: Sun, 6 Jul 2025 00:54:19 +0200 Subject: [PATCH 06/10] Enhance commission memo generation in transaction processing: Added discount handling to the commission memo in LamassuTransactionProcessor, allowing for the calculation of effective commission percentages when discounts are applied. This improves clarity in transaction details by providing a more accurate representation of commissions after discounts. --- transaction_processor.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/transaction_processor.py b/transaction_processor.py index 895f8d7..a32a32c 100644 --- a/transaction_processor.py +++ b/transaction_processor.py @@ -1061,7 +1061,19 @@ class LamassuTransactionProcessor: # Create invoice in commission wallet with DCA metrics fiat_amount = transaction.get("fiat_amount", 0) commission_percentage = transaction.get("commission_percentage", 0) * 100 # Convert to percentage - commission_memo = f"DCA Commission: {commission_amount_sats:,} sats • {commission_percentage:.1f}% • {fiat_amount:,} GTQ transaction" + discount = transaction.get("discount", 0.0) # Discount percentage + + # Calculate effective commission for display + if commission_percentage > 0: + effective_commission_percentage = commission_percentage * (100 - discount) / 100 + else: + effective_commission_percentage = 0.0 + + # Create detailed memo showing discount if applied + if discount > 0: + commission_memo = f"DCA Commission: {commission_amount_sats:,} sats • {commission_percentage:.1f}% - {discount:.1f}% discount = {effective_commission_percentage:.1f}% effective • {fiat_amount:,} GTQ transaction" + else: + commission_memo = f"DCA Commission: {commission_amount_sats:,} sats • {commission_percentage:.1f}% • {fiat_amount:,} GTQ transaction" commission_payment = await create_invoice( wallet_id=admin_config.commission_wallet_id, From 5d41e0c50e3b8c7d2c10298b1610c43440a8bd68 Mon Sep 17 00:00:00 2001 From: padreug Date: Sun, 6 Jul 2025 01:15:26 +0200 Subject: [PATCH 07/10] Refine distribution logic in transaction processing: Updated the LamassuTransactionProcessor to only create distributions for clients with positive allocated sats. Enhanced logging to indicate when clients are skipped due to zero amounts, improving clarity and accuracy in transaction reporting. --- transaction_processor.py | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/transaction_processor.py b/transaction_processor.py index a32a32c..7b08ac9 100644 --- a/transaction_processor.py +++ b/transaction_processor.py @@ -749,22 +749,26 @@ class LamassuTransactionProcessor: else: client_calculations[i % len(client_calculations)]['allocated_sats'] -= 1 - # Second pass: create distributions with final amounts + # Second pass: create distributions with final amounts (only for clients with positive allocations) for calc in client_calculations: client_id = calc['client_id'] client_sats_amount = calc['allocated_sats'] proportion = calc['proportion'] - # Calculate equivalent fiat value in GTQ for tracking purposes - client_fiat_amount = round(client_sats_amount / exchange_rate, 2) if exchange_rate > 0 else 0.0 - - distributions[client_id] = { - "fiat_amount": client_fiat_amount, - "sats_amount": client_sats_amount, - "exchange_rate": exchange_rate - } - - logger.info(f"Client {client_id[:8]}... gets {client_sats_amount} sats (≈{client_fiat_amount:.2f} GTQ, {proportion:.2%} share)") + # Only create distributions for clients with positive sats amounts + if client_sats_amount > 0: + # Calculate equivalent fiat value in GTQ for tracking purposes + client_fiat_amount = round(client_sats_amount / exchange_rate, 2) if exchange_rate > 0 else 0.0 + + distributions[client_id] = { + "fiat_amount": client_fiat_amount, + "sats_amount": client_sats_amount, + "exchange_rate": exchange_rate + } + + logger.info(f"Client {client_id[:8]}... gets {client_sats_amount} sats (≈{client_fiat_amount:.2f} GTQ, {proportion:.2%} share)") + else: + logger.info(f"Client {client_id[:8]}... gets {client_sats_amount} sats (≈0.00 GTQ, {proportion:.2%} share) - SKIPPED (zero amount)") # Verification: ensure total distribution equals base amount total_distributed = sum(dist["sats_amount"] for dist in distributions.values()) @@ -796,8 +800,8 @@ class LamassuTransactionProcessor: # In a production system, you might want to recalculate the entire distribution logger.warning("Proceeding with original distribution despite balance warnings - manual review recommended") - logger.info(f"Distribution verified: {total_distributed} sats distributed across {len(final_distributions)} clients") - return final_distributions + logger.info(f"Distribution verified: {total_distributed} sats distributed across {len(distributions)} clients (clients with positive allocations only)") + return distributions except Exception as e: logger.error(f"Error calculating distribution amounts: {e}") From 4843b431478022dc83bab479e1e4571682f3e5e3 Mon Sep 17 00:00:00 2001 From: padreug Date: Sun, 6 Jul 2025 02:04:11 +0200 Subject: [PATCH 08/10] FIX: exclude flow_clients remaining_balance values less than 0.01 --- transaction_processor.py | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/transaction_processor.py b/transaction_processor.py index 7b08ac9..22c4edd 100644 --- a/transaction_processor.py +++ b/transaction_processor.py @@ -696,9 +696,14 @@ class LamassuTransactionProcessor: for client in flow_clients: # Get balance as of the transaction time for temporal accuracy balance = await get_client_balance_summary(client.id, as_of_time=transaction_time) - if balance.remaining_balance > 0: # Only include clients with remaining balance + # Only include clients with positive remaining balance + # NOTE: This works for fiat amounts that use cents + if balance.remaining_balance >= 0.01: client_balances[client.id] = balance.remaining_balance total_confirmed_deposits += balance.remaining_balance + logger.debug(f"Client {client.id[:8]}... included with balance: {balance.remaining_balance:.2f} GTQ") + else: + logger.info(f"Client {client.id[:8]}... excluded - zero/negative balance: {balance.remaining_balance:.2f} GTQ") if total_confirmed_deposits == 0: logger.info("No clients with remaining DCA balance - skipping distribution") @@ -749,26 +754,22 @@ class LamassuTransactionProcessor: else: client_calculations[i % len(client_calculations)]['allocated_sats'] -= 1 - # Second pass: create distributions with final amounts (only for clients with positive allocations) + # Second pass: create distributions with final amounts for calc in client_calculations: client_id = calc['client_id'] client_sats_amount = calc['allocated_sats'] proportion = calc['proportion'] - # Only create distributions for clients with positive sats amounts - if client_sats_amount > 0: - # Calculate equivalent fiat value in GTQ for tracking purposes - client_fiat_amount = round(client_sats_amount / exchange_rate, 2) if exchange_rate > 0 else 0.0 - - distributions[client_id] = { - "fiat_amount": client_fiat_amount, - "sats_amount": client_sats_amount, - "exchange_rate": exchange_rate - } - - logger.info(f"Client {client_id[:8]}... gets {client_sats_amount} sats (≈{client_fiat_amount:.2f} GTQ, {proportion:.2%} share)") - else: - logger.info(f"Client {client_id[:8]}... gets {client_sats_amount} sats (≈0.00 GTQ, {proportion:.2%} share) - SKIPPED (zero amount)") + # Calculate equivalent fiat value in GTQ for tracking purposes + client_fiat_amount = round(client_sats_amount / exchange_rate, 2) if exchange_rate > 0 else 0.0 + + distributions[client_id] = { + "fiat_amount": client_fiat_amount, + "sats_amount": client_sats_amount, + "exchange_rate": exchange_rate + } + + logger.info(f"Client {client_id[:8]}... gets {client_sats_amount} sats (≈{client_fiat_amount:.2f} GTQ, {proportion:.2%} share)") # Verification: ensure total distribution equals base amount total_distributed = sum(dist["sats_amount"] for dist in distributions.values()) From d1242e5cd2400c19d8c79f2ce76be81c60f0c447 Mon Sep 17 00:00:00 2001 From: padreug Date: Tue, 8 Jul 2025 06:14:52 +0200 Subject: [PATCH 09/10] Refactor transaction data handling in LamassuTransactionProcessor: Improved consistency in processing None and empty values during data ingestion. Defaulted numeric fields to 0 and percentage fields to 0.0 for better error handling. Ensured clean extraction of transaction details, enhancing reliability in transaction processing. --- transaction_processor.py | 40 ++++++++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/transaction_processor.py b/transaction_processor.py index 22c4edd..a07be52 100644 --- a/transaction_processor.py +++ b/transaction_processor.py @@ -493,14 +493,26 @@ class LamassuTransactionProcessor: # Convert string values to appropriate types processed_row = {} for key, value in row.items(): - if value == '': - processed_row[key] = None + # Handle None/empty values consistently at data ingestion boundary + if value == '' or value is None: + if key in ['fiat_amount', 'crypto_amount']: + processed_row[key] = 0 # Default numeric fields to 0 + elif key in ['commission_percentage', 'discount']: + processed_row[key] = 0.0 # Default percentage fields to 0.0 + else: + processed_row[key] = None # Keep None for non-numeric fields elif key in ['transaction_id', 'device_id', 'crypto_code', 'fiat_code']: processed_row[key] = str(value) elif key in ['fiat_amount', 'crypto_amount']: - processed_row[key] = int(float(value)) if value else 0 + try: + processed_row[key] = int(float(value)) + except (ValueError, TypeError): + processed_row[key] = 0 # Fallback to 0 for invalid values elif key in ['commission_percentage', 'discount']: - processed_row[key] = float(value) if value else 0.0 + try: + processed_row[key] = float(value) + except (ValueError, TypeError): + processed_row[key] = 0.0 # Fallback to 0.0 for invalid values elif key == 'transaction_time': from datetime import datetime # Parse PostgreSQL timestamp format and ensure it's in UTC for consistency @@ -628,11 +640,11 @@ class LamassuTransactionProcessor: logger.info("No Flow Mode clients found - skipping distribution") return {} - # Extract transaction details with None-safe defaults - crypto_atoms = transaction.get("crypto_amount") # Total sats with commission baked in - fiat_amount = transaction.get("fiat_amount") # Actual fiat dispensed (principal only) - commission_percentage = transaction.get("commission_percentage") # Already stored as decimal (e.g., 0.045) - discount = transaction.get("discount") # Discount percentage + # Extract transaction details - guaranteed clean from data ingestion + crypto_atoms = transaction.get("crypto_amount", 0) # Total sats with commission baked in + fiat_amount = transaction.get("fiat_amount", 0) # Actual fiat dispensed (principal only) + commission_percentage = transaction.get("commission_percentage", 0.0) # Already stored as decimal (e.g., 0.045) + discount = transaction.get("discount", 0.0) # Discount percentage transaction_time = transaction.get("transaction_time") # ATM transaction timestamp for temporal accuracy # Normalize transaction_time to UTC if present @@ -995,11 +1007,11 @@ class LamassuTransactionProcessor: async def store_lamassu_transaction(self, transaction: Dict[str, Any]) -> Optional[str]: """Store the Lamassu transaction in our database for audit and UI""" try: - # Extract and validate transaction data + # Extract transaction data - guaranteed clean from data ingestion boundary crypto_atoms = transaction.get("crypto_amount", 0) fiat_amount = transaction.get("fiat_amount", 0) - commission_percentage = transaction.get("commission_percentage") or 0.0 - discount = transaction.get("discount") or 0.0 + commission_percentage = transaction.get("commission_percentage", 0.0) + discount = transaction.get("discount", 0.0) transaction_time = transaction.get("transaction_time") # Normalize transaction_time to UTC if present @@ -1145,8 +1157,8 @@ class LamassuTransactionProcessor: # Calculate commission amount for sending to commission wallet crypto_atoms = transaction.get("crypto_amount", 0) - commission_percentage = transaction.get("commission_percentage") or 0.0 - discount = transaction.get("discount") or 0.0 + commission_percentage = transaction.get("commission_percentage", 0.0) + discount = transaction.get("discount", 0.0) if commission_percentage and commission_percentage > 0: effective_commission = commission_percentage * (100 - discount) / 100 From bd7a72f3c0ad9bda558c80c0e53e5af02c75e6b5 Mon Sep 17 00:00:00 2001 From: padreug Date: Sat, 19 Jul 2025 00:21:13 +0200 Subject: [PATCH 10/10] Improve balance verification in LamassuTransactionProcessor: Added rounding to two decimal places for balance and fiat amounts to ensure precision in comparisons. Enhanced logging for insufficient balance scenarios, improving clarity in transaction processing and error reporting. --- transaction_processor.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/transaction_processor.py b/transaction_processor.py index a07be52..9a58408 100644 --- a/transaction_processor.py +++ b/transaction_processor.py @@ -849,10 +849,13 @@ class LamassuTransactionProcessor: logger.error(f"CRITICAL: Client {client_id[:8]}... has negative balance ({current_balance.remaining_balance:.2f} GTQ) - REFUSING payment of {distribution['sats_amount']} sats") continue - # Verify balance is sufficient for this distribution + # Verify balance is sufficient for this distribution (round to 2 decimal places to match DECIMAL(10,2) precision) fiat_equivalent = distribution["fiat_amount"] # Amount in GTQ - if current_balance.remaining_balance < fiat_equivalent: - logger.error(f"CRITICAL: Client {client_id[:8]}... insufficient balance ({current_balance.remaining_balance:.2f} < {fiat_equivalent:.2f} GTQ) - REFUSING payment") + # Round both values to 2 decimal places to match database precision and avoid floating point comparison issues + balance_rounded = round(current_balance.remaining_balance, 2) + amount_rounded = round(fiat_equivalent, 2) + if balance_rounded < amount_rounded: + logger.error(f"CRITICAL: Client {client_id[:8]}... insufficient balance ({balance_rounded:.2f} < {amount_rounded:.2f} GTQ) - REFUSING payment") continue logger.info(f"Client {client_id[:8]}... pre-payment balance check: {current_balance.remaining_balance:.2f} GTQ - SUFFICIENT for {fiat_equivalent:.2f} GTQ payment")