Desarrollo en Apex sobre REDA
El Apex personalizado alrededor de REDA Financials sigue una regla de oro: construir contra la superficie global y los objetos, nunca contra los internos. Aquí está el conjunto de herramientas estable y los patrones que lo aprovechan bien.
La superficie global — trece clases e interfaces
En un paquete gestionado, solo el código Apex global es visible para tu código; todo lo public vive detrás del muro del espacio de nombres (namespace). REDA Financials mantiene esa superficie admitida deliberadamente pequeña — trece tipos globales para el código de clientes y socios. Cinco son los puntos de entrada del día a día; el resto son los contratos de extensión de facturación electrónica y los tipos de datos que intercambian:
| Clase | Tipo | Para qué sirve |
|---|---|---|
refin.GlobalTaxService | global with sharing | Fachada sobre los motores de impuestos y de retención (WHT); dos métodos de cálculo estáticos, sin DML |
refin.PDFGeneratorAPI | global with sharing | Generación de PDF — una acción invocable de Flow más un método Apex global |
refin.BankFeedProvider | global abstract | Contrato de extensión para proveedores de feed bancario (Plaid, Belvo, …) — tú lo implementas, el paquete lo invoca |
refin.EInvoiceProvider | global abstract | Contrato de extensión para proveedores de facturación electrónica (Sovos, Peppol, ZATCA, SAT, SDI, …) |
refin.OCREmailHandler | global, implementa Messaging.InboundEmailHandler | Manejador de correo entrante que convierte los adjuntos PDF en registros de la bandeja de OCR para la automatización de cuentas por pagar |
refin.EInvoiceStatusPollSchedulable | global with sharing, implementa Schedulable | La superficie de programación destinada al suscriptor para el sondeo de estado de facturación electrónica: prográmalo con tu propio cron en lugar de usar la tarjeta de la Consola de administración |
refin.EInvoiceTrailCreator | global with sharing | Crea un rastro de factura electrónica para facturas de venta contabilizadas; también se expone como la acción de Flow Create E-Invoice Trail |
refin.EInvoiceQueueProvider | interfaz global | Contrato de cola para extensiones de facturación electrónica. Se añadió junto a EInvoiceProvider y no como métodos suyos, porque una clase abstracta global ya publicada no puede ganar métodos sin romper a quienes la implementan |
refin.EInvoiceDocumentProvider | interfaz global | Contrato de recuperación de documentos para extensiones de facturación electrónica |
refin.EInvoiceProvisioner | interfaz global | Contrato de aprovisionamiento para extensiones de facturación electrónica |
refin.EInvoiceDocument · refin.EInvoiceEvent · refin.EInvoiceProvisionResult | clases globales (DTO) | Los tipos de transferencia de datos que los contratos anteriores reciben y devuelven |
Los contratos de proveedor abstractos se explican en Paquetes de extensión; este artículo se enfoca en las dos clases que tú llamas. Todas las clases de la API se distribuyen en la versión de API 65.0, y aplica la promesa estándar de los paquetes gestionados: una vez publicadas, las firmas globales no pueden eliminarse ni cambiarse por debajo de ti — son el contrato estable.
Lo que no puedes llamar
La capa de servicio interna que hace el trabajo real — contabilización, importación de estados de cuenta bancarios, renderizado de PDF, llamadas de IA y de tipo de cambio, actualizaciones masivas de estado, carga inicial de impuestos — es public, no global. El código Apex del suscriptor no puede llamar estas clases en absoluto: el compilador simplemente no las ve del otro lado del límite de espacio de nombres, y su forma interna puede cambiar en cualquier versión. Si una capacidad que necesitas vive solo detrás de una clase pública, la vía admitida es el comportamiento que esa clase habilita — la contabilización mediante el campo de estado, la generación de PDF mediante refin.PDFGeneratorAPI, la carga inicial de impuestos mediante el Asistente de carga inicial de impuestos empaquetado — no la clase en sí.
refin — refin.GlobalTaxService.calculateTax(...), refin.PDFGeneratorAPI.generateOne(...). Ese es el nombre que tu organización ve para las clases dentro del paquete gestionado.
refin.GlobalTaxService — el cálculo de impuestos como servicio
Dos métodos estáticos le dan a tu código los mismos motores que usan los documentos, sin efectos secundarios:
calculateTax(TaxRequest)— impuesto indirecto (IVA/GST/impuesto sobre ventas). Aliméntalo con entidad, cuenta, fecha, lado ('Sales'/'Purchase') y líneas (importe + producto o perfil fiscal explícito, con tu propiolineKeypara correlación); obtén de vuelta filas de impuesto por línea totalmente resueltas — tasa, importe, indicador de recuperabilidad, y la cuenta contable a la que se contabilizaría cada impuesto.calculateWithholdingTax(WHTRequest)— el motor de retención de impuestos, misma forma: líneas de entrada, retención por línea de salida (tasa, base, importe, motivos de exención, cuenta contable).- El contrato: sin DML, sin registros creados, nunca lanza una excepción — los errores se devuelven como
success=falsecon un mensaje. La persistencia es enteramente tuya: perfecto para cotizaciones, calculadoras, estimaciones previas a la contabilización o verificaciones de paridad con sistemas externos.
La referencia completa
Ambos puntos de entrada son estáticos:
global static TaxResponse calculateTax(TaxRequest request)
global static WHTResponse calculateWithholdingTax(WHTRequest request)
La documentación de la clase declara el contrato con claridad: "No se realiza DML. No se crean registros de Source_Line_Tax ni WHT_Transaction. El llamador es dueño de la persistencia y de la contabilización." Una respuesta "siempre se devuelve, nunca lanza una excepción al llamador" — los problemas de validación (entidad, cuenta, fecha, lado o líneas faltantes, o un valor de lado inválido) vuelven como success=false con la razón en errorMessage, en el mismo lenguaje de validación que usan los propios documentos. Existe una clase GlobalTaxServiceException en la superficie global, pero bajo el contrato normal lees la respuesta en lugar de capturarla.
Los DTO de solicitud y respuesta son clases globales con miembros globales — todo lo siguiente es tuyo para establecer o leer:
| DTO | Miembro | Qué contiene |
|---|---|---|
TaxRequest | entityId | La entidad cuya configuración fiscal aplica |
accountId | La cuenta de cliente o proveedor en la transacción | |
transactionDate | Fecha para la que se resuelven las tasas y reglas | |
side | 'Sales' o 'Purchase' | |
fiscalOperationId / lines | Anulación opcional de operación fiscal; la lista de filas TaxLineRequest | |
TaxLineRequest | lineKey | Tu clave de correlación — se devuelve tal cual en la línea de resultado correspondiente |
amount | El importe de la línea sobre el que calcular | |
productId / taxProfileId | El producto impulsa la búsqueda del perfil fiscal; un taxProfileId explícito lo anula y lo omite | |
taxBase | 'Exclusive of Tax' (predeterminado), 'Inclusive of Tax', o 'Tax Exempt' | |
TaxResponse | success / errorMessage | El contrato de resultado — verifica success antes de leer los resultados |
nexusEstablished / totalTaxAmount / totalEmbeddedTaxAmount / lines | Resultados a nivel de encabezado más el detalle por línea | |
TaxResultLine | lineKey / taxTypeId / taxRuleId | Qué línea, qué impuesto, qué regla se activó |
taxBase / taxRate / taxAmount / isTaxIncludedInPrice / isRecoverable / sequence | El cálculo resuelto, exactamente como lo calcularía la contabilización | |
glAccountId / deductionGlAccountId | La cuenta contable a la que se contabilizaría el impuesto; la cuenta contable de deducción es distinta de nulo solo para impuestos incluidos (por dentro) |
El conjunto de retención de impuestos lo refleja:
| DTO | Miembro | Qué contiene |
|---|---|---|
WHTRequest | entityId / accountId / transactionDate / side / documentCurrencyCode / lines | Misma forma de encabezado que el impuesto; documentCurrencyCode es opcional y se usa para la conversión del umbral en escenarios multimoneda |
WHTLineRequest | lineKey / lineSubtotal / lineTaxAmount / ruleOverrideId | lineTaxAmount alimenta el modo Base incluye impuesto; ruleOverrideId tiene prioridad sobre la regla predeterminada de la cuenta |
WHTResponse | success / errorMessage / totalWHTAmount / lines | totalWHTAmount suma solo las líneas no exentas |
WHTResultLine | lineKey / ruleId / rateId / rate / base / amount / isExempt / exemptReason / glAccountId | Retención por línea, incluyendo por qué una línea está exenta |
Cómo se resuelven las cuentas contables. Las cuentas contables de impuesto indirecto provienen de los registros de mapeo contable por tipo de impuesto (refin__Tax_Type_GL_Mapping__c): un mapeo específico de la entidad tiene prioridad sobre uno de toda la organización, indexado por tipo de impuesto y recuperabilidad para el lado de la solicitud; los impuestos incluidos además reciben una cuenta contable de deducción del lado 'Sales Deduction' / 'Purchase Deduction' del mapeo. Las cuentas contables de retención provienen de la cuenta contable en la Tasa de retención de impuestos coincidente, y por defecto usan la cuenta por pagar de retención predeterminada de la entidad. En otras palabras, la respuesta te indica no solo cuánto sino dónde aterrizaría — la misma resolución que realiza la contabilización.
Una llamada mínima, al estilo Meridian — la entidad de EE. UU. cotizando la línea de renta de un inquilino antes de que exista alguna factura:
refin.GlobalTaxService.TaxRequest req = new refin.GlobalTaxService.TaxRequest();
req.entityId = meridianUsEntityId;
req.accountId = brightlineAccountId;
req.transactionDate = Date.today();
req.side = 'Sales';
refin.GlobalTaxService.TaxLineRequest line = new refin.GlobalTaxService.TaxLineRequest();
line.lineKey = 'unit-4b-rent';
line.amount = 9000;
line.productId = officeRentProductId;
req.lines = new List<refin.GlobalTaxService.TaxLineRequest>{ line };
refin.GlobalTaxService.TaxResponse res = refin.GlobalTaxService.calculateTax(req);
if (res.success) {
for (refin.GlobalTaxService.TaxResultLine t : res.lines) {
System.debug(t.lineKey + ' -> rate ' + t.taxRate + ', tax ' + t.taxAmount
+ ', GL ' + t.glAccountId);
}
} else {
System.debug(res.errorMessage);
}
El gemelo de retención sigue el mismo ritmo. La entidad de India de Meridian, estimando el TDS sobre la factura de un contratista antes de registrarla: arma un WHTRequest con side = 'Purchase', un WHTLineRequest por línea de factura que lleva lineSubtotal, llama a calculateWithholdingTax, y lee cada WHTResultLine — rate, base, amount, y isExempt con su exemptReason cuando un certificado o umbral exime la línea. Como nada se persiste, el equipo de cuentas por pagar puede cotizar el neto a pagar al proveedor y las cifras coincidirán con el documento contabilizado hasta el último centavo.
refin.PDFGeneratorAPI — documentos desde código
generateOne(sourceRecordId, sourceObjectApiName) renderiza y adjunta el PDF de una Factura de venta, Nota de crédito, Recibo de caja, Comprobante de egreso u Orden de compra contabilizados — devolviendo los IDs de contenido y, donde el territorio habilita enlaces públicos, la URL. El gemelo en Flow se explica en Flows Integration.
La firma:
global static PDFResult generateOne(Id sourceRecordId, String sourceObjectApiName)
En las propias palabras de la clase, "Los paquetes de extensión llaman a esto desde Apex cuando necesitan generación programática de PDF fuera del contexto de Flow." El sourceObjectApiName debe ser uno de los objetos de origen admitidos:
| Nombre de API del objeto | Documento | Nota |
|---|---|---|
refin__Sales_Invoice__c | Factura de venta | Las Notas de crédito también pasan por aquí — una Nota de crédito es un tipo de Factura de venta (refin__Sales_Invoice_Type__c), así que pasas el nombre del objeto de la factura |
refin__Cash_Receipt__c | Recibo de caja | — |
refin__Cash_Disbursement__c | Comprobante de egreso | — |
refin__Purchase_Order__c | Orden de compra | — |
El PDF resultante se adjunta al registro de origen como un ContentVersion con un ContentDocumentLink — un Archivo estándar de Salesforce que puedes consultar, compartir o enviar por correo. El publicUrl del resultado solo se completa "si la Entidad tiene habilitado Auto Public URL; nulo en caso contrario" — así que el código que envía enlaces por correo a clientes debe verificar si es nulo en lugar de asumir que existe una URL. Y es posible que ni siquiera necesites código: cada entidad lleva interruptores de generación automática por tipo de documento (refin__Auto_Gen_PDF_Sales_Invoice__c, refin__Auto_Gen_PDF_Cash_Receipt__c, refin__Auto_Gen_PDF_Cash_Disbursement__c, refin__Auto_Gen_PDF_Purchase_Order__c, refin__Auto_Gen_PDF_Credit_Memo__c) — generateOne es para los momentos fuera de esos, como regenerar bajo demanda o adjuntar a un envío personalizado.
PDFGeneratorAPI, GlobalTaxService, OCREmailHandler) en su acceso a clases Apex. Si tus llamadores necesitan acceso explícito a la clase depende de cómo y dónde se ejecuta el código — verifícalo en un sandbox con un usuario realista antes de publicar, y otorga el acceso en tu propio conjunto de permisos si las llamadas quedan bloqueadas.
Contabilizar desde Apex — el campo de estado es la API
No existe un método post() para llamar, por diseño: el motor de contabilización se ejecuta en la fase de trigger de la propia actualización del objeto de origen. Tu código Apex contabiliza un documento de la misma forma que lo hace un usuario — cambia refin__Status__c de 'Draft' a 'Posted' con una actualización de DML, y se ejecuta toda la maquinaria: los motores de impuestos y de retención, el marcado del tipo de cambio en la transición a Contabilizado, y todos los guardianes (control de aprobación, validación de período, bloqueos de entidad/cuenta contable inactiva). Una contabilización rechazada se manifiesta como una excepción de DML que lleva el mensaje exacto del guardián — trátala como el contrato de la API, no como ruido.
El patrón de creación confiable son dos pasos: insertar como Borrador (encabezado y líneas), y luego actualizar el estado a 'Posted' en una segunda llamada. Al insertar, un estado nulo toma por defecto 'Draft', los números de documento se asignan automáticamente desde la secuencia de documentos de la entidad (predefinir el Name implica numeración manual, y se valida la unicidad), y los valores predeterminados de la entidad se completan automáticamente — libro mayor, período contable y fecha de vencimiento a partir de la condición de pago del proveedor. Ten en cuenta que cambiar la entidad o el tipo de factura en un Borrador reasigna su número, así que haz tus ediciones antes de contabilizar.
TriggerHandler sí lleva un bypass interno que usan las rutas de migración del propio motor, pero es public, no global: invisible al cruzar el espacio de nombres, así que el código del suscriptor y de las extensiones no puede alcanzarlo. Los registros contabilizados quedan bloqueados excepto por campos de flujo de trabajo en lista blanca como refin__Approval_Status__c, y la Anulación es inmutable. Nunca escribas registros de refin__Accounting_Transaction__c por tu cuenta; los guardianes existen precisamente para impedir asientos de libro mayor forjados a mano.
Reaccionar a eventos contables
Tus triggers y Flows disparados por registro sobre objetos refin__ coexisten sin problema con los del propio paquete (el orden entre paquetes y la automatización de la organización sigue las reglas estándar de la plataforma — mantén los tuyos idempotentes). El patrón de mayor valor: reaccionar a las transiciones de estado — un trigger en Factura de venta que detecta Draft→Posted puede alimentar tu sistema descendente con el documento ya completamente validado y numerado. Reacciona después del trabajo del paquete: lee el estado final del registro en lugar de intentar interponerte.
Las reglas que te mantienen a salvo en las actualizaciones
| Hacer | No hacer |
|---|---|
| Llamar a las clases globales documentadas | Usar reflexión hacia clases public del paquete — son invisibles entre espacios de nombres y cambian sin aviso |
Contabilizar actualizando refin__Status__c (los motores se ejecutan por ti) | Escribir registros de refin__Accounting_Transaction__c por tu cuenta — los guardianes existen precisamente para impedir asientos de libro mayor forjados a mano |
| Respetar los errores de DML como el contrato de la API (los mensajes literales del guardián) | Silenciar los fallos de contabilización — son verdades contables, no ruido |
| Extender los datos mediante FieldSets y tus propios campos | Depender del texto de ayuda de los campos del paquete o de detalles internos de la interfaz |
GlobalTaxService.calculateTax con importes de línea provisionales para mostrarle a un inquilino la renta con impuesto incluido antes de que exista alguna factura — mismas tasas, mismas reglas, cero registros creados. Cuando se firma el contrato de arrendamiento, la integración crea la plantilla recurrente real y las cifras coinciden hasta el centavo, porque es un solo motor.
Errores comunes que evitar
| Error | Por qué perjudica | Haz esto en su lugar |
|---|---|---|
Persistir los resultados de GlobalTaxService como tus propios registros de impuesto | El contrato es explícito: no se realiza DML y "El llamador es dueño de la persistencia y de la contabilización" — pero las filas de impuesto armadas a mano se desalinean de lo que calculará la contabilización | Usa la respuesta para mostrar información y para la lógica de decisión; deja que la contabilización del documento escriba los registros de impuesto reales |
| Envolver el DML de actualización de estado en un try/catch general que registra y continúa | Una contabilización rechazada es una respuesta contable; silenciarla deja documentos sin contabilizar sin que nadie lo note | Captura, expón el mensaje literal a una persona o a una cola, y trata lo no contabilizado como un elemento de trabajo |
Llamar a generateOne para objetos que no admite | El conjunto admitido es exactamente SI, CR, CD, PO (las notas de crédito viajan con el tipo SI) — cualquier otro falla | Revisa la tabla de objetos admitidos anterior antes de conectarle un botón |
| Reaccionar a Draft→Posted antes de que el paquete termine | Tu lógica lee un estado a medio contabilizar — números o filas de libro mayor aún no definitivos | Reacciona en after-update sobre el estado final, o de forma asíncrona (patrones de tipo queueable/caché de plataforma) una vez que la transacción se confirma |
| Probar contra un sandbox sin una licencia activa | Las escrituras contables fallan con el mensaje de licencia, y el equipo depura "errores" fantasma | Activa la licencia en cada sandbox y asigna al usuario que ejecuta las pruebas antes de probar el código de integración |
Qué sigue
- Paquetes de extensión — los contratos de proveedor para ISV.
- Lightning Web Components — la contraparte de interfaz.
- Descripción general de la API — el mapa general.
Preguntas frecuentes
Sí — actualiza el estado dentro de tu transacción; el motor de contabilización se ejecuta en la misma fase de trigger de esa transacción. Presupuesta el CPU en consecuencia (los motores de impuestos no son gratis) y maneja la ruta de excepción de DML: una contabilización rechazada revierte con el mensaje del guardián.
Las palancas admitidas son de configuración: cuentas contables predeterminadas de la entidad, mapeos contables de impuestos, cuentas contables por tipo de activo, marcos. Deliberadamente no hay un gancho de código dentro de la contabilización — el determinismo es el producto. La lógica contable personalizada va antes de la contabilización (elegir las cuentas en las líneas) o después (asientos contables).
Lee errorMessage — usa el mismo lenguaje de validación que los documentos (entidad faltante, lado inválido, sin líneas). Tu código llamador decide si eso es un mensaje de cara al usuario o una corrección de datos.
Los métodos de cálculo de GlobalTaxService tienen el contrato de no hacerlo: los resultados "siempre se devuelven, nunca lanzan una excepción al llamador" — los errores viajan en success=false más errorMessage. Existe una clase GlobalTaxServiceException en la superficie global, pero tu codificación defensiva debe centrarse en el indicador de la respuesta, no en bloques catch.
PDFGeneratorAPI y EInvoiceTrailCreator los otorgan los diez conjuntos de permisos empaquetados, porque las acciones de Flow empaquetadas los llaman. GlobalTaxService, BankFeedProvider, OCREmailHandler y los tipos de contrato de facturación electrónica no los otorga ninguno: otorga tú mismo el acceso a la clase Apex al usuario que hace la llamada. Verifícalo en tu organización; el requisito varía según cómo se invoque el código.
Opéralos en su lugar desde la pestaña Operaciones de la Consola de administración — esa es la superficie de control admitida, con el estado de la programación, los resultados y los registros de auditoría en un solo lugar. Las clases de los lotes en sí no forman parte del contrato global.
¿Te resultó útil?