diff --git a/pos/blueprints/invoicing_bp.py b/pos/blueprints/invoicing_bp.py index 5327d86..40a02d5 100644 --- a/pos/blueprints/invoicing_bp.py +++ b/pos/blueprints/invoicing_bp.py @@ -668,14 +668,15 @@ def facturapi_setup(): (result["org_id"],), ) - cur.execute( - """ - INSERT INTO tenant_config (key, value) - VALUES ('cfdi_facturapi_key', %s) - ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value - """, - (result["api_key"],), - ) + if result.get("api_key"): + cur.execute( + """ + INSERT INTO tenant_config (key, value) + VALUES ('cfdi_facturapi_key', %s) + ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value + """, + (result["api_key"],), + ) log_action(conn, "FACTURAPI_SETUP", "tenant_config", None, new_value={"org_id": result["org_id"]}) @@ -683,12 +684,19 @@ def facturapi_setup(): cur.close() conn.close() - return jsonify( - { - "org_id": result["org_id"], - "message": "Facturapi organization created. Complete pending steps in Facturapi dashboard.", - } - ) + status_conn = get_tenant_conn(g.tenant_id) + try: + status_cur = status_conn.cursor() + status = facturapi_service.get_org_status(_get_issuer_config(status_cur)) + status["org_id"] = result["org_id"] + if not result.get("legal_updated"): + status["error"] = status.get("error") or "Datos fiscales no pudieron configurarse automáticamente. Configúralos en el dashboard de Facturapi." + status_cur.close() + status_conn.close() + except Exception: + status_conn.close() + raise + return jsonify(status) except ValueError as e: conn.rollback() diff --git a/pos/services/facturapi_service.py b/pos/services/facturapi_service.py index eb86bd1..0b4abd4 100644 --- a/pos/services/facturapi_service.py +++ b/pos/services/facturapi_service.py @@ -206,7 +206,7 @@ def find_organization_by_rfc(tenant_config: dict) -> dict | None: def create_organization(tenant_config: dict) -> dict: - """Create a new Facturapi organization for the tenant and return live key. + """Create or reuse a Facturapi organization, configure legal data and return a live key. Requires FACTURAPI_USER_KEY env or a user key (sk_user_*) in tenant_config. Uses tenant RFC/razon_social if available. @@ -227,13 +227,56 @@ def create_organization(tenant_config: dict) -> dict: if not org_id: raise FacturapiError("Could not create organization: no id returned") + # Configure fiscal/legal data (required before Live mode can be used) + try: + update_organization_legal(tenant_config, org_id) + except FacturapiError: + # If legal update fails, still return org info so the caller can surface it, + # but do not generate a live key because it would be unusable. + return {"org_id": org_id, "api_key": None, "legal_updated": False} + # Generate live secret key key_resp = _request("PUT", f"/organizations/{org_id}/apikeys/live", user_key, json_payload={}, timeout=60) live_key = key_resp.get("key") if isinstance(key_resp, dict) else str(key_resp) if not live_key: raise FacturapiError(f"Could not generate live key for org {org_id}") - return {"org_id": org_id, "api_key": live_key} + return {"org_id": org_id, "api_key": live_key, "legal_updated": True} + + +def _build_legal_payload(tenant_config: dict) -> dict: + """Build Facturapi /organizations/{id}/legal payload from tenant config.""" + rfc = (tenant_config.get("rfc") or "").upper().strip() + legal_name = (tenant_config.get("razon_social") or "").strip() + tax_system = (tenant_config.get("regimen_fiscal") or "").strip() or "601" + zip_code = (tenant_config.get("cp") or tenant_config.get("tenant_cp") or "").strip() or "00000" + if not rfc or not legal_name: + raise FacturapiError("RFC y Razón Social son obligatorios para configurar la organización en Facturapi") + return { + "tax_id": rfc, + "legal_name": legal_name, + "tax_system": tax_system, + "address": { + "zip": zip_code, + "street": (tenant_config.get("direccion") or "").strip(), + "exterior": "", + "interior": "", + "neighborhood": "", + "city": "", + "municipality": "", + "state": "", + }, + } + + +def update_organization_legal(tenant_config: dict, org_id: str) -> dict: + """Update the legal/fiscal data of a Facturapi organization. + + Uses the user key (admin) because the org may not have a live key yet. + """ + user_key = _get_user_key_for_tenant(tenant_config) + payload = _build_legal_payload(tenant_config) + return _request("PUT", f"/organizations/{org_id}/legal", user_key, json_payload=payload, timeout=60) def get_org_status(tenant_config: dict) -> dict: @@ -264,21 +307,31 @@ def get_org_status(tenant_config: dict) -> dict: result["has_org_id"] = True result["org_id"] = org_id - try: + def _fetch(): org = get_organization(org_id, api_key) legal = org.get("legal", {}) cert = org.get("certificate", {}) - result.update( - { - "configured": True, - "has_csd": bool(cert.get("has_certificate")), - "legal_name": legal.get("name") or legal.get("legal_name"), - "tax_id": legal.get("tax_id"), - "pending_steps": org.get("pending_steps", []), - } - ) + return { + "configured": True, + "has_csd": bool(cert.get("has_certificate")), + "legal_name": legal.get("name") or legal.get("legal_name"), + "tax_id": legal.get("tax_id"), + "pending_steps": org.get("pending_steps", []), + } + + try: + result.update(_fetch()) except FacturapiError as e: - result["error"] = str(e) + # If the org reports "not configured for Live" (401) and we have a user key, + # try to backfill legal data and retry. + if e.status_code == 401 and _get_user_key_for_tenant(tenant_config): + try: + update_organization_legal(tenant_config, org_id) + result.update(_fetch()) + except FacturapiError as e2: + result["error"] = str(e2) + else: + result["error"] = str(e) return result diff --git a/pos/static/js/invoicing.js b/pos/static/js/invoicing.js index def585b..056b442 100644 --- a/pos/static/js/invoicing.js +++ b/pos/static/js/invoicing.js @@ -354,10 +354,18 @@ const Invoicing = (() => { let pendingHtml = ''; if (status.pending_steps && status.pending_steps.length) { pendingHtml = '
Error: ${escapeHtml(status.error)}
` : ''} + ${retryButton} `; } catch (e) { container.innerHTML = `Error: ${e.message}
`; @@ -450,10 +461,17 @@ const Invoicing = (() => { btn.textContent = 'Configurando...'; try { const res = await api('/facturapi/setup', { method: 'POST' }); - alert('Organización vinculada: ' + res.org_id); + if (res.error) { + alert('Aviso: ' + res.error); + } else if (res.configured) { + alert('Organización vinculada y configurada: ' + res.org_id); + } else { + alert('Organización vinculada: ' + res.org_id + '. Revisa pasos pendientes.'); + } loadFacturapiStatus(); } catch (e) { alert('Error: ' + e.message); + } finally { btn.disabled = false; btn.textContent = 'Crear / Vincular Organización'; } diff --git a/pos/templates/invoicing.html b/pos/templates/invoicing.html index 04e0d93..66f0bc2 100644 --- a/pos/templates/invoicing.html +++ b/pos/templates/invoicing.html @@ -1069,7 +1069,7 @@ - +