Merchant Creation Architecture
Developer documentation explaining what happens behind the scenes when a new merchant is created.
Overview
When a merchant is created through the Admin Dashboard, a multi-step process occurs that creates the merchant record, generates a unique ID, and updates routing configuration. This document explains each step in detail.
Architecture Flow
Admin Dashboard Form
↓
Frontend Validation (merchants.js)
↓
POST /api/merchants (Merchant API)
↓
Generate FinMatch ID
↓
Write to merchants.json (GCS)
↓
Write to merchant-router.json (GCS)
↓
Return Success Response
↓
Refresh Dashboard
Step-by-Step Process
1. Frontend Form Submission
Location: admin/js/merchants.js → submitAddMerchant()
Actions:
- Validates required fields (Company Name, Domain)
- Normalizes domain URL (adds
https://if missing) - Prepares merchant and profile objects
- Sends POST request to Merchant API
Code Reference:
// Normalize domain URL
let normalizedDomain = domain;
if (!normalizedDomain.startsWith('http://') && !normalizedDomain.startsWith('https://')) {
normalizedDomain = 'https://' + normalizedDomain;
}
2. API Endpoint Processing
Location: cloud-run/merchant-api/index.js → POST /api/merchants
Actions:
- Validates request body structure
- Validates merchant data using
validateMerchantData() - Generates unique FinMatch ID
- Writes to
merchants.json - Writes to
merchant-router.json - Returns success response
3. FinMatch ID Generation
Location: cloud-run/merchant-api/index.js → generateMerchantId()
Process:
- Retrieves all existing merchant IDs from
merchants.json - Generates random ID in format:
MXXXXXX(new format)- 6-digit number (000001-999999), zero-padded
- Example:
M000101
- Checks for uniqueness (up to 100 attempts)
- Returns unique ID
Code Reference:
function generateMerchantId(existingIds) {
let attempts = 0;
let merchantId;
do {
// Generate MXXXXXX format (6 digits, zero-padded)
const numericPart = Math.floor(Math.random() * 999999) + 1;
merchantId = `M${String(numericPart).padStart(6, '0')}`;
attempts++;
} while (existingIds.includes(merchantId) && attempts < 100);
if (attempts >= 100) {
throw new Error('Unable to generate unique merchant ID after 100 attempts');
}
return merchantId;
}
Result: Unique ID like M000101
Backward Compatibility: Existing merchants with FM-XXXX-XXXX-XXXX format continue to work. See Merchant ID Architecture for details.
4. Writing to merchants.json
Location: cloud-run/merchant-api/index.js → atomicWrite() to merchants.json
Storage: Google Cloud Storage bucket finmatch-shared
Structure:
{
"profiles": {
"FM-0294-8617-5039": {
"finmatchId": "FM-0294-8617-5039",
"merchantName": "Company Name",
"companyName": "Company Name",
"domain": "https://example.com",
"companyNo": "12345678",
"environment": "p",
"status": "pending",
"createdAt": "2025-01-09T12:00:00.000Z",
"lastUpdated": "2025-01-09T12:00:00.000Z"
}
},
"versionStamp": "Updated: 2025-01-09T12:00:00.000Z - Added merchant FM-0294-8617-5039"
}
Fields Created:
finmatchId- The generated FinMatch IDmerchantName- From form fieldcompanyName- Same as merchantNamedomain- Normalized domain URLcompanyNo- From form field (optional)environment- From form field (default: 'p')status- Always set to 'pending' on creationcreatedAt- ISO timestamplastUpdated- ISO timestamp
5. Writing to merchant-router.json
Location: cloud-run/merchant-api/index.js → atomicWrite() to merchant-router.json
Purpose: Used for domain-based routing to determine which merchant and environment to use
Storage: Google Cloud Storage bucket finmatch-shared
Structure:
{
"M000101": {
"environment": "p",
"domain": "https://example.com"
},
"FM-0294-8617-5039": {
"environment": "p",
"domain": "https://example.com"
}
}
Process:
- Derives environment from domain (if possible) or uses form value
- Defaults to 'p' (Production) if not derivable
- Creates entry with merchant ID as key
- Environment is assigned based on:
- Form field value (if provided)
- Domain analysis (if contains "staging", "test", etc.)
- Default to 'p' (Production)
Code Reference:
// Add to merchant-router.json with default environment 'p' or derived
const routerFile = 'merchant-router.json';
const defaultEnv = deriveEnvironment(result.profiles[merchantId].domain) || profile.environment || 'p';
result.profiles[merchantId].environment = defaultEnv; // Set in profile too
await atomicWrite(storage, bucketName, routerFile, (routerData) => {
routerData[merchantId] = {
environment: defaultEnv,
domain: result.profiles[merchantId].domain || ''
};
return routerData;
});
Environment Assignment Priority:
- Form field value (
profile.environment) - Domain derivation (
deriveEnvironment()) - Default to 'p' (Production)
Note: The environment is always assigned and saved to both merchants.json and merchant-router.json to ensure consistency across systems.
What Happens Automatically
CORS Policy Update
Status: ✅ AUTOMATIC (as of latest update)
What: The merchant's domain is automatically added to the CORS whitelist
Process:
- Domain extracted from merchant profile
- Added to
gs://finmatch-shared/cors.json(backup copy) - CORS policy applied to
gs://finmatch-finance-marketing-assetsbucket - Both base domain and www variant added
Manual Override (if needed):
- Edit
gs://finmatch-shared/cors.json - Apply:
gsutil cors set cors.json gs://finmatch-finance-marketing-assets - Verify:
gsutil cors get gs://finmatch-finance-marketing-assets
Reference: See Merchant ID Architecture for technical details
Monitor Service Check
Status: ❌ NOT Automatic on Creation
What: The monitor service does NOT automatically check if the FinMatch snippet is deployed
When It Runs:
- When you load the merchants page (checks all merchants)
- When you manually trigger a deployment status check
- Via API endpoint:
GET /api/merchants/:id/deployment-status
How It Works:
- Merchant API calls Cloud Function:
monitor - Monitor function crawls merchant website
- Checks for FinMatch snippet in
<head>or entire DOM - Detects environment from snippet path (
/t/scripts/,/s/scripts/,/p/scripts/) - Updates
snippetStatusin merchant profile
Code Reference: cloud-run/merchant-api/index.js → GET /api/merchants/:id/deployment-status
Stripe Connection
Status: ❌ NOT Automatic
What: No Stripe customer is automatically linked
How to Link: Use the "Link Customer" button in the merchants table or merchant details page
Data Flow Diagram
┌─────────────────┐
│ Admin Dashboard │
│ (Form Submit) │
└────────┬────────┘
│
│ POST /api/merchants
│ { merchant, profile }
▼
┌─────────────────┐
│ Merchant API │
│ (Cloud Run) │
└────────┬────────┘
│
├─► Generate FM-ID
│
├─► Write to merchants.json (GCS)
│ └─► profiles[FM-ID] = { ... }
│
└─► Write to merchant-router.json (GCS)
└─► [FM-ID] = { environment, domain }
│
│ Response: { success, merchantId, merchant }
▼
┌─────────────────┐
│ Admin Dashboard │
│ (Refresh) │
└─────────────────┘
Atomic Writes
All writes to Google Cloud Storage use atomic write operations with retry logic to prevent conflicts when multiple requests occur simultaneously.
Implementation: cloud-run/merchant-api/lib/jsonStore.js → atomicWrite()
Benefits:
- Prevents data corruption
- Handles concurrent updates
- Retries on conflict errors
Validation Rules
Domain Validation
Location: cloud-run/merchant-api/lib/validation.js
Regex Pattern: /^https?:\/\/[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/
Requirements:
- Must start with
http://orhttps:// - Must contain valid domain name
- Must have valid TLD (at least 2 characters)
Example Valid Domains:
- ✅
https://example.com - ✅
http://test.example.com - ✅
https://subdomain.example.co.uk
Example Invalid Domains:
- ❌
example.com(missing protocol - but frontend normalizes this) - ❌
https://(no domain) - ❌
https://example(no TLD)
Error Handling
ID Generation Failure
If 100 attempts fail to generate a unique ID:
- Error: "Unable to generate unique merchant ID after 100 attempts"
- HTTP Status: 500
- Action: Retry the request
Validation Failure
If domain or company name validation fails:
- Error: Specific validation message
- HTTP Status: 400
- Action: Fix form data and resubmit
Storage Write Failure
If atomic write fails:
- Error: Storage error message
- HTTP Status: 500
- Action: Check GCS permissions, retry request
Related Documentation
- Adding Merchants - User guide for adding merchants
- Editing Merchants - How to update merchant data
- Deployment Status - How snippet monitoring works