Fusion Architecture Guide
Document Info
| Field | Value |
|---|---|
| Author | Jason Welch |
| Status | Active / Implemented |
| Created | December 5, 2025 |
| Last Updated | February 24, 2026 |
Executive Summary
Fusion is a unified platform that replaces the fragmented integrations across Elm Street Technology's product ecosystem (IDX Broker, Outbound Engine, VoicePad, AIVA, CRM) with a clean, modular architecture.
At its core is the Party Model — an industry-standard pattern (Salesforce, SAP, Oracle) that represents any participant in the system — person, organization, or external system — in a consistent, extensible way. Around this identity core, Fusion layers Billing, Subscriptions, and Product Catalog modules that integrate with Zuora as the billing provider.
This architecture solves key challenges around:
- Connecting Auth0 SSO identities to business entities
- Representing MLS listing/agent/office relationships
- Supporting multiple agent roles (listing agent, buyer agent, co-agent, etc.)
- Centralizing billing and subscription management through Zuora
- Managing product catalogs with rate plan groups and dependencies
- Supporting developer partner billing hierarchies (direct, partner-controlled, aggregate)
- Building a foundation for CRM functionality
- Maintaining a single source of truth for identity, billing, and subscription data
The Problem Fusion Solves
Fragmented Identity
| Today | Problem |
|---|---|
| Auth0 knows who logs in | But not who they are in the business |
| IDX Broker has agent IDs | But they're flat fields, not relationships |
| Developer Partners | Are they vendors? Customers? Admins? All three? |
| MLS data has listings | With agents, co-agents, offices — no way to model this |
Fragmented Products
| Today | Problem |
|---|---|
| Tight coupling between products | Updates in one app break another |
| Complex integrations | IDX B ↔ IXACT Contact relies on custom Auth0 hacks |
| No unified lead flow | Contacts scattered across products with no routing logic |
| No cross-product visibility | Can't answer "what does this customer subscribe to?" |
Fragmented Billing
| Today | Problem |
|---|---|
| Zuora IDs are flat fields | No structured relationship to identity |
| Dev partner billing is ad-hoc | No clear model for aggregate vs partner-controlled |
| Subscription data lives in Zuora | No local system of record for subscription state |
| Rate plan changes are unguarded | No enforcement of business rules around plan dependencies |
Result: We can't answer simple questions like:
- "Show me all accounts this person has access to"
- "Who are the agents under this developer partner?"
- "What role does this person play on this listing?"
- "What subscriptions does this customer have and what do they cost?"
- "Which rate plans can be added given the current subscription?"
Example Scenario
Natalie More is:
- An Auth0 user who logs into Fusion
- An individual agent with her own IDX Broker account (#60869)
- A member of SWFLGC MLS with agent ID
26563084 - The listing agent on property
2025017196 - Managed by developer partner "BulletProof Real Estate Agent"
- Billed via Zuora with a
client_directbilling arrangement - Subscribed to IDX Broker Core (Monthly) with an MLS fee
Question: How do we connect all of these identities, products, and billing relationships?
System Architecture
Module Structure
Fusion uses a modular Laravel architecture with four bounded modules:
modules/
├── Identity/ Core identity: parties, users, products, customer provisioning
├── Billing/ Billing accounts, invoices, payments, payment methods
├── Subscriptions/ Subscriptions, rate plans, catalog, amendments
└── Zuora/ Zuora API adapter implementing Billing + Subscription contracts
Each module has its own models, actions, controllers, tests, events, and service provider. Modules communicate through contracts (interfaces), never through direct class references to another module's internals.
Architecture Layers
┌─────────────────────────────────────────────────────────────────────────────────┐
│ AUTHENTICATION LAYER │
│ Auth0 (External IdP) │
│ • Handles SSO, social login, MFA │
│ • Owns: sub (subject identifier), email │
└───────────────────────────────────────────────┬─────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────────┐
│ APPLICATION LAYER │
│ users table │
│ • Links Auth0 to internal system │
│ • Owns: auth0_sub, email │
│ • References: party_id → The person behind this login │
└───────────────────────────────────────────────┬─────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────────┐
│ PARTY LAYER (Identity Module) │
│ parties — The canonical identity AND customer account │
│ • type: person, organization, system │
│ • Owns: first_name/last_name (persons) / display_name (orgs) │
│ │
│ party_identifiers — External system IDs (MLS member IDs, idx_id) │
│ party_groups / party_group_members — Flexible grouping with roles │
│ party_roles — Role a party plays on business objects (listing_agent, etc.) │
│ party_product — Links parties to products with roles (owner, agent, admin) │
└───────────────────────┬───────────────────────┬─────────────────────────────────┘
│ │
┌──────────────┘ └──────────────┐
▼ ▼
┌─────────────────────────────────┐ ┌──────────────────────────────────────────┐
│ PRODUCT LAYER (Identity) │ │ BILLING LAYER (Billing Module) │
│ │ │ │
│ products │ │ billing_providers (Zuora) │
│ • IDX Broker, Outbound Engine │ │ billing_accounts │
│ • VoicePad, AIVA, CRM │ │ • Links Party to BillingProvider │
│ │ │ • external_account_id (Zuora ID) │
│ product_accounts │ │ • billing_type: client_direct, │
│ • Specific account instance │ │ partner_controlled, aggregate │
│ • product_account_id: 60869 │ │ │
│ • billing_account_id → billing │ │ invoices, payments, payment_methods │
│ │ │ • Full billing lifecycle │
└─────────────────────────────────┘ └──────────────────────────────────────────┘
│ │
└──────────────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────────┐
│ SUBSCRIPTION LAYER (Subscriptions Module) │
│ │
│ subscriptions — Synced from Zuora via SyncAccountSubscriptions │
│ subscription_items — Individual charges on a subscription │
│ │
│ PRODUCT CATALOG: │
│ rate_plan_groups — Organize rate plans (selection_mode: one/many) │
│ product_rate_plans — Catalog entries with group + dependency references │
│ product_pricing — Charges/prices for each rate plan │
│ mls_fee_mappings — MLS-specific fee rate plans │
│ │
│ AMENDMENT SERVICE: │
│ SubscriptionAmendmentService — Single entry point for all plan changes │
│ • Enforces exclusive groups, required groups, cascading dependencies │
│ • Idempotent: duplicate adds and missing removes are safe no-ops │
└─────────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────────┐
│ PROVIDER LAYER (Zuora Module) │
│ │
│ ZuoraAdapter — Implements BillingProviderContract + SubscriptionProviderContract│
│ • Account CRUD, invoices, payments, payment methods │
│ • Subscription CRUD, rate plan add/remove/swap │
│ • Product catalog sync │
│ • Mappers convert Zuora responses to provider-agnostic data objects │
└─────────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────────┐
│ BUSINESS OBJECTS LAYER (Future) │
│ listings, transactions, leads, contacts │
│ • Connected to parties via party_roles │
│ • Routed via subscription data (Contact Service) │
└─────────────────────────────────────────────────────────────────────────────────┘
Entity Relationship Diagram
erDiagram
Auth0 ||--o| User : authenticates
User ||--o| Party : "is a"
Party ||--o{ PartyIdentifier : has
Party ||--o{ PartyGroupMember : "member of"
Party ||--o{ PartyRole : plays
Party ||--o{ PartyProduct : "has access to"
Party ||--o{ BillingAccount : "billed via"
PartyGroup ||--o{ PartyGroupMember : contains
PartyGroup }o--o| Party : "created by"
PartyType ||--o{ Party : classifies
PartySubtype ||--o{ Party : classifies
PartyType ||--o{ PartySubtype : contains
RoleType ||--o{ PartyRole : defines
PartyProduct }o--|| Product : "for"
PartyProduct }o--o| ProductAccount : references
Product ||--o{ ProductAccount : has
Product ||--o{ RatePlanGroup : "catalog groups"
ProductAccount }o--o| BillingAccount : "billed via"
BillingAccount }o--|| BillingProvider : "at"
BillingAccount ||--o{ Subscription : has
BillingAccount ||--o{ Invoice : has
BillingAccount ||--o{ PaymentMethod : has
Invoice ||--o{ Payment : "paid by"
Subscription ||--o{ SubscriptionItem : contains
RatePlanGroup ||--o{ ProductRatePlan : contains
RatePlanGroup }o--o| RatePlanGroup : "depends on"
ProductRatePlan ||--o{ ProductPricing : "priced by"
ProductRatePlan }o--o| ProductRatePlan : "requires"
PartyRole }o--|| Listing : "on"
Identity Module
Party — "Who Is This?"
A Party is any entity that participates in business relationships. This is both the identity and the customer account in one model — no separate "Tenant" or "Account" model needed.
Every person, company, MLS, and system in Fusion is a Party.
Party Types
| Type | Description | Subtype Examples | Real Examples |
|---|---|---|---|
| Person | An individual human | agent, broker, client | Natalie More, Anthony Colletti |
| Organization | A company, office, or group | office, developer_partner, team | Realty One Group MVP, BulletProof Real Estate Agent |
| System | An external system or data source | mls | SWFLGC MLS (b001) |
Party Structure
Party
├── first_name / last_name (for persons)
├── display_name (for organizations)
├── metadata (JSON — phones, emails, extra data)
├── PartyIdentifiers → External IDs (MLS agent #, idx_id)
├── PartyGroups → Group memberships (via PartyGroupMember)
├── PartyRoles → Roles on business objects (listings)
├── PartyProducts → Product access with roles
├── BillingAccounts → Billing relationships
└── User → Auth0 login (optional — not all parties log in)
PartyIdentifier — "What External IDs Do They Have?"
External identifiers from other systems that belong to a Party. Parties can have multiple IDs from multiple sources.
| Party | Source | Type | External ID |
|---|---|---|---|
| Natalie More | SWFLGC MLS | member_id |
26563084 |
| Realty One Group MVP | SWFLGC MLS | office_id |
5887258 |
| SWFLGC MLS | — | idx_id |
b001 |
| BulletProof Real Estate Agent | Zuora | zuora_account_id |
8a368af09222dcf101922ad92eb127b2 |
PartyGroup — "What Groups Are They In?"
A named group of Parties used to organize entities together. Members join groups via PartyGroupMember with a role and status.
Why groups? Groups replace individual party-to-party relationships with a single, flexible many-to-many pattern. Office affiliations, developer partner client lists, brokerage teams, and employment all use the same mechanism — a PartyGroup with members who have explicit roles. This avoids a proliferation of relationship types and makes it easy to query "who belongs to what" in one place.
Group Structure
PartyGroup
├── name → Human-readable group name
├── description → Optional description
├── created_by_party_id → Party that created the group
└── members → Parties in this group (via PartyGroupMember)
PartyGroupMember (Pivot)
| Field | Type | Description |
|---|---|---|
party_group_id |
uuid | The group |
party_id |
uuid | The party member |
role |
string | Role within the group (e.g., "admin", "member", "client") |
status |
enum | active, pending |
Common Group Patterns
Developer Partner Managing Clients:
PartyGroup: "BulletProof Clients"
├── created_by: BulletProof Real Estate Agent
├── PartyGroupMember: BulletProof Real Estate Agent (role: admin)
├── PartyGroupMember: Natalie More (role: client)
└── PartyGroupMember: Bob Smith (role: client)
Office Affiliation (replaces affiliated_with relationship):
PartyGroup: "Realty One Group MVP Agents"
├── created_by: Realty One Group MVP
├── PartyGroupMember: Realty One Group MVP (role: owner)
├── PartyGroupMember: Bob Broker (role: team_lead)
├── PartyGroupMember: Natalie More (role: member)
└── PartyGroupMember: Alice Agent (role: member)
Internal Team (replaces employed_by relationship):
PartyGroup: "BulletProof Team"
├── created_by: BulletProof Real Estate Agent
├── PartyGroupMember: BulletProof Real Estate Agent (role: admin)
└── PartyGroupMember: Anthony Colletti (role: member)
PartyRole — "What Role Do They Play on Things?"
A role that a Party plays on a business object (listing, transaction, lead). Uses polymorphic relationships to connect parties to any subject type.
| ID | Name | Category | Description |
|---|---|---|---|
| 1 | listing_agent |
real_estate | Represents seller on listing |
| 2 | co_listing_agent |
real_estate | Co-agent on listing |
| 3 | buyer_agent |
real_estate | Represents buyer |
| 4 | listing_office |
real_estate | Office associated with listing |
| 5 | selling_office |
real_estate | Office that brought the buyer |
Listing "2025017196"
├── PartyRole: Natalie More (listing_agent, is_primary: true)
├── PartyRole: Bob Smith (co_listing_agent)
└── PartyRole: Realty One Group MVP (listing_office)
PartyProduct — "What Products Can They Access?"
Links a Party to a Product with a specific role and account instance. Multiple people can access the same product account with different permissions.
| Role | Meaning |
|---|---|
owner |
Owns the account, can close it |
admin |
Can manage settings and users |
agent |
Can use the product (on a brokerage account) |
developer_partner |
External partner with admin access |
Individual Agent:
Party: Natalie More
└── PartyProduct (role: owner)
├── Product: IDX Broker
└── ProductAccount: #60869
Brokerage Office:
Party: Bob Broker ──── PartyProduct (role: owner) ────┐
│
Party: Natalie More ── PartyProduct (role: agent) ────┼──▶ ProductAccount: #78901
│ Product: IDX Broker
Party: Alice Agent ─── PartyProduct (role: agent) ────┘
Developer Partner Managing Client:
Party: Natalie More ───────── PartyProduct (role: owner) ──────────┐
│
Party: BulletProof Real Estate ── PartyProduct (role: developer_partner) ──▶ ProductAccount: #60869
User — "How Do They Log In?"
An application login account linked to Auth0. Separates authentication from identity — not every Party needs to log in (imported agents, offices, MLSs don't).
| Field | Description |
|---|---|
auth0_sub |
Auth0 subject identifier (unique) |
email |
Email for lookups and notifications |
party_id |
Reference to the Party this user represents |
The user's name is NOT stored on User — it lives on the Party. Access via $user->party->first_name.
Product & ProductAccount
Product is a product offering in our portfolio:
| ID | Name |
|---|---|
| 1 | IDX Broker |
| 2 | Outbound Engine |
| 3 | AIVA |
| 4 | VoicePad |
| 5 | CRM |
ProductAccount is a specific instance of a product subscription:
ProductAccount
├── product_id: 1 (IDX Broker)
├── product_account_id: 60869
├── product_account_id_type: int
└── billing_account_id → BillingAccount
Customer View
The customers table is a database view (not a physical table) that aggregates data across Party, ProductAccount, BillingAccount, and Product into a flat read-only representation. Useful for listing/searching customers without complex joins.
The Customer model includes a product-aware account() relationship — when product_name is "IDX Broker", it resolves to the IDX Broker clientInfo table via product_account_id → accountID.
IDX Broker Integration
Fusion connects to the existing IDX Broker system through several touch points:
- Separate Database Connection —
idx_connectionconnects to the IDX Broker MySQL database - IDX Client Model (
app/Models/IDX/Client.php) — reads from theclientInfotable - ProductAccount —
product_account_idmaps to IDX Broker'saccountID - PartyIdentifier — stores
idx_idfor MLS parties (e.g.,b001for SWFLGC MLS) - Customer View — aggregates Party + ProductAccount + IDX Client data for display
Billing Module
BillingProvider
An external billing system we integrate with. Currently Zuora is the sole provider.
BillingProvider
├── code: "zuora"
├── name: "Zuora"
└── is_active: true
BillingAccount — "How Does Billing Work?"
Links a Party to a billing provider with billing configuration. Tracks the billing relationship, external IDs, and preferences.
Billing Types
| Billing Type | How It Works |
|---|---|
client_direct |
Client pays their own bill directly |
partner_controlled |
Dev partner's card charged per client transaction |
aggregate |
Dev partner invoiced monthly for all their clients |
Each billing type maps to different Zuora account settings:
| Setting | client_direct | partner_controlled | aggregate |
|---|---|---|---|
| auto_pay | true | true | true |
| payment_term | Due Upon Receipt | Due Upon Receipt | Net 30 |
| batch | Batch1 | Batch1 | Batch2 |
| bill_cycle_day | 0 (signup day) | 0 (signup day) | 25 |
BillingAccount Fields
| Field | Type | Description |
|---|---|---|
party_id |
uuid (FK) | The party being billed |
billing_provider_id |
int (FK) | The billing provider (Zuora) |
external_account_id |
string | ID in the external billing system |
billing_type |
enum | client_direct, partner_controlled, aggregate |
bill_cycle_day |
int | Day of month for billing |
currency |
string | Currency code (default: USD) |
payment_status |
string | Current payment status |
status |
enum | active, suspended, closed |
synced_at |
timestamp | Last sync with billing provider |
Related Billing Models
| Model | Description |
|---|---|
Invoice |
Invoices generated for billing accounts |
InvoiceItem |
Line items on invoices |
PaymentMethod |
Payment methods attached to accounts |
Payment |
Payment records for invoices |
Subscriptions Module
The Subscriptions module is the local system of record for subscription data. While Zuora is the source of truth for billing, Fusion maintains a synced copy of subscription state to enable:
- Fast API responses without hitting Zuora on every request
- Business rule enforcement for rate plan changes
- Local querying of subscription data across the platform
Subscription — "What Are They Subscribed To?"
A subscription belongs to a BillingAccount and contains SubscriptionItems (individual charges).
Subscription
├── billing_account_id → BillingAccount
├── external_subscription_id → Zuora subscription ID
├── status: active, cancelled, suspended, pending
├── current_term_start / current_term_end
├── synced_at → Last sync timestamp (freshness check)
└── items[] → SubscriptionItem (the actual charges)
SubscriptionItem — "What Charges Are On This Subscription?"
Each item represents a single charge/rate plan on the subscription.
| Field | Description |
|---|---|
subscription_id |
Parent subscription |
product_account_id |
Optional link to ProductAccount |
external_item_id |
Zuora charge ID |
name |
Charge name (e.g., "IDX Broker Core") |
description |
Rate plan name from Zuora |
quantity |
Number of units |
unit_amount |
Price per unit |
charge_type |
recurring, one_time, usage |
billing_frequency |
monthly, annual |
effective_start_date |
When this charge became active |
effective_end_date |
When terminated (null = still active) |
synced_at |
Last sync timestamp |
Active items have effective_end_date = null. When a rate plan is removed from Zuora, the corresponding item gets an effective_end_date set.
Product Catalog: Rate Plan Groups
The product catalog organizes Zuora rate plans into groups that define selection behavior and dependencies. This is the "Fusion group method" — it maps Zuora's flat catalog into a structured, rule-enforced UI.
RatePlanGroup
Groups organize rate plans for a product with selection rules:
| Field | Description |
|---|---|
product_id |
Which product this group belongs to |
slug |
URL-friendly identifier (e.g., "plan-tier") |
name |
Display name (e.g., "Plan Tier") |
selection_mode |
one (radio — exclusive) or many (checkbox — multi-select) |
is_required |
If true, at least one plan in this group must always be active |
sort_order |
Display ordering |
requires_group_id |
Another group that must have an active plan before this one can be used |
ProductRatePlan
Individual rate plans from the Zuora catalog, mapped to local groups:
| Field | Description |
|---|---|
product_id |
Product this plan belongs to |
billing_provider_id |
Zuora |
rate_plan_group_id |
Which group this plan is in |
external_product_id |
Zuora product ID |
external_rate_plan_id |
Zuora rate plan ID |
name |
Plan name (e.g., "IDX Broker Core") |
billing_frequency |
monthly or annual |
is_default |
Whether this is the default plan in its group |
sort_order |
Display ordering within group |
requires_rate_plan_id |
Another plan that must be active before this one |
How Groups Work — Example
Product: IDX Broker
│
├── RatePlanGroup: "Plan Tier" (selection_mode: one, is_required: true)
│ ├── ProductRatePlan: "IDX Broker Lite" (Monthly)
│ ├── ProductRatePlan: "IDX Broker Core" (Monthly) ← is_default
│ └── ProductRatePlan: "IDX Broker Platinum" (Monthly)
│
├── RatePlanGroup: "Add-Ons" (selection_mode: many, requires_group: "Plan Tier")
│ ├── ProductRatePlan: "Social Pro" (requires: "IDX Broker Core")
│ └── ProductRatePlan: "Home Valuation"
│
└── RatePlanGroup: "MLS Fees" (selection_mode: many, requires_group: "Plan Tier")
├── ProductRatePlan: "SWFLGC MLS Fee"
└── ProductRatePlan: "CRMLS Fee"
Selection rules enforced by SubscriptionAmendmentService:
- Exclusive groups (
selection_mode: one): Selecting a new plan auto-swaps the existing one. You can't have both "Core" and "Platinum" — choosing Platinum replaces Core. - Multi-select groups (
selection_mode: many): Multiple plans can be active simultaneously. You can have both "Social Pro" and "Home Valuation" add-ons. - Required groups (
is_required: true): You cannot remove the last plan from a required group. The customer must always have a plan tier. - Group dependencies (
requires_group_id): "Add-Ons" requires "Plan Tier" — you can't add an add-on if no plan tier is active. Removing all plans from "Plan Tier" cascades removal of dependent group items. - Rate plan dependencies (
requires_rate_plan_id): "Social Pro" requires "IDX Broker Core" — if Core is swapped to Lite, Social Pro is automatically removed.
ProductPricing
Each rate plan has pricing entries (charges):
| Field | Description |
|---|---|
external_charge_id |
Zuora charge ID |
name |
Charge name |
charge_type |
recurring, one_time, usage |
amount |
Price |
currency |
Currency code (USD) |
pricing_model |
flat_fee, per_unit, tiered, volume |
MlsFeeMapping
Maps MLS codes to Zuora rate plans for per-MLS billing:
| Field | Description |
|---|---|
mls_code |
MLS identifier |
mls_name |
Display name |
external_rate_plan_id |
Zuora rate plan ID for fee |
external_charge_id |
Zuora charge ID |
amount |
Fee amount |
Subscription Amendment Service
The SubscriptionAmendmentService is the single entry point for all subscription rate plan changes. It orchestrates business rules that the underlying actions are intentionally unaware of.
Operations
| Method | What It Does |
|---|---|
addRatePlan |
Adds a plan; auto-swaps in exclusive groups; checks deps |
removeRatePlan |
Removes a plan; cascades dependent removals; guards required |
swapRatePlan |
Explicitly swaps one plan for another |
removeByItem |
Removes by SubscriptionItem (resolves the plan internally) |
Safety Guarantees
- Idempotent: Adding an already-active plan is a no-op. Removing a missing plan is a no-op.
- Cascading removals: Removing a plan that other plans depend on cascades the removal.
- Exclusive group enforcement: Adding to an exclusive group auto-swaps the existing plan.
- Required group protection: Cannot remove the last plan from a required group.
- Mutation safety: All mutations go through
SubscriptionMutationGuardwhich validates subscription state and acquires a lock before executing.
Mutation Guard
Every subscription mutation passes through the SubscriptionMutationGuard:
SubscriptionMutationGuard
├── SubscriptionStateValidator.preCheck() → Quick validation (is subscription active?)
├── SubscriptionLockService.executeWithLock() → Acquires advisory lock
└── SubscriptionStateValidator.validateForMutation() → Full validation under lock
Subscription Sync Flow
Fusion keeps local subscription data fresh by syncing from Zuora on demand:
1. API request hits SubscriptionController
2. SyncAccountSubscriptions checks freshness (configurable, default 5 min)
3. If stale → calls SubscriptionProviderContract.getSubscriptionsForAccount()
4. ZuoraAdapter queries Zuora API with expanded rate plans and charges
5. SubscriptionMapper converts Zuora response → provider-agnostic SubscriptionDetailsData
6. SyncSubscription.syncFromData() upserts local Subscription + SubscriptionItems
7. Items no longer present in Zuora get terminated (effective_end_date set)
Zuora Module
The Zuora module is a pure adapter — it implements two contracts from other modules and converts between Zuora's API format and Fusion's provider-agnostic data objects.
Contracts Implemented
| Contract | Module | Purpose |
|---|---|---|
BillingProviderContract |
Billing | Account CRUD, invoices, payments |
SubscriptionProviderContract |
Subscriptions | Subscription CRUD, catalog, rate plans |
Mappers
| Mapper | Converts |
|---|---|
AccountMapper |
Zuora account ↔ AccountData |
SubscriptionMapper |
Zuora subscription ↔ SubscriptionDetailsData |
InvoiceMapper |
Zuora invoice ↔ InvoiceData |
PaymentMapper |
Zuora payment/method ↔ PaymentData/PaymentMethodData |
ProductMapper |
Zuora catalog ↔ ProductData/MlsFeeRatePlanData |
Key Design Decision: Provider Agnostic
All data flowing between modules uses provider-agnostic data objects (SubscriptionDetailsData, RatePlanData, RatePlanChargeData, etc.). If Zuora were replaced with another billing provider, only the Zuora module would change — the Billing and Subscriptions modules would remain untouched.
Customer Provisioning
CreateCustomerAccount Flow
When a new IDX Broker customer is created, CreateCustomerAccount orchestrates the full provisioning:
CreateCustomerAccount
├── CreateMlsParty → Party (type: system, subtype: mls) + PartyIdentifier (idx_id)
├── CreateOfficeParty → Party (type: org, subtype: office) + PartyIdentifier (office_id)
├── CreateDeveloperPartner → Party (type: org, subtype: developer_partner) + owner Party
├── CreateAgentParty → Party (type: person, subtype: agent) + identifiers + group memberships
├── CreateUser → User (auth0_sub, email, party_id)
├── CreateTenant → Tenant + party_tenant links (legacy, being phased out)
├── CreateProductAccount → ProductAccount + BillingAccount + tenant_products link
└── PartyGroup → agent added to developer partner's client group
ProvisionCustomer (Batch Import)
For bulk imports, ProvisionCustomer provides a streamlined path:
ProvisionCustomer
├── Get or create Party
├── Find Product by name
├── Ensure BillingAccount (if Zuora ID provided)
├── Create ProductAccount with billing_account_id
└── Link Party → Product → ProductAccount via party_product
Complete Data Flow — Real-World Example
Natalie More: Individual Agent with Developer Partner
User (nataliejmore@gmail.com)
│ auth0_sub: "auth0|6762de3a385c95f62d29e2cf"
│
└──▶ Party: Natalie More (person/agent)
│
├── PartyIdentifier
│ ├── source: SWFLGC MLS (b001)
│ ├── type: "member_id"
│ └── external_id: "26563084"
│
├── PartyGroupMember
│ ├── group: "Realty One Group MVP Agents" (role: member)
│ └── group: "BulletProof Clients" (role: client)
│
├── PartyProduct (role: owner)
│ ├── Product: IDX Broker
│ └── ProductAccount: #60869
│ └── billing_account_id → BillingAccount
│
├── BillingAccount
│ ├── billing_provider: Zuora
│ ├── external_account_id: "8a3694b893d3e0000193da3422b80bb9"
│ ├── billing_type: client_direct
│ └── status: active
│ │
│ └── Subscription (synced from Zuora)
│ ├── status: active
│ ├── SubscriptionItem: "IDX Broker Core" ($49.99/mo, recurring)
│ └── SubscriptionItem: "SWFLGC MLS Fee" ($10.00/mo, recurring)
│
└── PartyRole
├── role_type: "listing_agent"
├── subject_type: "listing"
└── subject_id: "2025017196"
Developer Partner: BulletProof Real Estate Agent
Party: BulletProof Real Estate Agent (org/developer_partner)
│
├── BillingAccount
│ ├── billing_provider: Zuora
│ ├── external_account_id: "8a368af09222dcf101922ad92eb127b2"
│ ├── billing_type: aggregate
│ └── bill_cycle_day: 25
│
├── PartyGroup: "BulletProof Clients"
│ ├── BulletProof Real Estate Agent (role: admin)
│ ├── Natalie More (role: client)
│ └── Bob Smith (role: client)
│
├── PartyProduct (role: developer_partner)
│ └── ProductAccount: #60869 (Natalie's account — admin access)
│
└── PartyGroup: "BulletProof Team"
├── BulletProof Real Estate Agent (role: admin)
└── Anthony Colletti (role: member)
└── User: webteam@bulletproofrealestateagent.com
Complete Data Flow Diagram
flowchart TD
subgraph auth [Authentication]
Auth0[Auth0 IdP]
end
subgraph app [Application]
User[User<br/>auth0_sub, party_id]
end
subgraph identity [Identity Module]
Party[Party<br/>type, name]
PartyId[PartyIdentifier<br/>MLS IDs, idx_id]
PartyGroup[PartyGroup<br/>named groups]
PartyGroupMember[PartyGroupMember<br/>role, status]
PartyRole[PartyRole<br/>listing_agent]
end
subgraph products [Products]
PartyProduct[PartyProduct<br/>role: owner/agent/admin]
Product[Product<br/>IDX Broker, OE, etc.]
ProductAccount[ProductAccount<br/>#60869]
end
subgraph billing [Billing Module]
BillingProvider[BillingProvider<br/>Zuora]
BillingAccount[BillingAccount<br/>billing_type, status]
Invoice[Invoice]
PaymentMethod[PaymentMethod]
end
subgraph subscriptions [Subscriptions Module]
Subscription[Subscription<br/>status, term dates]
SubItem[SubscriptionItem<br/>name, amount, charge_type]
RatePlanGrp[RatePlanGroup<br/>selection_mode, dependencies]
ProdRatePlan[ProductRatePlan<br/>catalog entries]
AmendmentSvc[SubscriptionAmendmentService<br/>add, remove, swap]
end
subgraph zuora [Zuora Module]
ZuoraAdapter[ZuoraAdapter<br/>BillingProvider + SubscriptionProvider]
Mappers[Mappers<br/>Account, Subscription, Invoice, Product]
end
subgraph business [Business Objects — Future]
Listing[Listings]
Transaction[Transactions]
end
Auth0 --> User
User --> Party
Party --> PartyId
Party --> PartyGroupMember
PartyGroupMember --> PartyGroup
Party --> PartyRole
Party --> PartyProduct
Party --> BillingAccount
PartyProduct --> Product
PartyProduct --> ProductAccount
ProductAccount --> BillingAccount
BillingAccount --> BillingProvider
BillingAccount --> Invoice
BillingAccount --> PaymentMethod
BillingAccount --> Subscription
Subscription --> SubItem
Product --> RatePlanGrp
RatePlanGrp --> ProdRatePlan
AmendmentSvc --> Subscription
ZuoraAdapter --> BillingAccount
ZuoraAdapter --> Subscription
Mappers --> ZuoraAdapter
PartyRole --> Listing
PartyRole --> Transaction
Table Specifications
parties
| Column | Type | Description |
|---|---|---|
id |
uuid | Primary key |
party_type_id |
int (FK) | person, organization, or system |
party_subtype_id |
int (FK) | agent, broker, office, mls, etc. (optional) |
first_name |
string | For persons |
last_name |
string | For persons |
display_name |
string | For organizations |
metadata |
json | Flexible additional data (phones, emails, etc.) |
created_at |
timestamp | |
updated_at |
timestamp |
party_identifiers
| Column | Type | Description |
|---|---|---|
id |
uuid | Primary key |
party_id |
uuid (FK) | The party this identifier belongs to |
source_party_id |
uuid (FK) | The system that issued this ID (nullable) |
identifier_type |
string(50) | member_id, office_id, idx_id, zuora_account_id |
external_id |
string(100) | The actual ID value |
data_type |
string(20) | int, varchar, guid (for proper casting) |
created_at |
timestamp | |
updated_at |
timestamp |
party_groups
| Column | Type | Description |
|---|---|---|
id |
uuid | Primary key |
name |
string | Name of the group (max 150 chars) |
description |
text | Optional description |
created_by_party_id |
uuid (FK) | Party that created this group |
created_at |
timestamp | |
updated_at |
timestamp |
party_group_members
| Column | Type | Description |
|---|---|---|
id |
uuid | Primary key |
party_group_id |
uuid (FK) | The group |
party_id |
uuid (FK) | The party member |
role |
string | Role within the group (default: "member") |
status |
enum | active, pending (default: active) |
created_at |
timestamp | |
updated_at |
timestamp |
Unique constraint: (party_group_id, party_id)
party_roles
| Column | Type | Description |
|---|---|---|
id |
uuid | Primary key |
party_id |
uuid (FK) | The party playing this role |
role_type_id |
int (FK) | Type of role |
subject_type |
string(50) | Polymorphic type (listing, transaction, lead) |
subject_id |
uuid | Polymorphic ID |
is_primary |
boolean | Is this the primary party for this role? |
created_at |
timestamp | |
updated_at |
timestamp |
party_product
| Column | Type | Description |
|---|---|---|
id |
uuid | Primary key |
party_id |
uuid (FK) | The party |
product_id |
int (FK) | The product |
product_account_id |
uuid (FK) | The specific account instance (nullable) |
role |
string | owner, admin, agent, developer_partner |
created_at |
timestamp | |
updated_at |
timestamp |
users
| Column | Type | Description |
|---|---|---|
id |
uuid | Primary key |
auth0_sub |
string | Auth0 subject identifier (unique) |
party_id |
uuid (FK) | Reference to the party this user represents |
email |
string | Email for lookups and notifications |
created_at |
timestamp | |
updated_at |
timestamp |
billing_providers
| Column | Type | Description |
|---|---|---|
id |
int | Primary key |
code |
string | Unique code (e.g., "zuora") |
name |
string | Display name |
is_active |
boolean | Whether provider is active |
created_at |
timestamp | |
updated_at |
timestamp |
billing_accounts
| Column | Type | Description |
|---|---|---|
id |
uuid | Primary key |
party_id |
uuid (FK) | The party being billed |
billing_provider_id |
int (FK) | The billing provider (e.g., Zuora) |
external_account_id |
string | ID in the external billing system |
billing_type |
string | client_direct, partner_controlled, aggregate |
bill_cycle_day |
int | Day of month for billing |
currency |
string | Currency code (default: USD) |
payment_status |
string | Current payment status |
status |
string | active, suspended, closed |
synced_at |
timestamp | Last sync with billing provider |
created_at |
timestamp | |
updated_at |
timestamp |
Unique constraint: (billing_provider_id, external_account_id)
subscriptions
| Column | Type | Description |
|---|---|---|
id |
uuid | Primary key |
billing_account_id |
uuid (FK) | The billing account |
external_subscription_id |
string | Zuora subscription ID |
status |
enum | active, cancelled, suspended, pending |
current_term_start |
date | Current term start date |
current_term_end |
date | Current term end date |
cancelled_at |
datetime | When cancelled (if applicable) |
synced_at |
datetime | Last sync from provider |
external_data |
json | Raw provider data (optional) |
created_at |
timestamp | |
updated_at |
timestamp |
subscription_items
| Column | Type | Description |
|---|---|---|
id |
uuid | Primary key |
subscription_id |
uuid (FK) | Parent subscription |
product_account_id |
uuid (FK) | Optional ProductAccount link |
external_item_id |
string | Zuora charge ID |
name |
string | Charge name |
description |
string | Rate plan name |
quantity |
int | Number of units |
unit_amount |
decimal(8,2) | Price per unit |
charge_type |
enum | recurring, one_time, usage |
billing_frequency |
enum | monthly, annual (nullable) |
effective_start_date |
date | When charge started |
effective_end_date |
date | When terminated (null = active) |
synced_at |
datetime | Last sync from provider |
created_at |
timestamp | |
updated_at |
timestamp |
rate_plan_groups
| Column | Type | Description |
|---|---|---|
id |
int | Primary key |
product_id |
int (FK) | Product this group belongs to |
slug |
string | URL-friendly identifier |
name |
string | Display name |
selection_mode |
enum | one (exclusive) or many (multi-select) |
is_required |
boolean | Must always have at least one active plan |
sort_order |
int | Display ordering |
requires_group_id |
int (FK) | Prerequisite group (nullable) |
created_at |
timestamp | |
updated_at |
timestamp |
product_rate_plans
| Column | Type | Description |
|---|---|---|
id |
uuid | Primary key |
product_id |
int (FK) | Product |
billing_provider_id |
int (FK) | Billing provider (Zuora) |
rate_plan_group_id |
int (FK) | Group this plan belongs to (nullable) |
external_product_id |
string | Zuora product ID |
external_rate_plan_id |
string | Zuora rate plan ID |
name |
string | Plan name |
billing_frequency |
enum | monthly, annual |
is_default |
boolean | Default plan in group |
sort_order |
int | Display ordering within group |
requires_rate_plan_id |
uuid (FK) | Prerequisite rate plan (nullable) |
synced_at |
datetime | Last catalog sync |
created_at |
timestamp | |
updated_at |
timestamp |
product_pricing
| Column | Type | Description |
|---|---|---|
id |
uuid | Primary key |
product_rate_plan_id |
uuid (FK) | Parent rate plan |
external_charge_id |
string | Zuora charge ID |
name |
string | Charge name |
charge_type |
enum | recurring, one_time, usage |
amount |
decimal(8,2) | Price |
currency |
string | Currency code (USD) |
pricing_model |
enum | flat_fee, per_unit, etc. |
synced_at |
datetime | Last catalog sync |
created_at |
timestamp | |
updated_at |
timestamp |
mls_fee_mappings
| Column | Type | Description |
|---|---|---|
id |
uuid | Primary key |
mls_code |
string | MLS identifier (unique) |
mls_name |
string | Display name |
billing_provider_id |
int (FK) | Billing provider (Zuora) |
external_rate_plan_id |
string | Zuora rate plan ID for fee |
external_charge_id |
string | Zuora charge ID |
amount |
decimal(8,2) | Fee amount |
synced_at |
datetime | Last sync |
created_at |
timestamp | |
updated_at |
timestamp |
Reference Data
party_types
| ID | Name | Description |
|---|---|---|
| 1 | person | An individual human |
| 2 | organization | A company, office, or group |
| 3 | system | An external system (MLS, API, etc.) |
party_subtypes
| ID | party_type_id | Name | Description |
|---|---|---|---|
| 1 | 1 (person) | agent | Real estate agent |
| 2 | 1 (person) | broker | Licensed broker |
| 3 | 1 (person) | client | A customer/lead |
| 4 | 2 (org) | office | Real estate brokerage |
| 5 | 2 (org) | corporation | Corporate entity |
| 6 | 2 (org) | team | Agent team |
| 7 | 3 (system) | mls | Multiple Listing Service |
| 8 | 2 (org) | developer_partner | Developer partner company |
role_types
| ID | Name | Category | Description |
|---|---|---|---|
| 1 | listing_agent | real_estate | Represents seller on listing |
| 2 | co_listing_agent | real_estate | Co-agent on listing |
| 3 | buyer_agent | real_estate | Represents buyer |
| 4 | listing_office | real_estate | Office associated with listing |
| 5 | selling_office | real_estate | Office that brought the buyer |
party_group_roles (conventions)
| Role | Context | Description |
|---|---|---|
admin |
All groups | Can manage group and members |
member |
General | Standard group member |
client |
Dev Partners | Client managed by the group |
team_lead |
Brokerage Teams | Team leader |
owner |
Business | Owner of the represented entity |
product_account_roles
| Role | Description |
|---|---|
owner |
Owns the account, can close it |
admin |
Can manage settings and users |
agent |
Can use the product (brokerage account members) |
developer_partner |
External partner with admin access |
billing_types
| Type | Description | Zuora Settings |
|---|---|---|
client_direct |
Client pays their own bill directly | auto_pay, Due Upon Receipt |
partner_controlled |
Dev partner's card charged per client transaction | auto_pay, Due Upon Receipt |
aggregate |
Dev partner invoiced monthly for all their clients | auto_pay, Net 30, bill day 25 |
subscription_statuses
| Status | Description |
|---|---|
active |
Currently active subscription |
cancelled |
Subscription has been cancelled |
suspended |
Temporarily suspended |
pending |
Awaiting activation |
charge_types
| Type | Description |
|---|---|
recurring |
Charged every billing period |
one_time |
Charged once |
usage |
Charged based on consumption |
selection_modes
| Mode | Behavior |
|---|---|
one |
Exclusive — only one plan active per group |
many |
Multi-select — multiple plans can be active |
Why This Architecture?
| Benefit | How It Helps |
|---|---|
| Single Source of Truth | Person's name in one place (Party), not duplicated across tables |
| Party = Identity + Account | No separate "Tenant" model — Party serves both purposes |
| Flexible Grouping | PartyGroup allows any grouping pattern (teams, clients, affiliations) |
| Clear Roles on Products | Explicit role on party_product (owner vs agent vs developer_partner) |
| MLS Ready | Listings can have multiple agents with different roles via party_roles |
| CRM Foundation | Same Party model works for leads, contacts, clients |
| Billing Clarity | BillingAccount handles money; Party handles identity |
| Auth0 Clean | User is just login credentials; Party is identity |
| Developer Partner Support | PartyGroup for client management, BillingType for billing arrangements |
| Provider Agnostic | Zuora adapter behind contracts — swap billing providers without module changes |
| Rate Plan Groups | Structured catalog with selection rules and dependency enforcement |
| Subscription Safety | Amendment service with locking, idempotency, and cascading business rules |
| Sync-on-Demand | Freshness-based caching keeps local data current without constant polling |
| Extensible | New party types, roles, products, and billing arrangements added easily |
Key Principles
| Principle | Explanation |
|---|---|
| Party = Identity | Who someone IS in the business world |
| PartyGroup = Organization | How parties are grouped (teams, clients, etc.) |
| PartyProduct = Access | What products they can use, with what role |
| BillingAccount = Billing | How money flows through billing providers |
| Subscription = What They Pay For | Local record of subscription state synced from Zuora |
| RatePlanGroup = Catalog Structure | How rate plans are organized with selection rules |
| User = Login | How they authenticate (Auth0) |
| Contracts = Boundaries | Modules communicate through interfaces, not implementations |
| No Duplication | Name lives on Party, not User; subscription data synced, not copied |
| Roles Define Access | party_product.role defines what you can do |
Glossary
| Term | Definition |
|---|---|
| Party | Any business entity (person, org, system) that participates in relationships |
| PartyIdentifier | An external ID for a Party (MLS #, idx_id) |
| PartyGroup | A named group of parties with a purpose (team, client list, affiliation) |
| PartyGroupMember | Membership record linking a Party to a PartyGroup with role and status |
| PartyRole | Role a Party plays on a business object (listing_agent on a listing) |
| PartyProduct | Pivot linking Party to Product with a role (owner, agent, admin) |
| Product | A product we sell (IDX Broker, Outbound Engine, VoicePad, AIVA, CRM) |
| ProductAccount | A specific instance/account of a product (#60869) |
| BillingProvider | An external billing system (e.g., Zuora) |
| BillingAccount | Links Party to BillingProvider with billing type and configuration |
| Subscription | A billing subscription synced from Zuora, belonging to a BillingAccount |
| SubscriptionItem | An individual charge on a subscription (rate plan charge) |
| RatePlanGroup | Organizes catalog rate plans with selection mode and dependency rules |
| ProductRatePlan | A rate plan from the Zuora catalog, mapped to a local group |
| ProductPricing | Price/charge details for a rate plan |
| MlsFeeMapping | Maps an MLS code to a Zuora rate plan for per-MLS billing |
| SubscriptionAmendmentService | Single entry point for all subscription rate plan changes |
| SubscriptionMutationGuard | Validates state and acquires lock before any subscription mutation |
| User | Application login record linked to Auth0 and a Party |
| Customer | Database view aggregating Party + Product + Billing data |
| ZuoraAdapter | Implements billing and subscription contracts using Zuora's API |
GraphQL API
The GraphQL schema is organized by resource:
graphql/
├── schema.graphql # Root schema
├── shared/ # Common interfaces and error types
├── user/ # User queries and mutations
├── party/ # Party CRUD, type queries
├── product/ # Product queries
└── party-group/ # Party group queries and mutations
Key operations:
party(id)/parties— Query parties with type/subtype filteringcreateParty/updateParty— Party CRUDsubscribePartyToProduct— Link a party to a productcreatePartyGroup— Create a new party groupaddPartyToGroup— Add a party to a group with role/statuspartyTypes/partySubtypes/products— Reference data queries
REST API
The Subscriptions module exposes REST endpoints:
| Method | Endpoint | Action |
|---|---|---|
| GET | /subscriptions |
List all subscriptions |
| GET | /subscriptions/{subscription} |
Show subscription (auto-sync) |
| POST | /subscriptions |
Create subscription |
| DELETE | /subscriptions/{subscription} |
Delete subscription |
| POST | /subscriptions/{subscription}/cancel |
Cancel subscription |
| POST | /subscriptions/{subscription}/mls-fees |
Add MLS fee |
| DELETE | /subscriptions/{subscription}/mls-fees/{mlsCode} |
Remove MLS fee |
| GET | /parties/{party}/subscriptions |
List subscriptions for party |
References
- RESO Data Dictionary (Real Estate Standards Organization)
- Salesforce Party Model
- SAP Business Partner Model
- Auth0 Laravel SDK Documentation
- Zuora API Documentation
Document Status: Active / Implemented
Completed
- Identity Module — Party model with types, subtypes, identifiers, groups, roles
- PartyGroup and PartyGroupMember for flexible party organization
- Billing Module — BillingAccount, BillingProvider, Invoice, Payment, PaymentMethod
- Subscriptions Module — Subscription, SubscriptionItem with Zuora sync
- Product Catalog — RatePlanGroup, ProductRatePlan, ProductPricing, MlsFeeMapping
- SubscriptionAmendmentService with group enforcement and cascading dependencies
- Zuora Module — Full adapter with mappers for accounts, subscriptions, invoices, catalog
- GraphQL API for parties, groups, products
- REST API for subscriptions with sync-on-demand
- Customer provisioning (single and batch import)
- Product catalog sync command (
fusion:sync-product-catalog)
Next Steps
- Build out CRM features on Party foundation
- Implement Contact/Lead routing service using subscription data
- Implement MLS data import using Party model
- Event-driven architecture with Pub/Sub for cross-product communication