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:
| Class | Kind | What it's for |
|---|---|---|
refin.GlobalTaxService | global with sharing | Facade over the tax and WHT engines; two static calculation methods, no DML |
refin.PDFGeneratorAPI | global with sharing | PDF generation — a Flow invocable action plus a global Apex method |
refin.BankFeedProvider | global abstract | Extension contract for bank feed providers (Plaid, Belvo, …) — you implement it, the package calls it |
refin.EInvoiceProvider | global abstract | Extension contract for e-invoicing providers (Sovos, Peppol, ZATCA, SAT, SDI, …) |
refin.OCREmailHandler | global, implements Messaging.InboundEmailHandler | Inbound email handler that turns PDF attachments into OCR Inbox records for AP automation |
refin.EInvoiceStatusPollSchedulable | global with sharing, implements Schedulable | The 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.EInvoiceTrailCreator | global with sharing | Creates an E-Invoice Trail for posted sales invoices — also exposed as the Flow action Create E-Invoice Trail |
refin.EInvoiceQueueProvider | global interface | Queue 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.EInvoiceDocumentProvider | global interface | Document-retrieval contract for e-invoicing extensions |
refin.EInvoiceProvisioner | global interface | Provisioning contract for e-invoicing extensions |
refin.EInvoiceDocument · refin.EInvoiceEvent · refin.EInvoiceProvisionResult | global 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.
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 ownlineKeyfor 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=falsewith 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:
| DTO | Member | What it holds |
|---|---|---|
TaxRequest | entityId | The entity whose tax configuration applies |
accountId | The customer or vendor account on the transaction | |
transactionDate | Date the rates and rules are resolved for | |
side | 'Sales' or 'Purchase' | |
fiscalOperationId / lines | Optional fiscal-operation override; the list of TaxLineRequest rows | |
TaxLineRequest | lineKey | Your correlation key — echoed back on the matching result line |
amount | The line amount to calculate on | |
productId / taxProfileId | Product drives the tax profile lookup; an explicit taxProfileId overrides and bypasses it | |
taxBase | 'Exclusive of Tax' (default), 'Inclusive of Tax', or 'Tax Exempt' | |
TaxResponse | success / errorMessage | The outcome contract — check success before reading results |
nexusEstablished / totalTaxAmount / totalEmbeddedTaxAmount / lines | Header-level results plus the per-line detail | |
TaxResultLine | lineKey / taxTypeId / taxRuleId | Which line, which tax, which rule fired |
taxBase / taxRate / taxAmount / isTaxIncludedInPrice / isRecoverable / sequence | The resolved math, exactly as posting would compute it | |
glAccountId / deductionGlAccountId | The GL account the tax would post to; the deduction GL is non-null only for embedded (por-dentro) taxes |
The withholding set mirrors it:
| DTO | Member | What it holds |
|---|---|---|
WHTRequest | entityId / accountId / transactionDate / side / documentCurrencyCode / lines | Same header shape as tax; documentCurrencyCode is optional and used for multi-currency threshold conversion |
WHTLineRequest | lineKey / lineSubtotal / lineTaxAmount / ruleOverrideId | lineTaxAmount feeds Base-Includes-Tax mode; ruleOverrideId beats the account's default rule |
WHTResponse | success / errorMessage / totalWHTAmount / lines | totalWHTAmount sums non-exempt lines only |
WHTResultLine | lineKey / ruleId / rateId / rate / base / amount / isExempt / exemptReason / glAccountId | Per-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 name | Document | Note |
|---|---|---|
refin__Sales_Invoice__c | Sales Invoice | Credit 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__c | Cash Receipt | — |
refin__Cash_Disbursement__c | Cash Disbursement | — |
refin__Purchase_Order__c | Purchase 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.
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.
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
| Do | Don't |
|---|---|
| Call the documented global classes | Reflect 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 fields | Depend on package field help text or UI internals |
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
| Mistake | Why it hurts | Do this instead |
|---|---|---|
Persisting GlobalTaxService results as your own tax records | The contract is explicit: no DML is performed and "The caller owns persistence and posting" — but hand-built tax rows drift from what posting will calculate | Use 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 continues | A refused posting is an accounting answer; swallowing it leaves documents silently unposted | Catch, surface the verbatim message to a human or a queue, and treat unposted as a work item |
Calling generateOne for objects it doesn't support | The supported set is exactly SI, CR, CD, PO (credit memos ride the SI type) — anything else fails | Check the supported-objects table above before wiring a button to it |
| Reacting to Draft→Posted before the package finishes | Your logic reads half-posted state — numbers or ledger rows not yet final | React in after-update on the final state, or asynchronously (queueable/platform-cache patterns) once the transaction commits |
| Testing against a sandbox without an active licence | Accounting 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
- Extension Packages — the provider contracts for ISVs.
- Lightning Web Components — the UI counterpart.
- API Overview — the map.
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?