Local to Enterprise Wallet Merge
- Wallet Merge: Merges local wallets to existing enterprise wallets during
findOrCreateCustomer(POST /customers/find) - Triggered by:
TransferPaymentMethodCommandHandlerwhen a golden record is found for a customer that also has a local wallet - Payment Handling: De-duplicates using
vendorPaymentMethodFingerprint; transfers unique; updates or discards duplicates based onmodifiedTs - Bank Accounts: Only
ACTIVEbank accounts are transferred;INVALIDATEDones are deleted immediately - Migration Tracking:
CustomerMigrationentity tracks state (IN_PROGRESS → COMPLETED or FAILED) - Benefits: Preserves payment history, creates seamless cross-merchant experience
Overview
The Local to Enterprise Wallet Merge functionality provides a critical upgrade path within the Convenient Checkout Gateway (CCG) platform. This process is triggered automatically during the findOrCreateCustomer flow (POST /customers/find) when the system identifies that a customer has both a local wallet and a matching enterprise identity.
The merge is orchestrated by TransferPaymentMethodCommandHandler (invoked from CreateCustomerCommandHandler.findCustomerByGoldenRecord()) and PaymentMethodDeDupeCommandHandler for handling duplicate payment methods.
Key advantages of this functionality include:
- Identity Unification: Combining fragmented customer identities
- Payment Method Consolidation: Preserving and consolidating payment methods
- Cross-Merchant Experience: Enabling wallet use across multiple merchants
- Data Continuity: Maintaining transaction history and preferences
Architecture Diagram
Merge Process
A wallet merge happens when a local wallet needs upgrading to an enterprise wallet, facilitating unified identity. The process is implemented primarily in TransferPaymentMethodCommandHandler.
Process Flow
-
Detect Merge Trigger (
identifyLocalToEnterpriseMerge)- During
findOrCreateCustomer, after a golden record is found, the system checks if anEnterpriseCustomerexists for the resolved customer. - If an enterprise customer exists, it searches for a local customer in the same merchant using
customerQueryHandler.findLocalCustomer(merchantId, metadata). - Decision: Is there a matching active enterprise wallet and a separate active local wallet?
- If the local customer ID equals the enterprise customer ID, the merge is skipped (already the same customer).
Merge Trigger Scenarios — Same Merchant, Same Group, Different GroupsMerge/upgrade can be triggered across different merchant and group configurations. For a full breakdown of when and how this trigger fires — including same-merchant precedence groups, same merchant group, different merchant groups, and why enterprise-only merchants never trigger a merge — see Merge / Upgrade Summary in the Enterprise Wallet Upgradation guide.
- During
-
Check for Existing or Failed Migration (
resumeFailedMigration)- Queries
CustomerMigrationRepository.findByMerchantCustomerIdAndEnterpriseCustomerId(). - If a migration exists with status COMPLETED or IN_PROGRESS, throws
CustomerMigrationAlreadyExistException(silently ignored viaonErrorComplete). - If a migration exists with status FAILED, resumes it by setting status back to IN_PROGRESS.
- Queries
-
Create Migration Record (
createMigrationRecord)- If no migration exists, creates a new
CustomerMigrationentity with:merchantCustomerId: the local customer's IDenterpriseCustomerId: the enterprise customer's IDmerchantId: from theX-Merchant-IdheadervendorMerchantCustomerIdandvendorEnterpriseCustomerId: fetched fromVendorCustomerRepositorystatus: IN_PROGRESS
- If no migration exists, creates a new
-
Deactivate Local Customer (
markMerchantCustomerAsInactive)- Marks the local customer as inactive using
CustomerUtil.updatedCustomerToInactive()and saves it. - Retains the record for audit/history.
- Marks the local customer as inactive using
-
Identify and Transfer Payment Methods (
getLocalAndEnterprisePaymentMethods)- Fetches all payment methods for both the local customer and enterprise customer in parallel via
Mono.zipDelayError(). - If the local customer has no payment methods, throws
NoPaymentMethodsAvailableExceptionand marks the migration as COMPLETED. - Filters out
INVALIDATEDbank accounts — these are deleted immediately viapaymentMethodDeDupeCommandHandler.deleteLocalWallet(). - Special handling for payment method types:
- Cards: All valid cards are eligible for transfer.
- ACH/Bank Accounts: Only
ACTIVEaccounts are transferred;INVALIDATEDones are deleted.
- Fetches all payment methods for both the local customer and enterprise customer in parallel via
-
De-duplicate Payment Methods (
identifyNonUniquePaymentMethods/identifyUniquePaymentMethodsInMerchantCustomer)- Payment methods are categorized using their
vendorPaymentMethodFingerprint:- Unique (fingerprint exists only in local wallet): Eligible for transfer to enterprise wallet.
- Non-unique (fingerprint exists in both wallets): Handled by
PaymentMethodDeDupeCommandHandler.
- For non-unique (duplicate) payment methods:
- Compares
modifiedTstimestamps of the local and enterprise versions. - If the local wallet's payment method is more recent: Updates the enterprise payment method with local details (card:
nameOnCard,expiryMonth,expiryYear,zipCode; ACH:accountType,nameOnAccount,status) and then deletes the local version. - If the enterprise wallet's payment method is more recent or equal: Retains the enterprise version and deletes the local version without updating.
- Compares
- For unique payment methods:
- Builds a
TransferPaymentMethodsEventcontaining source (local) and destination (enterprise) customer details plus payment method transfer details.
- Builds a
- Payment methods are categorized using their
-
Publish Events
- For each unique payment method being transferred: publishes
TRANSFER_PAYMENT_METHODS_EVENTcontaining migration ID, source customer, destination customer, and payment method details. - For each non-unique payment method where local is newer and enterprise is updated: publishes
PAYMENT_METHOD_UPDATEDevent with migration metadata (source: CCG_WALLET_MERGE,id: migrationId). - For each deleted local payment method (both unique and non-unique): publishes
PAYMENT_METHOD_REPLACEDevent with the enterprise payment method data but the local customer's ID.
- For each unique payment method being transferred: publishes
-
Finalize Migration
- If no more unique payment methods remain after de-duplication, marks migration as COMPLETED.
- Otherwise, migration completes asynchronously after the
TRANSFER_PAYMENT_METHODS_EVENTis processed.
-
Error Handling & Recovery
- If any step fails, migration status remains IN_PROGRESS or is set to FAILED by error handlers.
CustomerMigrationAlreadyExistExceptionis silently completed viaonErrorComplete— no error propagated to the caller.- Failed migrations can be retried on the next
findOrCreateCustomercall for the same customer.
When payment methods are deleted as part of the merge process, PAYMENT_METHOD_DELETED events are not sent to the merchant. Instead, PAYMENT_METHOD_REPLACED is published (pointing to the enterprise version).
Migration States
The CustomerMigration entity (stored in the customer_migration table) tracks the following states:
| Status | Description | Behavior on Next findOrCreateCustomer Call |
|---|---|---|
| IN_PROGRESS | Migration started but not completed | Skipped via CustomerMigrationAlreadyExistException |
| COMPLETED | All payment methods successfully transferred | Skipped via CustomerMigrationAlreadyExistException |
| FAILED | Error occurred during migration | Resumed — status reset to IN_PROGRESS and process retried |
CustomerMigration Entity Fields
| Field | Type | Description |
|---|---|---|
id | UUID | Auto-generated migration record ID |
merchantCustomerId | UUID | The local customer's wallet ID |
enterpriseCustomerId | UUID | The enterprise customer's wallet ID |
merchantId | UUID | The merchant ID from the request |
vendorMerchantCustomerId | String | Stripe customer ID for the local customer |
vendorEnterpriseCustomerId | String | Stripe customer ID for the enterprise customer |
status | CustomerMigrationStatus | Current migration state |
error | ErrorResponse (JSON) | Error details if migration failed |
Testing
-
Basic Merge:
- Local wallet with unique payment methods is merged into an enterprise wallet.
- Verify all payment methods and history are preserved.
-
Duplicate Payment Methods:
- Both local and enterprise wallets have the same card or bank account.
- Ensure deduplication logic retains only the most recent/valid method.
-
Mixed Payment Types:
- Local wallet has cards, enterprise wallet has ACH/bank accounts (and vice versa).
- Confirm all valid payment methods are transferred and properly categorized.
-
Failed Migration and Retry:
- Simulate a migration failure (e.g., network error, data conflict).
- Ensure migration can be retried and completes successfully without data loss.
-
Inactive Local Customer:
- After merge, local customer should be marked inactive and not usable for new transactions.
-
Event Publishing:
- Confirm that all relevant events (e.g., PAYMENT_METHOD_REPLACED) are published and received by downstream systems.
-
Edge Cases:
- Local wallet with no payment methods.
- Enterprise wallet already contains all local payment methods.
- Large number of payment methods (stress test).
-
Audit and History:
- Verify that transaction and payment history is preserved and accessible post-merge.
-
Orphan/Residual Data:
- Ensure no orphaned or duplicate payment methods or customer records remain after merge.