Skip to main content
Contafy’s expense tracking system handles both XML-based CFDI expenses and manually entered expenses, providing complete visibility into business costs.

Upload and validation

Expense sources

Expenses in Contafy can originate from:
  • XML upload - CFDI expense receipts from suppliers (tipo_origen: 'XML')
  • Manual entry - Manually created expense records (tipo_origen: 'MANUAL')
Both types support the same payment tracking and reporting features.

CFDI expense types

Like invoices, expenses support all Mexican CFDI payment types:
  • PUE - Paid in full at time of expense
  • PPD - Deferred or partial payment
  • COMPLEMENTO_PAGO - Payment complements referencing PPD expenses

Validation process

interface ValidationState {
  rfcVerificado: boolean;
  regimenFiscalVerificado: boolean;
  uuidDuplicado: boolean;
  advertencias: string[];
  errores: string[];
  valido: boolean;
}
Each uploaded expense XML is validated for:
  • RFC matching (receptor should match profile RFC)
  • Tax regime compatibility
  • UUID uniqueness
  • Profile-specific validation rules
Expense validation rules mirror invoice validation. If your profile has bloquearSiRFCNoCoincide enabled, expenses with mismatched RFCs will be rejected.

Manual expense entry

Manual expenses allow you to track costs without formal CFDI receipts.

Requirements

  • A specific profile must be selected (not “Todas las empresas”)
  • The period must have a valid period_id
  • Period cannot be “aggregated”

Manual expense fields

interface CreateAccruedExpenseRequest {
  profile_id: string;
  period_id: string;
  concept: string;
  subtotal: number;
  iva_amount?: number;
  fecha: string;
  type: 'manual';
  categoria?: string;
}
Supported fields:
  • Concept - Description of the expense
  • Subtotal - Base amount before tax
  • IVA amount - Value-added tax (optional)
  • Date - When the expense occurred
  • Category - Optional expense categorization

Payment status tracking

Manual expenses include payment tracking:
  • is_paid - Boolean indicating if expense has been paid
  • payment_date - Date when expense was paid (nullable)
Users can mark manual expenses as paid through the UI.
Manual expenses have uuid: null and tipo_origen: 'MANUAL'. They appear alongside XML-based expenses in the expenses list with a “Manual” badge.

Automatic payment status

For XML-based expenses, Contafy calculates payment status automatically:
interface EstadoPagoDetalle {
  estado: 'PAGADO' | 'PAGO_PARCIAL' | 'NO_PAGADO';
  totalFactura: number;
  totalPagado: number;
  saldoPendiente: number;
  porcentajePagado: number;
  completamentePagado: boolean;
  ultimoSaldoInsoluto: number | null;
  tieneComplementos: boolean;
  tienePagosManuales: boolean;
}
The system:
  • Links payment complements to parent PPD expenses
  • Calculates cumulative payments
  • Tracks outstanding balance
  • Determines payment percentage

Expense list interface

The expenses page supports:
  • Profile selector - View specific business or aggregate view
  • Month/year selection - Filter by time period
  • Régimen fiscal filter - Filter by tax regime
  • Text search - Search across expense fields
  • Pagination - 10 expenses per page with TanStack Query

Summary metrics

The expenses page displays:
  • Total expenses - Count of all expenses in the period
  • Total egresos devengados - Sum of accrued expenses
  • Gastos pendientes de pago - Count of unpaid/partially paid expenses
Expense metrics are calculated from the PeriodMetricsResponse which includes both cash flow (pagados) and accrual (devengados) metrics.

Data table columns

  • Date and concept/description
  • Emisor (supplier) information
  • Total amount with tax breakdown
  • Payment type (PUE/PPD/COMPLEMENTO_PAGO)
  • Payment status badge
  • Origin badge (XML or Manual)
  • Actions menu

Bulk upload

The expense upload interface (/dashboard/expenses/upload) provides:

Upload features

  • Drag-and-drop zone for XML files
  • Multiple file selection
  • Profile selection dropdown
  • Upload queue with real-time progress
  • Validation feedback per file
  • “Did You Know” educational cards

Upload workflow

  1. Navigate to “Subir gastos”
  2. Select the profile to associate expenses with
  3. Drop or select expense XML files
  4. Review files in queue
  5. Start upload
  6. View validation results
interface UploadExpenseResponse {
  message: string;
  data: Expense;
  validacion: ValidationState;
  tipo: 'gasto';
}

Tax information extraction

Contafy extracts comprehensive tax data from CFDI expenses:
  • IVA trasladado (iva_amount) - Input VAT, eligible for credit
  • Retención IVA (retencion_iva_amount) - VAT withheld
  • Retención ISR (retencion_isr_amount) - Income tax withheld
This data feeds into tax calculations in the metrics and reports.
The IVA acreditable (creditable input VAT) shown in dashboard metrics comes from the iva_amount field of expenses. This is crucial for VAT return calculations.

Subscription limits

Expense tracking respects subscription plan limits:
  • The ExpensesPageClient component checks expensesUsed against plan limits
  • Users see warnings when approaching limits
  • Upload is restricted when limits are reached
const expensesUsed = expensesQuery.data?.pagination?.total ?? 0;

Accrued expenses API

Manual expenses use a separate API endpoint (/api/accrued-expenses):
  • POST - Create new manual expense
  • PUT - Update manual expense (including payment status)
  • DELETE - Remove manual expense
  • GET - List manual expenses for a period

Update example

interface UpdateAccruedExpenseRequest {
  concept?: string;
  subtotal?: number;
  iva_amount?: number;
  is_paid?: boolean;
  payment_date?: string | null;
  categoria?: string | null;
}

Complete expense data structure

export interface Expense {
  id: string;
  profile_id: string;
  tipo_origen: 'XML' | 'MANUAL';
  fecha: string;
  mes: number;
  año: number;
  total: number;
  subtotal: number;
  iva: number;
  iva_amount?: number;
  retencion_iva_amount?: number;
  retencion_isr_amount?: number;
  is_paid?: boolean;
  payment_date?: string | null;
  concepto: string | null;
  categoria: string | null;
  uuid: string | null;
  tipo: 'PUE' | 'PPD' | 'COMPLEMENTO_PAGO' | null;
  rfc_emisor: string | null;
  nombre_emisor: string | null;
  rfc_receptor: string | null;
  nombre_receptor: string | null;
  pagos: Array<...>;
  complemento_pago: {...} | null;
  validacion: ValidationState;
  estadoPago?: EstadoPagoDetalle | null;
  profile?: {
    id: string;
    nombre: string;
    rfc: string;
  };
  created_at: string;
  updated_at: string;
}