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.
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.
| Layer | Target Component | Legacy Equivalent |
|---|---|---|
| API Controller | CustomersController.cs | Customer section of Flask account_update() |
| Query | GetCustomerQuery.cs + GetCustomerQueryHandler.cs | bridge.py:get_customer_for_account() via CBCUS01C |
| Command | UpdateCustomerCommand.cs + UpdateCustomerCommandHandler.cs | bridge.py:update_customer() → CCUSTUPC subprocess |
| Validator | UpdateCustomerCommandValidator.cs (FluentValidation) | CCUSTUPC validation section (CUSVALUS.cpy rules) |
| Data Access | EF Core: Customer table (PostgreSQL) | Sequential read/write of custdata.txt (500-byte records, CVCUS01Y layout) |
| React Component | CustomerUpdateForm.tsx (embedded in AccountUpdatePage) | Customer section of templates/account_update.html |
| React Hook | useCustomer.ts + useUpdateCustomer.ts | Server-side render; form POST |
| Script | Data migration script (reads CVCUS01Y layout) | scripts/fix_custdata_for_ccustupc.py |
/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
}
/api/customers/{id}
Updates customer profile fields. All CCUSTUPC validation rules apply. Returns 422 if no changes detected.
{
"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
}
| Code | Field(s) | Rule | Error Message (en) |
|---|---|---|---|
| CUST_SSN | ssn | Exactly 9 numeric digits | "SSN must be exactly 9 numeric digits." |
| CUST_ZIP | addressZip | Exactly 5 or 9 digits (no letters, no dashes) | "ZIP code must be exactly 5 or 9 digits." |
| CUST_STATE | addressStateCode | Exactly 2 characters; must be a valid US state code (per CUSVALUS.cpy) | "State code must be a valid 2-character US state abbreviation." |
| CUST_PHONE | phoneNumber1, phoneNumber2 | If provided, exactly 10 digits (stored padded to 15 chars in legacy) | "Phone number must be exactly 10 digits." |
| CUST_DOB | dateOfBirth | If 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_FICO | ficoCreditScore | Integer in range 300–850 | "FICO score must be between 300 and 850." |
| PRI_HOLDER | primaryCardHolder | Must be exactly "Y" or "N" | "Primary card holder indicator must be Y or N." |
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:
(212) 555-1234) and strips formatting before submitting.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:
PUT /api/customers/{id} is called, Then the response is 200 OK and the DB is updated.400 with error code CUST_ZIP.400 with error code CUST_PHONE.ficoCreditScore = 750, Then the update succeeds.ficoCreditScore = 200, Then the API returns 400 with error code CUST_FICO and message "FICO score must be between 300 and 850."ficoCreditScore = 851, Then same validation error.123456789, When the record is saved, Then the raw value stored in the customers.ssn column is NOT the plain 9-digit number.GET /api/customers/{id} is called, Then the response field ssnMasked shows only the last 4 digits (e.g., ***-**-6789); a raw SSN field is never included in the response._customer_form_unchanged()).
PUT /api/customers/{id} is called, Then the response is 422 with message "No changes detected — record not updated."CUSVALUS.cpy) so that invalid state entries are rejected.
addressStateCode = "NY", Then the update succeeds.addressStateCode = "ZZ", Then the API returns 400 with error code CUST_STATE.app/cobol/CUSVALUS.cpy (generated by scripts/gen_cusvalus_cpy.py).| Points | Example |
|---|---|
| 1–2 | Change address field max length. Add country code validation. |
| 3–5 | Implement 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. |
PUT /api/customers/{id}: <200ms P95 (validation + encrypted SSN write).Portable.BouncyCastle or .NET native AesGcm) — no homebrew crypto.customers.last_name to support future customer search.