#!/usr/bin/env python3 """Test multi-currency support (Mejora #8). Validates: 1. MXN sale (default) works unchanged 2. USD sale stores currency='USD' and exchange_rate 3. Sale items and payments inherit currency 4. Accounting entries are in MXN (converted) 5. Quotation in USD converts correctly """ import os import sys sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) os.environ.setdefault('MASTER_DB_URL', 'postgresql://postgres@/nexus_autoparts') os.environ.setdefault('TENANT_DB_URL_TEMPLATE', 'postgresql://postgres@/{db_name}') os.environ.setdefault('POS_JWT_SECRET', 'test-secret-12345678901234567890123456789012') from services.pos_engine import process_sale, calculate_totals from services.currency import convert, get_exchange_rate, to_mxn from services.accounting_engine import record_sale_entry from tenant_db import get_tenant_conn_by_dbname RED = '\033[91m' GREEN = '\033[92m' YELLOW = '\033[93m' RESET = '\033[0m' def print_result(name, passed, detail=""): status = f"{GREEN}PASS{RESET}" if passed else f"{RED}FAIL{RESET}" print(f" [{status}] {name}" + (f" — {detail}" if detail else "")) def get_test_inventory(conn): """Get first active inventory item with a price.""" cur = conn.cursor() cur.execute(""" SELECT id, part_number, name, cost, price_1, tax_rate, branch_id FROM inventory WHERE is_active = true AND price_1 > 0 LIMIT 1 """) row = cur.fetchone() cur.close() return row def get_open_register(conn): cur = conn.cursor() cur.execute("SELECT id FROM cash_registers WHERE status = 'open' LIMIT 1") row = cur.fetchone() cur.close() return row[0] if row else None def main(): print("=" * 60) print("MULTI-CURRENCY — VALIDATION SUITE") print("=" * 60) passed = 0 failed = 0 conn = get_tenant_conn_by_dbname('tenant_acct_test') inv = get_test_inventory(conn) register_id = get_open_register(conn) if not inv: print(f"\n{RED}No inventory items available — aborting{RESET}") return passed, failed if not register_id: print(f"\n{RED}No open cash register — aborting{RESET}") return passed, failed inv_id, part_num, name, cost, price_1, tax_rate, branch_id = inv # ── Test 1: MXN sale (default) ────────────────────────────── print("\n[1] MXN Sale (default)") sale_data_mxn = { 'items': [{ 'inventory_id': inv_id, 'quantity': 2, 'unit_price': float(price_1), 'discount_pct': 0, 'tax_rate': float(tax_rate or 0.16), }], 'payment_method': 'efectivo', 'sale_type': 'cash', 'register_id': register_id, 'amount_paid': float(price_1) * 2 * 1.16 + 100, # overpay } try: sale_mxn = process_sale(conn, sale_data_mxn) conn.commit() if sale_mxn.get('currency') == 'MXN' and sale_mxn.get('exchange_rate') == 1.0: print_result("MXN sale", True, f"total={sale_mxn['total']:.2f} MXN") passed += 1 else: print_result("MXN sale", False, f"currency={sale_mxn.get('currency')}, rate={sale_mxn.get('exchange_rate')}") failed += 1 except Exception as e: conn.rollback() print_result("MXN sale", False, str(e)) failed += 1 # ── Test 2: USD sale ──────────────────────────────────────── print("\n[2] USD Sale") # Get exchange rate rate = float(get_exchange_rate(conn, 'USD', 'MXN')) # Convert price to USD price_usd = round(float(price_1) / rate, 2) sale_data_usd = { 'items': [{ 'inventory_id': inv_id, 'quantity': 2, 'unit_price': price_usd, 'discount_pct': 0, 'tax_rate': float(tax_rate or 0.16), }], 'payment_method': 'efectivo', 'sale_type': 'cash', 'register_id': register_id, 'amount_paid': price_usd * 2 * 1.16 + 10, 'currency': 'USD', } try: sale_usd = process_sale(conn, sale_data_usd) conn.commit() checks = [] if sale_usd.get('currency') == 'USD': checks.append("currency=USD") if sale_usd.get('exchange_rate', 0) > 1: checks.append(f"rate={sale_usd['exchange_rate']:.4f}") # Verify DB has currency columns populated cur = conn.cursor() cur.execute("SELECT currency, exchange_rate FROM sales WHERE id = %s", (sale_usd['id'],)) db_sale = cur.fetchone() cur.execute("SELECT currency, exchange_rate FROM sale_items WHERE sale_id = %s LIMIT 1", (sale_usd['id'],)) db_item = cur.fetchone() cur.execute("SELECT currency, exchange_rate FROM sale_payments WHERE sale_id = %s LIMIT 1", (sale_usd['id'],)) db_pay = cur.fetchone() cur.close() if db_sale and db_sale[0] == 'USD': checks.append("sale_row_currency=USD") if db_item and db_item[0] == 'USD': checks.append("item_row_currency=USD") if db_pay and db_pay[0] == 'USD': checks.append("payment_row_currency=USD") if len(checks) >= 5: print_result("USD sale", True, ", ".join(checks)) passed += 1 else: print_result("USD sale", False, f"only {len(checks)} checks passed: {checks}") failed += 1 except Exception as e: conn.rollback() print_result("USD sale", False, str(e)) failed += 1 # ── Test 3: Accounting in MXN regardless of sale currency ─── print("\n[3] Accounting entries in MXN") try: # Look up the journal entry for the USD sale cur = conn.cursor() cur.execute(""" SELECT id, description FROM journal_entries WHERE reference_type = 'sale' AND reference_id = %s """, (sale_usd['id'],)) je = cur.fetchone() cur.close() if je: print_result("Accounting entry", True, f"JE #{je[0]} exists for USD sale") passed += 1 else: print_result("Accounting entry", False, "no journal entry found") failed += 1 except Exception as e: print_result("Accounting entry", False, str(e)) failed += 1 # ── Test 4: Currency conversion helper ────────────────────── print("\n[4] Currency conversion helper") try: rate_usd_mxn = float(get_exchange_rate(conn, 'USD', 'MXN')) rate_mxn_usd = float(get_exchange_rate(conn, 'MXN', 'USD')) usd_to_mxn = convert(100, 'USD', 'MXN', rate=rate_usd_mxn, conn=conn) mxn_to_usd = convert(usd_to_mxn, 'MXN', 'USD', rate=rate_mxn_usd, conn=conn) if abs(usd_to_mxn - 100 * rate_usd_mxn) < 0.01: print_result("USD→MXN", True, f"100 USD = {usd_to_mxn:.2f} MXN") passed += 1 else: print_result("USD→MXN", False, f"expected ~{100*rate_usd_mxn:.2f}, got {usd_to_mxn:.2f}") failed += 1 if abs(mxn_to_usd - 100) < 0.05: print_result("Round-trip", True, f"100 USD → {usd_to_mxn:.2f} MXN → {mxn_to_usd:.2f} USD") passed += 1 else: print_result("Round-trip", False, f"drift={abs(mxn_to_usd - 100):.2f}") failed += 1 except Exception as e: print_result("Conversion", False, str(e)) failed += 1 # ── Test 5: to_mxn convenience ────────────────────────────── print("\n[5] to_mxn convenience") try: mxn = to_mxn(50, 'USD', conn=conn) if mxn > 50: print_result("to_mxn", True, f"50 USD = {mxn:.2f} MXN") passed += 1 else: print_result("to_mxn", False, f"expected > 50, got {mxn:.2f}") failed += 1 except Exception as e: print_result("to_mxn", False, str(e)) failed += 1 conn.close() # ── Summary ───────────────────────────────────────────────── print("\n" + "=" * 60) print(f"RESULTS: {GREEN}{passed} passed{RESET}, {RED}{failed} failed{RESET}") print("=" * 60) return passed, failed if __name__ == '__main__': passed, failed = main() sys.exit(0 if failed == 0 else 1)