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
189 lines
6.4 KiB
Python
189 lines
6.4 KiB
Python
"""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.',
|
|
}
|