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:
233
pos/blueprints/crm_bp.py
Normal file
233
pos/blueprints/crm_bp.py
Normal file
@@ -0,0 +1,233 @@
|
||||
"""CRM Blueprint: activities, tags, loyalty, analytics.
|
||||
|
||||
Prefixes:
|
||||
/pos/api/customers/<id>/activities
|
||||
/pos/api/customers/<id>/tags
|
||||
/pos/api/customers/<id>/loyalty
|
||||
/pos/api/customers/<id>/analytics
|
||||
/pos/api/tags
|
||||
/pos/api/rewards
|
||||
"""
|
||||
|
||||
from flask import Blueprint, request, jsonify, g
|
||||
from middleware import require_auth
|
||||
from tenant_db import get_tenant_conn
|
||||
from services.crm_engine import (
|
||||
log_activity, get_activities,
|
||||
create_tag, list_tags, assign_tag, remove_tag, get_customer_tags,
|
||||
add_loyalty_points, redeem_points, get_loyalty_history,
|
||||
create_reward, list_rewards,
|
||||
get_customer_analytics,
|
||||
)
|
||||
|
||||
crm_bp = Blueprint('crm', __name__, url_prefix='/pos/api')
|
||||
|
||||
|
||||
# ─── Customer Activities ─────────────────────────────
|
||||
|
||||
@crm_bp.route('/customers/<int:customer_id>/activities', methods=['GET'])
|
||||
@require_auth('customers.view')
|
||||
def customer_activities(customer_id):
|
||||
activity_type = request.args.get('type')
|
||||
limit = min(int(request.args.get('limit', 50)), 200)
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
try:
|
||||
activities = get_activities(conn, customer_id, activity_type=activity_type, limit=limit)
|
||||
return jsonify({'activities': activities})
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@crm_bp.route('/customers/<int:customer_id>/activities', methods=['POST'])
|
||||
@require_auth('customers.edit')
|
||||
def add_customer_activity(customer_id):
|
||||
data = request.get_json() or {}
|
||||
activity_type = data.get('activity_type', 'note')
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
try:
|
||||
activity_id = log_activity(
|
||||
conn, customer_id, activity_type,
|
||||
title=data.get('title'),
|
||||
description=data.get('description'),
|
||||
metadata=data.get('metadata'),
|
||||
employee_id=getattr(g, 'employee_id', None),
|
||||
)
|
||||
return jsonify({'id': activity_id, 'message': 'Activity logged'}), 201
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
# ─── Customer Tags ─────────────────────────────
|
||||
|
||||
@crm_bp.route('/tags', methods=['GET'])
|
||||
@require_auth('customers.view')
|
||||
def get_tags():
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
try:
|
||||
tags = list_tags(conn, g.tenant_id)
|
||||
return jsonify({'tags': tags})
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@crm_bp.route('/tags', methods=['POST'])
|
||||
@require_auth('customers.edit')
|
||||
def create_new_tag():
|
||||
data = request.get_json() or {}
|
||||
if not data.get('name'):
|
||||
return jsonify({'error': 'name is required'}), 400
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
try:
|
||||
tag_id = create_tag(conn, g.tenant_id, data['name'],
|
||||
color=data.get('color', '#6B7280'),
|
||||
description=data.get('description'))
|
||||
return jsonify({'id': tag_id, 'message': 'Tag created'}), 201
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@crm_bp.route('/customers/<int:customer_id>/tags', methods=['GET'])
|
||||
@require_auth('customers.view')
|
||||
def get_customer_tags_endpoint(customer_id):
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
try:
|
||||
tags = get_customer_tags(conn, customer_id)
|
||||
return jsonify({'tags': tags})
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@crm_bp.route('/customers/<int:customer_id>/tags', methods=['POST'])
|
||||
@require_auth('customers.edit')
|
||||
def assign_customer_tag(customer_id):
|
||||
data = request.get_json() or {}
|
||||
tag_id = data.get('tag_id')
|
||||
if not tag_id:
|
||||
return jsonify({'error': 'tag_id is required'}), 400
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
try:
|
||||
assign_tag(conn, customer_id, tag_id, assigned_by=getattr(g, 'employee_id', None))
|
||||
return jsonify({'message': 'Tag assigned'})
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@crm_bp.route('/customers/<int:customer_id>/tags/<int:tag_id>', methods=['DELETE'])
|
||||
@require_auth('customers.edit')
|
||||
def remove_customer_tag(customer_id, tag_id):
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
try:
|
||||
remove_tag(conn, customer_id, tag_id)
|
||||
return jsonify({'message': 'Tag removed'})
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
# ─── Loyalty ─────────────────────────────
|
||||
|
||||
@crm_bp.route('/customers/<int:customer_id>/loyalty', methods=['GET'])
|
||||
@require_auth('customers.view')
|
||||
def get_loyalty(customer_id):
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
try:
|
||||
history = get_loyalty_history(conn, customer_id)
|
||||
# Get current balance
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT loyalty_points_balance, loyalty_tier FROM customers WHERE id = %s", (customer_id,))
|
||||
row = cur.fetchone()
|
||||
cur.close()
|
||||
return jsonify({
|
||||
'balance': row[0] or 0,
|
||||
'tier': row[1] or 'bronze',
|
||||
'history': history,
|
||||
})
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@crm_bp.route('/customers/<int:customer_id>/loyalty/add', methods=['POST'])
|
||||
@require_auth('customers.edit')
|
||||
def add_loyalty(customer_id):
|
||||
data = request.get_json() or {}
|
||||
points = int(data.get('points', 0))
|
||||
if points <= 0:
|
||||
return jsonify({'error': 'points must be > 0'}), 400
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
try:
|
||||
point_id = add_loyalty_points(
|
||||
conn, customer_id, points,
|
||||
points_type=data.get('points_type', 'earned'),
|
||||
source_type=data.get('source_type'),
|
||||
source_id=data.get('source_id'),
|
||||
description=data.get('description'),
|
||||
expires_at=data.get('expires_at'),
|
||||
)
|
||||
return jsonify({'id': point_id, 'message': f'{points} points added'}), 201
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@crm_bp.route('/customers/<int:customer_id>/loyalty/redeem', methods=['POST'])
|
||||
@require_auth('customers.edit')
|
||||
def redeem_loyalty(customer_id):
|
||||
data = request.get_json() or {}
|
||||
points = int(data.get('points', 0))
|
||||
if points <= 0:
|
||||
return jsonify({'error': 'points must be > 0'}), 400
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
try:
|
||||
redemption_id = redeem_points(
|
||||
conn, customer_id, points,
|
||||
reward_id=data.get('reward_id'),
|
||||
reward_value=data.get('reward_value'),
|
||||
description=data.get('description'),
|
||||
employee_id=getattr(g, 'employee_id', None),
|
||||
)
|
||||
return jsonify({'id': redemption_id, 'message': f'{points} points redeemed'}), 201
|
||||
except ValueError as e:
|
||||
return jsonify({'error': str(e)}), 400
|
||||
|
||||
|
||||
# ─── Rewards Catalog ─────────────────────────────
|
||||
|
||||
@crm_bp.route('/rewards', methods=['GET'])
|
||||
@require_auth('customers.view')
|
||||
def get_rewards():
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
try:
|
||||
rewards = list_rewards(conn, g.tenant_id)
|
||||
return jsonify({'rewards': rewards})
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@crm_bp.route('/rewards', methods=['POST'])
|
||||
@require_auth('customers.edit')
|
||||
def create_new_reward():
|
||||
data = request.get_json() or {}
|
||||
if not data.get('name') or data.get('points_cost') is None:
|
||||
return jsonify({'error': 'name and points_cost are required'}), 400
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
try:
|
||||
reward_id = create_reward(
|
||||
conn, g.tenant_id, data['name'], int(data['points_cost']),
|
||||
reward_type=data.get('reward_type', 'discount'),
|
||||
reward_value=data.get('reward_value'),
|
||||
description=data.get('description'),
|
||||
)
|
||||
return jsonify({'id': reward_id, 'message': 'Reward created'}), 201
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
# ─── Customer Analytics ─────────────────────────────
|
||||
|
||||
@crm_bp.route('/customers/<int:customer_id>/analytics', methods=['GET'])
|
||||
@require_auth('customers.view')
|
||||
def customer_analytics(customer_id):
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
try:
|
||||
analytics = get_customer_analytics(conn, customer_id)
|
||||
return jsonify(analytics)
|
||||
finally:
|
||||
conn.close()
|
||||
Reference in New Issue
Block a user