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
This commit is contained in:
136
pos/blueprints/notification_bp.py
Normal file
136
pos/blueprints/notification_bp.py
Normal file
@@ -0,0 +1,136 @@
|
||||
"""Notification Blueprint: templates, logs, preferences.
|
||||
|
||||
Prefix: /pos/api/notifications
|
||||
"""
|
||||
|
||||
from flask import Blueprint, request, jsonify, g
|
||||
from middleware import require_auth
|
||||
from tenant_db import get_tenant_conn
|
||||
from services.notification_engine import (
|
||||
get_templates, create_template, update_template,
|
||||
dispatch_notification, get_notification_logs, mark_as_read,
|
||||
notify_low_stock, notify_order_ready, notify_maintenance_due,
|
||||
notify_new_sale, notify_po_received,
|
||||
)
|
||||
|
||||
notification_bp = Blueprint('notifications', __name__, url_prefix='/pos/api/notifications')
|
||||
|
||||
|
||||
@notification_bp.route('/templates', methods=['GET'])
|
||||
@require_auth()
|
||||
def list_templates():
|
||||
event_type = request.args.get('event_type')
|
||||
channel = request.args.get('channel')
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
try:
|
||||
templates = get_templates(conn, g.tenant_id, event_type=event_type, channel=channel)
|
||||
return jsonify({'templates': templates})
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@notification_bp.route('/templates', methods=['POST'])
|
||||
@require_auth()
|
||||
def create_new_template():
|
||||
data = request.get_json() or {}
|
||||
required = ['event_type', 'channel', 'name', 'body_template']
|
||||
for field in required:
|
||||
if not data.get(field):
|
||||
return jsonify({'error': f'{field} is required'}), 400
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
try:
|
||||
tid = create_template(
|
||||
conn, g.tenant_id, data['event_type'], data['channel'], data['name'],
|
||||
data['body_template'], subject_template=data.get('subject_template'),
|
||||
is_active=data.get('is_active', True),
|
||||
)
|
||||
return jsonify({'id': tid, 'message': 'Template created'}), 201
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@notification_bp.route('/templates/<int:template_id>', methods=['PUT'])
|
||||
@require_auth()
|
||||
def update_existing_template(template_id):
|
||||
data = request.get_json() or {}
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
try:
|
||||
ok = update_template(conn, template_id, data)
|
||||
if not ok:
|
||||
return jsonify({'error': 'No fields to update'}), 400
|
||||
return jsonify({'message': 'Template updated'})
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@notification_bp.route('/logs', methods=['GET'])
|
||||
@require_auth()
|
||||
def list_logs():
|
||||
recipient_type = request.args.get('recipient_type')
|
||||
recipient_id = request.args.get('recipient_id', type=int)
|
||||
status = request.args.get('status')
|
||||
limit = min(int(request.args.get('limit', 50)), 200)
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
try:
|
||||
logs = get_notification_logs(
|
||||
conn, g.tenant_id, recipient_type=recipient_type,
|
||||
recipient_id=recipient_id, status=status, limit=limit,
|
||||
)
|
||||
return jsonify({'logs': logs})
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@notification_bp.route('/logs/<int:log_id>/read', methods=['PUT'])
|
||||
@require_auth()
|
||||
def mark_log_read(log_id):
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
try:
|
||||
mark_as_read(conn, log_id)
|
||||
return jsonify({'message': 'Marked as read'})
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@notification_bp.route('/dispatch', methods=['POST'])
|
||||
@require_auth()
|
||||
def manual_dispatch():
|
||||
"""Manually dispatch a notification."""
|
||||
data = request.get_json() or {}
|
||||
event_type = data.get('event_type')
|
||||
context = data.get('context', {})
|
||||
if not event_type:
|
||||
return jsonify({'error': 'event_type is required'}), 400
|
||||
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
try:
|
||||
log_ids = dispatch_notification(
|
||||
conn, g.tenant_id, event_type, context,
|
||||
recipient_type=data.get('recipient_type', 'owner'),
|
||||
recipient_id=data.get('recipient_id'),
|
||||
channels=data.get('channels'),
|
||||
)
|
||||
return jsonify({'log_ids': log_ids, 'message': 'Notification dispatched'})
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
# ─── Convenience endpoints ─────────────────────────────
|
||||
|
||||
@notification_bp.route('/test/low-stock', methods=['POST'])
|
||||
@require_auth()
|
||||
def test_low_stock():
|
||||
data = request.get_json() or {}
|
||||
inventory_id = data.get('inventory_id')
|
||||
if not inventory_id:
|
||||
return jsonify({'error': 'inventory_id required'}), 400
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
try:
|
||||
log_ids = notify_low_stock(
|
||||
conn, g.tenant_id, inventory_id,
|
||||
stock=data.get('stock', 0),
|
||||
reorder_point=data.get('reorder_point', 5),
|
||||
)
|
||||
return jsonify({'log_ids': log_ids, 'message': 'Low stock notification sent'})
|
||||
finally:
|
||||
conn.close()
|
||||
Reference in New Issue
Block a user