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:
188
pos/services/bnpl_engine.py
Normal file
188
pos/services/bnpl_engine.py
Normal file
@@ -0,0 +1,188 @@
|
||||
"""BNPL Engine: Buy Now Pay Later integration stub.
|
||||
|
||||
Supports APLAZO, Kueski, Clip as providers.
|
||||
This is an architecture stub — actual API integrations require:
|
||||
- APLAZO: merchant account + API credentials
|
||||
- Kueski: merchant account + API credentials
|
||||
- Clip: merchant account + API credentials
|
||||
|
||||
The tables (bnpl_transactions) are created in v2.6 migration.
|
||||
"""
|
||||
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
# Provider configuration stubs
|
||||
PROVIDER_CONFIGS = {
|
||||
'aplazo': {
|
||||
'api_base': 'https://api.aplazo.mx/v1',
|
||||
'auth_type': 'bearer', # OAuth2 client credentials
|
||||
'webhook_path': '/pos/api/bnpl/webhook/aplazo',
|
||||
},
|
||||
'kueski': {
|
||||
'api_base': 'https://api.kueskipay.io/v1',
|
||||
'auth_type': 'api_key',
|
||||
'webhook_path': '/pos/api/bnpl/webhook/kueski',
|
||||
},
|
||||
'clip': {
|
||||
'api_base': 'https://api.clip.mx/v1',
|
||||
'auth_type': 'bearer',
|
||||
'webhook_path': '/pos/api/bnpl/webhook/clip',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def get_provider_config(provider):
|
||||
return PROVIDER_CONFIGS.get(provider)
|
||||
|
||||
|
||||
def create_bnpl_transaction(conn, sale_id, customer_id, amount, provider='aplazo',
|
||||
installment_count=4, employee_id=None):
|
||||
"""Record a BNPL transaction in pending state.
|
||||
|
||||
In production, this would:
|
||||
1. Call the provider's API to create a checkout/session
|
||||
2. Store the provider's transaction ID
|
||||
3. Return a redirect URL for the customer to complete approval
|
||||
"""
|
||||
cur = conn.cursor()
|
||||
cur.execute("""
|
||||
INSERT INTO bnpl_transactions
|
||||
(tenant_id, customer_id, sale_id, provider, amount,
|
||||
installment_count, status)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, 'pending')
|
||||
RETURNING id
|
||||
""", (None, customer_id, sale_id, provider, amount, installment_count))
|
||||
txn_id = cur.fetchone()[0]
|
||||
conn.commit()
|
||||
cur.close()
|
||||
|
||||
return {
|
||||
'transaction_id': txn_id,
|
||||
'provider': provider,
|
||||
'status': 'pending',
|
||||
'amount': amount,
|
||||
'installment_count': installment_count,
|
||||
'checkout_url': None, # Would come from provider API
|
||||
'message': 'BNPL transaction recorded. Provider integration required for live checkout.',
|
||||
}
|
||||
|
||||
|
||||
def get_bnpl_transaction(conn, txn_id):
|
||||
cur = conn.cursor()
|
||||
cur.execute("""
|
||||
SELECT id, customer_id, sale_id, provider, provider_transaction_id,
|
||||
amount, status, installment_count, installment_amount,
|
||||
customer_fee, merchant_fee, provider_response, created_at, updated_at
|
||||
FROM bnpl_transactions WHERE id = %s
|
||||
""", (txn_id,))
|
||||
row = cur.fetchone()
|
||||
cur.close()
|
||||
if not row:
|
||||
return None
|
||||
return {
|
||||
'id': row[0], 'customer_id': row[1], 'sale_id': row[2],
|
||||
'provider': row[3], 'provider_transaction_id': row[4],
|
||||
'amount': float(row[5]) if row[5] else 0,
|
||||
'status': row[6], 'installment_count': row[7],
|
||||
'installment_amount': float(row[8]) if row[8] else None,
|
||||
'customer_fee': float(row[9]) if row[9] else 0,
|
||||
'merchant_fee': float(row[10]) if row[10] else 0,
|
||||
'provider_response': row[11],
|
||||
'created_at': str(row[12]), 'updated_at': str(row[13]),
|
||||
}
|
||||
|
||||
|
||||
def update_bnpl_status(conn, txn_id, new_status, provider_response=None,
|
||||
webhook_payload=None):
|
||||
"""Update BNPL transaction status (called by webhook or polling)."""
|
||||
cur = conn.cursor()
|
||||
cur.execute("""
|
||||
UPDATE bnpl_transactions
|
||||
SET status = %s,
|
||||
provider_response = COALESCE(provider_response, '{}'::jsonb) || COALESCE(%s, '{}'::jsonb),
|
||||
webhook_payload = COALESCE(webhook_payload, '{}'::jsonb) || COALESCE(%s, '{}'::jsonb),
|
||||
updated_at = NOW()
|
||||
WHERE id = %s
|
||||
""", (new_status, json.dumps(provider_response) if provider_response else None,
|
||||
json.dumps(webhook_payload) if webhook_payload else None, txn_id))
|
||||
conn.commit()
|
||||
cur.close()
|
||||
return True
|
||||
|
||||
|
||||
def list_bnpl_transactions(conn, status=None, customer_id=None, provider=None,
|
||||
page=1, per_page=50):
|
||||
cur = conn.cursor()
|
||||
where_clauses = []
|
||||
params = []
|
||||
if status:
|
||||
where_clauses.append("status = %s")
|
||||
params.append(status)
|
||||
if customer_id:
|
||||
where_clauses.append("customer_id = %s")
|
||||
params.append(customer_id)
|
||||
if provider:
|
||||
where_clauses.append("provider = %s")
|
||||
params.append(provider)
|
||||
|
||||
where = " AND ".join(where_clauses) if where_clauses else "true"
|
||||
|
||||
cur.execute(f"SELECT count(*) FROM bnpl_transactions WHERE {where}", params)
|
||||
total = cur.fetchone()[0]
|
||||
|
||||
cur.execute(f"""
|
||||
SELECT id, customer_id, sale_id, provider, amount, status,
|
||||
installment_count, created_at
|
||||
FROM bnpl_transactions
|
||||
WHERE {where}
|
||||
ORDER BY created_at DESC
|
||||
LIMIT %s OFFSET %s
|
||||
""", params + [per_page, (page - 1) * per_page])
|
||||
|
||||
txns = []
|
||||
for r in cur.fetchall():
|
||||
txns.append({
|
||||
'id': r[0], 'customer_id': r[1], 'sale_id': r[2],
|
||||
'provider': r[3], 'amount': float(r[4]) if r[4] else 0,
|
||||
'status': r[5], 'installment_count': r[6],
|
||||
'created_at': str(r[7]),
|
||||
})
|
||||
cur.close()
|
||||
return {
|
||||
'data': txns,
|
||||
'pagination': {'page': page, 'per_page': per_page, 'total': total}
|
||||
}
|
||||
|
||||
|
||||
# ─── APLAZO API Stub ─────────────────────────────
|
||||
|
||||
def aplazo_create_checkout(amount, customer_email, customer_phone, order_id,
|
||||
api_key=None, api_secret=None):
|
||||
"""Stub for APLAZO checkout creation.
|
||||
|
||||
Production implementation requires:
|
||||
1. OAuth2 token exchange: POST /oauth/token
|
||||
2. Create checkout: POST /checkouts
|
||||
Body: {amount, merchantOrderId, customer: {email, phone}, callbacks: {onSuccess, onCancel, onReject}}
|
||||
3. Return checkout URL for customer redirect
|
||||
"""
|
||||
return {
|
||||
'checkout_url': None,
|
||||
'aplazo_order_id': None,
|
||||
'status': 'not_implemented',
|
||||
'message': 'APLAZO integration requires merchant credentials. Contact APLAZO to activate.',
|
||||
}
|
||||
|
||||
|
||||
def aplazo_webhook_handler(payload):
|
||||
"""Stub for APLAZO webhook handler.
|
||||
|
||||
Expected events: checkout.approved, checkout.rejected, checkout.cancelled,
|
||||
installment.paid, installment.failed
|
||||
"""
|
||||
return {
|
||||
'event_type': payload.get('event'),
|
||||
'handled': False,
|
||||
'message': 'APLAZO webhook handler stub. Implement when APLAZO credentials are available.',
|
||||
}
|
||||
Reference in New Issue
Block a user