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
234 lines
7.8 KiB
Python
234 lines
7.8 KiB
Python
"""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()
|