Desenvolvimento em Apex na REDA
O Apex personalizado ao redor da REDA Financials segue uma regra de ouro: construa contra a superfície global e os objetos, nunca contra os componentes internos. Aqui está o conjunto de ferramentas estável e os padrões que o utilizam bem.
A superfície global — treze classes e interfaces
Em um pacote gerenciado, apenas o Apex global fica visível para o seu código; tudo o que é public vive atrás da barreira do namespace. A REDA Financials mantém essa superfície suportada deliberadamente pequena — treze tipos globais para código de clientes e parceiros. Cinco são os pontos de entrada do dia a dia; os demais são os contratos de extensão de nota fiscal eletrônica e os tipos de dados que eles trocam:
| Classe | Tipo | Para que serve |
|---|---|---|
refin.GlobalTaxService | global with sharing | Fachada sobre os motores de imposto e de retenção na fonte; dois métodos estáticos de cálculo, sem DML |
refin.PDFGeneratorAPI | global with sharing | Geração de PDF — uma ação invocável de Fluxo mais um método Apex global |
refin.BankFeedProvider | global abstract | Contrato de extensão para provedores de feed bancário (Plaid, Belvo, …) — você o implementa, o pacote o chama |
refin.EInvoiceProvider | global abstract | Contrato de extensão para provedores de nota fiscal eletrônica (Sovos, Peppol, ZATCA, SAT, SDI, …) |
refin.OCREmailHandler | global, implementa Messaging.InboundEmailHandler | Manipulador de e-mail de entrada que transforma anexos em PDF em registros da Caixa de entrada de OCR para automação de contas a pagar |
refin.EInvoiceStatusPollSchedulable | global with sharing, implementa Schedulable | A superfície de agendamento voltada ao assinante para a consulta de status de nota fiscal eletrônica: agende com o seu próprio cron em vez de usar o cartão do Console de administração |
refin.EInvoiceTrailCreator | global with sharing | Cria uma trilha de nota fiscal eletrônica para faturas de venda contabilizadas; também exposta como a ação de Flow Create E-Invoice Trail |
refin.EInvoiceQueueProvider | interface global | Contrato de fila para extensões de nota fiscal eletrônica. Foi adicionado ao lado de EInvoiceProvider e não como métodos dele, porque uma classe abstrata global já publicada não pode ganhar métodos sem quebrar quem a implementa |
refin.EInvoiceDocumentProvider | interface global | Contrato de recuperação de documentos para extensões de nota fiscal eletrônica |
refin.EInvoiceProvisioner | interface global | Contrato de provisionamento para extensões de nota fiscal eletrônica |
refin.EInvoiceDocument · refin.EInvoiceEvent · refin.EInvoiceProvisionResult | classes globais (DTOs) | Os tipos de transferência de dados que os contratos acima recebem e devolvem |
Os contratos abstratos de provedor são abordados em Pacotes de extensão; este artigo foca nas duas classes que você chama. Todas as classes de API são entregues na versão de API 65.0, e vale a promessa padrão de pacote gerenciado: uma vez lançada, uma assinatura global não pode ser removida nem alterada sob os seus pés — ela é o contrato estável.
O que você não pode chamar
A camada de serviço interna que faz o trabalho de verdade — contabilização, importação de extrato bancário, renderização de PDF, chamadas de IA e de câmbio, atualizações de status em massa, carga inicial de impostos — é public, não global. O Apex do assinante não consegue chamar essas classes de forma alguma: o compilador simplesmente não as enxerga através da fronteira do namespace, e sua forma interna pode mudar em qualquer versão. Se um recurso de que você precisa vive apenas atrás de uma classe pública, a rota suportada é o comportamento que ela viabiliza — contabilização pelo campo de status, geração de PDF via refin.PDFGeneratorAPI, carga inicial de impostos via o Assistente de Configuração de Impostos empacotado — não a classe em si.
refin — refin.GlobalTaxService.calculateTax(...), refin.PDFGeneratorAPI.generateOne(...). Esse é o nome que a sua organização enxerga para as classes dentro do pacote gerenciado.
refin.GlobalTaxService — o cálculo de impostos como serviço
Dois métodos estáticos dão ao seu código os mesmos motores que os documentos usam, sem efeitos colaterais:
calculateTax(TaxRequest)— imposto indireto (IVA/GST/imposto sobre vendas). Informe entidade, conta, data, lado ('Sales'/'Purchase') e linhas (valor + produto ou perfil fiscal explícito, com sua próprialineKeypara correlação); receba de volta linhas de imposto por linha totalmente resolvidas — alíquota, valor, sinalizador de recuperabilidade e a conta contábil em que cada imposto seria contabilizado.calculateWithholdingTax(WHTRequest)— o motor de retenção na fonte, mesma estrutura: linhas entram, retenção por linha sai (alíquota, base, valor, motivos de isenção, conta contábil).- O contrato: sem DML, nenhum registro criado, nunca lança exceção — os erros retornam como
success=falsecom uma mensagem. A persistência é inteiramente sua: perfeito para cotações, calculadoras, estimativas pré-contabilização ou verificações de paridade com sistemas externos.
A referência completa
Ambos os pontos de entrada são estáticos:
global static TaxResponse calculateTax(TaxRequest request)
global static WHTResponse calculateWithholdingTax(WHTRequest request)
A documentação da classe declara o contrato claramente: "Nenhum DML é executado. Nenhum registro de Source_Line_Tax ou WHT_Transaction é criado. Quem chama a classe é responsável pela persistência e pela contabilização." Uma resposta "sempre é retornada, nunca lança exceção para quem chamou" — problemas de validação (entidade, conta, data, lado ou linhas ausentes, ou um valor de lado inválido) retornam como success=false com o motivo em errorMessage, na mesma linguagem de validação que os próprios documentos usam. Existe uma classe GlobalTaxServiceException na superfície global, mas, sob o contrato normal, você lê a resposta em vez de capturar exceções.
Os DTOs de requisição e resposta são classes globais com membros globais — tudo abaixo é seu para definir ou ler:
| DTO | Membro | O que contém |
|---|---|---|
TaxRequest | entityId | A entidade cuja configuração fiscal se aplica |
accountId | A conta de cliente ou fornecedor na transação | |
transactionDate | Data para a qual as alíquotas e regras são resolvidas | |
side | 'Sales' ou 'Purchase' | |
fiscalOperationId / lines | Substituição opcional de operação fiscal; a lista de linhas TaxLineRequest | |
TaxLineRequest | lineKey | Sua chave de correlação — devolvida na linha de resultado correspondente |
amount | O valor da linha sobre o qual calcular | |
productId / taxProfileId | O produto direciona a busca do perfil fiscal; um taxProfileId explícito substitui e ignora essa busca | |
taxBase | 'Exclusive of Tax' (padrão), 'Inclusive of Tax' ou 'Tax Exempt' | |
TaxResponse | success / errorMessage | O contrato de resultado — verifique success antes de ler os resultados |
nexusEstablished / totalTaxAmount / totalEmbeddedTaxAmount / lines | Resultados em nível de cabeçalho mais o detalhe por linha | |
TaxResultLine | lineKey / taxTypeId / taxRuleId | Qual linha, qual imposto, qual regra foi acionada |
taxBase / taxRate / taxAmount / isTaxIncludedInPrice / isRecoverable / sequence | O cálculo resolvido, exatamente como a contabilização o calcularia | |
glAccountId / deductionGlAccountId | A conta contábil em que o imposto seria contabilizado; a conta de dedução é não nula apenas para impostos embutidos (por-dentro) |
O conjunto de retenção na fonte espelha isso:
| DTO | Membro | O que contém |
|---|---|---|
WHTRequest | entityId / accountId / transactionDate / side / documentCurrencyCode / lines | Mesma estrutura de cabeçalho do imposto; documentCurrencyCode é opcional e usado na conversão do limite de multimoeda |
WHTLineRequest | lineKey / lineSubtotal / lineTaxAmount / ruleOverrideId | lineTaxAmount alimenta o modo Base Inclui Imposto; ruleOverrideId prevalece sobre a regra padrão da conta |
WHTResponse | success / errorMessage / totalWHTAmount / lines | totalWHTAmount soma apenas as linhas não isentas |
WHTResultLine | lineKey / ruleId / rateId / rate / base / amount / isExempt / exemptReason / glAccountId | Retenção por linha, incluindo por que uma linha está isenta |
Como as contas contábeis são resolvidas. As contas contábeis de impostos indiretos vêm dos registros de Mapeamento de Tipo de Imposto para o Razão (refin__Tax_Type_GL_Mapping__c): um mapeamento específico da entidade prevalece sobre um mapeamento em nível de organização, indexado pelo tipo de imposto e pela recuperabilidade referente ao lado da requisição; impostos embutidos recebem adicionalmente uma conta de dedução do lado 'Sales Deduction' / 'Purchase Deduction' do mapeamento. As contas contábeis de retenção vêm da Conta Contábil na Alíquota de Retenção na Fonte correspondente, recorrendo à conta padrão de Retenção a Pagar da entidade quando não há correspondência. Em outras palavras, a resposta informa não apenas quanto, mas onde isso cairia — a mesma resolução que a contabilização realiza.
Uma chamada mínima, ao estilo Meridian — a entidade dos EUA precificando a linha de aluguel de um locatário antes de qualquer fatura existir:
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);
}
O gêmeo de retenção segue o mesmo ritmo. A entidade da Índia da Meridian, estimando o TDS sobre a fatura de um prestador antes de ela ser lançada: monte um WHTRequest com side = 'Purchase', um WHTLineRequest por linha da fatura carregando lineSubtotal, chame calculateWithholdingTax e leia cada WHTResultLine — rate, base, amount e isExempt com seu exemptReason quando um certificado ou limite isenta a linha. Como nada é persistido, a equipe de contas a pagar pode informar o valor líquido a pagar ao fornecedor e os números vão bater com o documento contabilizado até o último centavo.
refin.PDFGeneratorAPI — documentos a partir de código
generateOne(sourceRecordId, sourceObjectApiName) renderiza e anexa o PDF de uma Fatura de venda, Nota de crédito, Recebimento, Desembolso ou Ordem de compra contabilizados — retornando os IDs de conteúdo e, onde o território habilita links públicos, a URL. O gêmeo em Fluxo é abordado em Integração de Fluxos do Salesforce.
A assinatura:
global static PDFResult generateOne(Id sourceRecordId, String sourceObjectApiName)
Nas próprias palavras da classe, "pacotes de extensão chamam isso a partir do Apex quando precisam de geração programática de PDF fora do contexto de Fluxo." O sourceObjectApiName deve ser um dos objetos de origem suportados:
| Nome da API do objeto | Documento | Observação |
|---|---|---|
refin__Sales_Invoice__c | Fatura de venda | Notas de crédito também passam por aqui — uma Nota de crédito é um tipo de Fatura de venda (refin__Sales_Invoice_Type__c), então você passa o nome do objeto de fatura |
refin__Cash_Receipt__c | Recebimento | — |
refin__Cash_Disbursement__c | Desembolso | — |
refin__Purchase_Order__c | Ordem de compra | — |
O PDF gerado é anexado ao registro de origem como um ContentVersion com um ContentDocumentLink — um File normal do Salesforce que você pode consultar, compartilhar ou enviar por e-mail. O publicUrl do resultado só é preenchido "se a Entidade tiver a URL Pública Automática habilitada; caso contrário, é nulo" — portanto, o código que envia links por e-mail aos clientes deve verificar se é nulo em vez de presumir que existe uma URL. E você pode nem precisar de código: cada entidade traz alternadores de geração 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) — o generateOne é para os momentos fora desses casos, como regenerar sob demanda ou anexar a um envio personalizado.
PDFGeneratorAPI, GlobalTaxService, OCREmailHandler) em seu acesso a classes Apex. Se quem chama a classe precisa de acesso explícito depende de como e onde o código é executado — verifique em um sandbox com um usuário realista antes de lançar, e conceda acesso no seu próprio conjunto de permissões se as chamadas forem bloqueadas.
Contabilizando a partir do Apex — o campo de status é a API
Não existe um método post() para chamar, por design: o motor de contabilização é executado na fase de trigger da própria atualização do objeto de origem. Seu Apex contabiliza um documento da mesma forma que um usuário faz — mude refin__Status__c de 'Draft' para 'Posted' com uma atualização de DML, e toda a maquinaria é executada: os motores de imposto e retenção na fonte, a gravação da taxa de câmbio na transição para Posted, e todas as proteções (portão de aprovação, validação de período, bloqueios de entidade/conta contábil inativa). Uma contabilização recusada aparece como uma exceção de DML carregando a mensagem exata da proteção — trate isso como o contrato da API, não como ruído.
O padrão confiável de criação é em duas etapas: inserir como Draft (cabeçalho e linhas) e, em seguida, atualizar o status para 'Posted' em uma segunda chamada. Na inserção, um status nulo assume 'Draft' por padrão, os números de documento são atribuídos automaticamente a partir da sequência de documentos da entidade (predefinir o Name significa numeração manual, e a exclusividade é validada), e os padrões da entidade são preenchidos automaticamente — livro razão, período contábil e data de vencimento a partir da condição de pagamento do fornecedor. Observe que alterar a entidade ou o tipo de fatura em um Draft reordena sua numeração, então faça suas edições antes de contabilizar.
TriggerHandler tem, sim, um bypass interno usado pelos caminhos de migração do próprio motor, mas ele é public, não global: invisível ao cruzar o namespace, então o código do assinante e das extensões não consegue alcançá-lo. Os registros contabilizados ficam bloqueados, exceto para campos de fluxo de trabalho na lista de permissões, como refin__Approval_Status__c, e o Cancelamento é imutável. Nunca escreva linhas de refin__Accounting_Transaction__c você mesmo; as proteções existem precisamente para impedir lançamentos contábeis forjados manualmente.
Reagindo a eventos contábeis
Seus triggers e Fluxos disparados por registro nos objetos refin__ coexistem tranquilamente com os do próprio pacote (a ordem entre pacotes e a automação da organização segue as regras padrão da plataforma — mantenha os seus idempotentes). O padrão de maior valor: reagir a transições de status — um trigger na Fatura de venda que percebe Draft→Posted pode alimentar seu sistema downstream com o documento já totalmente validado e numerado. Reaja depois do trabalho do pacote: leia o estado final do registro em vez de tentar se intercalar.
As regras que mantêm você seguro em atualizações
| Faça | Não faça |
|---|---|
| Chame as classes globais documentadas | Use reflexão em classes public do pacote — elas são invisíveis entre namespaces e mudam sem aviso |
Contabilize atualizando refin__Status__c (os motores rodam por você) | Escreva linhas de refin__Accounting_Transaction__c você mesmo — as proteções existem precisamente para impedir lançamentos contábeis forjados manualmente |
| Respeite os erros de DML como o contrato da API (as mensagens literais das proteções) | Engula falhas de contabilização — elas são verdades contábeis, não ruído |
| Estenda dados via conjuntos de campos e seus próprios campos | Dependa do texto de ajuda de campos do pacote ou de detalhes internos da interface |
GlobalTaxService.calculateTax com valores de linha em rascunho para mostrar a um locatário o aluguel com imposto incluso antes de qualquer fatura existir — mesmas alíquotas, mesmas regras, zero registros criados. Quando o contrato é assinado, a integração cria o modelo recorrente real e os números batem até o centavo, porque é um único motor.
Erros comuns a evitar
| Erro | Por que prejudica | Faça isto em vez disso |
|---|---|---|
Persistir os resultados do GlobalTaxService como seus próprios registros de imposto | O contrato é explícito: nenhum DML é executado e "quem chama a classe é responsável pela persistência e pela contabilização" — mas linhas de imposto construídas manualmente se desviam do que a contabilização vai calcular | Use a resposta para exibição e lógica de decisão; deixe a contabilização do documento escrever os registros reais de imposto |
| Envolver o DML de atualização de status em um try/catch genérico que registra o erro e continua | Uma contabilização recusada é uma resposta contábil; engoli-la deixa documentos silenciosamente não contabilizados | Capture, exponha a mensagem literal a uma pessoa ou a uma fila, e trate o não contabilizado como um item de trabalho |
Chamar generateOne para objetos que ele não suporta | O conjunto suportado é exatamente SI, CR, CD, PO (notas de crédito usam o tipo SI) — qualquer outro objeto falha | Verifique a tabela de objetos suportados acima antes de conectar um botão a ele |
| Reagir a Draft→Posted antes de o pacote terminar | Sua lógica lê um estado parcialmente contabilizado — números ou linhas de livro razão ainda não finais | Reaja em after-update sobre o estado final, ou de forma assíncrona (padrões queueable/platform-cache) depois que a transação for confirmada |
| Testar contra um sandbox sem licença ativa | As gravações contábeis falham com a mensagem de licença, e a equipe passa a depurar "bugs" fantasmas | Ative a licença em todo sandbox e configure o usuário de execução antes de testar código de integração |
O que vem a seguir
- Pacotes de extensão — os contratos de provedor para ISVs.
- Lightning Web Components — a contraparte de interface.
- Visão geral da API — o mapa.
Perguntas frequentes
Sim — atualize o status na sua transação; o motor de contabilização é executado na mesma fase de trigger da transação. Faça o orçamento de CPU de acordo (os motores de impostos não são gratuitos) e trate o caminho de exceção de DML: uma contabilização recusada sofre rollback com a mensagem da proteção (guard).
As alavancas suportadas são de configuração: contas padrão da entidade, mapeamentos de impostos para o livro razão, contas contábeis por tipo de ativo, estruturas de relatórios. Deliberadamente não existe um hook de código dentro da contabilização — o determinismo é o produto. A lógica personalizada de contas contábeis deve ficar antes da contabilização (escolhendo as contas nas linhas) ou depois (lançamentos contábeis).
Leia errorMessage — é a mesma linguagem de validação usada pelos documentos (entidade ausente, lado inválido, sem linhas). Quem chama a classe decide se isso vira uma mensagem para o usuário ou uma correção de dados.
Os métodos de cálculo do GlobalTaxService têm o compromisso contratual de não lançar: os resultados "sempre são retornados, nunca lançam exceção para quem chamou" — os erros trafegam em success=false mais errorMessage. Existe uma classe GlobalTaxServiceException na superfície global, mas a sua codificação defensiva deve se basear na flag de resposta, não em blocos catch.
PDFGeneratorAPI e EInvoiceTrailCreator são concedidos pelos dez conjuntos de permissões empacotados, porque as ações de Fluxo empacotadas os chamam. GlobalTaxService, BankFeedProvider, OCREmailHandler e os tipos de contrato de nota fiscal eletrônica não são concedidos por nenhum — conceda você mesmo o acesso à classe Apex ao usuário que faz a chamada. Verifique na sua organização; a exigência varia conforme a forma como o código é invocado.
Opere-os pela guia Operações do Console de Administração — essa é a superfície de controle suportada, com estado da programação, resultados e logs de auditoria em um só lugar. As classes de lote em si não fazem parte do contrato global.
Isso foi útil?