fix(facturapi): configure legal data after org creation/reuse to enable Live mode
This commit is contained in:
@@ -668,14 +668,15 @@ def facturapi_setup():
|
|||||||
(result["org_id"],),
|
(result["org_id"],),
|
||||||
)
|
)
|
||||||
|
|
||||||
cur.execute(
|
if result.get("api_key"):
|
||||||
"""
|
cur.execute(
|
||||||
INSERT INTO tenant_config (key, value)
|
"""
|
||||||
VALUES ('cfdi_facturapi_key', %s)
|
INSERT INTO tenant_config (key, value)
|
||||||
ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value
|
VALUES ('cfdi_facturapi_key', %s)
|
||||||
""",
|
ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value
|
||||||
(result["api_key"],),
|
""",
|
||||||
)
|
(result["api_key"],),
|
||||||
|
)
|
||||||
|
|
||||||
log_action(conn, "FACTURAPI_SETUP", "tenant_config", None, new_value={"org_id": result["org_id"]})
|
log_action(conn, "FACTURAPI_SETUP", "tenant_config", None, new_value={"org_id": result["org_id"]})
|
||||||
|
|
||||||
@@ -683,12 +684,19 @@ def facturapi_setup():
|
|||||||
cur.close()
|
cur.close()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
return jsonify(
|
status_conn = get_tenant_conn(g.tenant_id)
|
||||||
{
|
try:
|
||||||
"org_id": result["org_id"],
|
status_cur = status_conn.cursor()
|
||||||
"message": "Facturapi organization created. Complete pending steps in Facturapi dashboard.",
|
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:
|
except ValueError as e:
|
||||||
conn.rollback()
|
conn.rollback()
|
||||||
|
|||||||
@@ -206,7 +206,7 @@ def find_organization_by_rfc(tenant_config: dict) -> dict | None:
|
|||||||
|
|
||||||
|
|
||||||
def create_organization(tenant_config: dict) -> dict:
|
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.
|
Requires FACTURAPI_USER_KEY env or a user key (sk_user_*) in tenant_config.
|
||||||
Uses tenant RFC/razon_social if available.
|
Uses tenant RFC/razon_social if available.
|
||||||
@@ -227,13 +227,56 @@ def create_organization(tenant_config: dict) -> dict:
|
|||||||
if not org_id:
|
if not org_id:
|
||||||
raise FacturapiError("Could not create organization: no id returned")
|
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
|
# Generate live secret key
|
||||||
key_resp = _request("PUT", f"/organizations/{org_id}/apikeys/live", user_key, json_payload={}, timeout=60)
|
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)
|
live_key = key_resp.get("key") if isinstance(key_resp, dict) else str(key_resp)
|
||||||
if not live_key:
|
if not live_key:
|
||||||
raise FacturapiError(f"Could not generate live key for org {org_id}")
|
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:
|
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["has_org_id"] = True
|
||||||
result["org_id"] = org_id
|
result["org_id"] = org_id
|
||||||
|
|
||||||
try:
|
def _fetch():
|
||||||
org = get_organization(org_id, api_key)
|
org = get_organization(org_id, api_key)
|
||||||
legal = org.get("legal", {})
|
legal = org.get("legal", {})
|
||||||
cert = org.get("certificate", {})
|
cert = org.get("certificate", {})
|
||||||
result.update(
|
return {
|
||||||
{
|
"configured": True,
|
||||||
"configured": True,
|
"has_csd": bool(cert.get("has_certificate")),
|
||||||
"has_csd": bool(cert.get("has_certificate")),
|
"legal_name": legal.get("name") or legal.get("legal_name"),
|
||||||
"legal_name": legal.get("name") or legal.get("legal_name"),
|
"tax_id": legal.get("tax_id"),
|
||||||
"tax_id": legal.get("tax_id"),
|
"pending_steps": org.get("pending_steps", []),
|
||||||
"pending_steps": org.get("pending_steps", []),
|
}
|
||||||
}
|
|
||||||
)
|
try:
|
||||||
|
result.update(_fetch())
|
||||||
except FacturapiError as e:
|
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
|
return result
|
||||||
|
|
||||||
|
|||||||
@@ -354,10 +354,18 @@ const Invoicing = (() => {
|
|||||||
let pendingHtml = '';
|
let pendingHtml = '';
|
||||||
if (status.pending_steps && status.pending_steps.length) {
|
if (status.pending_steps && status.pending_steps.length) {
|
||||||
pendingHtml = '<ul style="margin:var(--space-2) 0 0 0;padding-left:var(--space-5);color:var(--color-warning);">' +
|
pendingHtml = '<ul style="margin:var(--space-2) 0 0 0;padding-left:var(--space-5);color:var(--color-warning);">' +
|
||||||
status.pending_steps.map(s => `<li>${s.description || s.type}</li>`).join('') +
|
status.pending_steps.map(s => `<li>${s.description || s.type || s}</li>`).join('') +
|
||||||
'</ul>';
|
'</ul>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const configuredHtml = status.configured
|
||||||
|
? '<span style="color:var(--color-success);">Sí</span>'
|
||||||
|
: '<span style="color:var(--color-error);">No</span>';
|
||||||
|
|
||||||
|
const retryButton = !status.configured || status.error
|
||||||
|
? '<button class="btn btn--secondary" style="margin-top:var(--space-3);" onclick="Invoicing.setupFacturapi(this)">Reintentar configuración</button>'
|
||||||
|
: '';
|
||||||
|
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-4);">
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-4);">
|
||||||
<div>
|
<div>
|
||||||
@@ -367,13 +375,16 @@ const Invoicing = (() => {
|
|||||||
<div style="font-family:var(--font-mono);font-size:var(--text-caption);color:var(--color-text-muted);">${status.org_id || ''}</div>
|
<div style="font-family:var(--font-mono);font-size:var(--text-caption);color:var(--color-text-muted);">${status.org_id || ''}</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div style="font-size:var(--text-caption);color:var(--color-text-muted);">CSD</div>
|
<div style="font-size:var(--text-caption);color:var(--color-text-muted);">Configurada</div>
|
||||||
|
<div style="font-weight:var(--font-weight-semibold);">${configuredHtml}</div>
|
||||||
|
<div style="font-size:var(--text-caption);color:var(--color-text-muted);margin-top:var(--space-2);">CSD</div>
|
||||||
<div style="font-weight:var(--font-weight-semibold);">${csdHtml}</div>
|
<div style="font-weight:var(--font-weight-semibold);">${csdHtml}</div>
|
||||||
<div style="font-size:var(--text-caption);color:var(--color-text-muted);margin-top:var(--space-2);">Pasos pendientes</div>
|
<div style="font-size:var(--text-caption);color:var(--color-text-muted);margin-top:var(--space-2);">Pasos pendientes</div>
|
||||||
${pendingHtml || '<span style="color:var(--color-success);">Ninguno</span>'}
|
${pendingHtml || '<span style="color:var(--color-success);">Ninguno</span>'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
${status.error ? `<p style="color:var(--color-error);margin-top:var(--space-3);">Error: ${escapeHtml(status.error)}</p>` : ''}
|
${status.error ? `<p style="color:var(--color-error);margin-top:var(--space-3);">Error: ${escapeHtml(status.error)}</p>` : ''}
|
||||||
|
${retryButton}
|
||||||
`;
|
`;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
container.innerHTML = `<p style="color:var(--color-error);">Error: ${e.message}</p>`;
|
container.innerHTML = `<p style="color:var(--color-error);">Error: ${e.message}</p>`;
|
||||||
@@ -450,10 +461,17 @@ const Invoicing = (() => {
|
|||||||
btn.textContent = 'Configurando...';
|
btn.textContent = 'Configurando...';
|
||||||
try {
|
try {
|
||||||
const res = await api('/facturapi/setup', { method: 'POST' });
|
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();
|
loadFacturapiStatus();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert('Error: ' + e.message);
|
alert('Error: ' + e.message);
|
||||||
|
} finally {
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.textContent = 'Crear / Vincular Organización';
|
btn.textContent = 'Crear / Vincular Organización';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1069,7 +1069,7 @@
|
|||||||
<script src="/pos/static/js/splash-loader.js?v=33" defer></script>
|
<script src="/pos/static/js/splash-loader.js?v=33" defer></script>
|
||||||
<script src="/pos/static/js/pos-utils.js?v=33" defer></script>
|
<script src="/pos/static/js/pos-utils.js?v=33" defer></script>
|
||||||
<script src="/pos/static/js/sidebar.js?v=46" defer></script>
|
<script src="/pos/static/js/sidebar.js?v=46" defer></script>
|
||||||
<script src="/pos/static/js/invoicing.js?v=34" defer></script>
|
<script src="/pos/static/js/invoicing.js?v=35" defer></script>
|
||||||
<script src="/pos/static/js/sync-engine.js" defer></script>
|
<script src="/pos/static/js/sync-engine.js" defer></script>
|
||||||
<script>if('serviceWorker' in navigator){navigator.serviceWorker.register('/pos/sw.js',{scope:'/pos/'});}</script>
|
<script>if('serviceWorker' in navigator){navigator.serviceWorker.register('/pos/sw.js',{scope:'/pos/'});}</script>
|
||||||
<script src="/pos/static/js/pwa-install.js" defer></script>
|
<script src="/pos/static/js/pwa-install.js" defer></script>
|
||||||
|
|||||||
Reference in New Issue
Block a user