Skip to main content

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:

  1. Retrieves all existing merchant IDs from merchants.json
  2. Generates random ID in format: MXXXXXX (new format)
    • 6-digit number (000001-999999), zero-padded
    • Example: M000101
  3. Checks for uniqueness (up to 100 attempts)
  4. 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: gs://finmatch-secrets/merchants.json (MERCHANTS_JSON_BUCKET; merchant-api is the only writer). Do not fetch gs://finmatch-shared/merchants.json.

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",
"billing": { "status": "inactive" },
"finmatchApproval": { "status": "none" },
"merchantStatus": "suspended",
"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 ID
  • merchantName - From form field
  • companyName - Same as merchantName
  • domain - Normalized domain URL
  • companyNo - From form field (optional)
  • environment - From form field (default: 'p')
  • billing / finmatchApproval / lenderApprovals / liveToggle / termination — seeded by initMerchantStatusFields
  • merchantStatus — computed (suspended when billing is inactive; not a stored status: 'pending' field)
  • createdAt - ISO timestamp
  • lastUpdated - 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: gs://finmatch-secrets/merchant-router.json (MERCHANT_ROUTER_BUCKET)

Structure:

{
"M000101": {
"environment": "p",
"domain": "https://example.com"
},
"FM-0294-8617-5039": {
"environment": "p",
"domain": "https://example.com"
}
}

Process:

  1. Derives environment from domain (if possible) or uses form value
  2. Defaults to 'p' (Production) if not derivable
  3. Creates entry with merchant ID as key
  4. 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:

  1. Form field value (profile.environment)
  2. Domain derivation (deriveEnvironment())
  3. 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:

  1. Domain extracted from merchant profile
  2. Added to gs://finmatch-secrets/cors.json (CORS_JSON_BUCKET)
  3. CORS policy applied to gs://finmatch-finance-marketing-assets bucket
  4. Both base domain and www variant added

Manual Override (if needed):

  1. Edit gs://finmatch-secrets/cors.json
  2. Apply: gsutil cors set cors.json gs://finmatch-finance-marketing-assets
  3. 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: Creating a merchant does not crawl the domain for the SDK.

When snippet status updates:

  • Shared daily snapshot: GET /api/snippets/snapshots/today (create via POST /api/snippets/snapshots if missing)
  • On-demand Check snippet on the merchant page (POST /api/snippets/snapshots/merchant/:id)

Loading the merchants table does not crawl every merchant. See Snippet status and Snippet snapshot.

Code Reference: cloud-run/merchant-api/index.js → /api/snippets/snapshots/*

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:// or https://
  • 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