REDA One Help Center
EN
Language
English EN Español ES Português PT Italiano coming soon Français FR Deutsch coming soon العربية coming soon
reda.one ↗
Home / REDA Financials AI / API & Integration / Custom Development / Apex Development on REDA

Apex Development on REDA

Custom Apex around REDA Financials follows one golden rule: build against the global surface and the objects, never against internals. Here's the stable toolkit and the patterns that use it well.

The global surface — thirteen classes and interfaces

In a managed package, only global Apex is visible to your code; everything public lives behind the namespace wall. REDA Financials keeps that supported surface deliberately small — thirteen global types for customer and partner code. Five carry the day-to-day entry points; the rest are the e-invoicing extension contracts and the data types they exchange:

ClassKindWhat it's for
refin.GlobalTaxServiceglobal with sharingFacade over the tax and WHT engines; two static calculation methods, no DML
refin.PDFGeneratorAPIglobal with sharingPDF generation — a Flow invocable action plus a global Apex method
refin.BankFeedProviderglobal abstractExtension contract for bank feed providers (Plaid, Belvo, …) — you implement it, the package calls it
refin.EInvoiceProviderglobal abstractExtension contract for e-invoicing providers (Sovos, Peppol, ZATCA, SAT, SDI, …)
refin.OCREmailHandlerglobal, implements Messaging.InboundEmailHandlerInbound email handler that turns PDF attachments into OCR Inbox records for AP automation
refin.EInvoiceStatusPollSchedulableglobal with sharing, implements SchedulableThe subscriber-facing scheduling surface for the e-invoice status poll — schedule it yourself with your own cron instead of using the Admin Console card
refin.EInvoiceTrailCreatorglobal with sharingCreates an E-Invoice Trail for posted sales invoices — also exposed as the Flow action Create E-Invoice Trail
refin.EInvoiceQueueProviderglobal interfaceQueue contract for e-invoicing extensions. Added alongside EInvoiceProvider rather than as methods on it, because a released global abstract class cannot gain methods without breaking implementors
refin.EInvoiceDocumentProviderglobal interfaceDocument-retrieval contract for e-invoicing extensions
refin.EInvoiceProvisionerglobal interfaceProvisioning contract for e-invoicing extensions
refin.EInvoiceDocument · refin.EInvoiceEvent · refin.EInvoiceProvisionResultglobal classes (DTOs)The data-transfer types the provider contracts above pass and return

The abstract provider contracts are covered in Extension Packages; this article focuses on the two classes you call. All of the API classes ship at API version 65.0, and the standard managed-package promise applies: once released, global signatures cannot be removed or changed out from under you — they are the stable contract.

What you cannot call

The internal service layer that does the real work — posting, bank-statement import, PDF rendering, AI and FX callouts, bulk status updates, tax seeding — is public, not global. Subscriber Apex cannot call these classes at all: the compiler simply doesn't see them across the namespace boundary, and their internal shape can change in any release. If a capability you need lives only behind a public class, the supported route is the behavior it powers — posting via the status field, PDF generation via refin.PDFGeneratorAPI, tax seeding via the packaged Tax Setup Wizard — not the class itself.

Namespace matters. Always qualify calls with the refin namespace — refin.GlobalTaxService.calculateTax(...), refin.PDFGeneratorAPI.generateOne(...). That's the name your org sees for classes inside the managed package.

refin.GlobalTaxService — tax math as a service

Two static methods give your code the same engines the documents use, with no side effects:

  • calculateTax(TaxRequest) — indirect tax (VAT/GST/sales tax). Feed it entity, account, date, side ('Sales'/'Purchase') and lines (amount + product or explicit tax profile, with your own lineKey for correlation); get back per-line tax rows fully resolved — rate, amount, recoverable flag, and the GL account each tax would post to.
  • calculateWithholdingTax(WHTRequest) — the WHT engine, same shape: lines in, per-line withholding out (rate, base, amount, exempt reasons, GL).
  • The contract: no DML, no records created, never throws — errors return as success=false with a message. Persistence is entirely yours: perfect for quotes, calculators, pre-posting estimates, or external-system parity checks.

The full reference

Both entry points are static:

global static TaxResponse calculateTax(TaxRequest request)
global static WHTResponse calculateWithholdingTax(WHTRequest request)

The class doc states the contract plainly: "No DML is performed. No Source_Line_Tax or WHT_Transaction records are created. The caller owns persistence and posting." A response is "always returned, never throws to caller" — validation problems (missing entity, account, date, side or lines, or an invalid side value) come back as success=false with the reason in errorMessage, in the same validation language the documents themselves use. A GlobalTaxServiceException class exists in the global surface, but under the normal contract you read the response rather than catch.

The request and response DTOs are global classes with global members — everything below is yours to set or read:

DTOMemberWhat it holds
TaxRequestentityIdThe entity whose tax configuration applies
accountIdThe customer or vendor account on the transaction
transactionDateDate the rates and rules are resolved for
side'Sales' or 'Purchase'
fiscalOperationId / linesOptional fiscal-operation override; the list of TaxLineRequest rows
TaxLineRequestlineKeyYour correlation key — echoed back on the matching result line
amountThe line amount to calculate on
productId / taxProfileIdProduct drives the tax profile lookup; an explicit taxProfileId overrides and bypasses it
taxBase'Exclusive of Tax' (default), 'Inclusive of Tax', or 'Tax Exempt'
TaxResponsesuccess / errorMessageThe outcome contract — check success before reading results
nexusEstablished / totalTaxAmount / totalEmbeddedTaxAmount / linesHeader-level results plus the per-line detail
TaxResultLinelineKey / taxTypeId / taxRuleIdWhich line, which tax, which rule fired
taxBase / taxRate / taxAmount / isTaxIncludedInPrice / isRecoverable / sequenceThe resolved math, exactly as posting would compute it
glAccountId / deductionGlAccountIdThe GL account the tax would post to; the deduction GL is non-null only for embedded (por-dentro) taxes

The withholding set mirrors it:

DTOMemberWhat it holds
WHTRequestentityId / accountId / transactionDate / side / documentCurrencyCode / linesSame header shape as tax; documentCurrencyCode is optional and used for multi-currency threshold conversion
WHTLineRequestlineKey / lineSubtotal / lineTaxAmount / ruleOverrideIdlineTaxAmount feeds Base-Includes-Tax mode; ruleOverrideId beats the account's default rule
WHTResponsesuccess / errorMessage / totalWHTAmount / linestotalWHTAmount sums non-exempt lines only
WHTResultLinelineKey / ruleId / rateId / rate / base / amount / isExempt / exemptReason / glAccountIdPer-line withholding, including why a line is exempt

How the GL accounts resolve. Indirect tax GLs come from the Tax Type GL Mapping records (refin__Tax_Type_GL_Mapping__c): an entity-specific mapping wins over an org-wide one, keyed by tax type and recoverability for the request's side; embedded taxes additionally receive a deduction GL from the 'Sales Deduction' / 'Purchase Deduction' side of the mapping. Withholding GLs come from the GL Account on the matched Withholding Tax Rate, falling back to the entity's Default WHT Payable account. In other words, the response tells you not just how much but where it would land — the same resolution posting performs.

A minimal call, Meridian-style — the US entity pricing a tenant's rent line before any invoice exists:

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);
}

The withholding twin follows the same rhythm. Meridian's India entity, estimating TDS on a contractor invoice before it's entered: build a WHTRequest with side = 'Purchase', one WHTLineRequest per invoice line carrying lineSubtotal, call calculateWithholdingTax, and read each WHTResultLine — rate, base, amount, and isExempt with its exemptReason when a certificate or threshold spares the line. Because nothing is persisted, the AP team can quote the net payable to the vendor and the numbers will match the posted document to the paisa.

refin.PDFGeneratorAPI — documents from code

generateOne(sourceRecordId, sourceObjectApiName) renders and attaches the PDF for a posted Sales Invoice, Credit Memo, Cash Receipt, Cash Disbursement or Purchase Order — returning the content IDs and, where the territory enables public links, the URL. The Flow twin is covered in Flows Integration.

The signature:

global static PDFResult generateOne(Id sourceRecordId, String sourceObjectApiName)

In the class's own words, "Extension packages call this from Apex when they need programmatic PDF generation outside of Flow context." The sourceObjectApiName must be one of the supported source objects:

Object API nameDocumentNote
refin__Sales_Invoice__cSales InvoiceCredit Memos also come through here — a Credit Memo is a Sales Invoice type (refin__Sales_Invoice_Type__c), so you pass the invoice object name
refin__Cash_Receipt__cCash Receipt—
refin__Cash_Disbursement__cCash Disbursement—
refin__Purchase_Order__cPurchase Order—

The output PDF is attached to the source record as a ContentVersion with a ContentDocumentLink — a normal Salesforce File you can query, share, or email. The result's publicUrl is populated only "if the Entity has Auto Public URL enabled; null otherwise" — so code that emails links to customers should check for null rather than assume a URL exists. And you may not need code at all: each entity carries per-document auto-generation toggles (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 is for the moments outside those, like regenerating on demand or attaching to a custom send.

Class access is not pre-granted. None of the five packaged permission sets list the global classes (PDFGeneratorAPI, GlobalTaxService, OCREmailHandler) in their Apex class access. Whether your callers need explicit class access depends on how and where the code runs — verify in a sandbox with a realistic user before you ship, and grant access on your own permission set if calls are blocked.

Posting from Apex — the status field is the API

There is no post() method to call, by design: the posting engine runs in the trigger phase of the source object's own update. Your Apex posts a document the same way a user does — flip refin__Status__c from 'Draft' to 'Posted' with a DML update, and the full machinery runs: tax and WHT engines, exchange-rate stamping on the transition to Posted, and every guard (approval gate, period validation, inactive entity/GL blocks). A refused posting surfaces as a DML exception carrying the guard's exact message — treat it as the API contract, not noise.

The reliable creation pattern is two steps: insert as Draft (header and lines), then update the status to 'Posted' in a second call. On insert, a null status defaults to 'Draft', document numbers auto-assign from the entity's document sequence (pre-setting the Name means manual numbering, and uniqueness is validated), and entity defaults auto-fill — ledger, accounting period, and due date from the vendor's payment term. Note that changing the entity or invoice type on a Draft re-sequences its number, so do your edits before you post.

There is no integration bypass. All 52 packaged triggers — one per transactional object — run on API DML exactly as they do in the UI, and no flag exists that you can switch them off with. The base TriggerHandler does carry an internal bypass used by the engine's own migration paths, but it is public, not global — invisible across the namespace boundary, so subscriber and extension code cannot reach it. Posted records are locked except for whitelisted workflow fields such as refin__Approval_Status__c, and Void is immutable. Never write refin__Accounting_Transaction__c rows yourself; the guards exist precisely to stop hand-forged ledger entries.

Reacting to accounting events

Your triggers and record-triggered Flows on refin__ objects coexist happily with the package's own (order between packages and org automation follows standard platform rules — keep yours idempotent). The high-value pattern: react to status transitions — a trigger on Sales Invoice noticing Draft→Posted can feed your downstream system with the document already fully validated and numbered. React after the package's work: read the record's final state rather than trying to interpose.

The rules that keep you upgrade-safe

DoDon't
Call the documented global classesReflect into public package classes — they're invisible cross-namespace and change without notice
Post by updating refin__Status__c (the engines run for you)Write refin__Accounting_Transaction__c rows yourself — the guards exist precisely to stop hand-forged ledger entries
Respect DML errors as the API contract (the verbatim guard messages)Swallow posting failures — they're accounting truths, not noise
Extend data via field sets and your own fieldsDepend on package field help text or UI internals
Worked example: a leasing calculator. Meridian's custom "lease quote" LWC calls GlobalTaxService.calculateTax with draft line amounts to show a tenant the tax-inclusive rent before any invoice exists — same rates, same rules, zero records created. When the lease signs, the integration creates the real recurring template and the numbers match to the cent, because it's one engine.

Common mistakes to avoid

MistakeWhy it hurtsDo this instead
Persisting GlobalTaxService results as your own tax recordsThe contract is explicit: no DML is performed and "The caller owns persistence and posting" — but hand-built tax rows drift from what posting will calculateUse the response for display and decision logic; let document posting write the real tax records
Wrapping the status-update DML in a blanket try/catch that logs and continuesA refused posting is an accounting answer; swallowing it leaves documents silently unpostedCatch, surface the verbatim message to a human or a queue, and treat unposted as a work item
Calling generateOne for objects it doesn't supportThe supported set is exactly SI, CR, CD, PO (credit memos ride the SI type) — anything else failsCheck the supported-objects table above before wiring a button to it
Reacting to Draft→Posted before the package finishesYour logic reads half-posted state — numbers or ledger rows not yet finalReact in after-update on the final state, or asynchronously (queueable/platform-cache patterns) once the transaction commits
Testing against a sandbox without an active licenceAccounting writes fail with the licence message, and the team debugs phantom "bugs"Activate the licence in every sandbox and seat the running user before testing integration code

Where to go next

  1. Extension Packages — the provider contracts for ISVs.
  2. Lightning Web Components — the UI counterpart.
  3. API Overview — the map.
Official Salesforce documentation Apex is the Salesforce platform language — everything about its syntax, limits and testing lives in the Apex Developer Guide. Links open in a new tab.

Frequently asked questions

Yes — update the status in your transaction; the posting engine runs in the same transaction's trigger phase. Budget CPU accordingly (tax engines aren't free) and handle the DML exception path: a refused posting rolls back with the guard's message.

The supported levers are configuration: entity default accounts, tax GL mappings, asset-type GLs, frameworks. There's deliberately no code hook inside posting — determinism is the product. Custom GL logic belongs before posting (choose the accounts on the lines) or after (journals).

Read errorMessage — it's the same validation language as the documents (missing entity, invalid side, no lines). Your caller decides whether that's a user-facing message or a data fix.

GlobalTaxService's calc methods are contract-bound not to: results are "always returned, never throws to caller" — errors travel in success=false plus errorMessage. A GlobalTaxServiceException class exists in the global surface, but your defensive coding centers on the response flag, not catch blocks.

PDFGeneratorAPI and EInvoiceTrailCreator are granted by all ten packaged permission sets, because packaged Flow actions call them. GlobalTaxService, BankFeedProvider, OCREmailHandler and the e-invoicing contract types are granted by none — grant Apex class access to the calling user yourself. Verify in your org; the requirement varies by how the code is invoked.

Operate them from the Admin Console's Operations tab instead — that's the supported control surface, with schedule state, outcomes and audit logs in one place. The batch classes themselves aren't part of the global contract.

Was this article helpful?