👤 Module: Customer Management

ID: customer-management  |  Legacy Source: CCUSTUPC.cbl + CBCUS01C.cbl + bridge.py:update_customer()

This guide covers the Customer Management module — view and update of customer profiles (name, address, contact, financial identifiers). In the legacy system, customer update was invoked from the Account Update screen (POST to Flask, which called CCUSTUPC if customer data changed). In the new system, customer updates are a dedicated REST endpoint, but can still be triggered from the Account Update page.

🔐 Security Notice — SSN Handling
The legacy system stored CUST-SSN as a plain 9-digit integer in custdata.txt. In the new system, SSN must be encrypted at rest using field-level encryption before storage in PostgreSQL. The API must never return a full SSN; it should be masked in responses (e.g., ***-**-4321). Implement this before the data migration sprint.

🏗️ Technical Foundation

LayerTarget ComponentLegacy Equivalent
API ControllerCustomersController.csCustomer section of Flask account_update()
QueryGetCustomerQuery.cs + GetCustomerQueryHandler.csbridge.py:get_customer_for_account() via CBCUS01C
CommandUpdateCustomerCommand.cs + UpdateCustomerCommandHandler.csbridge.py:update_customer() → CCUSTUPC subprocess
ValidatorUpdateCustomerCommandValidator.cs (FluentValidation)CCUSTUPC validation section (CUSVALUS.cpy rules)
Data AccessEF Core: Customer table (PostgreSQL)Sequential read/write of custdata.txt (500-byte records, CVCUS01Y layout)
React ComponentCustomerUpdateForm.tsx (embedded in AccountUpdatePage)Customer section of templates/account_update.html
React HookuseCustomer.ts + useUpdateCustomer.tsServer-side render; form POST
ScriptData migration script (reads CVCUS01Y layout)scripts/fix_custdata_for_ccustupc.py

🌐 Public APIs

GET /api/customers/{id}

Returns the customer profile. SSN is masked in the response.

{
  "id": 111111111,
  "firstName": "John",
  "middleName": "A",
  "lastName": "Doe",
  "addressLine1": "123 Main St",
  "addressLine2": "",
  "addressLine3": "New York",
  "addressStateCode": "NY",
  "addressCountryCode": "USA",
  "addressZip": "10001",
  "phoneNumber1": "2125551234",
  "phoneNumber2": "",
  "ssnMasked": "***-**-1234",
  "govtIssuedId": "DL-NY-12345",
  "dateOfBirth": "1985-06-15",
  "eftAccountId": "EFT12345",
  "primaryCardHolder": "Y",
  "ficoCreditScore": 720
}
PUT /api/customers/{id}

Updates customer profile fields. All CCUSTUPC validation rules apply. Returns 422 if no changes detected.

Request Body

{
  "firstName": "John",
  "middleName": "A",
  "lastName": "Doe",
  "addressLine1": "456 Park Ave",
  "addressLine2": "",
  "addressLine3": "New York",
  "addressStateCode": "NY",
  "addressCountryCode": "USA",
  "addressZip": "10022",
  "phoneNumber1": "2125559876",
  "phoneNumber2": "",
  "ssn": "123456789",
  "govtIssuedId": "DL-NY-12345",
  "dateOfBirth": "1985-06-15",
  "eftAccountId": "EFT12345",
  "primaryCardHolder": "Y",
  "ficoCreditScore": 720
}

📋 Business Rules (Extracted from CCUSTUPC.cbl + CUSVALUS.cpy)

CodeField(s)RuleError Message (en)
CUST_SSNssnExactly 9 numeric digits"SSN must be exactly 9 numeric digits."
CUST_ZIPaddressZipExactly 5 or 9 digits (no letters, no dashes)"ZIP code must be exactly 5 or 9 digits."
CUST_STATEaddressStateCodeExactly 2 characters; must be a valid US state code (per CUSVALUS.cpy)"State code must be a valid 2-character US state abbreviation."
CUST_PHONEphoneNumber1, phoneNumber2If provided, exactly 10 digits (stored padded to 15 chars in legacy)"Phone number must be exactly 10 digits."
CUST_DOBdateOfBirthIf provided, YYYY-MM-DD format and a valid calendar date; must be in the past"Date of birth must be a valid past date in YYYY-MM-DD format."
CUST_FICOficoCreditScoreInteger in range 300–850"FICO score must be between 300 and 850."
PRI_HOLDERprimaryCardHolderMust be exactly "Y" or "N""Primary card holder indicator must be Y or N."

📐 Phone Number Format Note

The legacy system stores phone numbers as 15-character padded strings (matching COBOL CUST-PHONE-NUM-1 PIC X(15)). The Python bridge splits the form's 3-part input (area code + 3 + 4 digits) and pads to 15. In the new system:

📐 SSN Split-Field Legacy Pattern

The legacy HTML form split SSN into 3 fields: ACTSSN1 (3 digits), ACTSSN2 (2 digits), ACTSSN3 (4 digits). The Python bridge reassembled them via _caup_ssn_from_form(). In the new system:

🎯 User Story Development Patterns

US-CM-01 · Update Customer Contact Information (3 pts)
As a bank clerk, I want to update a customer's address, phone numbers, and ZIP code so that contact information stays current.
US-CM-02 · Update Customer FICO Score (2 pts)
As a bank clerk, I want to update a customer's FICO credit score (range 300–850) so that the credit profile reflects the latest bureau data.
US-CM-03 · SSN Must Be Stored Encrypted (5 pts)
As a security officer, I want the customer SSN to be encrypted at rest in the database so that a database breach does not expose full SSNs.
US-CM-04 · Customer Update No-Change Guard (2 pts)
As the system, I want to skip a customer update if the submitted data matches the stored record so that unnecessary DB writes are avoided (mirrors legacy _customer_form_unchanged()).
US-CM-05 · Validate US State Code (2 pts)
As the system, I want to validate that the state code is a recognized US state abbreviation (per the US Postal Service list in CUSVALUS.cpy) so that invalid state entries are rejected.

📊 Story Complexity Guidelines

PointsExample
1–2Change address field max length. Add country code validation.
3–5Implement SSN encryption + masking end-to-end. Add customer search by name.
8+Customer audit trail (every field change logged with who/when/from-to). Customer deduplication detection.

⚡ Performance Requirements