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

@@ -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() {
<td className="py-3">
<div className="flex items-center gap-0">
{(cfdi as any).source === 'facturapi' && (cfdi.status === 'Vigente' || cfdi.status === '1') && (
// Solo el contribuyente emisor puede cancelar su propia factura
((!selectedContribuyenteId && !cfdi.contribuyenteId) ||
(selectedContribuyenteId && cfdi.contribuyenteId === selectedContribuyenteId)) && (
<Button
variant="ghost"
size="icon"
@@ -2096,7 +2099,7 @@ export default function CfdiPage() {
>
<XCircle className="h-4 w-4" />
</Button>
)}
))}
<Button
variant="ghost"
size="icon"

View File

@@ -112,8 +112,8 @@ export const updatePaqueteCatalogo = (id: number, data: { precio?: number; activ
apiClient.put<PaqueteCatalogoAdmin>(`/facturacion/timbres/paquetes-catalogo/${id}`, data).then(r => r.data);
export const emitirFactura = (data: InvoiceData) =>
apiClient.post<InvoiceResult>('/facturacion/emitir', data).then(r => r.data);
export const cancelarFactura = (uuid: string, motive?: string, substitution?: string) =>
apiClient.post(`/facturacion/cancelar/${uuid}`, { motive, substitution }).then(r => r.data);
export const cancelarFactura = (uuid: string, motive?: string, substitution?: string, contribuyenteId?: string) =>
apiClient.post(`/facturacion/cancelar/${uuid}`, { motive, substitution, contribuyenteId }).then(r => r.data);
export const downloadPdf = (id: string) =>
apiClient.get(`/facturacion/pdf/${id}`, { responseType: 'blob' }).then(r => r.data);
export const downloadXml = (id: string) =>