Files
Autoparts-DB/pos/services/bnpl_engine.py
Nexus Dev 9ff3dc4c8b 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
2026-04-27 05:23:30 +00:00

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.',
}