Les Comptes Personnels et le multi-devises sont deux fonctionnalités Salesforce qui requièrent une gestion dbt spécifique non couverte par les packages dbt standards. Les Comptes Personnels sont standards dans les orgs B2C ; le multi-devises est courant dans toute org avec des ventes internationales. Les deux cassent les hypothèses par défaut de modélisation en entrepôt (séparation Account/Contact et montants en devise unique).
Comptes Personnels
Les Comptes Personnels fusionnent les objets Account et Contact en un seul enregistrement. Au lieu d’une entreprise (Account) avec des personnes associées (Contacts), un Compte Personnel est une personne qui est également un compte. Cela existe parce que Salesforce a été conçu pour les ventes B2B (les entreprises achètent des choses), mais les entreprises B2C (les individus achètent des choses) avaient besoin d’un moyen de représenter les clients individuels comme des entités de premier ordre.
Comment ils apparaissent dans votre entrepôt
Quand les Comptes Personnels sont activés, la table Account gagne des champs spécifiques aux contacts : FirstName, LastName, Email, Phone. Le booléen IsPersonAccount distingue les comptes personnels des comptes d’entreprise. Certains champs se comportent différemment selon cet indicateur :
Namesur un compte d’entreprise est le nom de la société (« Acme Corp »). Sur un compte personnel, c’est la concaténation deFirstNameetLastName(« Jane Smith »).OwnerIda la même signification, mais le modèle de propriété peut différer — les comptes personnels représentent souvent des clients finaux gérés par des équipes customer success plutôt que des commerciaux.
La table Contact a également des enregistrements pour les comptes personnels, mais ils sont liés 1-à-1 avec l’enregistrement Account. Joindre Account à Contact pour les comptes personnels vous donne la même personne deux fois dans des tables différentes.
Le pattern de modélisation
L’approche la plus simple : créer des modèles de base séparés pour les comptes personnels et les comptes d’entreprise.
-- base__salesforce__account_business.sqlSELECT id AS account__id, name AS account__name, industry AS account__industry, annual_revenue AS account__annual_revenue, owner_id AS account__owner_id, parent_id AS account__parent_id, 'business' AS account__typeFROM {{ source('salesforce', 'account') }}WHERE NOT _fivetran_deleted AND NOT is_person_account-- base__salesforce__account_person.sqlSELECT id AS account__id, first_name AS account__first_name, last_name AS account__last_name, person_email AS account__email, owner_id AS account__owner_id, 'person' AS account__typeFROM {{ source('salesforce', 'account') }}WHERE NOT _fivetran_deleted AND is_person_accountSi vos consommateurs en aval ont besoin des deux dans une seule vue (par exemple, une liste de clients incluant à la fois des entreprises et des individus), faites un UNION dans un modèle intermédiaire avec un nommage de colonnes cohérent :
-- int__account_unified.sqlSELECT account__id, account__name AS account__display_name, account__type, account__owner_idFROM {{ ref('base__salesforce__account_business') }}
UNION ALL
SELECT account__id, CONCAT(account__first_name, ' ', account__last_name) AS account__display_name, account__type, account__owner_idFROM {{ ref('base__salesforce__account_person') }}Séparer les comptes personnels et les comptes d’entreprise au niveau de la couche de base — plutôt que d’utiliser CASE WHEN is_person_account THEN ... partout — produit des modèles plus maintenables. Les ensembles de champs diffèrent suffisamment pour qu’un seul modèle avec une logique de colonne conditionnelle devienne difficile à lire. Fusionnez au niveau de la couche intermédiaire si une vue unifiée est nécessaire en aval.
Points de vigilance dans les jointures
Quand vous joignez Opportunities à Accounts, votre logique de jointure existante fonctionne pour les comptes personnels et d’entreprise — l’AccountId sur Opportunity référence le Account indépendamment du type. Mais si vos modèles intermédiaires joignent également Account à Contact (pour obtenir des détails de contact), les comptes personnels créeront des lignes en double parce que la personne a des enregistrements dans les deux tables. Filtrez la jointure Contact pour exclure les contacts de comptes personnels, ou utilisez les champs de la personne au niveau Account à la place.
Multi-devises
Les orgs Salesforce multi-devises stockent les montants monétaires dans la devise de l’enregistrement, pas dans une seule devise d’entreprise. Une Opportunity créée par un commercial à Tokyo stocke son montant en JPY, tandis que celle à Paris stocke l’EUR. Vos rapports de pipeline et tableaux de bord de revenus ont besoin de tout en une seule devise.
Ce que vous devez extraire
Le multi-devises nécessite deux objets supplémentaires que votre outil d’extraction doit synchroniser :
CurrencyType— la liste des devises actives dans l’org, avec leurs codes ISO et les taux de change de l’entreprise.DatedConversionRate— les taux de change historiques par plage de dates, si votre org utilise la « Gestion Avancée des Devises ». Sans cela, vous n’obtenez que le taux actuel.
Chaque enregistrement monétaire a également un champ CurrencyIsoCode indiquant sa devise. Ce champ est sur Opportunity, Account et tout objet personnalisé avec des champs de devise.
Conversion dans dbt
Convertissez les montants dans dbt en utilisant le taux de change de l’entreprise :
-- int__opportunity_currency_normalized.sqlWITH
opportunities AS ( SELECT opportunity__id, opportunity__amount, opportunity__currency_iso_code, opportunity__close_at FROM {{ ref('base__salesforce__opportunity') }}),
record_rates AS ( SELECT iso_code AS currency__iso_code, conversion_rate AS currency__record_rate FROM {{ ref('base__salesforce__currency_type') }} WHERE is_active),
corporate_rate AS ( SELECT iso_code AS currency__iso_code, conversion_rate AS currency__corporate_rate FROM {{ ref('base__salesforce__currency_type') }} WHERE is_corporate)
SELECT opportunity__id, opportunity__amount, opportunity__currency_iso_code, opportunity__amount * ( corporate.currency__corporate_rate / record.currency__record_rate ) AS opportunity__amount_corporate_currencyFROM opportunitiesLEFT JOIN record_rates AS record ON opportunities.opportunity__currency_iso_code = record.currency__iso_codeCROSS JOIN corporate_rate AS corporateLa formule amount * (corporate_rate / record_rate) convertit de la devise de l’enregistrement vers la devise de l’entreprise via une base commune. C’est la même arithmétique que Salesforce utilise en interne.
Avantages de la conversion de devises dans dbt
Convertir les devises dans dbt plutôt que de s’appuyer sur la conversion en temps réel de Salesforce offre :
- Contrôle de version — la logique de conversion est du SQL que vous pouvez réviser, tester et auditer.
- Cohérence — chaque modèle utilise la même approche de conversion. Pas de risque que certains rapports utilisent la conversion Salesforce et d’autres une méthode différente.
- Précision historique — si vous utilisez
DatedConversionRate, vous pouvez convertir les montants historiques au taux qui était en vigueur à la clôture du deal, pas le taux d’aujourd’hui. C’est important pour le reporting financier. - Indépendance du timing de sync — les champs de montant converti de Salesforce sont des champs de formule, ce qui signifie qu’ils souffrent de l’angle mort de sync des champs de formule. Calculer la conversion dans dbt évite entièrement ce problème.
Taux datés vs. non datés
Si votre org a la Gestion Avancée des Devises activée, utilisez DatedConversionRate pour la précision historique. Joignez sur le taux qui était actif à la date de clôture du deal :
LEFT JOIN dated_rates ON opportunities.opportunity__currency_iso_code = dated_rates.currency__iso_code AND opportunities.opportunity__close_at >= dated_rates.start_date AND opportunities.opportunity__close_at < dated_rates.next_start_dateSans Gestion Avancée des Devises, vous n’avez que le taux actuel dans CurrencyType. C’est bien pour le reporting du pipeline actuel mais produit une analyse historique inexacte quand les taux de change ont significativement évolué.