ID: account-update | Legacy Source: CACTUPC.cbl + bridge.py:update_account() + Flask GET|POST /account/<id>/update
This guide covers the Account Update module — the most business-rule-dense part of the system. All 11 validation rules currently enforced by the COBOL program CACTUPC must be faithfully re-implemented as explicit C# validators in the .NET Domain layer. No rule may be silently dropped or altered.
| Layer | Target Component | Legacy Equivalent |
|---|---|---|
| API Controller | AccountsController.cs | Flask GET|POST /account/<acct_id>/update |
| Command | UpdateAccountCommand.cs | Form POST body parsed in app.py:account_update() |
| Command Handler | UpdateAccountCommandHandler.cs | bridge.py:update_account() + CACTUPC subprocess |
| Validator | UpdateAccountCommandValidator.cs (FluentValidation) | CACTUPC VALIDATE-INPUT section (exit code 3 = validation failure) |
| Domain Service | AccountDomainService.cs | CACTUPC APPLY-ACCT-UPD section |
| React Page | AccountUpdatePage.tsx | templates/account_update.html |
| React Component | AccountUpdateForm.tsx | Account fields section of account_update.html |
| React Hook | useUpdateAccount.ts (TanStack Query mutation) | Form POST + redirect in Flask |
/api/accounts/{id}
Updates account fields after passing all business-rule validators. Returns 200 OK on success, 400 Bad Request with error codes on validation failure, 422 Unprocessable Entity if no changes were detected.
{
"activeStatus": "Y",
"addressZip": "10001",
"groupId": "GOLD",
"openDate": "2018-03-15",
"expirationDate": "2028-03-15",
"reissueDate": "2023-03-15",
"currentBalance": 1234.56,
"creditLimit": 5000.00,
"cashCreditLimit": 2500.00,
"currentCycleCredit": 0.00,
"currentCycleDebit": 0.00
}
{
"errors": [
{ "code": "CASH_GT_CREDIT", "message": "Cash credit limit cannot exceed the credit limit." },
{ "code": "DATE_ORDER", "message": "Open date must be on or before expiration date." }
]
}
{ "message": "No changes detected — record not updated." }
All rules below are extracted from app/cobol/CACTUPC.cbl VALIDATE-INPUT section. Each rule must have a corresponding unit test in AccountValidatorTests.cs.
| Code | Field(s) | Rule | Error Message (en) |
|---|---|---|---|
| ACCT_ID | accountId | Exactly 11 numeric digits, non-zero | "Account ID must be exactly 11 numeric digits and non-zero." |
| STATUS_YN | activeStatus | Must be exactly "Y" or "N" (case-sensitive) | "Active status must be Y or N." |
| DATE_LEN | openDate, expirationDate, reissueDate | If provided, must be exactly 10 characters | "Date must be in YYYY-MM-DD format (10 characters)." |
| DATE_DASH | openDate, expirationDate, reissueDate | Dashes required at positions 5 and 8 | "Date must use dashes at positions 5 and 8 (YYYY-MM-DD)." |
| DATE_NUM | openDate, expirationDate, reissueDate | Year, month, and day components must be numeric | "Date year, month, and day must all be numeric." |
| DATE_CAL | openDate, expirationDate, reissueDate | Components must form a valid calendar date (e.g., no Feb 30) | "Date components must form a valid calendar date." |
| DATE_ORDER | openDate, expirationDate, reissueDate | openDate ≤ expirationDate; reissueDate between openDate and expirationDate | "Open date must be on or before expiration date; reissue date must fall between them." |
| AMT_CHAR | balance, creditLimit, cashCreditLimit, cycleCredit, cycleDebit | Only digits, one decimal point, optional leading sign | "Amount must be a valid decimal number." |
| CASH_GT_CREDIT | cashCreditLimit, creditLimit | Cash credit limit must not exceed credit limit | "Cash credit limit cannot exceed the credit limit." |
| ZIP_DIG | addressZip | Must be exactly 5 or 9 digits (no letters, no dashes) | "ZIP code must be exactly 5 or 9 digits." |
| GROUP_ALNUM | groupId | Alphanumeric characters and spaces only; max 10 chars | "Group ID must contain only alphanumeric characters and spaces." |
The legacy system (Python _account_form_unchanged()) skips COBOL invocation entirely if submitted data matches the stored record. This must be replicated in the .NET handler:
// UpdateAccountCommandHandler.cs (pseudo-code)
var current = await _repository.GetByIdAsync(command.Id);
if (current.HasNoChangesComparedTo(command))
return Result.NoChanges("No changes detected — record not updated.");
// Only reach here if data actually changed
await _validator.ValidateAndThrowAsync(command);
current.ApplyUpdate(command);
await _repository.SaveAsync();
Monetary field comparison: The legacy bridge rounds both values to 2 decimal places before comparing. The C# implementation must do the same: Math.Round(incoming, 2) == Math.Round(stored, 2).
activeStatus = "Y" and groupId = "PLATINUM", Then PUT /api/accounts/{id} returns 200 OK and the DB is updated.activeStatus = "X", Then the API returns 400 with error code STATUS_YN.creditLimit = 5000 and cashCreditLimit = 6000, Then the API returns 400 with error code CASH_GT_CREDIT.cashCreditLimit = 5000 and creditLimit = 5000 (equal), Then the update succeeds (equal is allowed per COBOL rule).openDate = "2025-01-01" and expirationDate = "2024-12-31", Then the API returns 400 with error code DATE_ORDER.reissueDate = "2029-01-01" and expirationDate = "2028-12-31", Then the API returns 400 with error code DATE_ORDER.PUT /api/accounts/{id} is called, Then the response is 422 with message "No changes detected — record not updated."| Points | Example |
|---|---|
| 1–2 | Improve error message text. Add a "Reset to Current Values" button. |
| 3–5 | Implement a single business rule end-to-end (validator + unit tests + UI error display). |
| 8+ | Full audit trail for account updates: log who changed what, when, and from what value (requires AuditLog table). |
PUT /api/accounts/{id}: <200ms P95 (validation + DB write in a single transaction).