feat: facturación primer pago, fixes SAT/MP, autocompletado RFCs/conceptos

Backend:
- Notificación email al admin cuando llega primer pago aprobado (sin factura auto)
- Endpoints GET /pagos-sin-factura y POST /emitir-factura-pago para admin global
- Fix vinculación org Facturapi Horux 360 (69f23a5a242e0af47a41fa0d)
- Fix webhook MP: validación defensiva de x-signature header
- Fix autocompleto RFCs: eliminado filtro por contribuyenteId
- Fix autocompleto conceptos: eliminado filtro por contribuyenteId
- SAT fixes: anti-bot CSF scraper, request reuse, date range fix, stale job thresholds
- SAT sync request reuse across jobs para evitar agotar cuota diaria
- Typo fix MP_ACCESS_TOKEN en .env
- Trial invitations system backend

Frontend:
- Nueva página /admin/facturas-pendientes con tabla y emisión manual
- Métrica 'Facturas pendientes' en /clientes (clickable)
- Navegación onboarding FIEL/CSD corregida
- Sidebar themes sincronizados
- Fix SAT portal migration scraper (NetIQ)
- Trial invitation acceptance pages
This commit is contained in:
Horux Dev
2026-05-09 21:56:42 +00:00
parent b00b677c54
commit 9f11a0ba39
70 changed files with 2801 additions and 609 deletions

View File

@@ -0,0 +1,70 @@
import { apiClient } from './client';
export interface TrialInvitation {
id: string;
tenantId: string;
plan: string;
durationDays: number;
status: string;
token: string;
sentAt: string;
expiresAt: string;
acceptedAt: string | null;
tenant: {
nombre: string;
rfc: string;
} | null;
}
export async function getPendingInvitation(): Promise<TrialInvitation | null> {
try {
const response = await apiClient.get<TrialInvitation | null>('/invitations/trial/pending');
return response.data;
} catch (error: any) {
if (error?.response?.status === 404) return null;
throw error;
}
}
export async function getInvitationByToken(token: string): Promise<TrialInvitation | null> {
try {
const response = await apiClient.get<TrialInvitation | null>(`/invitations/trial/token/${token}`);
return response.data;
} catch (error: any) {
if (error?.response?.status === 404) return null;
throw error;
}
}
export async function acceptInvitation(token: string): Promise<{
success: boolean;
trialEndsAt: string;
plan: string;
durationDays: number;
}> {
const response = await apiClient.post(`/invitations/trial/${token}/accept`);
return response.data;
}
// Admin endpoints
export async function getAllInvitations(filters?: { tenantId?: string; status?: string }): Promise<TrialInvitation[]> {
const params = new URLSearchParams();
if (filters?.tenantId) params.append('tenantId', filters.tenantId);
if (filters?.status) params.append('status', filters.status);
const response = await apiClient.get<TrialInvitation[]>(`/invitations/trial?${params.toString()}`);
return response.data;
}
export async function createInvitation(data: {
tenantId: string;
plan?: string;
durationDays: number;
}): Promise<TrialInvitation> {
const response = await apiClient.post<TrialInvitation>('/invitations/trial', data);
return response.data;
}
export async function cancelInvitation(id: string): Promise<TrialInvitation> {
const response = await apiClient.post<TrialInvitation>(`/invitations/trial/${id}/cancel`);
return response.data;
}