diff --git a/apps/api/src/controllers/facturacion.controller.ts b/apps/api/src/controllers/facturacion.controller.ts index f866728..2f19dff 100644 --- a/apps/api/src/controllers/facturacion.controller.ts +++ b/apps/api/src/controllers/facturacion.controller.ts @@ -15,6 +15,7 @@ import { prisma } from '../config/database.js'; import { AppError } from '../middlewares/error.middleware.js'; import { hasPlatformRole } from '../utils/platform-admin.js'; import { auditFromReq } from '../utils/audit.js'; +import { recomputarSaldoPendiente } from '../utils/saldo.js'; function effectiveTenantId(req: Request): string { return req.viewingTenantId || req.user!.tenantId; @@ -272,6 +273,11 @@ export async function emitir(req: Request, res: Response, next: NextFunction) { contribuyenteId ?? null, xmlString, ]); + // Inicializar saldo pendiente para I/PPD (igual que el flujo SAT) + if (parsed.tipoComprobante === 'I' && parsed.metodoPago === 'PPD' && parsed.uuid) { + await recomputarSaldoPendiente(pool, [parsed.uuid]); + } + // Enviar por email si el receptor tiene email — ruteado a la org correcta const customerEmail = req.body.customer?.email; if (customerEmail) { @@ -325,7 +331,7 @@ export async function cancelar(req: Request, res: Response, next: NextFunction) try { const tenantId = effectiveTenantId(req); const { uuid } = req.params; - const { motive, substitution } = req.body; + const { motive, substitution, contribuyenteId: bodyContribuyenteId } = req.body; const pool = req.tenantPool!; const { rows } = await pool.query( @@ -340,6 +346,12 @@ export async function cancelar(req: Request, res: Response, next: NextFunction) const facturapiId = rows[0].facturapi_id; const cfdiContribuyenteId = rows[0].contribuyente_id as string | null; + // En modelo multi-contribuyente: si el caller envía un contribuyenteId, + // solo puede cancelar facturas de ESE contribuyente. + if (bodyContribuyenteId && cfdiContribuyenteId && bodyContribuyenteId !== cfdiContribuyenteId) { + return res.status(403).json({ message: 'No tienes permiso para cancelar esta factura' }); + } + const result = cfdiContribuyenteId ? await cancelInvoiceContribuyente(pool, cfdiContribuyenteId, facturapiId, motive || '02', substitution) : await facturapiService.cancelInvoice(tenantId, facturapiId, motive || '02', substitution); diff --git a/apps/web/app/(dashboard)/cfdi/page.tsx b/apps/web/app/(dashboard)/cfdi/page.tsx index 620fc92..14e839b 100644 --- a/apps/web/app/(dashboard)/cfdi/page.tsx +++ b/apps/web/app/(dashboard)/cfdi/page.tsx @@ -909,7 +909,7 @@ export default function CfdiPage() { } setCancelling(true); try { - await cancelarFactura(cancelTarget.uuid, cancelMotive, cancelMotive === '01' ? cancelSubstitution.trim() : undefined); + await cancelarFactura(cancelTarget.uuid, cancelMotive, cancelMotive === '01' ? cancelSubstitution.trim() : undefined, selectedContribuyenteId || undefined); await queryClient.invalidateQueries({ queryKey: ['cfdis'] }); setCancelTarget(null); alert('Factura cancelada. El estatus final depende del SAT (puede quedar en "pendiente" si requiere aceptación del receptor).'); @@ -2087,6 +2087,9 @@ export default function CfdiPage() {