#!/usr/bin/env python3 """FASE 6 — VALIDATION SUITE Tests: 1. Notifications: templates, dispatch, logs 2. Savings: retail price, calculation, reports 3. Logistics: couriers, shipments, tracking 4. Public API: keys, validation, rate limiting """ import os, sys, time sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from tenant_db import get_master_conn, get_tenant_conn_by_dbname from services.notification_engine import ( get_templates, create_template, dispatch_notification, get_notification_logs, mark_as_read, notify_low_stock, ) from services.savings_engine import ( calculate_item_savings, record_sale_savings, get_customer_savings_report, get_global_savings_stats, ) from services.logistics_engine import ( create_shipment, get_shipment, list_shipments, update_shipment_status, get_couriers, add_courier, ) from services.public_api_engine import ( create_api_key, validate_api_key, check_rate_limit, increment_rate_limit, list_api_keys, revoke_api_key, ) MASTER_DB_URL = os.environ.get('MASTER_DB_URL', 'postgresql://postgres@/nexus_autoparts') TENANT_TEMPLATE = os.environ.get('TENANT_DB_URL_TEMPLATE', 'postgresql://postgres@/{db_name}') PASS = '\033[92mPASS\033[0m' FAIL = '\033[91mFAIL\033[0m' passed = 0 failed = 0 def ok(label, condition, detail=''): global passed, failed if condition: print(f" [{PASS}] {label}") passed += 1 else: print(f" [{FAIL}] {label} {detail}") failed += 1 # Get a test tenant master = get_master_conn() cur = master.cursor() cur.execute("SELECT db_name FROM tenants WHERE is_active = true ORDER BY id LIMIT 1") row = cur.fetchone() cur.close(); master.close() if not row: print("No active tenant found!") sys.exit(1) db_name = row[0] conn = get_tenant_conn_by_dbname(db_name) print("=" * 60) print("FASE 6: Notificaciones + Ahorro + Logística + API Pública") print("=" * 60) # ─── NOTIFICATIONS ───────────────────────────── print("\n[NOTIFICACIONES]") try: # Templates templates = get_templates(conn, 1, event_type='low_stock') ok("Get templates", len(templates) >= 1, f"count={len(templates)}") # Create custom template tid = create_template(conn, 1, 'test_event', 'push', 'Test Template', 'Hello {name}', subject_template='Test: {name}') if not tid: # Template may already exist from previous run cur = conn.cursor() cur.execute("SELECT id FROM notification_templates WHERE tenant_id = 1 AND event_type = 'test_event' AND channel = 'push'") row = cur.fetchone() tid = row[0] if row else None cur.close() ok("Create template", tid is not None) # Dispatch log_ids = dispatch_notification(conn, 1, 'test_event', {'name': 'World'}, recipient_type='owner') ok("Dispatch notification", len(log_ids) >= 1) # Logs logs = get_notification_logs(conn, 1, event_type='test_event') ok("Get logs", len(logs) >= 1) if logs: mark_as_read(conn, logs[0]['id']) logs2 = get_notification_logs(conn, 1, status='read') ok("Mark as read", any(l['id'] == logs[0]['id'] for l in logs2)) # Low stock convenience cur = conn.cursor() cur.execute("SELECT id FROM inventory WHERE is_active = true LIMIT 1") inv_row = cur.fetchone() cur.close() if inv_row: ls_ids = notify_low_stock(conn, 1, inv_row[0], stock=2, reorder_point=5) ok("Low stock notify", len(ls_ids) >= 1) except Exception as e: conn.rollback() ok("Notifications", False, str(e)) # ─── SAVINGS ───────────────────────────── print("\n[AHORRO]") try: # Calculation savings, pct = calculate_item_savings(100, 150, 2) ok("Calculate savings", savings == 100.0 and pct == 33.3, f"savings={savings}, pct={pct}") # Zero retail price s2, p2 = calculate_item_savings(100, 0, 1) ok("Zero retail price", s2 == 0.0 and p2 == 0.0) # Set retail price on inventory cur = conn.cursor() cur.execute("SELECT id, price_1 FROM inventory WHERE is_active = true LIMIT 1") inv_row = cur.fetchone() if inv_row: inv_id, price = inv_row retail = float(price) * 1.5 if price else 200.00 cur.execute("UPDATE inventory SET retail_price = %s WHERE id = %s", (retail, inv_id)) conn.commit() # Create a sale with that item to test record_sale_savings cur.execute(""" INSERT INTO sales (branch_id, customer_id, employee_id, subtotal, discount_total, tax_total, total, payment_method, status, sale_type) VALUES (NULL, NULL, NULL, 100, 0, 16, 116, 'efectivo', 'completed', 'cash') RETURNING id """) sale_id = cur.fetchone()[0] cur.execute(""" INSERT INTO sale_items (sale_id, inventory_id, part_number, name, quantity, unit_price, unit_cost, discount_pct, discount_amount, tax_rate, tax_amount, subtotal, retail_price) VALUES (%s, %s, 'TEST', 'Test Item', 1, 100, 50, 0, 0, 0.16, 16, 100, %s) """, (sale_id, inv_id, retail)) conn.commit() cur.close() total_saved = record_sale_savings(conn, sale_id) ok("Record sale savings", total_saved > 0, f"saved={total_saved}") # Check sale has savings cur = conn.cursor() cur.execute("SELECT total_savings FROM sales WHERE id = %s", (sale_id,)) sale_savings = cur.fetchone()[0] cur.close() ok("Sale savings recorded", sale_savings is not None and float(sale_savings) > 0, f"sale_savings={sale_savings}") else: cur.close() ok("Savings inventory", False, "No inventory item found") # Global stats stats = get_global_savings_stats(conn, 1) ok("Global savings stats", isinstance(stats, dict) and 'total_saved' in stats) except Exception as e: conn.rollback() ok("Savings", False, str(e)) # ─── LOGISTICS ───────────────────────────── print("\n[LOGÍSTICA]") try: couriers = get_couriers(conn, 1) ok("Default couriers", len(couriers) >= 4, f"count={len(couriers)}") # Add courier courier_code = f'test_courier_{int(time.time())}' cid = add_courier(conn, 1, 'Test Courier', courier_code, tracking_url_template='https://track.example.com/{tracking_number}') ok("Add courier", cid is not None) # Create shipment result = create_shipment(conn, { 'tenant_id': 1, 'shipment_type': 'outbound', 'related_type': 'sale', 'related_id': 1, 'courier_id': cid, 'tracking_number': 'TEST123456', 'destination_address': 'Calle Falsa 123, CDMX', 'recipient_name': 'Juan Pérez', 'recipient_phone': '5551234567', 'shipping_cost': 150.00, 'notes': 'Test shipment', }) ok("Create shipment", result.get('shipment_id') is not None and result.get('tracking_url') is not None) shipment_id = result['shipment_id'] # Get shipment ship = get_shipment(conn, shipment_id) ok("Get shipment", ship is not None and ship['status'] == 'pending') # Update status update_shipment_status(conn, shipment_id, 'in_transit', location='CDMX Hub', description='Paquete en tránsito') ship2 = get_shipment(conn, shipment_id) ok("Update status", ship2['status'] == 'in_transit') ok("Tracking history", len(ship2.get('tracking_history', [])) >= 2) # List shipments shipments = list_shipments(conn, 1) ok("List shipments", shipments.get('data') is not None) except Exception as e: conn.rollback() ok("Logistics", False, str(e)) # ─── PUBLIC API ───────────────────────────── print("\n[API PÚBLICA]") try: # Create key key_id, full_key = create_api_key(conn, 1, 'Test Key', scopes=['read', 'write']) ok("Create API key", key_id is not None and full_key.startswith('nx_')) # Validate key info = validate_api_key(conn, full_key) ok("Validate key", info is not None and info.get('valid') is True) ok("Key scopes", 'read' in info.get('scopes', [])) # Invalid key bad = validate_api_key(conn, 'nx_invalid_key_here') ok("Invalid key rejected", bad is None or bad.get('valid') is False) # Rate limit check allowed, headers = check_rate_limit(conn, key_id, 60, 10000) ok("Rate limit check", allowed is True) ok("Rate limit headers", 'X-RateLimit-Limit-Minute' in headers) # Increment and check again increment_rate_limit(conn, key_id) allowed2, _ = check_rate_limit(conn, key_id, 60, 10000) ok("Rate limit increment", allowed2 is True) # Revoke revoke_api_key(conn, key_id) revoked = validate_api_key(conn, full_key) ok("Revoke key", revoked is not None and revoked.get('valid') is False) # List keys keys = list_api_keys(conn, 1) ok("List keys", isinstance(keys, list)) except Exception as e: conn.rollback() ok("Public API", False, str(e)) conn.close() print("\n" + "=" * 60) print(f"RESULTS: {PASS} {passed} passed, {FAIL} {failed} failed") print("=" * 60) sys.exit(0 if failed == 0 else 1)