fix: facturapi onboarding, CSF scraper, SAT sync initial, doc notifications
- Auto-update fiscal data on org creation via updateOrgLegalOnCreate - Add Carta Manifiesto embedded iframe in CSD config page - Fix CSF scraper: 60s timeout + manual RFC fallback when SAT doesn't auto-populate - Fix contribuyenteId propagation in constancia frontend hooks/API - Fix needsInitialSync to check per-contribuyente, not just per-tenant - Fix documento notifications for global_admin using viewingTenantId - Extract CSF manually for Carlos Husberto Torres Romero - Trigger initial SAT sync for Carlos Husberto Torres Romero - Update org legal data in Facturapi for Carlos Husberto (tax_system 612 + address) Files changed: - apps/api/src/controllers/documentos.controller.ts - apps/api/src/jobs/sat-sync.job.ts - apps/api/src/services/constancia.service.ts - apps/api/src/services/contribuyente-facturapi.service.ts - apps/api/src/services/sat/sat-csf-login.ts - apps/web/app/(dashboard)/configuracion/csd/page.tsx - apps/web/lib/api/constancias.ts - apps/web/lib/hooks/use-constancias.ts - docs/sessions/2026-05-17-facturapi-csf-sync-notifications.md
This commit is contained in:
@@ -92,6 +92,8 @@ export async function createOrgContribuyente(
|
||||
// Idempotente: si existe en ambos lados, asegurar que la live key está
|
||||
// cacheada (puede faltar en orgs legacy creadas antes del refactor live).
|
||||
await ensureLiveKeyCached(pool, existingId);
|
||||
// Backfill: si la org fue creada antes de este fix, sincronizar datos fiscales.
|
||||
await updateOrgLegalOnCreate(pool, existingId, contribuyenteId);
|
||||
return { orgId: existingId, reused: true };
|
||||
} catch {
|
||||
const org = await client.organizations.create({ name: nombre });
|
||||
@@ -101,6 +103,7 @@ export async function createOrgContribuyente(
|
||||
);
|
||||
// Eager: generar y cachear live key para que la org quede lista para emitir.
|
||||
await ensureLiveKeyCached(pool, org.id);
|
||||
await updateOrgLegalOnCreate(pool, org.id, contribuyenteId);
|
||||
return { orgId: org.id, recreated: true };
|
||||
}
|
||||
}
|
||||
@@ -113,6 +116,7 @@ export async function createOrgContribuyente(
|
||||
);
|
||||
// Eager: generar y cachear live key inmediatamente tras crear la org.
|
||||
await ensureLiveKeyCached(pool, org.id);
|
||||
await updateOrgLegalOnCreate(pool, org.id, contribuyenteId);
|
||||
return { orgId: org.id };
|
||||
}
|
||||
|
||||
@@ -132,6 +136,98 @@ async function ensureLiveKeyCached(pool: Pool, orgId: string): Promise<void> {
|
||||
await persistEncryptedKey(pool, orgId, apiKey);
|
||||
}
|
||||
|
||||
interface ContribuyenteFiscalData {
|
||||
rfc: string;
|
||||
razon_social: string | null;
|
||||
regimen_fiscal: string | null;
|
||||
codigo_postal: string | null;
|
||||
domicilio: any;
|
||||
}
|
||||
|
||||
async function fetchContribuyenteFiscalData(
|
||||
pool: Pool,
|
||||
contribuyenteId: string,
|
||||
): Promise<ContribuyenteFiscalData> {
|
||||
const { rows } = await pool.query<ContribuyenteFiscalData>(
|
||||
`SELECT c.rfc, r.razon_social, c.regimen_fiscal, c.codigo_postal, c.domicilio
|
||||
FROM contribuyentes c
|
||||
LEFT JOIN rfcs r ON UPPER(r.rfc) = UPPER(c.rfc)
|
||||
WHERE c.entidad_id = $1`,
|
||||
[contribuyenteId],
|
||||
);
|
||||
if (rows.length === 0) throw new Error('Contribuyente no encontrado');
|
||||
return rows[0];
|
||||
}
|
||||
|
||||
async function buildLegalPayload(
|
||||
contrib: ContribuyenteFiscalData,
|
||||
chosenTaxSystem: string,
|
||||
currentLegal?: any,
|
||||
) {
|
||||
const domicilio = (contrib.domicilio || {}) as any;
|
||||
return {
|
||||
name: contrib.razon_social || currentLegal?.name || '',
|
||||
legal_name: contrib.razon_social || currentLegal?.legal_name || '',
|
||||
tax_system: chosenTaxSystem,
|
||||
address: {
|
||||
street: domicilio.calle || currentLegal?.address?.street || '',
|
||||
exterior: domicilio.numExterior || currentLegal?.address?.exterior || '',
|
||||
interior: domicilio.numInterior || currentLegal?.address?.interior || '',
|
||||
neighborhood: domicilio.colonia || currentLegal?.address?.neighborhood || '',
|
||||
city: domicilio.ciudad || currentLegal?.address?.city || '',
|
||||
municipality: domicilio.municipio || currentLegal?.address?.municipality || '',
|
||||
state: domicilio.estado || currentLegal?.address?.state || '',
|
||||
zip: contrib.codigo_postal || domicilio.codigoPostal || currentLegal?.address?.zip || '',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function putOrgLegal(orgId: string, payload: any): Promise<void> {
|
||||
const userKey = env.FACTURAPI_USER_KEY;
|
||||
if (!userKey) throw new Error('FACTURAPI_USER_KEY no configurada');
|
||||
const putRes = await fetch(`https://www.facturapi.io/v2/organizations/${orgId}/legal`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${userKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!putRes.ok) {
|
||||
const errText = await putRes.text();
|
||||
throw new Error(
|
||||
`Error actualizando datos fiscales de la organización Facturapi (${putRes.status}): ${errText}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Actualiza los datos fiscales de una organización Facturapi recién creada
|
||||
* usando la información del contribuyente. Se usa el primer régimen fiscal
|
||||
* registrado. No-op si no hay régimen fiscal o razón social que setear.
|
||||
*/
|
||||
async function updateOrgLegalOnCreate(
|
||||
pool: Pool,
|
||||
orgId: string,
|
||||
contribuyenteId: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const contrib = await fetchContribuyenteFiscalData(pool, contribuyenteId);
|
||||
const allowed = (contrib.regimen_fiscal || '')
|
||||
.split(',')
|
||||
.map(s => s.trim())
|
||||
.filter(Boolean);
|
||||
if (!allowed.length || !contrib.razon_social) {
|
||||
// Datos incompletos: no fallar la creación de la org, solo loguear silenciosamente.
|
||||
return;
|
||||
}
|
||||
const payload = await buildLegalPayload(contrib, allowed[0]);
|
||||
await putOrgLegal(orgId, payload);
|
||||
} catch {
|
||||
// No bloquear la creación de la org si el update legal falla.
|
||||
}
|
||||
}
|
||||
|
||||
export async function getOrgStatusContribuyente(
|
||||
pool: Pool,
|
||||
contribuyenteId: string
|
||||
@@ -409,22 +505,7 @@ async function ensureOrgLegalForEmit(
|
||||
const userKey = env.FACTURAPI_USER_KEY;
|
||||
if (!userKey) throw new Error('FACTURAPI_USER_KEY no configurada');
|
||||
|
||||
// Datos fiscales del contribuyente (razón social + domicilio)
|
||||
const { rows } = await pool.query<{
|
||||
rfc: string;
|
||||
razon_social: string | null;
|
||||
regimen_fiscal: string | null;
|
||||
codigo_postal: string | null;
|
||||
domicilio: any;
|
||||
}>(
|
||||
`SELECT c.rfc, r.razon_social, c.regimen_fiscal, c.codigo_postal, c.domicilio
|
||||
FROM contribuyentes c
|
||||
LEFT JOIN rfcs r ON UPPER(r.rfc) = UPPER(c.rfc)
|
||||
WHERE c.entidad_id = $1`,
|
||||
[contribuyenteId],
|
||||
);
|
||||
if (rows.length === 0) throw new Error('Contribuyente no encontrado');
|
||||
const contrib = rows[0];
|
||||
const contrib = await fetchContribuyenteFiscalData(pool, contribuyenteId);
|
||||
|
||||
// Validar que el régimen elegido esté entre los registrados del contrib
|
||||
const allowed = (contrib.regimen_fiscal || '')
|
||||
@@ -458,36 +539,6 @@ async function ensureOrgLegalForEmit(
|
||||
return;
|
||||
}
|
||||
|
||||
const domicilio = (contrib.domicilio || {}) as any;
|
||||
const legalPayload = {
|
||||
name: contrib.razon_social || currentLegal.name || '',
|
||||
legal_name: contrib.razon_social || currentLegal.legal_name || '',
|
||||
tax_system: chosenTaxSystem,
|
||||
address: {
|
||||
street: domicilio.calle || currentLegal.address?.street || '',
|
||||
exterior: domicilio.numExterior || currentLegal.address?.exterior || '',
|
||||
interior: domicilio.numInterior || currentLegal.address?.interior || '',
|
||||
neighborhood: domicilio.colonia || currentLegal.address?.neighborhood || '',
|
||||
city: domicilio.ciudad || currentLegal.address?.city || '',
|
||||
municipality: domicilio.municipio || currentLegal.address?.municipality || '',
|
||||
state: domicilio.estado || currentLegal.address?.state || '',
|
||||
zip: contrib.codigo_postal || domicilio.codigoPostal || currentLegal.address?.zip || '',
|
||||
},
|
||||
};
|
||||
|
||||
const putRes = await fetch(`https://www.facturapi.io/v2/organizations/${orgId}/legal`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${userKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(legalPayload),
|
||||
});
|
||||
|
||||
if (!putRes.ok) {
|
||||
const errText = await putRes.text();
|
||||
throw new Error(
|
||||
`Error actualizando datos fiscales de la organización Facturapi (${putRes.status}): ${errText}`,
|
||||
);
|
||||
}
|
||||
const payload = await buildLegalPayload(contrib, chosenTaxSystem, currentLegal);
|
||||
await putOrgLegal(orgId, payload);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user