Développement Apex sur REDA
Le code Apex personnalisé autour de REDA Financials obéit à une règle d’or : s’appuyer sur la surface globale et les objets, jamais sur les éléments internes. Voici la boîte à outils stable et les modèles qui en tirent le meilleur parti.
La surface globale — treize classes et interfaces
Dans un package géré, seul le code Apex global est visible par votre code ; tout ce qui est public reste derrière la barrière de l’espace de noms. REDA Financials maintient délibérément cette surface prise en charge réduite — treize types globaux pour le code des clients et des partenaires. Cinq d’entre eux portent les points d’entrée du quotidien ; les autres sont les contrats d’extension de facturation électronique et les types de données qu’ils échangent :
| Classe | Type | À quoi elle sert |
|---|---|---|
refin.GlobalTaxService | global with sharing | Façade sur les moteurs de taxe et de retenue à la source ; deux méthodes de calcul statiques, aucun DML |
refin.PDFGeneratorAPI | global with sharing | Génération de PDF — une action invocable de flux et une méthode Apex globale |
refin.BankFeedProvider | global abstract | Contrat d’extension pour les fournisseurs de flux bancaire (Plaid, Belvo, …) — vous l’implémentez, le package l’appelle |
refin.EInvoiceProvider | global abstract | Contrat d’extension pour les fournisseurs de facturation électronique (Sovos, Peppol, ZATCA, SAT, SDI, …) |
refin.OCREmailHandler | global, implémente Messaging.InboundEmailHandler | Gestionnaire d’e-mails entrants qui transforme les pièces jointes PDF en enregistrements de boîte de réception OCR pour l’automatisation des comptes fournisseurs |
refin.EInvoiceStatusPollSchedulable | global with sharing, implémente Schedulable | La surface de planification exposée aux abonnés pour l’interrogation du statut des factures électroniques — planifiez-la vous-même avec votre propre expression cron au lieu d’utiliser la carte de la console d’administration |
refin.EInvoiceTrailCreator | global with sharing | Crée un historique de facturation électronique pour les factures de vente comptabilisées — également exposée comme action de flux Create E-Invoice Trail |
refin.EInvoiceQueueProvider | interface globale | Contrat de file d’attente pour les extensions de facturation électronique. Ajouté à côté de EInvoiceProvider plutôt que sous forme de méthodes de celle-ci, car une classe abstraite globale déjà publiée ne peut pas recevoir de nouvelles méthodes sans casser les implémentations existantes |
refin.EInvoiceDocumentProvider | interface globale | Contrat de récupération de documents pour les extensions de facturation électronique |
refin.EInvoiceProvisioner | interface globale | Contrat de provisionnement pour les extensions de facturation électronique |
refin.EInvoiceDocument · refin.EInvoiceEvent · refin.EInvoiceProvisionResult | classes globales (DTO) | Les types de transfert de données que les contrats de fournisseur ci-dessus transmettent et renvoient |
Les contrats de fournisseur abstraits sont présentés dans Packages d’extension ; cet article se concentre sur les deux classes que vous appelez. Toutes les classes de l’API sont livrées en version d’API 65.0, et la garantie standard des packages gérés s’applique : une fois publiées, les signatures globales ne peuvent être ni supprimées ni modifiées à votre insu — elles constituent le contrat stable.
Ce que vous ne pouvez pas appeler
La couche de services interne qui fait le vrai travail — comptabilisation, import des relevés bancaires, rendu des PDF, appels sortants d’IA et de taux de change, mises à jour de statut en masse, initialisation des taxes — est public, et non globale. Le code Apex de l’abonné ne peut pas du tout appeler ces classes : le compilateur ne les voit tout simplement pas au-delà de la frontière de l’espace de noms, et leur structure interne peut changer à chaque version. Si une capacité dont vous avez besoin n’existe que derrière une classe publique, la voie prise en charge est le comportement qu’elle alimente — la comptabilisation via le champ Statut, la génération de PDF via refin.PDFGeneratorAPI, l’initialisation des taxes via l’assistant d’initialisation des taxes fourni avec le package — et non la classe elle-même.
refin — refin.GlobalTaxService.calculateTax(...), refin.PDFGeneratorAPI.generateOne(...). C’est sous ce nom que votre organisation voit les classes du package géré.
refin.GlobalTaxService — le calcul des taxes en tant que service
Deux méthodes statiques donnent à votre code accès aux mêmes moteurs que ceux qu’utilisent les documents, sans aucun effet de bord :
calculateTax(TaxRequest)— taxe indirecte (TVA/GST/taxe sur les ventes). Fournissez-lui l’entité, le compte, la date, le côté ('Sales'/'Purchase') et les lignes (montant + produit ou profil fiscal explicite, avec votre proprelineKeypour la corrélation) ; vous récupérez, pour chaque ligne, des lignes de taxe entièrement résolues — taux, montant, indicateur de récupérabilité, et le compte général sur lequel chaque taxe serait comptabilisée.calculateWithholdingTax(WHTRequest)— le moteur de retenue à la source, avec la même structure : des lignes en entrée, la retenue de chaque ligne en sortie (taux, base, montant, motifs d’exonération, compte général).- Le contrat : aucun DML, aucun enregistrement créé, aucune exception levée — les erreurs reviennent sous la forme
success=falseavec un message. La persistance vous appartient entièrement : idéal pour les devis, les calculateurs, les estimations avant comptabilisation ou les contrôles de concordance avec des systèmes externes.
La référence complète
Les deux points d’entrée sont statiques :
global static TaxResponse calculateTax(TaxRequest request)
global static WHTResponse calculateWithholdingTax(WHTRequest request)
La documentation de la classe énonce clairement le contrat : « Aucun DML n’est effectué. Aucun enregistrement Source_Line_Tax ou WHT_Transaction n’est créé. L’appelant est responsable de la persistance et de la comptabilisation. » Une réponse est « toujours renvoyée, sans jamais lever d’exception vers l’appelant » — les problèmes de validation (entité, compte, date, côté ou lignes manquants, ou valeur de côté non valide) reviennent sous la forme success=false, avec le motif dans errorMessage, dans le même langage de validation que celui des documents eux-mêmes. Une classe GlobalTaxServiceException existe dans la surface globale, mais dans le cadre du contrat normal, vous lisez la réponse plutôt que d’intercepter une exception.
Les DTO de requête et de réponse sont des classes globales dotées de membres globaux — vous pouvez définir ou lire tout ce qui suit :
| DTO | Membre | Contenu |
|---|---|---|
TaxRequest | entityId | L’entité dont la configuration fiscale s’applique |
accountId | Le compte client ou fournisseur de la transaction | |
transactionDate | La date à laquelle les taux et les règles sont résolus | |
side | 'Sales' ou 'Purchase' | |
fiscalOperationId / lines | Remplacement facultatif de l’opération fiscale ; la liste des lignes TaxLineRequest | |
TaxLineRequest | lineKey | Votre clé de corrélation — renvoyée telle quelle sur la ligne de résultat correspondante |
amount | Le montant de ligne sur lequel porte le calcul | |
productId / taxProfileId | Le produit détermine la recherche du profil fiscal ; un taxProfileId explicite prend le dessus et court-circuite cette recherche | |
taxBase | 'Exclusive of Tax' (par défaut), 'Inclusive of Tax' ou 'Tax Exempt' | |
TaxResponse | success / errorMessage | Le contrat de résultat — vérifiez success avant de lire les résultats |
nexusEstablished / totalTaxAmount / totalEmbeddedTaxAmount / lines | Les résultats au niveau de l’en-tête, plus le détail par ligne | |
TaxResultLine | lineKey / taxTypeId / taxRuleId | Quelle ligne, quelle taxe, quelle règle s’est appliquée |
taxBase / taxRate / taxAmount / isTaxIncludedInPrice / isRecoverable / sequence | Le calcul résolu, exactement tel que la comptabilisation l’effectuerait | |
glAccountId / deductionGlAccountId | Le compte général sur lequel la taxe serait comptabilisée ; le compte général de déduction n’est renseigné que pour les taxes incluses (por-dentro) |
L’ensemble dédié à la retenue à la source en est le reflet :
| DTO | Membre | Contenu |
|---|---|---|
WHTRequest | entityId / accountId / transactionDate / side / documentCurrencyCode / lines | Même structure d’en-tête que pour la taxe ; documentCurrencyCode est facultatif et sert à la conversion des seuils en multidevise |
WHTLineRequest | lineKey / lineSubtotal / lineTaxAmount / ruleOverrideId | lineTaxAmount alimente le mode Base taxe comprise ; ruleOverrideId l’emporte sur la règle par défaut du compte |
WHTResponse | success / errorMessage / totalWHTAmount / lines | totalWHTAmount ne totalise que les lignes non exonérées |
WHTResultLine | lineKey / ruleId / rateId / rate / base / amount / isExempt / exemptReason / glAccountId | La retenue de chaque ligne, y compris pourquoi une ligne est exonérée |
Comment les comptes généraux sont résolus. Les comptes généraux de la taxe indirecte proviennent des enregistrements de correspondance comptable par type de taxe (refin__Tax_Type_GL_Mapping__c) : une correspondance propre à l’entité l’emporte sur une correspondance valable pour toute l’organisation, la recherche se faisant par type de taxe et par récupérabilité pour le côté de la requête ; les taxes incluses reçoivent en outre un compte général de déduction issu du côté « Déduction sur ventes » / « Purchase Deduction » de la correspondance. Les comptes généraux de retenue proviennent du champ Compte général du taux de retenue à la source correspondant, avec repli sur le compte Retenue à payer par défaut de l’entité. Autrement dit, la réponse vous indique non seulement combien, mais aussi où le montant serait comptabilisé — la même résolution que celle qu’effectue la comptabilisation.
Un appel minimal, à la manière de Meridian — l’entité américaine chiffre la ligne de loyer d’un locataire avant qu’aucune facture n’existe :
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);
}
Le pendant pour la retenue à la source suit le même rythme. L’entité indienne de Meridian estime la retenue TDS sur la facture d’un prestataire avant sa saisie : construisez un WHTRequest avec side = 'Purchase', un WHTLineRequest par ligne de facture portant lineSubtotal, appelez calculateWithholdingTax, puis lisez chaque WHTResultLine — rate, base, amount et isExempt avec son exemptReason lorsqu’un certificat ou un seuil exempte la ligne. Comme rien n’est persisté, l’équipe des comptes fournisseurs peut annoncer au fournisseur le montant net à payer, et les chiffres correspondront au paisa près au document comptabilisé.
refin.PDFGeneratorAPI — des documents depuis le code
generateOne(sourceRecordId, sourceObjectApiName) génère et joint le PDF d’un document comptabilisé — facture de vente, avoir, encaissement, décaissement ou bon de commande — et renvoie les ID de contenu ainsi que, lorsque le territoire active les liens publics, l’URL. Le pendant côté flux est présenté dans Intégration avec les flux Salesforce.
La signature :
global static PDFResult generateOne(Id sourceRecordId, String sourceObjectApiName)
Selon les termes mêmes de la classe, « les packages d’extension l’appellent depuis Apex lorsqu’ils ont besoin d’une génération de PDF programmatique en dehors d’un contexte de flux ». Le paramètre sourceObjectApiName doit désigner l’un des objets source pris en charge :
| Nom d’API de l’objet | Document | Remarque |
|---|---|---|
refin__Sales_Invoice__c | Facture de vente | Les avoirs passent aussi par ici — un avoir est un type de facture de vente (refin__Sales_Invoice_Type__c), vous passez donc le nom de l’objet facture |
refin__Cash_Receipt__c | Encaissement | — |
refin__Cash_Disbursement__c | Décaissement | — |
refin__Purchase_Order__c | Bon de commande | — |
Le PDF produit est joint à l’enregistrement source sous forme de ContentVersion avec un ContentDocumentLink — un fichier Salesforce ordinaire que vous pouvez interroger, partager ou envoyer par e-mail. Le publicUrl du résultat n’est renseigné que « si l’entité a l’option Auto Public URL activée ; null sinon » — le code qui envoie des liens aux clients par e-mail doit donc tester la valeur null plutôt que de supposer qu’une URL existe. Et vous n’aurez peut-être même pas besoin de code : chaque entité dispose d’options de génération automatique par type de document (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 sert aux cas qui sortent de ce cadre, comme une régénération à la demande ou l’ajout du PDF à un envoi personnalisé.
PDFGeneratorAPI, GlobalTaxService, OCREmailHandler) dans son accès aux classes Apex. Le besoin d’un accès explicite aux classes pour vos appelants dépend de la manière dont le code s’exécute et de l’endroit où il s’exécute — vérifiez-le dans une sandbox avec un utilisateur réaliste avant la mise en production, et accordez l’accès dans votre propre ensemble d’autorisations si les appels sont bloqués.
Comptabiliser depuis Apex — le champ Statut est l’API
Il n’existe pas de méthode post() à appeler, et c’est voulu : le moteur de comptabilisation s’exécute dans la phase des déclencheurs de la mise à jour de l’objet source lui-même. Votre code Apex comptabilise un document de la même manière qu’un utilisateur — faites passer refin__Status__c de « Brouillon » à « Comptabilisé » par une mise à jour DML, et toute la mécanique s’exécute : moteurs de taxe et de retenue à la source, inscription du taux de change lors du passage au statut Comptabilisé, et chaque garde-fou (contrôle d’approbation, validation de la période, blocages sur une entité ou un compte général inactif). Une comptabilisation refusée se manifeste par une exception DML portant le message exact du garde-fou — traitez-la comme le contrat de l’API, pas comme du bruit.
Le modèle de création fiable se déroule en deux temps : insérer en Brouillon (en-tête et lignes), puis passer le statut à « Comptabilisé » dans un second appel. À l’insertion, un statut nul prend par défaut la valeur « Brouillon », les numéros de document sont attribués automatiquement à partir de la séquence de documents de l’entité (prédéfinir le champ Name revient à une numérotation manuelle, dont l’unicité est validée), et les valeurs par défaut de l’entité sont renseignées automatiquement — le Grand livre, la Période comptable et la Date d’échéance d’après la Condition de paiement du fournisseur. Notez que modifier l’Entité ou le Type de facture d’un document en Brouillon le renumérote : effectuez donc vos modifications avant de comptabiliser.
TriggerHandler comporte bien un contournement interne utilisé par les propres chemins de migration du moteur, mais il est public, et non global — invisible au-delà de la frontière de l’espace de noms, si bien que le code des abonnés et des extensions ne peut pas l’atteindre. Les enregistrements comptabilisés sont verrouillés, à l’exception de champs de workflow autorisés tels que refin__Approval_Status__c, et le statut Annulé est immuable. N’écrivez jamais vous-même de lignes refin__Accounting_Transaction__c ; les garde-fous existent précisément pour empêcher les écritures de grand livre forgées à la main.
Réagir aux événements comptables
Vos déclencheurs et vos flux déclenchés par un enregistrement sur les objets refin__ cohabitent sans problème avec ceux du package (l’ordre d’exécution entre les packages et les automatisations de l’organisation suit les règles standard de la plateforme — gardez les vôtres idempotents). Le modèle le plus rentable : réagir aux transitions de statut — un déclencheur sur la facture de vente qui détecte le passage Brouillon→Comptabilisé peut alimenter votre système en aval avec un document déjà entièrement validé et numéroté. Réagissez après le travail du package : lisez l’état final de l’enregistrement plutôt que d’essayer de vous interposer.
Les règles qui préservent la compatibilité avec les mises à niveau
| À faire | À éviter |
|---|---|
| Appeler les classes globales documentées | Accéder par réflexion aux classes public du package — elles sont invisibles d’un espace de noms à l’autre et changent sans préavis |
Comptabiliser en mettant à jour refin__Status__c (les moteurs s’exécutent pour vous) | Écrire vous-même des lignes refin__Accounting_Transaction__c — les garde-fous existent précisément pour empêcher les écritures de grand livre forgées à la main |
| Respecter les erreurs DML comme contrat de l’API (les messages exacts des garde-fous) | Étouffer les échecs de comptabilisation — ce sont des vérités comptables, pas du bruit |
| Étendre les données via les ensembles de champs et vos propres champs | Dépendre du texte d’aide des champs du package ou des mécanismes internes de l’interface utilisateur |
GlobalTaxService.calculateTax avec des montants de lignes provisoires pour montrer à un locataire le loyer toutes taxes comprises avant qu’aucune facture n’existe — mêmes taux, mêmes règles, zéro enregistrement créé. À la signature du bail, l’intégration crée le véritable modèle récurrent et les chiffres correspondent au centime près, car il s’agit d’un seul et même moteur.
Erreurs courantes à éviter
| Erreur | Pourquoi c’est un problème | Que faire à la place |
|---|---|---|
Persister les résultats de GlobalTaxService sous forme de vos propres enregistrements de taxe | Le contrat est explicite : aucun DML n’est effectué et « L’appelant est responsable de la persistance et de la comptabilisation » — mais des lignes de taxe construites à la main s’écartent de ce que la comptabilisation calculera | Utilisez la réponse pour l’affichage et la logique de décision ; laissez la comptabilisation du document écrire les véritables enregistrements de taxe |
| Envelopper le DML de mise à jour du statut dans un try/catch fourre-tout qui journalise et continue | Une comptabilisation refusée est une réponse comptable ; l’étouffer laisse des documents non comptabilisés sans que personne ne s’en aperçoive | Interceptez l’exception, transmettez le message exact à une personne ou à une file d’attente, et traitez chaque document non comptabilisé comme une tâche à traiter |
Appeler generateOne pour des objets qu’elle ne prend pas en charge | L’ensemble pris en charge se limite exactement aux factures de vente, encaissements, décaissements et bons de commande (les avoirs passent par le type de facture de vente) — tout le reste échoue | Consultez le tableau des objets pris en charge ci-dessus avant d’y associer un bouton |
| Réagir au passage Brouillon→Comptabilisé avant que le package ait terminé | Votre logique lit un état à moitié comptabilisé — des numéros ou des lignes de grand livre pas encore définitifs | Réagissez en after-update sur l’état final, ou de manière asynchrone (modèles queueable/cache de plateforme) une fois la transaction validée |
| Tester dans une sandbox sans licence active | Les opérations d’écriture sur les données comptables échouent avec le message de licence, et l’équipe débogue des « bugs » fantômes | Activez la licence dans chaque sandbox et attribuez une licence à l’utilisateur qui exécute le code avant de tester le code d’intégration |
Pour aller plus loin
- Packages d’extension — les contrats de fournisseur destinés aux ISV.
- Lightning Web Components — le pendant côté interface utilisateur.
- Présentation de l’API — la vue d’ensemble.
Questions fréquentes
Oui — mettez à jour le statut dans votre transaction ; le moteur de comptabilisation s’exécute dans la phase des déclencheurs de cette même transaction. Prévoyez votre budget CPU en conséquence (les moteurs de taxe ne sont pas gratuits) et gérez le chemin d’exception DML : une comptabilisation refusée provoque la restauration de la transaction et renvoie le message du garde-fou.
Les leviers pris en charge relèvent de la configuration : comptes par défaut de l’entité, correspondances comptables des taxes, comptes généraux des types d’immobilisations, référentiels de reporting. Il n’existe délibérément aucun point d’extension de code à l’intérieur de la comptabilisation — le déterminisme est le produit. Une logique de comptes généraux personnalisée intervient avant la comptabilisation (choisissez les comptes sur les lignes) ou après (écritures comptables).
Lisez errorMessage — il emploie le même langage de validation que les documents (entité manquante, côté non valide, aucune ligne). C’est votre code appelant qui décide s’il s’agit d’un message destiné à l’utilisateur ou d’une correction de données.
Les méthodes de calcul de GlobalTaxService s’engagent contractuellement à ne pas le faire : leurs résultats sont « toujours renvoyés, sans jamais lever d’exception vers l’appelant » — les erreurs transitent par success=false accompagné de errorMessage. Une classe GlobalTaxServiceException existe dans la surface globale, mais votre programmation défensive repose sur l’indicateur de la réponse, pas sur des blocs catch.
PDFGeneratorAPI et EInvoiceTrailCreator sont accordées par les dix ensembles d’autorisations du package, car des actions de flux du package les appellent. GlobalTaxService, BankFeedProvider, OCREmailHandler et les types de contrat de facturation électronique ne sont accordés par aucun — accordez vous-même l’accès aux classes Apex à l’utilisateur appelant. Vérifiez dans votre organisation ; l’exigence varie selon la manière dont le code est appelé.
Pilotez-les plutôt depuis l’onglet Opérations de la console d’administration — c’est la surface de contrôle prise en charge, qui réunit l’état de planification, les résultats et les journaux d’audit en un seul endroit. Les classes de lot elles-mêmes ne font pas partie du contrat global.
Cet article vous a-t-il été utile ?