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