fix(facturacion): saldo pendiente PPD + seguridad cancelación multi-contribuyente

- Inicializar saldo_pendiente_mxn al emitir facturas I/PPD vía Facturapi
  (antes quedaba NULL y no aparecían en complemento de pago)
- Validar ownership en cancelación: backend rechaza 403 si el caller
  intenta cancelar una factura de otro contribuyente
- Frontend: ocultar botón cancelar si no se es el emisor de la factura
- Frontend: enviar contribuyenteId en la petición de cancelación
This commit is contained in:
Horux Dev
2026-05-20 05:18:34 +00:00
parent 98e982c260
commit b5e307e142
3 changed files with 20 additions and 5 deletions

View File

@@ -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);