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.jssubmitAddMerchant()

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.jsPOST /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.jsgenerateMerchantId()

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.jsatomicWrite() 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 ID
  • merchantName - From form field
  • companyName - Same as merchantName
  • domain - Normalized domain URL
  • companyNo - From form field (optional)
  • environment - From form field (default: 'p')
  • status - Always set to 'pending' on creation
  • createdAt - ISO timestamp
  • lastUpdated - ISO timestamp

5. Writing to merchant-router.json

Location: cloud-run/merchant-api/index.jsatomicWrite() 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:

  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-shared/cors.json (backup copy)
  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-shared/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: 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:

  1. Merchant API calls Cloud Function: monitor
  2. Monitor function crawls merchant website
  3. Checks for FinMatch snippet in <head> or entire DOM
  4. Detects environment from snippet path (/t/scripts/, /s/scripts/, /p/scripts/)
  5. Updates snippetStatus in merchant profile

Code Reference: cloud-run/merchant-api/index.jsGET /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.jsatomicWrite()

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