Customer Look up Logic using enterpriseSettings in CCG Database
When a customer cannot be resolved through direct identifiers (walletCustomerId, enterpriseId, or hsid), the system falls back to a metadata-based CCG database lookup that also leverages the enterprise settings configuration.
The CCG DB metadata lookup is the final fallback in the customer resolution chain. It runs after all direct identifier lookups have been exhausted — not as a replacement for EIMP, but as a complementary database-level search that reuses the same enterprise settings configuration.
Resolution Chain — Where CCG DB Lookup Fits
Customer Resolution Priority Order
The customer resolution order in CustomerQueryHandler.findCustomer():
| Priority | Strategy | Lookup Target | Method |
|---|---|---|---|
| 1st | Direct ID | customer table by walletCustomerId | findById() |
| 2nd | Enterprise ID | enterprise_customer table by enterpriseId | getCustomerByEnterpriseId() |
| 3rd | HSID | enterprise_customer table by hsid | getCustomerByHsid() |
| 4th | Metadata (Enterprise Settings) | merchant_customer_metadata table | getCustomerByMetadata() |
| 5th | Base HSID | merchant_customer table | getCustomerByBaseHsid() |
CCG DB Lookup Algorithm
Step-by-Step Process
The lookup is implemented in CustomerQueryHandler.getCustomerByMetadata() and uses merchantMetadataKey (not merchantSearchKey) to bridge the request metadata to the database:
-
Guard check — If the query has no
metadata, skip directly togetCustomerByBaseHsid() -
Load merchant and verify enterprise settings
- Retrieve the
Merchantentity usingmerchantRepository.findByMerchantId() - Confirm
orderedCustomerSearchCriteriais non-empty; if empty, skip to fallback
- Retrieve the
-
Expand search to the entire merchant group
- Retrieve the
merchantGroupIdfrom the requesting merchant - Load all merchants in that group via
merchantRepository.findByMerchantGroupId() - This enables cross-merchant customer discovery within the same merchant group
- Retrieve the
-
For each merchant in the group:
- Sort
OrderedCustomerSearchCriteriaby outerprecedence(ascending) - Sort each inner
CustomerSearchCriterialist by innerprecedence(ascending)
- Sort
-
Validate metadata completeness —
isMetadataContains()- Extract all
merchantMetadataKeyvalues markedrequired:truefrom the criteria - Verify the request metadata contains all required keys
- If no criteria are marked required, at least one configured key must be present
- If validation fails, skip this criteria set
- Extract all
-
Filter metadata to configured keys —
filterMetadata()- From the request metadata, keep only entries whose key matches a
merchantMetadataKeyin the criteria - Uses a
LinkedHashMapto preserve insertion order (respects inner precedence) - Example: Request
{"subscriberId": "ABC", "dependentCode": "01", "phone": "555-1234"}with criteria keys["subscriberId", "dependentCode"]→ filtered to{"subscriberId": "ABC", "dependentCode": "01"}
- From the request metadata, keep only entries whose key matches a
-
Execute SQL query against
merchant_customer_metadata- Call
merchantCustomerMetadataRepository.findByMetadataAndMerchantId(filteredMetadata, merchantId) - Returns
Optional<UUID>(thecustomer_id)
- Call
-
Load full customer — If a
customer_idis found, load theCustomerviacustomerRepository.findOptionalCustomerById() -
First match wins —
.reduce((first, next) -> first.isPresent() ? first : next)takes the first non-empty result across all merchants in the group -
Final fallback — If no metadata match found, fall through to
getCustomerByBaseHsid()
SQL Query Construction
Dynamic Query Building
The SQL is built dynamically by MerchantCustomerMetadataRepository.buildMetadataQuery() using nested subqueries for multi-key intersection:
Single metadata key (e.g., {"subscriberId": "ABC123"}):
SELECT customer_id FROM merchant_customer_metadata mcm
JOIN customer cus ON cus.id = mcm.customer_id
WHERE mcm.metadata_key = 'subscriberId'
AND mcm.metadata_value = 'ABC123'
AND mcm.merchant_id = '<merchant-uuid>'
AND (cus.inactive IS NULL OR cus.inactive = false)
Multiple metadata keys (e.g., {"subscriberId": "ABC123", "dependentCode": "01"}):
SELECT customer_id FROM merchant_customer_metadata mcm
JOIN customer cus ON cus.id = mcm.customer_id
WHERE mcm.metadata_key = 'subscriberId'
AND mcm.metadata_value = 'ABC123'
AND mcm.merchant_id = '<merchant-uuid>'
AND (cus.inactive IS NULL OR cus.inactive = false)
AND customer_id IN (
SELECT customer_id FROM merchant_customer_metadata mcm
JOIN customer cus ON cus.id = mcm.customer_id
WHERE mcm.metadata_key = 'dependentCode'
AND mcm.metadata_value = '01'
AND mcm.merchant_id = '<merchant-uuid>'
AND (cus.inactive IS NULL OR cus.inactive = false)
)
Each additional metadata key adds another nested AND customer_id IN (...) subquery, forming an intersection — the customer must have matching rows for every filtered metadata key.
Database Tables and Schema
Tables Involved
| Table | Columns Used | Role |
|---|---|---|
merchant | enterprise_settings (JSONB), merchant_group_id | Stores enterprise settings config; groups merchants |
merchant_customer_metadata | customer_id, merchant_id, metadata_key, metadata_value | Key-value pairs linking customers to merchant-specific identifiers |
customer | id, inactive | JOIN filter to exclude inactive customers |
The merchant_customer_metadata entity:
@Entity
@Table(name = "merchant_customer_metadata")
public class MerchantCustomerMetadata {
UUID id;
UUID customerId; // → customer.id
UUID merchantId; // → merchant.wallet_merchant_id
String key; // metadata_key (e.g., "subscriberId")
String value; // metadata_value (e.g., "ABC123")
}
Sequence Diagram
CCG DB Metadata Lookup Flow
merchantMetadataKey — EIMP vs CCG DB Usage
Dual Purpose of merchantMetadataKey
The merchantMetadataKey field serves different purposes in different lookup phases:
| Phase | How merchantMetadataKey Is Used | Other Keys Used |
|---|---|---|
| EIMP Lookup | ❌ Not used | merchantSearchKey (JSONPath extraction), enterpriseSearchKey, enterpriseValueKey |
| CCG DB Lookup | ✅ Matches request metadata keys to DB query keys | — |
| Wallet Upgrade | ✅ Maps EIMP response values (via enterpriseResponseSearchPath) to metadata storage keys in merchant_customer_metadata | enterpriseResponseSearchPath |
During EIMP lookups, the system uses merchantSearchKey (a JSONPath) to extract values from the full customer request object. During CCG DB lookups, the system uses merchantMetadataKey to match against the flat metadata map that was passed in the request.
Example:
Enterprise Settings:
merchantSearchKey: "$.metadata.subscriberId" ← used by EIMP lookup (JSONPath on full request)
merchantMetadataKey: "subscriberId" ← used by CCG DB lookup (key match on metadata map)
Request:
{ "metadata": { "subscriberId": "ABC123" } }
EIMP: extracts "ABC123" via JSONPath "$.metadata.subscriberId"
CCG DB: filters metadata map to {"subscriberId": "ABC123"}, queries merchant_customer_metadata table
Merchant Group Cross-Lookup
Cross-Merchant Customer Discovery
A key feature of the CCG DB lookup is that it searches across all merchants in the same merchant group, not only the requesting merchant. This enables scenarios where a customer was originally created under one merchant but needs to be discovered when accessing through another merchant in the same group.
Merchant Group "Healthcare Corp" (merchantGroupId: MG-001)
├── Merchant A (walletMerchantId: M-001) — customer created here
│ └── merchant_customer_metadata: {subscriberId: "ABC123"} → customer_id: C-001
├── Merchant B (walletMerchantId: M-002) — customer requests here
│ └── merchant_customer_metadata: (no entry for this customer)
└── Merchant C (walletMerchantId: M-003)
When a request arrives at Merchant B with metadata: {"subscriberId": "ABC123"}:
- System loads Merchant B → gets
merchantGroupId: MG-001 - Loads all merchants in group: A, B, C
- For each merchant, runs the metadata query against
merchant_customer_metadata - Finds the match under Merchant A → returns
customer_id: C-001
Local Customer Lookup During Payment Method Transfer
Reuse in Wallet Merge Flow
The same getCustomerByMetadata() flow is reused by CustomerQueryHandler.findLocalCustomer(), called during the Local to Enterprise Wallet Merge process:
public Mono<MerchantCustomer> findLocalCustomer(String merchantId, Map<String, String> metadata) {
return getCustomerByMetadata(CustomerQuery.builder()
.walletMerchantId(merchantId)
.metadata(metadata)
.build())
.flatMap(localCustomer -> merchantCustomerRepo
.findByMerchantIdAndCustomerIdAndNotEnterpriseCustomer(merchantId, localCustomer.getId()));
}
This adds an extra filter: the found customer must not already be an enterprise customer (verified via NOT EXISTS against the enterprise_customer table). This ensures only genuinely local customers are identified for migration.
Related Documentation
- Enterprise Settings Logic (EIMP Lookup) — How enterprise settings drive EIMP identity lookups
- Customer Creation — End-to-end customer creation flow
- Wallet Upgrade Logic — Local to enterprise wallet upgrade process
- Local to Enterprise Wallet Merge — Payment method transfer and wallet merge