Files
Autoparts-DB/pos/tests/test_fase5.py
Nexus Dev 9ff3dc4c8b FASE 4-5-6: Infraestructura, CRM, Service Orders, Notificaciones, Ahorro, Logistica, API Publica
FASE 4:
- Redis cache de stock con fallback graceful
- Multi-moneda (MXN/USD) con contabilidad en MXN
- Proveedores y ordenes de compra completo
- Meilisearch 1.5M+ partes indexadas
- Metabase KPIs con dashboard auto-generado

FASE 5:
- CRM mejorado: activities, tags, loyalty program, analytics
- Imagenes de partes: upload, resize, thumbnails WebP
- Ordenes de servicio Kanban: received->diagnosis->repair->ready->delivered
- Garantias/RMA, alertas de reorden, multi-sucursal
- Stubs BNPL (APLAZO) y ERP Sync (Aspel/Contpaqi)

FASE 6:
- Notificaciones automaticas: push/WhatsApp/email/in-app
- Reportes de ahorro vs retail_price
- Logistica + tracking: DHL, FedEx, Estafeta, 99min, Uber
- API Publica: API keys, rate limiting, catalog search

Migraciones: v1.9-v3.0
Tests: 93/93 pasando
Backup: nexus_backup_20260427_045859.tar.gz
2026-04-27 05:23:30 +00:00

243 lines
8.4 KiB
Python

#!/usr/bin/env python3
"""FASE 5 — VALIDATION SUITE
Tests:
1. CRM: activities, tags, loyalty, analytics
2. Service Orders: create, status transitions, items, labor, kanban
3. Images: upload info delete
"""
import os, sys, io, time
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from PIL import Image
from tenant_db import get_master_conn, get_tenant_conn_by_dbname
from services.crm_engine import (
log_activity, get_activities, create_tag, list_tags,
assign_tag, get_customer_tags, add_loyalty_points,
redeem_points, get_loyalty_history, get_customer_analytics,
)
from services.service_order_engine import (
create_service_order, get_service_order, list_service_orders,
update_status, add_item, add_labor, get_kanban_summary,
)
from services.image_service import save_image, delete_image, get_image_info
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 5: CRM + Service Orders + Images — VALIDATION")
print("=" * 60)
# ─── Find test customer ─────────────────────────────
cur = conn.cursor()
cur.execute("SELECT id FROM customers WHERE is_active = true LIMIT 1")
cust_row = cur.fetchone()
customer_id = cust_row[0] if cust_row else None
cur.close()
if not customer_id:
print("No customer found — creating one")
cur = conn.cursor()
cur.execute("INSERT INTO customers (name, phone, is_active) VALUES ('Test CRM', '5550000000', true) RETURNING id")
customer_id = cur.fetchone()[0]
conn.commit()
cur.close()
print(f"\nTest customer_id: {customer_id}")
# ─── CRM: Activities ─────────────────────────────
print("\n[CRM ACTIVITIES]")
try:
act_id = log_activity(conn, customer_id, 'note', title='Test note', description='Hello CRM')
ok("Log activity", act_id is not None)
acts = get_activities(conn, customer_id)
ok("Get activities", len(acts) >= 1, f"count={len(acts)}")
except Exception as e:
conn.rollback()
ok("CRM activities", False, str(e))
# ─── CRM: Tags ─────────────────────────────
print("\n[CRM TAGS]")
try:
tag_name = f"VIP-{int(time.time())}"
tag_id = create_tag(conn, 1, tag_name, color='#FFD700', description='Very Important Customer')
ok("Create tag", tag_id is not None)
tags = list_tags(conn, 1)
ok("List tags", any(t['name'] == tag_name for t in tags))
assign_tag(conn, customer_id, tag_id)
cust_tags = get_customer_tags(conn, customer_id)
ok("Assign tag", any(t['name'] == tag_name for t in cust_tags))
except Exception as e:
conn.rollback()
ok("CRM tags", False, str(e))
# ─── CRM: Loyalty ─────────────────────────────
print("\n[CRM LOYALTY]")
try:
# Add enough points for gold tier
pid = add_loyalty_points(conn, customer_id, 2500, points_type='earned',
source_type='sale', description='Test points')
ok("Add loyalty points", pid is not None)
history = get_loyalty_history(conn, customer_id)
ok("Loyalty history", len(history) >= 1)
# Check balance updated (may have points from previous runs)
cur = conn.cursor()
cur.execute("SELECT loyalty_points_balance, loyalty_tier FROM customers WHERE id = %s", (customer_id,))
bal, tier = cur.fetchone()
cur.close()
ok("Balance updated", bal >= 2500, f"balance={bal}")
ok("Tier updated", tier in ('gold', 'platinum'), f"tier={tier}") # 2500+ pts = gold or platinum
except Exception as e:
conn.rollback()
ok("CRM loyalty", False, str(e))
# ─── CRM: Analytics ─────────────────────────────
print("\n[CRM ANALYTICS]")
try:
analytics = get_customer_analytics(conn, customer_id)
ok("Analytics computed", isinstance(analytics, dict))
ok("Analytics has LTV", 'ltv' in analytics)
ok("Analytics has churn_risk", analytics.get('churn_risk') in ['low', 'medium', 'high'])
except Exception as e:
conn.rollback()
ok("CRM analytics", False, str(e))
# ─── Service Orders ─────────────────────────────
print("\n[SERVICE ORDERS]")
try:
so = create_service_order(conn, {
'tenant_id': 1, 'branch_id': None, 'customer_id': customer_id,
'priority': 'high', 'reception_notes': 'Test order',
'estimated_cost': 1500.00, 'mileage_in': 50000,
})
ok("Create SO", so.get('service_order_id') is not None, f"id={so.get('service_order_id')}")
so_id = so['service_order_id']
so_detail = get_service_order(conn, so_id)
ok("Get SO detail", so_detail is not None and so_detail['status'] == 'received')
ok("SO has order_number", so_detail.get('order_number', '').startswith('SO-'))
# Status transition: received -> diagnosis
result = update_status(conn, so_id, 'diagnosis', changed_by=1, notes='Starting diagnosis')
ok("Status received->diagnosis", result['new_status'] == 'diagnosis')
# Invalid transition
try:
update_status(conn, so_id, 'delivered')
ok("Invalid transition blocked", False, "Should have raised ValueError")
except ValueError:
ok("Invalid transition blocked", True)
# Add item
item_id = add_item(conn, so_id, {
'part_number': 'BP-123', 'name': 'Brake Pads', 'quantity': 2,
'unit_price': 450.00, 'status': 'pending',
})
ok("Add item", item_id is not None)
# Add labor
labor_id = add_labor(conn, so_id, {
'description': 'Replace brake pads', 'hours': 2.0, 'hourly_rate': 350.00,
})
ok("Add labor", labor_id is not None)
# Refresh detail
so_detail2 = get_service_order(conn, so_id)
ok("SO has items", len(so_detail2.get('items', [])) >= 1)
ok("SO has labor", len(so_detail2.get('labor', [])) >= 1)
# List orders
orders = list_service_orders(conn, status='diagnosis')
ok("List orders", orders.get('data') is not None)
# Kanban summary
summary = get_kanban_summary(conn)
ok("Kanban summary", 'received' in summary or 'diagnosis' in summary)
except Exception as e:
conn.rollback()
import traceback
ok("Service Orders", False, f"{e}\n{traceback.format_exc()}")
# ─── Images ─────────────────────────────
print("\n[IMAGES]")
try:
# Create a test image in memory
img = Image.new('RGB', (800, 600), color='red')
buf = io.BytesIO()
img.save(buf, format='PNG')
buf.seek(0)
# Need a tenant_id and item_id
tenant_id = 1
cur = conn.cursor()
cur.execute("SELECT id FROM inventory WHERE is_active = true LIMIT 1")
inv_row = cur.fetchone()
if inv_row:
item_id = inv_row[0]
else:
cur.execute("INSERT INTO inventory (part_number, name, is_active) VALUES ('IMG-TEST', 'Image Test', true) RETURNING id")
item_id = cur.fetchone()[0]
conn.commit()
cur.close()
result = save_image(tenant_id, item_id, file_obj=buf, filename_hint='test.png')
ok("Save image", result.get('image_url') is not None)
info = get_image_info(tenant_id, item_id)
ok("Get image info", info['has_image'] is True)
delete_image(tenant_id, item_id)
info2 = get_image_info(tenant_id, item_id)
ok("Delete image", info2['has_image'] is False)
except Exception as e:
conn.rollback()
import traceback
ok("Images", False, f"{e}\n{traceback.format_exc()}")
conn.close()
print("\n" + "=" * 60)
print(f"RESULTS: {PASS} {passed} passed, {FAIL} {failed} failed")
print("=" * 60)
sys.exit(0 if failed == 0 else 1)