Major features: - Pixel-Perfect glassmorphism design (landing + POS + public catalog) - OEM/Local catalog toggle with Nexpart taxonomy (14 groups, 108 subgroups, 558 part types) - Marketplace B2B Phase 1 (bodegas, POs, status machine, WA+email notifications) - Peer-to-peer inventory (multi-instance, LAN discovery) - WhatsApp: photo→Vision AI, voice→Whisper, conversational quotations - Smart unified search (VIN/plate/part_number/keyword auto-detect) - Shop Supplies tab (vehicle-independent parts) - Chatbot AI fallback chain (5 models) + response cache - CSV inventory import tool + setup_instance.sh installer - Tablet-responsive CSS + sidebar toggle - Filters, export CSV, employee edit, business data save - Quotation system (WA→POS) with auto-print on confirmation - Live stats on landing page Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
96 lines
3.3 KiB
Python
96 lines
3.3 KiB
Python
"""
|
|
Peer API — public endpoints for inter-instance communication.
|
|
|
|
These endpoints do NOT require auth (they're called machine-to-machine by
|
|
other Nexus instances on the network). They expose read-only inventory data
|
|
so the marketplace can aggregate stock across the whole Nexus network.
|
|
|
|
Routes:
|
|
GET /pos/api/peer/health — instance status + inventory count
|
|
GET /pos/api/peer/inventory — search this instance's inventory
|
|
"""
|
|
|
|
from flask import Blueprint, request, jsonify, g
|
|
from tenant_db import get_tenant_conn
|
|
from services import peer_service
|
|
|
|
peer_bp = Blueprint('peer', __name__, url_prefix='/pos/api/peer')
|
|
|
|
|
|
# ─── Which tenant to use for the peer endpoint? ──────────────────────────
|
|
# In production each instance serves one tenant. For the demo, we hardcode
|
|
# tenant_id=11 (the demo refaccionaria). This will be read from a config
|
|
# file in the future when each instance has exactly 1 active tenant.
|
|
|
|
import os
|
|
import json
|
|
|
|
def _get_local_tenant_id():
|
|
"""Read the local tenant ID from peers.json or fall back to 11."""
|
|
try:
|
|
cfg_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'peers.json')
|
|
with open(cfg_path, 'r') as f:
|
|
cfg = json.load(f)
|
|
return cfg.get('tenant_id', 11)
|
|
except Exception:
|
|
return 11
|
|
|
|
|
|
@peer_bp.route('/health', methods=['GET'])
|
|
def peer_health():
|
|
"""Public health check — no auth. Returns instance name + basic stats."""
|
|
tenant_id = _get_local_tenant_id()
|
|
inventory_count = 0
|
|
try:
|
|
conn = get_tenant_conn(tenant_id)
|
|
cur = conn.cursor()
|
|
cur.execute("""
|
|
SELECT COUNT(*) FROM inventory i
|
|
WHERE i.is_active = TRUE
|
|
AND COALESCE((SELECT SUM(quantity) FROM inventory_operations WHERE inventory_id = i.id), 0) > 0
|
|
""")
|
|
inventory_count = cur.fetchone()[0]
|
|
cur.close()
|
|
conn.close()
|
|
except Exception as e:
|
|
print(f'[peer] health check DB error: {e}')
|
|
|
|
return jsonify({
|
|
'status': 'ok',
|
|
'instance_name': peer_service.get_instance_name(),
|
|
'instance_id': peer_service.get_instance_id(),
|
|
'inventory_count': inventory_count,
|
|
'peer_count': len(peer_service.get_peers()),
|
|
})
|
|
|
|
|
|
@peer_bp.route('/inventory', methods=['GET'])
|
|
def peer_inventory():
|
|
"""Public inventory search — no auth.
|
|
|
|
Called by other Nexus instances to see what this refaccionaria has in stock.
|
|
Returns minimal data: part number, name, brand, price, stock hint.
|
|
Does NOT expose exact stock quantities (competitive info).
|
|
|
|
Query params:
|
|
q: search term (optional — without it, returns popular/all items)
|
|
limit: max results (default 50, max 200)
|
|
"""
|
|
q = request.args.get('q', '').strip() or None
|
|
limit = min(request.args.get('limit', 50, type=int), 200)
|
|
|
|
tenant_id = _get_local_tenant_id()
|
|
try:
|
|
conn = get_tenant_conn(tenant_id)
|
|
data = peer_service.get_local_inventory(conn, query=q, limit=limit)
|
|
conn.close()
|
|
except Exception as e:
|
|
print(f'[peer] inventory query error: {e}')
|
|
data = []
|
|
|
|
return jsonify({
|
|
'instance_name': peer_service.get_instance_name(),
|
|
'data': data,
|
|
'count': len(data),
|
|
})
|