CM07

Offer Structuring & Finalization

Journey: Customer Mobile
Duration: ~1 minute
AI Agent: None (User confirmation)
External APIs: None (Internal only)
1

User Interface Layer

3-step wizard for finalizing loan terms and payment setup

📝 Step 1: Loan Details Confirmation

  • Loan Amount: Disabled input showing £150,000 (from CM06 choice)
  • Loan Term: Disabled input showing "3 years (36 months)"
  • Primary Purpose: Dropdown selector (Expansion, Equipment, Working Capital, Refinance, Other)
  • Specific Use: Text input for detailed description (e.g., "Opening second café in Dalston")
  • Loan Summary Card: Amount, term, APR (4.5%), monthly payment (£4,440) in highlighted panel
Step 1 Data Example
{
  "loan_amount": 150000,
  "term_months": 36,
  "apr": 4.5,
  "monthly_payment": 4440,
  "primary_purpose": "expansion",
  "specific_use": "Opening second café location in Dalston"
}

💳 Step 2: Payment Setup

  • Payment Date: Dropdown (1st of month, 15th of month, Last day of month)
  • Direct Debit Account: Radio buttons - "Use existing" (****1234 - Barclays) or "Use different account"
  • First Payment Date: Auto-calculated display (e.g., "1 December 2025") based on selected payment date
  • New Account Entry: Conditional form fields if "Use different account" selected (sort code, account number, account holder)
💡 First Payment Calculation

Logic: If today is 23 Nov and customer selects "1st of month", first payment = 1 Dec 2025

Grace Period: Minimum 7 days between loan finalization and first payment

Example: Finalized on 25 Nov → First payment can't be 1 Dec (only 6 days), so pushed to 1 Jan 2026

✅ Step 3: Final Review

  • Complete Application Summary: All details in one consolidated card
  • Key Details Displayed: Loan amount, term, purpose, monthly payment, payment date, first payment date
  • Special Notes: Optional text input for any additional information
  • Submit Button: Large, prominent "Submit Application" CTA
  • Save & Continue Later: Secondary button to pause and resume

🔄 Progress Indicator

  • 3-Step Visual: Numbered circles showing Details → Payment → Review
  • Active Step: Highlighted in blue, current step number displayed in header
  • Completed Steps: Show checkmark icon, gray background
  • Navigation: Back button (hidden on step 1), Continue/Submit button

🎨 UI Feedback & Validation

  • Required Field Indicators: Red asterisk (*) next to required fields
  • Real-time Validation: Highlight missing required fields on Continue click
  • Help Text: Light gray subtext under fields (e.g., "Approved up to £250,000")
  • Disabled Inputs: Grayed out for pre-selected values from CM06
  • Summary Card Highlighting: Gradient background for key financial information
2

API / Backend-for-Frontend Layer

Single finalization endpoint to lock in loan offer

🔒 POST /api/v1/applications/{id}/finalize-offer

  • Purpose: Lock in customer's final loan structure and payment preferences
  • When Called: When customer clicks "Submit Application" on Step 3
  • Authentication: JWT Bearer token from login session
  • Rate Limit: 10 requests/minute per user (prevents accidental duplicates)
  • Response Time: <500ms (database update + status change)
Request Example
POST /api/v1/applications/APP-2025-001234/finalize-offer
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json

{
  "loan_details": {
    "amount": 150000,
    "term_months": 36,
    "monthly_payment": 4440.12,
    "apr": 4.5,
    "primary_purpose": "expansion",
    "specific_use": "Opening second café location in Dalston"
  },
  "payment_setup": {
    "payment_day": 1, // 1st of month
    "account_type": "existing",
    "account_id": "ACC-1234",
    "first_payment_date": "2025-12-01"
  },
  "special_notes": "",
  "finalized_at": "2025-11-23T14:45:00Z"
}
Response Example (200 OK)
{
  "request_id": "req_cm07_20251123_144500",
  "status": "success",
  "application_id": "APP-2025-001234",
  "offer_status": "finalized",
  "offer_reference": "OFFER-2025-001234",
  "next_step": {
    "screen": "CM08",
    "action": "document_upload",
    "message": "Please upload supporting documents"
  },
  "timeline": {
    "offer_finalized": "2025-11-23T14:45:00Z",
    "first_payment": "2025-12-01",
    "estimated_disbursement": "2025-11-27" // After docs + signature
  },
  "processing_time_ms": 324
}

💾 POST /api/v1/applications/{id}/save-draft

  • Purpose: Save partial progress without finalizing
  • Trigger: "Save & continue later" button on any step
  • Behavior: Saves current step data, allows resume from same step later
Draft Save Request
POST /api/v1/applications/APP-2025-001234/save-draft

{
  "current_step": 2,
  "draft_data": {
    "loan_details": { /* from step 1 */ },
    "payment_setup": { /* partial from step 2 */ }
  },
  "saved_at": "2025-11-23T14:40:00Z"
}

📋 GET /api/v1/applications/{id}/draft

  • Purpose: Retrieve saved draft to resume where customer left off
  • When Called: On page load if application status = "draft"
  • Response: Returns current step number + all saved form data

🔐 Security & Validation

  • Idempotency: offer_reference prevents duplicate submissions (return same response if called twice)
  • Data Validation: Server-side validation of all fields (amount matches CM06, payment date valid, etc.)
  • Rate Freezing: APR locked in for 7 days from CM05 credit assessment
  • Expiry Check: Reject if >7 days since CM05, force re-run of credit check
⚠️ Offer Expiry: If APR expired (>7 days since CM05), return 409 Conflict with message: "Interest rate expired. Please return to eligibility check." Redirect customer to CM05.
3

Business Logic Layer

Validation rules and first payment calculation

✅ Validation Rules Engine

  • Amount Consistency: Loan amount must match value from CM06 (prevent tampering)
  • Term Consistency: Term must match CM06 selection (36 months in Olivia's case)
  • APR Validity: Rate must not be expired (within 7 days of CM05 assessment)
  • Purpose Required: Primary purpose field cannot be empty
  • Account Required: Must select an account for direct debit setup
  • First Payment Valid: Must be at least 7 days after finalization
Validation Logic
function validateFinalization(application, request) {
  // Check amount matches CM06
  if (request.loan_details.amount !== application.chosen_amount) {
    throw new ValidationError("Amount mismatch");
  }
  
  // Check APR not expired
  const daysSinceAssessment = daysBetween(
    application.credit_assessed_at, 
    new Date()
  );
  if (daysSinceAssessment > 7) {
    throw new RateExpiredError("Rate expired, re-run CM05");
  }
  
  // Check first payment date valid
  const daysUntilFirstPayment = daysBetween(
    new Date(), 
    request.payment_setup.first_payment_date
  );
  if (daysUntilFirstPayment < 7) {
    throw new ValidationError("First payment too soon");
  }
  
  return true;
}

📅 First Payment Date Calculation

Automatically calculate first payment date based on customer's selected payment day and 7-day grace period:

First Payment Logic
function calculateFirstPaymentDate(selectedDay, finalizationDate) {
  // Start with next occurrence of selected day
  let firstPayment = nextOccurrenceOf(selectedDay, finalizationDate);
  
  // Ensure 7-day minimum gap
  const minDate = addDays(finalizationDate, 7);
  if (firstPayment < minDate) {
    // Push to next month if too soon
    firstPayment = nextOccurrenceOf(selectedDay, minDate);
  }
  
  return firstPayment;
}

// Example:
// Finalized: 23 Nov 2025, Selected Day: 1st
// Next 1st: 1 Dec 2025 (8 days away) ✓ Valid
// Result: 2025-12-01

// Counter-example:
// Finalized: 25 Nov 2025, Selected Day: 1st
// Next 1st: 1 Dec 2025 (6 days away) ✗ Too soon
// Result: Push to 1 Jan 2026 (37 days away)

🔄 Status Transition Logic

  • Before Finalization: application.status = "scenario_selected" (from CM06)
  • After Finalization: application.status = "offer_finalized"
  • Next Status: "documents_pending" (after moving to CM08)
  • Workflow Guard: Cannot finalize unless status = "scenario_selected"
Status Transition
async function finalizeOffer(applicationId, offerData) {
  // Check current status
  const app = await Application.findById(applicationId);
  if (app.status !== "scenario_selected") {
    throw new WorkflowError("Invalid state for finalization");
  }
  
  // Validate and finalize
  validateFinalization(app, offerData);
  
  // Update application
  app.status = "offer_finalized";
  app.offer_reference = generateOfferReference();
  app.offer_finalized_at = new Date();
  app.primary_purpose = offerData.loan_details.primary_purpose;
  app.specific_use = offerData.loan_details.specific_use;
  app.payment_day = offerData.payment_setup.payment_day;
  app.first_payment_date = offerData.payment_setup.first_payment_date;
  
  await app.save();
  return app;
}

📋 Offer Reference Generation

Generate unique offer reference for tracking and audit:

Reference Format
function generateOfferReference() {
  const year = new Date().getFullYear();
  const sequentialId = getNextSequenceNumber();
  
  return `OFFER-${year}-${sequentialId.toString().padStart(6, '0')}`;
}

// Example: OFFER-2025-001234

🎯 Business Rules Summary

  • No Changes Allowed: Amount and term are locked from CM06, cannot be modified
  • Purpose Required: Must select primary purpose (regulatory requirement)
  • Payment Setup Required: Must configure direct debit before finalizing
  • 7-Day APR Validity: Offer expires if not finalized within 7 days of CM05
  • 7-Day Grace Period: First payment must be at least 7 days after finalization
4

Integration & Middleware Layer

API Gateway, caching, and notification triggers

⚡ API Gateway Routing

  • Finalize Endpoint: POST /applications/* → Application Service
  • Draft Save: POST /applications/*/save-draft → Application Service
  • Draft Retrieve: GET /applications/*/draft → Application Service
  • Rate Limiting: 10 requests/minute per user (prevent duplicate submissions)
  • Timeout: 10-second timeout for finalization (database heavy)

💾 Caching Strategy

  • Draft Data: Cache in Redis with 24-hour TTL for quick resume
  • Offer Validation: Cache validation results for 5 minutes to prevent duplicate checks
  • Account Details: Cache connected account info from CM03 for 1 hour
  • Cache Invalidation: Clear all caches when offer finalized

📨 Notification Triggers

  • Customer Email: Send "Offer Finalized" email with next steps (CM08 document upload)
  • RM Notification: Alert relationship manager that application moved to "Offer Finalized" stage
  • SMS Confirmation: Text customer with offer reference number and next step
  • Webhook: Trigger external CRM webhook for pipeline tracking
Notification Payload
{
  "event_type": "offer_finalized",
  "application_id": "APP-2025-001234",
  "offer_reference": "OFFER-2025-001234",
  "customer": {
    "email": "olivia@smithscafe.com",
    "phone": "+44 7700 900123"
  },
  "loan_details": {
    "amount": 150000,
    "term": 36,
    "monthly_payment": 4440
  },
  "next_action": "Upload supporting documents",
  "timestamp": "2025-11-23T14:45:00Z"
}

🔄 Event Streaming

  • Event Bus: Publish "OfferFinalized" event to Kafka/RabbitMQ
  • Subscribers: Email service, SMS service, CRM webhook, analytics pipeline
  • Async Processing: All notifications sent asynchronously (don't block API response)
  • Retry Logic: Failed notifications queued for retry (max 3 attempts)

📊 Analytics Tracking

  • Conversion Tracking: Track CM06 → CM07 completion rate (target: >95%)
  • Time on Screen: Measure average time to finalize (target: 1-2 minutes)
  • Drop-off Analysis: Identify which step has highest abandonment
  • Payment Day Preferences: Track most popular payment dates (1st, 15th, last)
Analytics Event
analytics.track('Offer Finalized', {
  application_id: 'APP-2025-001234',
  time_on_screen: 87, // seconds
  steps_completed: 3,
  payment_day_selected: 1,
  used_existing_account: true,
  purpose: 'expansion'
});

🔐 Security Measures

  • CSRF Protection: Validate CSRF token on all POST requests
  • Idempotency Key: Use offer_reference to prevent duplicate processing
  • Input Sanitization: Sanitize "specific_use" and "special_notes" text inputs
  • SQL Injection Prevention: Use parameterized queries for all database operations
5

External Systems Integration

No external dependencies - fully internal process

✅ No External Systems Required

This screen operates entirely with internal data and processes. All necessary information is already captured from previous screens:
  • Loan amount and term from CM06 scenario explorer
  • APR and credit decision from CM05 assessment
  • Connected bank account from CM03 (for direct debit)
  • Customer identity from CM04 KYC

🔗 Data Dependencies (From Previous Screens)

  • CM06 (Scenario Explorer):
    → Chosen loan amount: £150,000
    → Chosen term: 36 months
    → Calculated monthly payment: £4,440
    → Total interest: £9,840
  • CM05 (Credit Assessment):
    → APR: 4.5%
    → Credit decision timestamp (for expiry check)
    → Approval status: Approved
  • CM03 (Consent & Connection):
    → Connected bank accounts for direct debit
    → Account details: ****1234 - Barclays Business
  • CM01 (Intent Capture):
    → Original intent: "£150K to expand my café"
    → Customer's stated purpose (pre-populated)

💡 Why No External APIs?

  • Confirmation Only: This is a finalization step, no new data needed from external sources
  • Speed: No API calls = instant response, better UX
  • Cost: No external API fees
  • Reliability: No external dependencies = no failure points
  • Data Already Captured: All necessary information collected in previous 6 screens

🔮 Potential Future Integrations

  • Direct Debit Mandate API: GoCardless or Bacs for setting up direct debit automatically
  • Insurance Integration: Optional loan protection insurance quotes during finalization
  • Core Banking System: Pre-create loan account in core banking (currently done after CM10)
  • CRM Sync: Real-time sync to Salesforce/HubSpot for RM visibility
💡 Design Decision: When to Create Loan Account?

We intentionally delay loan account creation until after CM10 (final confirmation) because:

  • Customer might still abandon after CM07 (docs, signature pending)
  • Reduces orphaned accounts in core banking system
  • Allows for cleaner rollback if issues arise
6

Data Persistence Layer

Database updates and audit logging

📝 Update applications Table

Update main applications table with finalized offer details:

SQL: Finalize Offer
UPDATE applications
SET
    current_status = 'offer_finalized',
    offer_reference = 'OFFER-2025-001234',
    offer_finalized_at = CURRENT_TIMESTAMP,
    primary_purpose = 'expansion',
    specific_use = 'Opening second café location in Dalston',
    payment_day = 1,
    first_payment_date = '2025-12-01',
    direct_debit_account_id = 'ACC-1234',
    special_notes = '',
    updated_at = CURRENT_TIMESTAMP
WHERE id = 'APP-2025-001234'
  AND current_status = 'scenario_selected'; -- Workflow guard

📋 Audit Log (Elasticsearch)

Log offer finalization event for compliance:

JSON: Offer Finalization Event
{
  "event_type": "offer_finalized",
  "application_id": "APP-2025-001234",
  "offer_reference": "OFFER-2025-001234",
  "customer_id": "CUST-001234",
  "timestamp": "2025-11-23T14:45:00Z",
  "actor": {
    "type": "customer",
    "id": "CUST-001234",
    "name": "Olivia Thompson"
  },
  "offer_details": {
    "amount": 150000,
    "term_months": 36,
    "apr": 4.5,
    "monthly_payment": 4440.12,
    "primary_purpose": "expansion",
    "specific_use": "Opening second café location in Dalston"
  },
  "payment_setup": {
    "payment_day": 1,
    "first_payment_date": "2025-12-01",
    "account_id": "ACC-1234"
  },
  "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0)",
  "ip_address": "86.134.x.x"
}

💾 Draft Storage (Optional)

If customer clicks "Save & continue later", store draft in separate table:

Column Type Description Example
id UUID Primary key 7a3e8900-e29b...
application_id VARCHAR(50) Foreign key APP-2025-001234
current_step INTEGER Step number (1-3) 2
draft_data JSONB Partial form data {"loan_details": {...}}
saved_at TIMESTAMP When draft saved 2025-11-23 14:40:00
expires_at TIMESTAMP Draft expiry (24hr) 2025-11-24 14:40:00

📊 Data Retention Policy

  • Finalized Offers: Retained permanently in applications table (contractual record)
  • Draft Data: Purged after 24 hours if not finalized
  • Audit Logs: Retained for 7 years in Elasticsearch (regulatory compliance)
  • Notification Logs: Retained for 90 days (troubleshooting)

🔍 Query: Check Offer Validity

SQL: Verify Offer Not Expired
SELECT
    id,
    offer_reference,
    credit_assessed_at,
    EXTRACT(DAY FROM