Skip to content

Wire a new external provider through Party.ExternalMappings

Every billing- or accounting-side provider stores its own customer identifier directly on the Granit Party aggregate, in Party.ExternalMappings. There is one canonical place to read or write the link — no per-provider mapping aggregate, no parallel table, no bespoke EF context. This is the polyglot contract the framework expects every new provider to honour.

The first iteration of Granit had a ProviderCustomerMapping aggregate in Granit.Payments and an OdooPartnerMapping aggregate in Granit.Invoicing.Odoo. Both did the same thing — map a Granit identity to a provider’s customer ID — at the cost of two storage schemas, two stores, two migration paths, and two ways for downstream code to reach the same fact. PR #1224 collapsed both into Party.ExternalMappings. Future providers stay on this rail.

A PartyExternalMapping is a (ProviderName, ExternalId) pair attached to a Party. Constraints:

  • ProviderName is the canonical key. Reuse the constants from Granit.Parties.Domain.PartyExternalProviderNames — never invent a new string when one of the reserved values fits:

    ConstantProvider
    StripeStripe payments
    MollieMollie payments
    OdooOdoo res.partner
    SageSage customer
    NetSuiteNetSuite customer
    TenantReverse-link from a host-scoped party to the Granit tenant

    If your provider is not on this list, add a constant in a follow-up PR rather than coining a string ad hoc.

  • ExternalId is the provider-natural string key for the customer in that provider — cus_abc123 for Stripe, the integer res.partner.id rendered as a string for Odoo, etc. It is opaque to Granit and never reused across providers.

  • Uniqueness per (PartyId, ProviderName) is enforced by the aggregate — Party.AddExternalMapping(...) rejects duplicate provider names.

Party? party = await partyReader.GetByIdAsync(partyId, ct);
string? externalId = party?.FindExternalId(PartyExternalProviderNames.Stripe);

The party may be host-scoped or tenant-scoped. When you do not know the active scope (typical for a Wolverine handler running in some tenant context), wrap the read in IDataFilter.Disable<IMultiTenant>() — see Granit.Payments.Stripe.Internal.StripePaymentMethodManager for the canonical pattern.

After creating the customer in the provider:

party.AddExternalMapping(guidGenerator.Create(), "stripe", customer.Id);
await partyWriter.UpdateAsync(party, ct);

Always go through IGuidGenerator.Create() (Granit framework rule) — never Guid.NewGuid(). The aggregate emits PartyExternalMappingAddedEto so downstream consumers (Privacy, audit, sync orchestrators) observe the link through the integration-event bus.

A provider integration that needs the mapping — Granit.Payments.Stripe, Granit.Invoicing.Odoo, a hypothetical Granit.Invoicing.Sage — references Granit.Parties (full base, not just .Abstractions) because it needs both IPartyReader and IPartyWriter. It does not create its own EF configuration for the mapping — the Parties module already maps PartyExternalMapping as a child entity of the Party aggregate.

A provider integration that only publishes or consumes Eto events — for example, a webhook handler reacting to PartyExternalMappingAddedEto — should reference only Granit.Parties.Abstractions to keep its dependency surface minimal.

  • Do not create a new aggregate *ProviderMapping (Stripe / Sage / NetSuite / …). Use Party.ExternalMappings.
  • Do not add a (ProviderName, TenantId) row outside the party aggregate. Tenant ↔ party already lives in Party.ExternalMappings under the reserved provider name tenant (seeded automatically by Granit.Parties.MultiTenancy).
  • Do not reuse an ExternalId across providers. Each provider lives in its own mapping entry, even if the IDs happen to look the same.
  • Granit.Parties module reference (TODO once the reference page lands)
  • Granit.Payments.Stripe.Internal.StripePaymentMethodManager — production reference for read-or-create-customer flow
  • Granit.Invoicing.Odoo.Internal.OdooInvoiceSyncProvider — same flow for an accounting provider
  • Granit.Parties.MultiTenancy.Handlers.SeedDefaultPartyOnTenantCreatedHandler — runtime seeder that registers the reserved tenant mapping at provisioning time