ACAS Modernization — Executive Summary
ACAS (Applewood Computers Accounting System) is a long-lived COBOL accounting platform — 1.36 million lines across 449 source files, 36 business functions, 56 aggregate roots, 308 enforced business rules. Its Payment Processing subsystem (~30 files, ~14,500 lines) handles supplier disbursement (cash posting, cheque writing, BACS issuance, remittance) and is the first subsystem chosen for modernization. Today it runs on GnuCOBOL with dual ISAM/MySQL storage, a fixed 80×24 terminal UI, COMP-3 packed decimals, and a 4-character password field — a stack that limits operator productivity, blocks integration with modern banking APIs, and depends on retired runtimes. Three independent agent runs converged unanimously on Payment Processing as the right pilot (composite scores +2.60 / +2.80 / +2.85).
This report recommends a strangler-fig modernization using the SAROC coexistence pattern (Snapshot and Replay Ordered Cutover) to a Python / FastAPI / Pydantic v2 / SQLAlchemy 2.0 stack on Docker Compose locally and AWS ECS Fargate in the cloud, with PostgreSQL 16, RabbitMQ for the legacy bridge, structlog for JSON logging, and OpenTelemetry for tracing and metrics. The subsystem decomposes into 3 microservices — payment-api (sync HTTP), payment-batch (async/queue-driven, hosts the SAROC consumer and writes the inline GL journal), and payment-reporting (read-replica + S3 PDF) — selected by a 3-perspective consensus (DDD / Technical / Business) with the Technical proposal winning at 7.80 / 10. SAROC is wired end-to-end (implementationStatus: full), with a working Docker bridge already running under strangler-bridge/. On the front end, the fixed 80×24 terminal screens give way to a modernized single-page application that presents the entire supplier-disbursement journey as one continuous flow — no green-screen page-cycling between menus — served by the same FastAPI contract the services expose.
Of 10 Payment Processing behavioral rules extracted at Concho confidence 0.60–0.88 (average 0.77), 9 transfer verbatim; only BR-PAY-008 (99-item batch soft cap) requires documented mitigation. BR-PAY-005 and BR-PAY-010 are delivery refactors — the per-payment behavior is identical, only the materialization changes (one file → cheque PDF + BACS event for 005; print stream → PDF + S3 for 010). Platform-affinity analysis identified 12 consensus implementation constraints, 100% hard-coded at Concho’s catalog level (197 total system-wide), and the target stack retires or mitigates all 12 — including the 80×24 terminal, the COMP-3 packed decimal format, the 4-character credential length cap, and the YYYYMMDD packed-date format. Verification ran clean: 12 of 12 sampled claims verified against Concho cycle 8 (hallucination rate 0%), 0 Mermaid syntax issues, 0 cross-section inconsistencies (final iteration after 6 targeted fixes). Report generation took approximately 90 minutes of orchestration including the 3-perspective consensus runs and one validator-driven remediation cycle.
What Happens Next — Automated Code Generation
This report is not the end of the process — it is the input to a companion agentic code-generation workflow that turns the architecture, schema decisions, business-rule specifications, and UI patterns documented here into runnable artifacts on Docker Compose / AWS ECS Fargate. That workflow runs a BDD/TDD iterate-till-green loop with an adversarial validator at every step, so a human never reviews a broken artifact — it arrives already verified rather than waiting to be polished. The specific handoff artifacts, and how to iterate on them, are detailed in the Stakeholder Reading Guide (Section 1.4).
Section Takeaways
| § | Section | Key Takeaway |
|---|---|---|
| 4 | Target Architecture | 3-service Python / FastAPI fleet on AWS ECS Fargate behind an Application Load Balancer; PostgreSQL 16 on RDS; RabbitMQ for the SAROC bridge; ADOT sidecar exporting OpenTelemetry traces to X-Ray and metrics to CloudWatch; structlog JSON logs; Secrets Manager and SSM Parameter Store for config; inline GL posting in the same DB transaction as the appropriation. |
| 5 | Platform Affinity Analysis | 3-run consensus classified 12 platform constraints across 4 categories (Capacity, Processing Model, UI, Data Type); 100% hard-coded rate (197 of 197 constraints system-wide); brittleness 0.49. 10 of 12 retired outright by the target; 2 hybrids preserve the business invariant while eliminating the COBOL artifact. |
| 6 | Technical Debt Analysis | For ACAS the source is COBOL on GnuCOBOL with no external dependency manifest, so this section is a deliberate stub — the legacy stack is itself the debt and is wholesale replaced by the target stack covered in Section 4. The full Tech Debt catalog (library EOL, CVE exposure, transitive-dep graph) re-engages on modernizations whose source has a real package manifest (npm, Maven, NuGet, pip, Gemfile, …). |
| 7 | UI/UX Transformation | Six legacy 80×24 terminal screens (pl900 menu, pl080 entry, pl090 proof, pl100 cash post, pl940 cheque run, pl960 remittance) transformed into modern React + FastAPI components; a seventh Payment Cycle Workbench consolidates the entire payment lifecycle into one SPA page — impossible on the legacy terminal. |
| 8 | Code Translation | COBOL → Python (FastAPI + Pydantic v2 + SQLAlchemy 2.0): WORKING-STORAGE becomes Pydantic models with validators; ACCEPT/DISPLAY loops become React forms backed by REST endpoints; ISAM READ NEXT loops become SQL queries with composite-key indexes; CALL dispatch becomes ALB path routing; COMP-3 packed decimals become NUMERIC(p,s) with Decimal arithmetic. |
| 9 | Business Rules Analysis | 10 behavioral rules (validation, calculation, state transition, workflow) with formal Given-When-Then specs, every rule grounded to a Concho entity at file:line precision and a confidence score (0.60–0.88, average 0.77). 9 transfer verbatim; only BR-PAY-008 requires behavioral mitigation (99-item cap relaxed across SAROC). BR-PAY-005 and BR-PAY-010 are delivery refactors (single file path → two pipelines / PDF+S3) — per-payment behavior identical. The unverified “9-invoice allocation limit” myth was explicitly excluded. |
| 10 | Data Mapping Strategy | 7 target tables mapped from the COBOL copybooks (fdpay.cob, fdoi4.cob, plfdoi5.cob, fdbatch.cob) to PostgreSQL 16: payment, open_item, batch_control, gl_posting, payment_audit plus reporting sidecars. Composite key oi-b-nos * 1000 + oi-b-item preserved; the COBOL OCCURS arrays (9-invoice remittance, 99-item batch) become child tables with no row ceiling. |
| 11 | Modernization Strategy | SAROC pattern (Snapshot and Replay Ordered Cutover) — file-watcher CDC against pay.dat / openitm5.dat / purchled.dat → translator → RabbitMQ topic exchange (sage-domain-event-v1) → payment-batch consumer with natural-key idempotency → PostgreSQL. Implementation status: full. Live PoC at strangler-bridge/. No request-path strangler-fig router required because the legacy is a green-screen COBOL terminal — SAROC observes the data path instead. |
| 12 | How Concho Helped This Modernization Planning | Concho Context Graph against the full 1.36M-line codebase at Concho cycle 8 provided every fact in this report. Three-way comparison against LLM-sampling and manual archaeology in 6 dimensions; the “9-invoice allocation limit” myth caught and corrected before it propagated; the SAROC bridge seam discovered via Concho’s architectural commentary on the FA-RDBMS-Flat-Statuses dual-storage flag. |
1. Introduction
1.1 Purpose and Scope
This document presents a comprehensive modernization analysis for the Payment Processing subsystem within the Applewood Computers Accounting System (ACAS), examining the transformation path from a legacy GnuCOBOL-on-Linux architecture with dual ISAM/MySQL data storage and a terminal-based UI to a containerized Python/FastAPI deployment running on Docker Compose for development and AWS ECS Fargate in production. The subsystem analyzed — Payment Processing — was selected by the customer, ensuring alignment with actual business modernization priorities rather than a generic recommendation.
The analysis addresses a critical business challenge facing organizations with decades-old COBOL systems: how to modernize proven accounting logic while eliminating platform-imposed constraints that hinder productivity. ACAS represents a 48+-year-old open-source accounting platform serving small-to-medium businesses with a terminal-based user interface (80×24 character screens) and a dual-mode ISAM-file and MySQL/MariaDB persistence layer. The Payment Processing subsystem handles cash posting, payment data entry, payment amendment, and payment proof processing — critical financial operations requiring behavioral fidelity during modernization while enabling modern user experience improvements and cloud-native scalability.
1.2 What Concho Provided
This report leverages two distinct Concho capabilities:
1. Pre-Built Legacy System Analysis: Before this modernization report was generated, Concho performed deep analysis of the entire ACAS codebase — 1.36 million total lines across the repository, with 381,073 lines classified into architectural layers — discovering 36 business functions, 6 behavioral rules specific to Payment Processing (within a broader catalog of 308 rules enforced across the system), architectural layers, data dependencies, and platform constraints. This foundational analysis (conducted once for the entire codebase) provides the raw intelligence that makes rapid, accurate modernization planning possible.
2. Concho Modernization Report Workflow: This document was generated in approximately 90 minutes using Concho's modernization workflow, which leverages the pre-built analysis to produce customer-specific modernization plans. The workflow combines Concho's pre-computed insights (business rules, constraints, dependencies, aggregate roots, integration boundaries) with AI-generated architecture designs, code translation examples, and deployment artifacts tailored to Docker Compose and AWS ECS Fargate patterns. This two-stage approach — comprehensive upfront analysis followed by rapid report generation — explains why Concho can produce in hours what traditionally requires weeks of manual effort.
This report is the human-readable view of a machine-consumable artifact set. The agent/skill combination that produces every section of this report also produces a companion handoff artifact for that section — schema decisions, behavioral-rule catalogs in Given/When/Then form, UI storyboard JSON, target-pattern resolutions, coexistence-bridge declarations — that feeds directly into a companion agentic code-generation workflow. That workflow consumes these handoffs to emit runnable artifacts (services, entity models, UI components, database schemas, Dockerfiles, IaC templates, CI/CD pipelines, Playwright behavior tests) and iterates each one to green using BDD/TDD before any human reviews it. The report you are reading is both the stakeholder-review artifact and the executable spec for the downstream workflow.
The pre-built analysis achieved 100% code coverage (vs. 20-30% sampling in manual reviews), extracted 308 behavioral rules with source code traceability (for example, PaymentBatchControlLimits — the 99-items-per-batch ceiling — is grounded at purchase/pl080.cbl:733), and discovered platform constraints like the 80×24 terminal-dimension requirement and 40-line-item purchase-invoice ceiling that modernization can eliminate. The modernization workflow then used this intelligence to design a 3-microservice AWS ECS Fargate architecture, generate PostgreSQL schemas, create Python (FastAPI) code translations with behavioral rule cross-references, and document UI transformations from 80×24 terminal screens to responsive web interfaces. All analysis outputs include confidence scores and evidence classification, enabling architects to understand the certainty behind each finding.
This Is a Modernization Stakeholder Review
This document is the output of an automated multi-agent workflow that completed in approximately 90 minutes — analysis, planning, validation, and report generation, all powered by Concho MCP queries against the Context Graph. It is structured as a stakeholder review — the way a senior architect would present a modernization plan to an organization for feedback and alignment. Executives see the business case and modernization scope. Architects see the target design, platform affinity analysis, and service decomposition rationale. Developers see the code translations, data migration schemas, UI transformations, and business rule specifications. Infrastructure teams see the deployment automation and containerization strategy.
The stakeholder review is designed for human-in-the-loop iteration — read it, challenge it, request course corrections, and iterate. It becomes the artifact that drives organizational alignment and execution. What would traditionally take months of manual analysis and weeks of document preparation is delivered in hours, with 100% codebase coverage and full source traceability behind every claim.
1.3 Report Organization
This report is organized to serve both strategic decision-makers and implementation teams. Each numbered section produces a structured handoff that feeds the companion code-generation workflow described above:
- Section 1 — Introduction: Purpose, scope, and the two-stage Concho capability (this section).
- Section 2 — Modernization Scope: Subsystem selection rationale and the explicit candidate set considered.
- Section 3 — Legacy System Analysis: Baseline architecture, technology stack, business functions, and constraints across the ACAS codebase.
- Section 4 — Target Architecture: Docker Compose / AWS ECS Fargate service topology, runtime, and platform-level decisions.
- Section 5 — Platform Affinity Analysis: Per-component justification for the chosen target platform.
- Section 6 — Technical Debt Analysis: For ACAS the source has no external dependency manifest so this section is a deliberate stub — the legacy stack is itself the debt and is wholesale replaced by the target stack covered in Section 4. The full Tech Debt catalog (library EOL, CVE exposure, transitive-dep graph) re-engages on modernizations whose source has a real package manifest (npm, Maven, NuGet, pip, Gemfile, …).
- Section 7 — UI/UX Transformation Examples: Storyboarded transition from terminal screens to a modern web UI.
- Section 8 — Code Translation Examples: Side-by-side COBOL-to-Python (FastAPI) translations with behavioral-rule cross-references.
- Section 9 — Business Rules Analysis: Given/When/Then behavioral-rule catalog for Payment Processing.
- Section 10 — Data Mapping Strategy: Legacy ISAM/MySQL records to PostgreSQL schema.
- Section 11 — Modernization Strategy: Strangler-fig phasing, the SAROC coexistence bridge, and rollout sequencing.
- Section 12 — How Concho Helped This Modernization Planning: The Concho MCP query trace and analysis lineage that produced the findings — how the planning agents consumed the Context Graph.
- Appendix A — Multi-Agent Subsystem Selection: Three-run consensus selection log.
- Appendix B — Service Architecture: Per-service contracts, ports, and dependency map for the target.
- Appendix C — Deployment: docker-compose, ECS task definitions, and CI/CD pipeline artifacts.
What this report does not include: dollar-value cost estimates, calendar-date timelines, staffing plans, or organizational change management recommendations. Those belong in the engagement-specific delivery plan that follows stakeholder alignment on this analysis.
1.4 Stakeholder Reading Guide
1. This Is a Modernization Stakeholder Review
This document is the output of an automated multi-agent modernization workflow — analysis, target-architecture design, behavioral-rule extraction, code translation, data mapping, and a legacy/modern coexistence plan, all composed in roughly 90 minutes from a single Concho Context Graph of the ACAS codebase. It is structured as a stakeholder review — the way a senior architect would present a modernization proposal to an organization for feedback and alignment.
- Executives see the business case, the scope of the Payment Processing slice, and the modernization strategy at a glance.
- Architects see the target Python / FastAPI design on Docker Compose / AWS ECS Fargate, the platform-affinity table, and the 3-microservice decomposition rationale.
- Developers see concrete COBOL-to-Python (FastAPI) code translations, Given-When-Then business-rule specifications, and data-mapping examples grounded in ACAS's copybook / ISAM dual-storage patterns.
- Infrastructure teams see the ECS Fargate deployment pattern, the PostgreSQL data tier, and the SAROC coexistence bridge that lets a live ACAS instance keep operating during a strangler-fig cutover.
2. Human-in-the-Loop Iteration
This stakeholder review is designed to be read, challenged, and iterated. Disagree with the subsystem boundary? Adjust the scope in Section 2 and the agents will replan. Want a different AWS target (ECS Fargate vs. EKS vs. App Runner)? Swap the target profile and rerun. Think a particular business rule was misclassified? Drop it from the catalog, add a counter-example, and the rule-extraction phase will reconcile. The point of the document is to compress weeks of manual architectural discovery into a single artifact that can absorb stakeholder feedback at the speed of conversation, rather than at the speed of a consulting engagement.
And because this is a Claude Code workflow, iterating is a conversation. Tell Claude — in plain language — “retarget this to Azure Container Apps instead of ECS Fargate,” and it does the right thing: it knows the framework’s structured set of agents, skills, prompts, and stored handoff artifacts, so it reruns only the steps that actually change (target architecture, platform affinity, deployment, IaC) and leaves the language-level analysis and business-rule catalog untouched. The same applies to swapping a UI framework, a data-access library, or an auth pattern. For finer control, the decisions live in low-level JSON artifacts you can edit directly — and Claude can tell you exactly which file and field to change. There is no proprietary console to learn: the operating principles are encoded in the framework, and Claude already knows how to drive them.
3. This Report Feeds Automation — A Human Never Reviews a Broken Artifact
Every section of this document produces structured handoff artifacts — the chosen subsystem boundary, the platform-affinity decisions, the Given-When-Then behavioral-rule catalog, the entity-to-schema mapping, the UI storyboard JSON, and the SAROC coexistence-bridge declarations — that feed directly into a companion agentic code-generation workflow. That workflow consumes the handoffs and emits runnable artifacts on Docker Compose / AWS ECS Fargate:
- Services — FastAPI web APIs scaffolded from the platform-affinity decisions and the Concho-vended entry-point catalog (
payment-api,payment-batch,payment-reporting). - Entity models — Pydantic v2 domain types and SQLAlchemy 2.0 configurations derived from ACAS's copybook definitions.
- UI components — React components rendered from the storyboard JSON, replacing the legacy 80×24 terminal screens with the Payment Cycle Workbench.
- Database schemas — PostgreSQL DDL generated from the entity model, with OCCURS arrays expanded into child tables.
- Dockerfiles & IaC templates — container build files and Terraform / CloudFormation for ECS Fargate, RDS PostgreSQL, RabbitMQ, and OpenTelemetry observability.
- CI/CD pipelines — build, test, and deploy definitions for the modernized stack.
- Behavioral tests — pytest suites for the rule catalog and Playwright end-to-end tests for the React UI.
The code-generation workflow runs a BDD/TDD iterate-till-green loop per artifact. The tests are generated from the derived business rules — every Given-When-Then specification in Section 9, plus the field-level rules auto-derived from the UI inventory in Section 9.8, becomes an executable pytest or Playwright assertion. The workflow generates code from those same specs, runs the suite against the generated code, inspects failures, self-corrects, and re-runs until green. Because both sides — the implementation and its test oracle — are derived from one source of truth, the rule catalog validates itself.
And an adversarial validation agent runs at every step, not just at the end. The same discipline that produced this report — every generation step is followed by an independent validator agent that tries to break the output, checks it against the source evidence, and either passes it or sends it back for correction — carries straight into code generation. Schema mapping, service scaffolding, UI rendering, and the rule-to-test translation are each adversarially checked before the next step consumes them. A human never reviews a broken artifact.
This means Sections 7 (UI), 8 (Code Translation), 9 (Business Rules), 10 (Data Mapping), and Appendices B–C are both human-readable evidence and machine-consumable specifications. The same artifact that aligns stakeholders is the artifact that drives the code generator.
2. Modernization Scope
This report leverages Concho's deep insight into all 36 subsystems in ACAS. A variety of criteria was applied against each one in order to find the right balance between technical feasibility, business value, and modernization risk in order to select a candidate subsystem for this first modernization project.
2.1 Customer-Specified Use Case
The Payment Processing subsystem was explicitly specified by the customer as the modernization target for this initiative. This decision was made by the customer's finance and IT leadership based on their assessment of business priorities and immediate operational needs. To validate that selection, Concho ran an independent multi-agent consensus methodology against the full 36-subsystem inventory; that methodology — described in Appendix A: Multi-Agent Subsystem Selection Methodology — arrived at the same recommendation through three independently-weighted scoring runs.
Selected Modernization Candidate: Payment Processing
Customer pre-selection confirmed by three independent agent runs with full consensus (weighted scores +2.60, +2.80, and +2.85 on the standard modernization-scoring rubric).
Why Payment Processing is the Right First Modernization
- Bounded, identifiable scope. Approximately 30 strong-association COBOL programs (~14,800 LOC) cluster cleanly across
purchase/pl08x-pl9xx(supplier outflow),sales/sl08x-sl1xx(customer receipts), and a sharedcommon/paymentsMTData Access Layer — small enough for a first pilot, substantial enough to prove the modernization pattern. - High executive visibility. Cash disbursement (supplier payments via cheque and BACS) and cash receipt allocation are the modules a CFO sees every day; the remittance advice output (
pl960) is a customer-facing artifact. A successful pilot delivers tangible, finance-relevant business value rather than back-office plumbing. - Exercises the full SAROC coexistence pattern. The watched artifacts already named in
report-plan.json(pay.dat,openitm5.dat,purchled.dat) map one-to-one onto Payment Processing's strong-file set, so the file-watcher CDC → translator → RabbitMQ → Python/FastAPI consumer → PostgreSQL chain gets fully validated on this single subsystem. - Lower blast radius than the alternatives. Payment Processing posts to the General Ledger but is not posted-from by other modules, so the strangler-fig bridge fans out from a smaller surface than GL, Stock Control, or the Invoicing subsystems would offer.
- Pattern reuse multiplier. Two parallel mirror flows (PL outflow and SL inflow) implement essentially the same workflow against different ledgers, so the second flow validates the pattern proven by the first — and the dual-storage adapter, GL-integration callback, and external-document generation patterns then transfer directly to the next subsystems on the roadmap.
2.2 Methodology Validation Reference
While Payment Processing was customer-specified, organizations needing assistance selecting modernization candidates can use Concho's multi-agent consensus methodology. Three independent agent runs — emphasizing standard weighted scoring, technical feasibility, and business value respectively — evaluated all 36 ACAS subsystems against quantified criteria and reached full consensus on Payment Processing. See Appendix A: Multi-Agent Subsystem Selection Methodology for the complete scoring tables, run-by-run analysis, and consensus details.
3. Legacy System Analysis
TL;DR — What Concho discovered about ACAS
Concho's Context Graph covered 100% of the ACAS codebase: 1.36 million total lines across the repository (381,073 lines classified into architectural layers, the remainder split between documentation, user manuals, and tax-compliance reference material). From that analysis Concho discovered 56 aggregate roots distributed across 37 bounded contexts, 308 enforced business rules, 136 integration points, and 149 implementation constraints (including platform-imposed limits like the 80×24 terminal-dimension requirement, the 4-character password ceiling, and the COMP-3 packed-decimal financial-precision format).
This section is the baseline knowledge that Sections 4-11 build on. The two Mermaid diagrams below (Sections 3.1 and 3.6) are Concho-vended artifacts — pre-generated by the deep scan with confidence scores of 0.95 and 0.90 respectively — not approximations constructed by this report. Subsequent sections drill into Payment Processing specifically; this section establishes context for the whole system.
3.1 High-Level System Architecture
ACAS implements a classical layered architecture — presentation, application, data, integration, cross-cutting, infrastructure, build — with one distinguishing feature: a dual-mode storage strategy that allows runtime switching between traditional ISAM indexed files and MySQL/MariaDB databases through a unified Data Access Layer. The diagram below is the Concho-vended architecture layer artifact (artifact ID architecture_layer_diagram, confidence 0.95) emitted directly by the Context Graph; line counts on each layer come from the architectural classification of the codebase, not from this report.
graph TB
subgraph Presentation["Presentation Layer (9,042 lines)"]
MENUS["Terminal Menu Navigation
6,965 lines"]
ENQUIRY["Business Enquiry Interface
2,077 lines"]
end
subgraph Application["Application Layer (87,374 lines)"]
SALES["Sales Management
27,250 lines"]
PURCHASE["Purchase Management
18,234 lines"]
IRS["IRS Tax Processing
11,003 lines"]
GL["General Ledger
11,127 lines"]
PAYMENT["Payment Processing
7,142 lines"]
STOCK["Stock Control
7,343 lines"]
BACKORDER["Back Order Management
1,084 lines"]
BATCH["Batch Processing
2,204 lines"]
end
subgraph CrossCutting["Cross-Cutting Concerns (3,588 lines)"]
AUDIT["Audit Processing
356 lines"]
ERROR["Error Handling
141 lines"]
SECURITY["Security Framework
225 lines"]
LOGGING["Logging Utilities
402 lines"]
UTILITIES["Common Utilities
1,910 lines"]
end
subgraph Data["Data Layer (148,577 lines)"]
DBACCESS["Database Access Layer
78,239 lines"]
MIGRATION["Data Migration Utilities
30,104 lines"]
ISAM["ISAM File Handlers
19,592 lines"]
DBPROC["Database Procedures
13,844 lines"]
SCHEMA["File Definitions
3,710 lines"]
MYSQL["MySQL Schema
2,730 lines"]
end
subgraph Integration["Integration Layer (922 lines)"]
BRIDGE["MySQL COBOL Bridge
922 lines"]
end
subgraph Infrastructure["Infrastructure (10,854 lines)"]
DBCONN["Database Connectivity
5,023 lines"]
CONFIG["System Configuration
4,759 lines"]
BACKUP["Backup Scripts
101 lines"]
MONITOR["System Monitoring
249 lines"]
end
MENUS --> SALES
MENUS --> PURCHASE
MENUS --> IRS
MENUS --> GL
ENQUIRY --> SALES
ENQUIRY --> PURCHASE
SALES --> DBACCESS
PURCHASE --> DBACCESS
IRS --> DBACCESS
GL --> DBACCESS
PAYMENT --> DBACCESS
STOCK --> DBACCESS
SALES --> ISAM
PURCHASE --> ISAM
IRS --> ISAM
GL --> ISAM
DBACCESS --> BRIDGE
DBACCESS --> DBPROC
ISAM --> SCHEMA
BRIDGE --> DBCONN
DBPROC --> DBCONN
SALES --> AUDIT
PURCHASE --> AUDIT
GL --> AUDIT
IRS --> AUDIT
PAYMENT --> AUDIT
DBACCESS --> CONFIG
ISAM --> CONFIG
Source: Concho Context Graph — architecture_layer_diagram artifact (confidence 0.95). Line counts reflect Concho's architectural classification.
3.2 Architectural Layers
Concho's architectural classification distributes 381,073 lines of architectural code (excluding documentation and user-manual content) across seven primary layers. The Data Layer is by far the heaviest at 39.0%, reflecting the dual-storage abstraction (ISAM file handlers plus MySQL MT modules). The Application Layer at 22.9% holds the business-logic modules including Payment Processing. The Presentation Layer is unusually thin (2.4%) — characteristic of terminal-based systems where the UI is overwhelmingly menu navigation and screen formatting rather than rich client code.
| Layer | Lines | % of Architectural Code | Primary Responsibility |
|---|---|---|---|
| Data Layer | 148,577 | 39.0% | Dual-mode persistence; ISAM file handlers and MySQL MT modules |
| Documentation | 108,973 | 28.6% | System docs, user manuals, tax documentation |
| Application Layer | 87,374 | 22.9% | Domain business logic across Sales, Purchase, GL, IRS, Payment, Stock |
| Infrastructure | 10,854 | 2.8% | Database connectivity, system configuration, backup, monitoring |
| Build & Deployment | 9,197 | 2.4% | SQL preprocessing, compilation, installation scripts |
| Presentation Layer | 9,042 | 2.4% | Terminal menus and business-enquiry screens |
| Cross-Cutting Concerns | 3,588 | 0.9% | Audit, error handling, security, logging, utilities |
| Test | 2,546 | 0.7% | Data generation and MySQL integration testing |
| Integration Layer | 922 | 0.2% | COBOL-to-MySQL bridge |
Within the Application Layer, the eight major business sub-areas break down as: Sales Management (27,250 lines), Purchase Management (18,234), General Ledger (11,127), IRS Tax Processing (11,003), Stock Control (7,343), Payment Processing (7,142), Batch Processing (2,204), and Back Order Management (1,084). Payment Processing itself decomposes into four leaf-level capabilities: Payment Amendment (3,240 lines), Payment Data Entry (1,742), Cash Posting (1,602), and Payment Proof Processing (558).
3.3 Data Entities
Concho's deep scan identified 56 aggregate roots distributed across 37 bounded contexts, with an average of 6.86 relationships per entity and 308 business rules enforced across the entire domain model. Every aggregate root carries STRUCTURAL evidence (derived from code structure rather than inferred behavior), giving high confidence in the domain model. The table below shows the top aggregate roots by confidence score — including the Payment aggregate that this modernization centers on.
| Aggregate Root | Confidence | Relationships | Business Rules | Domain Focus |
|---|---|---|---|---|
| SalesInvoice | 0.92 | 10 | 9 | Invoice lifecycle, recurring invoices, GL posting |
| GeneralLedger | 0.90 | 8 | 8 | Chart of accounts, double-entry posting, period close |
| Payment | 0.90 | 8 | 6 | Payment workflows for debtor and creditor transactions |
| Transaction | 0.89 | 12 | 10 | Financial transaction posting, reversal, batch operations |
| StockItem | 0.89 | 11 | 12 | Inventory master records with 12-month transaction history |
| Batch | 0.89 | 9 | 8 | Transaction batch lifecycle across GL, Sales, Purchase |
| SecurityControl | 0.88 | 9 | 10 | Authentication, authorization, audit trail |
| ChartOfAccounts | 0.88 | 12 | 10 | 4-level hierarchical nominal ledger structure |
| ErrorHandlingSystem | 0.88 | 11 | 8 | File-status / SQL-state code translation, audit logging |
| DataAccessLayer | 0.87 | 17 | 13 | Dual-mode persistence abstraction; the data hub |
| PurchaseInvoice | 0.87 | 6 | 5 | Three transaction types (receipt, adjustment, credit) |
| Customer | 0.87 | 6 | 5 | Customer master, enquiry, statements, collections |
The relationships among these aggregates form a recognizable accounting domain model, with Transaction and DataAccessLayer acting as relational hubs. The simplified entity-relationship diagram below is derived from Concho's aggregate-root and bounded-context discovery; it is an agent-constructed view of the domain rather than a Concho-vended artifact.
erDiagram
Customer ||--o{ SalesInvoice : "is billed via"
SalesInvoice ||--o{ Transaction : "generates"
PurchaseInvoice ||--o{ Transaction : "generates"
Payment ||--o{ Transaction : "settles"
Transaction }o--|| GeneralLedger : "posts to"
Transaction }o--|| Batch : "is grouped in"
GeneralLedger ||--|{ ChartOfAccounts : "uses"
Payment }o--o{ PurchaseInvoice : "appropriates"
Payment }o--o{ SalesInvoice : "appropriates"
StockItem ||--o{ Transaction : "moves via"
CustomerStatement }o--|| Customer : "summarizes"
DataAccessLayer ||..o{ Customer : "persists"
DataAccessLayer ||..o{ SalesInvoice : "persists"
DataAccessLayer ||..o{ PurchaseInvoice : "persists"
DataAccessLayer ||..o{ Payment : "persists"
DataAccessLayer ||..o{ GeneralLedger : "persists"
DataAccessLayer ||..o{ StockItem : "persists"
SecurityControl ||..o{ Customer : "guards"
SecurityControl ||..o{ Payment : "guards"
ErrorHandlingSystem ||..o{ DataAccessLayer : "translates errors for"
Derived from Concho aggregate-root and bounded-context discovery. Solid lines indicate domain relationships; dotted lines indicate cross-cutting infrastructure relationships.
Field-level mapping from the legacy WS-Pay-Record and related copybooks to the target data model is covered in Section 10 (Data Mapping Strategy).
3.4 Business Rules & Behavior
Concho cataloged 308 business rules across the ACAS domain, with the Payment aggregate root contributing 6 directly. Among the Payment-specific rules are PaymentBatchControlLimits (99-item batch ceiling, enforced by the PIC 99 count field WS-Pay-Nos at copybooks/wspay.cob:13; sequential batch numbering at purchase/pl080.cbl:325; composite-key construction, batch × 1000 + item, at purchase/pl080.cbl:733), PaymentAppropriationLogic (automatic allocation against outstanding invoices with overpayment prevention and timing-based early-payment discount; purchase/pl080.cbl:421/492/592), PaymentPostingValidation, RemittanceAdviceProcessingRules, UnappliedBalanceAllocation, and PaymentReversalProcessing.
Detailed Given/When/Then specifications for these rules — including the full source-evidence trail and the corresponding behavioral tests — are presented in Section 9 (Business Rules Analysis).
3.5 Technology Stack
The ACAS technology stack reflects its origin as a GnuCOBOL system that has been extended with modern RDBMS integration while preserving backward compatibility with traditional file-based storage. Concho identified 32 distinct technology subjects across the codebase.
Reading the table: “Prevalence” is the share of all cataloged source files (~630) in which each technology appears. The first three rows all report 449 files (71%) because the language (COBOL), its runtime (GnuCOBOL), and the legacy ISAM file storage are three properties of the same 449-file COBOL core — this is one file set described from three angles, not three separate counts. Rows below it (MySQL, shell, C, SQL) describe progressively narrower technology layers.
| Category | Technology | Prevalence | Role |
|---|---|---|---|
| Language | COBOL | 449 files (71%) | Primary application language for all business logic and data access |
| Runtime / Framework | GnuCOBOL | 449 files (71%) | Open-source COBOL compiler and runtime environment |
| Storage (Legacy) | ISAM Files | 449 files (71%) | Traditional COBOL indexed-file storage; on-disk .dat files |
| Storage (Modern) | MySQL / MariaDB | 220 files (35%) | Relational backend with 39 tables (mysql/ACASDB.sql) |
| Glue / Tooling | Shell Scripting | 66 files (11%) | Build orchestration, backup, PDF generation pipelines |
| Native Integration | C | 4 files | FFI bridge between COBOL and MySQL C API |
| Schema / Migration | SQL | 2 files | Database schema definition and migration scripts |
| UI | Terminal Interface | 9,042 lines | 80×24 character-based menus and enquiry screens |
| Output | CUPS Print Spooling + PDF | Multiple modules | Report printing via lpr; PDF via enscript + ps2pdf14; email via mailx/mutt |
| Numeric Precision | COMP-3 Packed Decimal | System-wide | Monetary values stored as signed packed decimal (s9(8)v99) |
Three implications stand out for modernization. First, the dual-mode storage strategy means business logic is already decoupled from physical storage at the source-code level via the MT (MySQL Table) modules and ACAS file handlers (acas000-acas015) — this dramatically reduces the structural risk of swapping the persistence layer to PostgreSQL. Second, the COMP-3 packed-decimal precision (8 digits integer + 2 decimal) is a hard contract: any modernization must use a decimal type (Python Decimal, PostgreSQL NUMERIC(10,2)) and never IEEE-754 floats. Third, the terminal UI's 80×24 footprint sets an upper bound on field density per screen; modernized layouts can comfortably exceed it without behavioral risk because no business rule is keyed to screen position.
3.6 Integration Patterns
Concho identified 136 integration points across 189 files, dominated by file integrations (47, 35%) and internal RPC-style COBOL CALL operations (43, 32%). Critically, 63 of the integrations are bidirectional, indicating tight synchronous coupling characteristic of monolithic financial applications. The diagram below is the Concho-vended data-flow artifact (artifact ID data_flow_diagram, confidence 0.90) and shows how data enters the system through CLI and terminal interfaces, is routed by the Database Access Layer based on the FA-RDBMS-Flat-Statuses flag (value '66' means MySQL mode, otherwise ISAM), and exits via CUPS print spooling, PDF generation, and email delivery.
graph LR
subgraph Entry["Data Entry Points"]
CLI["CLI Interfaces
49% of entry points"]
MAIN["Main Programs
39% of entry points"]
SCHED["Scheduled Processes
3% of entry points"]
TERM["Terminal Interface
st010, sl810"]
end
subgraph Processing["Application Processing Layer"]
SALES["Sales Management
12.3% - domain core"]
PURCH["Purchase Management
7.5% - domain core"]
GL["General Ledger
5.0% - domain core"]
STOCK["Stock Control
3.4% - domain core"]
IRS["IRS Tax Processing
5.1% - domain core"]
PAY["Payment Processing
3.3% - domain core"]
end
subgraph Storage["Dual Storage Architecture"]
DAL["Database Access Layer
22.3% - data access"]
FH["ISAM File Handlers
7.6% - data access"]
MT["MySQL MT Modules
analMT, salesMT, etc."]
end
subgraph Persistence["Data Persistence"]
ISAM[("ISAM Files
Traditional Storage")]
MYSQL[("MySQL Database
39 Tables")]
AUDIT[("Audit Logs
fhlogger")]
end
subgraph Integration["External Integration"]
PDF["PDF Generation
enscript + ps2pdf14"]
EMAIL["Email Delivery
mailx/mutt"]
PRINT["CUPS Print Spooling"]
BACKUP["Backup Archives
tar.gz timestamped"]
end
CLI -->|user input| SALES
MAIN -->|batch data| PURCH
TERM -->|terminal forms| STOCK
SCHED -->|recurring transactions| PAY
SALES -->|invoice data| DAL
PURCH -->|supplier transactions| DAL
GL -->|journal entries| DAL
STOCK -->|inventory movements| DAL
IRS -->|tax reporting data| DAL
PAY -->|payment records| DAL
DAL -->|FA-RDBMS-Flat-Statuses='66'| MT
DAL -->|FS-Cobol-Files-Used=0| FH
FH -->|ISAM operations| ISAM
MT -->|SQL operations| MYSQL
DAL -->|audit trail| AUDIT
SALES -->|invoice reports| PDF
PDF -->|attachments| EMAIL
DAL -->|financial reports| PRINT
ISAM -->|.seq backup| BACKUP
MYSQL -->|database dump| BACKUP
Source: Concho Context Graph — data_flow_diagram artifact (confidence 0.90). Percentages reflect Concho's architectural-element weighting of the codebase.
Five integration patterns dominate and shape the modernization strategy:
- Dual-storage routing — every file operation passes through
acas000-acas015handlers that inspectFA-RDBMS-Flat-Statusesand dispatch to ISAM or MySQL. This is the SAROC coexistence bridge's natural seam. - COBOL-to-MySQL host-variable mapping — MT modules (
analMT.cbl,salesMT.cbl, etc.) translate COBOL record structures with COMP-3 fields into MySQL parameter binds via the C-level MySQL Bridge (922 lines,Integration Layer). - Error-code translation — SQL state codes (
23000duplicate key,0200nnot found) are mapped to COBOL file-status codes (22,10,23) and ACAS-specific99xxxcodes for validation errors. Modernization preserves this contract via mapped error responses. - External-process integration — CUPS (
lpr),enscript/ps2pdf14for PDF generation, andmailx/muttfor email delivery are invoked via shell-out from COBOL. Each becomes a microservice boundary in the target. - Centralized audit logging —
fhlogger.cblwrites a fixed 512-byte log record for every file/database operation. This is a single point of failure in the legacy system; modernization splits it into per-service structured logging plus a system-wide audit stream.
4. Target Architecture
This section defines the target Docker Compose / AWS ECS Fargate architecture for the modernized Payment Processing subsystem (~30 strong-association COBOL programs, ~14,800 LOC across purchase, sales, and shared payment modules). It covers the target architecture overview, target technology stack, code-level architecture decisions, target data model, and target service decomposition. A multi-agent service architecture analysis determined the optimal decomposition: 3 microservices (payment-api, payment-batch, payment-reporting) for this modernization. Migration sequencing, the SAROC coexistence bridge, and rollback approach are covered separately in Section 11.
The 3-service architecture was selected through consensus of Domain-Driven Design, Technical Architecture, and Business Capability analyses, with the Technical perspective winning a weighted score of 7.80 / 10 (passing the 7.0 quality gate). The decomposition aligns with the report-plan.json declared serviceCount = 3 and preserves the inline-GL-posting decision recorded below. Full multi-perspective analysis is summarized in Section 4.5 and detailed in Appendix B.
4.1 Target Architecture Overview
The modernized Payment Processing subsystem replaces the existing COBOL programs (the purchase-side pl080–pl100 and pl900–pl960 chain, the mirrored sales-side sl080–sl100 chain, and the shared common/paymentsMT data access layer) with a containerized Python application deployed on AWS ECS Fargate. The architecture preserves the single bounded context identified by Concho — PaymentProcessing (confidence 0.90) — while decomposing monolithic COBOL programs into well-defined API endpoints organized around the existing workflow stages: data entry, validation, appropriation, proof generation, cash posting, remittance advice generation, and amendment.
The target design exploits the natural modernization seam already present in the legacy system: the acas032 dual-mode file handler, which abstracts ISAM and MySQL access behind a unified interface and inspects the FA-RDBMS-Flat-Statuses switch to dispatch between backends. A file-watcher CDC adapter observes the three watched artifacts (pay.dat, openitm5.dat, purchled.dat) declared in report-plan.json, translates COBOL binary record changes into JSON domain events conforming to the sage-domain-event-v1 envelope, and publishes them to a durable RabbitMQ topic exchange. The modern Python/FastAPI consumer applies the events to PostgreSQL with natural-key idempotency — enabling zero-disruption modernization via the SAROC pattern described in Section 11: Modernization Strategy.
Key Target Architecture Principles
- API-First Design. All payment operations are exposed through versioned RESTful APIs with OpenAPI documentation auto-generated from Pydantic models, replacing the terminal-based menu navigation in
pl900(6-option dispatcher topl910–pl960) with structured HTTP endpoints. - Domain-Driven Boundaries. The single aggregate root (Payment, confidence 0.90) and single bounded context (PaymentProcessing, confidence 0.90) inform service boundaries. The 6 business rules (
PaymentBatchControlLimits,PaymentAppropriationLogic,PaymentPostingValidation,RemittanceAdviceProcessingRules,UnappliedBalanceAllocation,PaymentReversalProcessing) and 4 integration boundaries are preserved as explicit domain logic rather than embedded in procedural COBOL paragraphs. - Inline GL Posting. General Ledger journal entries are written in the same database transaction as the payment allocation, replacing the legacy overnight batch posting cycle (
pl100) with immediate, balanced GL entries that emit a domain event for downstream subsystems. - Observability by Default. Every service includes OpenTelemetry distributed tracing, structured JSON logging via structlog with correlation IDs propagated across all calls, and health-check endpoints (readiness, liveness, startup) for ECS task lifecycle management — capabilities entirely absent from the legacy terminal-based system where COBOL
DISPLAYwas the only diagnostic. - Infrastructure as Code. All infrastructure — ECS task definitions, RDS instance, RabbitMQ broker, message queue topology, and IAM policies — is declared in AWS CDK (TypeScript) for repeatable deployments. Local development runs an equivalent Docker Compose topology that mirrors the cloud composition.
- Legacy Stays Authoritative. Throughout modernization, the legacy COBOL system is never modified and remains the system of record. The SAROC bridge is passive: CDC observes ISAM file changes, the translator emits idempotent domain events, and the modern consumer applies them with natural-key idempotency so duplicate events are safely no-ops. Detailed coexistence sequencing, dual-write phasing, and rollback are owned by Section 11.
- Pattern Reuse for Follow-On Subsystems. The two parallel payment flows (purchase ledger supplier outflow and sales ledger customer receipts) implement essentially the same workflow against different ledgers. The dual-storage adapter, GL-integration callback, and external-document generation patterns proven on this subsystem then transfer directly to Supplier Management, Purchase Invoicing, and Sales Invoicing.
4.2 Target Architecture Diagram
graph TB
subgraph Client["Client Layer"]
WebUI["Payment Processing UI
(SPA)"]
end
subgraph AWS["AWS Cloud"]
ALB["Application Load Balancer"]
subgraph ECS["ECS Fargate Cluster"]
SVC1["Payment API Service
(FastAPI)"]
SVC2["Payment Batch Service
(FastAPI)"]
SVC3["Reporting Service
(FastAPI)"]
end
subgraph Data["Data Layer"]
RDS[("PostgreSQL RDS
payment, open_item,
batch_control, gl_posting,
payment_audit")]
end
subgraph Observability["Observability"]
OTEL["OpenTelemetry Collector
(traces, metrics, logs)"]
end
subgraph Secrets["Configuration"]
SM["AWS Secrets Manager
+ SSM Parameter Store"]
end
end
subgraph Bridge["SAROC Coexistence Bridge (Section 11)"]
CDC["File-Watcher CDC
(pay.dat / openitm5.dat /
purchled.dat)"]
TR["Event Translator
(sage-domain-event-v1)"]
MQ["RabbitMQ
(topic exchange,
persistent, DLQ)"]
CON["Background Consumer
(natural-key idempotency)"]
end
subgraph Legacy["Legacy ACAS (unchanged)"]
COBOL["COBOL Payment Programs
(pl080-pl100, pl900-pl960,
sl080-sl100, paymentsMT)"]
LegacyDB[("ISAM / MySQL
via acas032 dual-mode")]
end
WebUI --> ALB
ALB --> SVC1
ALB --> SVC2
ALB --> SVC3
SVC1 --> RDS
SVC2 --> RDS
SVC3 --> RDS
SVC1 -.->|"read config/secrets"| SM
SVC2 -.->|"read config/secrets"| SM
SVC3 -.->|"read config/secrets"| SM
COBOL --> LegacyDB
LegacyDB -.->|"file-watcher CDC"| CDC
CDC --> TR
TR -.->|"JSON domain events"| MQ
MQ -.->|"persistent subscribe"| CON
CON --> RDS
SVC1 --> OTEL
SVC2 --> OTEL
SVC3 --> OTEL
CON --> OTEL
style Client fill:#f5efe4,stroke:#1a2b3c,color:#1a2b3c
style AWS fill:#ffffff,stroke:#1a2b3c,color:#1a2b3c
style ECS fill:#c4dad2,stroke:#1a2b3c,color:#1a2b3c
style Data fill:#1b7a78,stroke:#1a2b3c,color:#1a2b3c
style Observability fill:#e8a598,stroke:#1a2b3c,color:#1a2b3c
style Secrets fill:#f0f9ff,stroke:#1a2b3c,color:#1a2b3c
style Legacy fill:#f5efe4,stroke:#999,color:#666,stroke-dasharray: 5 5
style Bridge fill:#fff3cd,stroke:#856404,color:#856404,stroke-dasharray: 5 5
Target architecture showing the stack for Payment Processing — 3 microservice architecture (payment-api, payment-batch, payment-reporting) selected via multi-agent consensus analysis. Python/FastAPI services on ECS Fargate with PostgreSQL, OpenTelemetry observability, and the passive SAROC coexistence bridge (file-watcher CDC → translator → RabbitMQ → consumer) that lets the legacy COBOL system remain authoritative throughout modernization. Full coexistence narrative in Section 11.
4.3 Target Technology Stack
| Category | Technology | Version | Rationale |
|---|---|---|---|
| Language | Python | 3.12+ | Type hints, async/await native, mature ecosystem for financial data processing. Replaces COBOL's procedural paradigm with modern type-safe patterns. |
| API Framework | FastAPI | 0.110+ | Async-native with automatic OpenAPI documentation generation from Pydantic models. Native request validation eliminates the manual input checking present in COBOL terminal programs (pl080, sl080). |
| Compute | AWS ECS Fargate | — | Serverless containers eliminate EC2 management while providing auto-scaling, health checks, and rolling deployments. Right-sized for the medium-fleet shape (3 services) without the operational overhead of EKS. Docker Compose mirrors the topology locally. |
| Database | PostgreSQL (Amazon RDS) | 16+ | ACID-compliant relational database replacing the dual ISAM/MySQL storage. JSONB columns accommodate variable-shape open-item records and payment-audit before/after state. Partitioning supports the 30/60/90+ day aging queries required by PaymentAgeCalculation. |
| ORM / Data Access | SQLAlchemy + Alembic | 2.0+ | Industry-standard Python ORM with async support. Alembic provides versioned schema migrations, replacing manual DDL scripts. Together they consolidate the legacy acas032 dual-mode file handler and the paymentsMT/paymentsLD/paymentsRES/paymentsUNL data access layer (~3,000 LOC). |
| Messaging (SAROC Bridge) | RabbitMQ | 3.13+ | Topic-exchange substrate (persistent + dead-letter) declared in report-plan.json as the SAROC messaging layer. Buffers domain events from the file-watcher CDC translator and delivers them to the Python background consumer with at-least-once semantics; natural-key idempotency on the target side makes duplicate deliveries safe. |
| Container Runtime | Docker | — | Multi-stage builds produce lightweight images. The same image runs locally under Docker Compose and in ECS Fargate task definitions, eliminating "works on my machine" drift. |
| Load Balancing | Application Load Balancer (ALB) | — | Layer 7 path-based routing to ECS services. Supports weighted target groups when phased traffic shifting is needed for follow-on subsystems. |
| Secrets Management | AWS Secrets Manager | — | Centralized credential storage with automatic rotation. ECS task definitions inject secrets via secrets[] entries referencing Secrets Manager ARNs, replacing hardcoded connection strings in legacy .cobconfig files. |
| Configuration | AWS Systems Manager Parameter Store | — | Non-secret configuration (feature flags, queue names, batch sizes) is read from SSM at startup via 12-factor environment variables. Keeps secrets and config on separate access paths. |
| Observability | OpenTelemetry + CloudWatch | — | Vendor-neutral distributed tracing, structured logging, and metrics. The ADOT (AWS Distro for OpenTelemetry) collector sidecar exports traces to X-Ray and metrics/logs to CloudWatch. Provides end-to-end payment transaction visibility that does not exist in the legacy terminal-based system. |
The technology stack was selected to maximize developer productivity and operational reliability while minimizing modernization risk. Python and FastAPI were chosen because Pydantic's strict typing maps cleanly to COBOL's strict data typing — every PIC X(N) and PIC S9(N)V9(M) field has a corresponding Pydantic field with equivalent validation constraints, and COMP-3 packed-decimal values translate to Python Decimal with explicit precision and scale.
PostgreSQL replaces the dual-mode ISAM/MySQL storage currently abstracted by acas032. The legacy copybooks (fdpay.cob, wspay.cob, plwspay.cob, selpay.cob) and the paymentsMT/paymentsLD/paymentsRES/paymentsUNL data access layer consolidate into SQLAlchemy models with Alembic-managed migrations. This eliminates the dual-storage complexity while preserving the data-access abstraction that already exists as a natural architectural seam.
General Ledger posting is performed inline — the appropriation endpoint creates GL journal entries in the same database transaction as the payment allocation, replacing the legacy overnight batch posting cycle in pl100. RabbitMQ provides the SAROC bridge substrate (Section 11), enabling zero-disruption modernization with ordered, idempotent event replay.
4.3.1 Target Code-Level Architecture Decisions
The following table documents the code-level conventions that all Payment Processing services must follow. These decisions are derived from the resolved targetPatterns for the Docker / ECS Fargate platform (skill defaults merged with the architecture template's platform-conditional overrides; no plan-level overrides in run-012) and govern all code examples in subsequent sections and downstream code-generation artifacts.
| Category | Decision | Library / Tool | Rationale |
|---|---|---|---|
| API Framework | Async-native REST APIs with auto-generated OpenAPI docs | FastAPI | Native async support handles concurrent payment operations that legacy COBOL processed sequentially. OpenAPI specs auto-generated from type annotations replace undocumented terminal interfaces. |
| Input Validation | Schema validation at all API boundaries | Pydantic v2 | Type-safe models enforce field constraints that mirror COBOL PIC clauses (e.g., PIC S9(9)V99 becomes Decimal with max_digits=11, decimal_places=2). Applied at every external and inter-service boundary so invalid data is rejected early. |
| Error Handling | Domain exception hierarchy with RFC 7807 responses | Custom exceptions + FastAPI handlers | Replaces COBOL WS-RETURN-CODE numeric error signaling with typed domain exceptions (ValidationError, NotFoundError, ConflictError, DependencyError). RFC 7807 Problem Details provides machine-readable error responses for API consumers. |
| Database Access | ORM with versioned schema migrations and application-level pooling | SQLAlchemy 2.0 + Alembic | Async SQLAlchemy replaces the acas032 dual-mode file handler and the paymentsMT data access layer. Application-level connection pooling is appropriate for Fargate (RDS Proxy is reserved for Lambda shapes). Alembic manages schema evolution that was previously handled by manual DDL. |
| Observability — Tracing | Distributed tracing with correlation IDs propagated end-to-end | OpenTelemetry (OTLP → X-Ray via ADOT) | Vendor-neutral CNCF standard. Correlation IDs propagated across all service calls and across the SAROC bridge enable end-to-end payment transaction tracing — a capability absent in the legacy terminal system. |
| Observability — Logging | Structured JSON logging with embedded trace context | structlog | JSON-formatted logs with embedded trace/span IDs enable CloudWatch Logs Insights queries. Event naming follows the <entity>.<action> convention (e.g., payment.created, payment.appropriated, payment.posted). |
| Observability — Metrics | Application metrics via OpenTelemetry | OpenTelemetry → CloudWatch Metrics | Unified with the tracing SDK. Custom metrics for payment throughput, batch sizes, and GL posting latency provide operational dashboards unavailable in the legacy system. |
| Health Checks | Readiness + liveness + startup probes | FastAPI health endpoints (/health/ready, /health/live, /health/startup) |
ECS Fargate uses these probes for container lifecycle management. Readiness checks verify PostgreSQL and RabbitMQ connectivity before routing traffic; startup probes prevent premature requests during Alembic migration execution. |
| Inter-Service Communication | REST for sync, RabbitMQ for the legacy bridge | httpx (sync REST) + aio-pika (AMQP consumer) | Synchronous REST for real-time inter-service payment queries. GL posting is inline (same DB transaction as the payment allocation). The SAROC RabbitMQ topic exchange — declared in report-plan.json as the messaging substrate — handles the legacy-to-modern data flow. |
| Configuration & Secrets | 12-factor env vars; secrets from Secrets Manager, config from SSM Parameter Store | boto3 + python-dotenv (local) | Configuration injected via ECS task-definition environment variables. Database credentials and queue passwords stored in Secrets Manager with automatic rotation, replacing hardcoded connection parameters in legacy .cobconfig files. |
| Idempotency | Natural-key idempotency on the SAROC consumer | Composite unique constraints + upsert | The SAROC consumer is at-least-once: each event carries the legacy natural key (payment reference + batch number + line) and is applied via PostgreSQL ON CONFLICT DO UPDATE. Duplicate deliveries are safe no-ops. |
| Testing | Pytest with 80% coverage target | pytest + pytest-asyncio | De facto Python testing standard with async support. The 80% coverage floor ensures critical payment paths (appropriation, batch control, GL posting, reversal) are verified. Replaces the absence of automated tests in the legacy COBOL system. |
4.4 Target Data Model
The target data model consolidates the legacy dual-mode storage (ISAM files plus MySQL tables) into a single PostgreSQL schema. The model preserves the Payment aggregate root (confidence 0.90) and its relationships to open items, batch control records, and ledger accounts as identified by Concho's entity analysis. The 25 entities that Concho surfaces for the Payment Processing subsystem at cycle 8 are normalized into 5 core PostgreSQL tables that capture the full payment lifecycle for both purchase ledger (supplier outflow) and sales ledger (customer receipts) flows.
erDiagram
PAYMENT {
uuid id PK
varchar payment_reference
int transaction_type
date payment_date
varchar supplier_code FK
varchar customer_code FK
decimal gross_amount
decimal discount_amount
decimal net_amount
int batch_number FK
int payment_flag
varchar payment_method
varchar cheque_number
varchar bank_reference
varchar legacy_natural_key
timestamp created_at
timestamp updated_at
}
OPEN_ITEM {
uuid id PK
uuid payment_id FK
varchar invoice_reference
int folio_number
date invoice_date
decimal invoice_amount
decimal applied_amount
decimal discount_taken
decimal deduction_amount
int age_days
varchar age_bucket
varchar transaction_type
timestamp created_at
}
BATCH_CONTROL {
int batch_number PK
date batch_date
int item_count
decimal batch_total
varchar batch_status
varchar created_by
timestamp created_at
timestamp closed_at
}
GL_POSTING {
uuid id PK
uuid payment_id FK
int batch_number FK
varchar debit_account
varchar credit_account
decimal amount
varchar posting_reference
boolean posted
timestamp posted_at
}
PAYMENT_AUDIT {
uuid id PK
uuid payment_id FK
varchar action
jsonb old_values
jsonb new_values
varchar performed_by
timestamp performed_at
}
PAYMENT ||--o{ OPEN_ITEM : "appropriates"
PAYMENT }o--|| BATCH_CONTROL : "belongs to"
PAYMENT ||--o{ GL_POSTING : "generates"
PAYMENT ||--o{ PAYMENT_AUDIT : "tracks"
BATCH_CONTROL ||--o{ GL_POSTING : "groups"
Diagram source: Concho codebase analysis — Payment Processing data model derived from COBOL copybook structures (fdpay.cob, wspay.cob, plwspay.cob, selpay.cob, fdoi4.cob, plfdoi5.cob).
| Table | Legacy Source | Key Indexes | Notes |
|---|---|---|---|
payment |
PLPAY-REC (fdpay.cob, wspay.cob, plwspay.cob, selpay.cob) |
PK: idUQ: legacy_natural_keyIDX: payment_reference, batch_number, supplier_code, customer_code, payment_date |
Aggregate root. Consolidates purchase-side (pl080) and sales-side (sl080) payment records into a unified table with a payment_method discriminator. Supports transaction types 1–6 as identified in the legacy system. The legacy_natural_key unique constraint is the idempotency anchor for the SAROC consumer. |
open_item |
OTM3/OTM5 (fdoi4.cob, plfdoi5.cob, slwsoi.cob) |
PK: idIDX: payment_id, invoice_reference, age_bucket |
Invoice appropriation records. Replaces fixed-size OCCURS arrays with dynamic rows. The age_bucket index supports the 30/60/90+ day aging queries used by the Reporting service. |
batch_control |
Batch fields in PLPAY-REC |
PK: batch_numberIDX: batch_status, batch_date |
Enforces the 99-item batch limit (PaymentBatchControlLimits rule at purchase/pl080.cbl:733, confidence 0.75). Extracted from embedded payment fields into a first-class entity with explicit lifecycle tracking. |
gl_posting |
GL batch records (integration via General Ledger Posting Integration, confidence 0.88) | PK: idIDX: payment_id, batch_number, posted |
Captures debit/credit allocations for General Ledger integration. GL entries are created inline in the same database transaction as the payment allocation, replacing the legacy overnight batch posting cycle in pl100. |
payment_audit |
Amendment tracking in pl085 / sl085 |
PK: idIDX: payment_id, performed_at |
JSONB columns capture before/after state for payment amendments and reversals. Replaces implicit audit via batch re-numbering with explicit change tracking required by the PaymentReversalProcessing rule. |
For detailed schema mappings from legacy data structures to this target data model, see Section 10: Data Mapping Strategy.
4.5 Target Service Architecture
report-plan.json declared service count of 3 and preserves the inline-GL-posting decision recorded in Section 4.
Decision Process
| Perspective | Recommendation | Key Rationale | Score |
|---|---|---|---|
| Domain-Driven Design | 1 service (payment-processing-service) |
The Payment Processing bounded context is single and cohesive (confidence 0.90); the Payment aggregate root is the only aggregate (confidence 0.90); the domain analysis explicitly merged candidate sub-contexts at 80% language overlap. Splitting fractures aggregate ownership. |
7.2 |
| Technical Architecture | 3 services (payment-api, payment-batch, payment-reporting) |
Three orthogonal cuts (data-coupling clusters, load-profile clusters, change-velocity clusters) all converge on the same three services. Shared schema and shared Pydantic models preserve aggregate-root integrity; load-shape independence is gained at the task-fleet level. | 7.8 |
| Business Capability | 4 services (Entry / Disbursement / Cash Posting & GL / Remittance) | Four organizational roles (AP clerk, treasurer, controller, AP correspondent) with four distinct cadences. Best Conway's-law alignment but requires an outbox pattern for GL posting (which contradicts the inline-GL decision already recorded in Section 4). | 7.3 |
Service Decomposition
- payment-api — Interactive payment lifecycle: entry (
pl080/sl080), amendment (pl085/sl085), batch open / lookup, and the synchronous read surface that replaces thepl900terminal menu. Sub-second p95 latency budget; autoscales on ALB request count (2–6 tasks). - payment-batch — Proof sort (
pl090), proof report (pl095), cash posting with inline GL writes (pl100), cheque batch (pl940), and payment register (pl950). Also hosts the SAROC consumer (background-thread RabbitMQ subscriber with natural-key idempotency) because the same async-runtime shape serves both legacy-event consumption and batch job execution. Autoscales on queue depth (1–4 tasks). - payment-reporting — Aged-creditor / aged-debtor analysis (
pl910/pl930) and remittance advice generation (pl960) with S3-backed PDF output. Reads route to a PostgreSQL read replica; only remittance-tracking writes hit the primary. Replaces the legacylprprint integration with S3 + optional email/portal delivery.
Integration Pattern
All three services share a single PostgreSQL schema (5 tables — payment, open_item, batch_control, gl_posting, payment_audit) and a shared payment-models Pydantic package. Write paths are gated by service-specific repository methods so the shared schema does not become a shared-database anti-pattern. Asynchronous communication uses the SAROC RabbitMQ topic exchange (envelope sage-domain-event-v1): payment-api emits payment.created/payment.amended/payment.reversed; payment-batch emits payment.posted/gl.journal-entry-created/cheque-batch.generated; payment-reporting consumes cheque-batch.generated to trigger remittance generation and emits remittance.generated/remittance.delivered. Inline GL posting is preserved: GL journal entries are written by payment-batch in the same database transaction as the payment-appropriation update.
For complete three-perspective analysis, scoring matrix, per-service API specifications, AWS topology diagrams, and the strangler-fig sequencing roadmap, see Appendix B: Target Service Architecture Analysis.
5. Platform Affinity Analysis
FinancialDataPrecisionRequirement and PaymentTransactionTypeFilter) split cleanly between a platform artifact that is eliminated and a business invariant that the target carries forward in a more flexible form. The headline unlock: a 100% hard-coded posture — literals scattered across copybooks and COBOL source — collapses into Pydantic models, Alembic migrations, environment variables, and service-owned templates.
Purpose: Identifying Legacy Constraints That Should NOT Transfer
Not all source-platform behavior should be transcoded 1:1 to the target. This section analyzes implementation constraints from the legacy COBOL environment that were necessary in the original system but should be eliminated or redesigned for the modern platform.
Scope: Focused on Payment Processing subsystem (data structures, batch processing, terminal UI, dual-storage dispatch).
Source: Concho-discovered implementation_constraint entities documented throughout this section. Every constraint cites a Concho entity name and at least one file:line reference verified via Concho MCP get_entity.
Note: For UI-specific platform affinity analysis (per-screen layout transformation, function-key replacement, accessibility), see Section 7: UI/UX Transformation Examples. This section covers the platform-level UI constraints (terminal dimensions, fixed display coordinates, credential-form field widths).
Platform affinity analysis classifies each legacy constraint as either platform-driven (ELIMINATE — the limit exists because of hardware/software limitations of the source platform, not because of a business rule), business-driven (PRESERVE — the limit exists because of a genuine business requirement that must survive modernization), or hybrid (the constraint contains both a platform-imposed ceiling that should be eliminated and a business intent that should be preserved in a more flexible form). Getting this distinction wrong replicates legacy limitations into the modern system, or breaks business rules that were disguised as platform constraints.
The catalog backing Section 5 is the 149–197 implementation_constraint entities Concho cycle 8 discovered across the full ACAS codebase. From that catalog, three independent platform-affinity agent runs each filtered for Payment-Processing relevance, scored each candidate for platform-vs-business driver, and emitted ELIMINATE / HYBRID / PRESERVE verdicts. The reconciler then took inclusion by majority (a constraint is in if 2 of 3 runs included it), classification by majority (the dominant verdict wins; ties resolve to the more conservative side), and category by majority (5.1/5.2/5.3/5.4). The 12 constraints below reflect that consensus.
5.1 Capacity Constraints
Capacity constraints impose a numeric ceiling on a count, length, or buffer size — how many items, how many bytes, how many characters. All four 5.1 entries below are platform artifacts of fixed-width COBOL records, OCCURS-clause array sizes, or fixed-length string buffers. PostgreSQL row-sets and SQLAlchemy streaming retire them outright; the modern stack has no per-cycle row caps, no per-statement buffer ceilings, and no fixed-width record allocation.
5.1.1 Batch Size Limitation (99-item per-batch ceiling)
| Aspect | Legacy System (COBOL) | Target System (Python / PostgreSQL) |
|---|---|---|
| Discovery |
Concho Entity: BatchSizeLimitation (confidence 0.92)Source: purchase/pl060.cbl:1000Root Cause: The 99-item ceiling is the maximum value of a PIC 9(2) batch-counter field. When the counter hits 99, the program auto-closes the current batch and re-opens a new one to prevent memory overflow.
|
PostgreSQL batch_control table with BIGINT item counts. Memory budget is set by ECS Fargate task sizing, not by counter-field width. Operator-driven batch grouping is preserved as a business workflow (see Section 9 — PaymentBatchControlLimits); the 99 ceiling itself is gone. |
| Legacy Behavior | Every accounting module enforces the 99-item-per-batch cap. When entry hits the threshold, the COBOL program auto-closes the current batch and re-opens a new one transparently — an operator workaround that hides the platform constraint from the user. | Batch size is configurable per tenant or per process; the default is unbounded. The natural batch boundary is a business decision (e.g., end-of-day, payment-run, supplier-payment-cycle), not a counter-field artifact. |
| Recommendation |
✅ ELIMINATE CONSTRAINT — Drop the 99-item ceiling. Rationale: Two of three runs scored this as a pure platform artifact; one scored HYBRID (preserving the batch-grouping concept). Consensus is ELIMINATE the ceiling, preserve the batch-grouping concept declaratively in batch_control.Implementation: batch_control.max_items is a nullable column with no default ceiling; the legacy 99 becomes a per-tenant override only when explicitly required during SAROC coexistence.
|
|
5.1.2 Remittance Advice Format Limitations (9-invoice cap, 5×32 address block, 645-byte cheque record)
| Aspect | Legacy System (COBOL) | Target System (Python / PostgreSQL) |
|---|---|---|
| Discovery |
Concho Entity: RemittanceAdviceFormatLimitations (confidence 0.85)Source: purchase/pl960.cbl:100, :113, :119, :130Root Cause: Max 9 invoice line items per remittance advice ( OCCURS 9); supplier addresses limited to 5 lines × 32 chars; cheque numbers 9 chars; 645-byte fixed cheque records. The 9-invoice cap is a 1970s-era memory and terminal/print-page artifact; the address block reflects window-envelope conventions of that era.
|
The Reporting service generates remittance PDFs from open_item rows with no upper bound on line items per remittance. Supplier-address fields are unicode-clean variable-length text; cheque IDs are UUIDs. |
| Legacy Behavior | If a supplier is owed 12 invoices, the COBOL chain generates two remittance documents because no single document can carry more than 9 invoices. The 645-byte cheque file record is fixed across the entire cheque-printing pipeline. | One remittance advice per supplier per cycle; line count grows naturally with the number of open items. The data structure decouples completely from any presentation-layer page-width assumption. |
| Recommendation |
✅ ELIMINATE CONSTRAINT — Retire the 9-invoice cap and the fixed-byte cheque record. Rationale: Two of three runs scored ELIMINATE; one scored HYBRID, flagging that the 5×32 address block partially reflects postal-envelope conventions. Consensus is ELIMINATE the platform ceilings; the postal-legibility intent is a downstream rendering concern handled by the Reporting service's PDF template, not a data-model constraint. Implementation: remittance_line rows are unbounded; PDF rendering paginates dynamically; cheque numbers become UUIDs with a human-readable display alias.
|
|
5.1.3 Purchase Invoice Line Item Limit (40-line ceiling)
| Aspect | Legacy System (COBOL) | Target System (Python / PostgreSQL) |
|---|---|---|
| Discovery |
Concho Entity: PurchaseInvoiceLineItemLimit (confidence 0.93)Source: purchase/pl020.cbl:190Root Cause: A 40-line-item ceiling per purchase invoice ( OCCURS 40). Pure COBOL memory/screen budget — not a business rule about how many lines belong on an invoice.
|
open_item rows in PostgreSQL have no count limit. Payment Processing's appropriation logic walks variable-length result sets via SQLAlchemy 2.0 async streaming. |
| Legacy Behavior | Invoices with more than 40 lines must be split into multiple invoice records, introducing artificial document boundaries that the matching/allocation logic must then reconcile. | One invoice equals one logical document equals one set of open_item rows, regardless of line count. No artificial splitting; no reconciliation tax. |
| Recommendation |
✅ ELIMINATE CONSTRAINT — Drop the 40-line ceiling. Rationale: Full consensus across all three runs. Textbook 5.1 example: an OCCURS array width forced an artificial boundary into the data model.Implementation: Schema has no upper bound; pagination at the API boundary is a query-time concern, not a data-model concern. |
|
5.1.4 MySQL Command Buffer Limitation (4096-char SQL statement budget)
| Aspect | Legacy System (COBOL) | Target System (Python / PostgreSQL) |
|---|---|---|
| Discovery |
Concho Entity: MySQL Command Buffer Limitation (confidence 0.87)Source: copybooks/mysql-variables.cpy:73, :93 (Ws-Mysql-Command declared as a 4096-char buffer)Root Cause: The COBOL→MySQL bridge constructs every SQL statement by concatenating into a fixed-length 4096-char working-storage buffer. Statements longer than 4096 chars truncate silently. The constraint is the buffer field, not the database. |
SQLAlchemy 2.0 parameterized queries have no client-side buffer; PostgreSQL accepts multi-MB statements. Common query construction is via the ORM expression language, not string concatenation. |
| Legacy Behavior | Complex multi-join queries on the MySQL bridge must be split into multiple smaller statements, hand-orchestrated by the COBOL caller. A subtle correctness risk: a long WHERE-IN list with many supplier IDs can truncate without warning. |
Parameterized SQL with bind parameters; the driver streams statements of any practical size. No truncation risk. |
| Recommendation |
✅ ELIMINATE CONSTRAINT — Retire the 4096-char ceiling. Rationale: Two of three runs included this; the third runs implicitly retired it by retiring the COBOL→MySQL bridge wholesale. Consensus is ELIMINATE. Implementation: Use SQLAlchemy bind parameters; never concatenate untrusted text into SQL strings. |
|
5.2 Processing Model Constraints
Processing-model constraints shape how code executes — the dispatch pattern, the file-organization requirement, the procedural-skip logic in a read loop. The two entries below are platform artifacts of COBOL's lack of polymorphism, the dual-storage abstraction, and procedural-filter idioms.
5.2.1 Dual Storage Architectural Pattern (acas032 flag-driven dispatch)
| Aspect | Legacy System (COBOL) | Target System (Python / PostgreSQL) |
|---|---|---|
| Discovery |
Concho Entity: DualStorageArchitecturalPattern (confidence 0.77)Source: purchase/pl090.cbl:199, common/acas032.cbl:303, common/paymentsLD.cbl:9Root Cause: Every payment data operation routes through acas032 (and paymentsLD) inspecting FS-Cobol-Files-Used / FA-RDBMS-Flat-Statuses to choose ISAM or MySQL. MySQL autocommit must be OFF for rollback support; programs gate themselves on file-mode at startup.
|
Single transactional path via SQLAlchemy 2.0 async sessions with native context managers. Transactions are declarative; the file-mode gate disappears entirely post-cutover. The dual-storage flag survives only as the SAROC bridge seam during coexistence (see Section 11). |
| Legacy Behavior | The ~650-LOC dual-storage abstraction in acas032 sits between every program and every data operation. Adding a new field or operation requires touching both the ISAM and MySQL branches. The flow shape of the entire payment domain is dictated by this dispatch. |
One repository class per aggregate, one transactional path, one storage backend. Adding a new field is an Alembic migration plus a Pydantic model update. |
| Recommendation |
✅ ELIMINATE CONSTRAINT — Collapse the dual-mode dispatch into a single SQLAlchemy repository. Rationale: Full consensus across all three runs. There is no business reason for the dual mode; it was a migration-era hedge from the original ISAM→MySQL transition. Implementation: Modern service uses one Postgres-backed repository per aggregate. SAROC bridge handles legacy coexistence; the modernized service has a single-storage path post-cutover (no flag, no dispatch). |
|
⚠️ SPECIAL CASE: 5.2.2 is BOTH platform-driven AND business-driven
The exclusion of transaction types 1 and 3, admitting only types 5 and 6 reflects a real business rule (only valid payment operations enter proof). The procedural-skip-inside-a-sort-loop implementation is a platform artifact of COBOL's lack of polymorphic dispatch. The target keeps the business filter while retiring the procedural-skip mechanism.
5.2.2 Payment Transaction Type Filter (admit types 5 & 6; exclude 1 & 3)
| Aspect | Legacy System (COBOL) | Target System (Python / PostgreSQL) |
|---|---|---|
| Platform Constraint (ELIMINATE) |
Concho Entity: PaymentTransactionTypeFilter (confidence 0.85)Source: purchase/pl090.cbl:264, :266Mechanism: Magic-number filter inside a sequential read loop; payment proof processing uses EVALUATE transaction-type-style chains to skip types 1 and 3 and zero-batch/zero-item rows.
|
Replace numeric type codes with a polymorphic Payment domain model where transaction type is a Python enum (e.g., PaymentType.PAYMENT, PaymentType.UNAPPLIED); filter via SQL WHERE payment_type IN (PAYMENT, UNAPPLIED) at the repository layer. |
| Business Requirement (PRESERVE) | The exclusion of types 1 and 3 is not arbitrary — it encodes the domain rule "only payment (type 5) and unapplied-cash (type 6) transactions are eligible for the payment-proof pipeline." This rule is also referenced by Section 9's UnappliedBalanceAllocation business rule. |
Encode the eligibility rule declaratively as a repository predicate; expose it as a domain-named filter (eligible_for_proof()) rather than as a magic-number test. |
| Recommendation |
⚖️ HYBRID APPROACH — Eliminate the magic-number routing; preserve the type-5-vs-type-6 distinction. Rationale: Full consensus across all three runs scored this HYBRID for the same reasons. Implementation: The enum lives in the domain model; the filter lives in the repository; the API exposes it as a named query parameter. The magic numbers 1/3/5/6 survive only as a SAROC translation table for legacy compatibility. |
|
Platform vs Business Driver Analysis
Driver Classification Summary
| Constraint | Category | Driver | Decision | Rationale (one-liner) |
|---|---|---|---|---|
BatchSizeLimitation (99-item ceiling) |
5.1 Capacity | Platform (PIC 9(2) counter) | ✅ ELIMINATE | 99 is the max value of the counter field, not a business rule. |
RemittanceAdviceFormatLimitations (9-invoice cap, etc.) |
5.1 Capacity | Platform (OCCURS 9 + fixed records) | ✅ ELIMINATE | Print-page and terminal-grid artifacts. |
PurchaseInvoiceLineItemLimit (40 lines) |
5.1 Capacity | Platform (OCCURS 40) | ✅ ELIMINATE | COBOL memory budget; no business intent. |
MySQL Command Buffer Limitation (4096-char) |
5.1 Capacity | Platform (fixed buffer field) | ✅ ELIMINATE | Bridge artifact; PostgreSQL has no such ceiling. |
DualStorageArchitecturalPattern (acas032 dispatch) |
5.2 Processing | Platform (migration-era hedge) | ✅ ELIMINATE | No business reason for dual mode; collapses to SQLAlchemy. |
PaymentTransactionTypeFilter (types 5,6 only) |
5.2 Processing | Platform (procedural skip) + Business (type set) | ⚖️ HYBRID | Eliminate magic-number routing; preserve eligibility rule. |
TerminalDimensionRequirements (80×24) |
5.3 UI | Platform (startup-gate) | ✅ ELIMINATE | Web UI is responsive; no fixed grid. |
TerminalDisplayPositioning (fixed coords) |
5.3 UI | Platform (hard-coded coords) | ✅ ELIMINATE | RFC 7807 Problem Details + component-level errors. |
CredentialLengthConstraints (4-char password) |
5.3 UI | Platform (PIC X(4) + substitution cipher) | ✅ ELIMINATE | OAuth2/Cognito + bcrypt; modern entropy policies. |
FinancialDataPrecisionRequirement (COMP-3 s9(8)v99) |
5.4 Data Type | Platform (COMP-3 encoding) + Business (exact decimal) | ⚖️ HYBRID | Eliminate $99,999,999.99 ceiling; preserve no-float invariant. |
DateFormatStandardization (yyyymmdd binary-long) |
5.4 Data Type | Platform (no native date type, century=20) | ✅ ELIMINATE | Postgres DATE/TIMESTAMPTZ + ISO 8601 at API. |
PaymentRecordStructuralIntegrity (237-byte fixed) |
5.4 Data Type | Platform (PIC clauses force fixed-width) | ✅ ELIMINATE | PostgreSQL row with named typed columns; ALTER TABLE instead of recompile cascade. |
5.3 User Interface Constraints
UI constraints below cover the platform-level display surface — terminal grid, fixed screen coordinates, credential-form field widths. Per-screen layout transformation (multi-screen-to-single-page consolidation, function-key replacement, accessibility) is owned by Section 7.
5.3.1 Terminal Dimension Requirements (80×24 startup gate)
| Aspect | Legacy System (COBOL) | Target System (Python / PostgreSQL) |
|---|---|---|
| Discovery |
Concho Entity: TerminalDimensionRequirements (confidence 0.93)Source: common/ACAS.cbl:354Root Cause: System startup validates that the terminal is at least 80 columns × 24 lines and refuses to launch otherwise. Every screen layout in pl080/pl085/pl090/pl100 is shaped to this grid, propagating the constraint downstream into every Payment-entry, allocation, and proof screen.
|
Responsive web UI (browser-served SPA) adapts to any viewport; no validation gate, no fixed grid. Layout decisions become CSS responsive-breakpoint concerns, not data-flow concerns. |
| Legacy Behavior | Users on smaller terminals cannot launch ACAS at all. Users on larger terminals see the same 80×24 layout with empty margins — the extra real estate is wasted. | The Payment-entry workflow that the legacy splits across three terminal screens (pl080/pl090/pl100) collapses into a single responsive web page with payment, allocation, and GL-journal preview presented simultaneously. See Section 7. |
| Recommendation |
✅ ELIMINATE CONSTRAINT — Retire the 80×24 floor. Rationale: Full consensus across all three runs. This is the canonical "platform forced into the data model" pattern — the terminal grid shaped not only the UI but the upstream record sizes and allocation algorithms. Implementation: Section 7 documents per-screen layout consolidation; Section 5.3.1 records the retirement of the startup-gate itself. |
|
5.3.2 Terminal Display Positioning (fixed error-message coordinates)
| Aspect | Legacy System (COBOL) | Target System (Python / PostgreSQL) |
|---|---|---|
| Discovery |
Concho Entity: TerminalDisplayPositioning (confidence 0.88)Source: copybooks/irsfsdispfe.cob:4, :7Root Cause: Error messages are hard-coded to terminal positions 2430, 2443, 2448, 2472 (offsets in the 80×24 grid). Foreground color 4 (red) requires a color-capable terminal. |
RFC 7807 Problem Details JSON responses at the API boundary; HTML/component-level error banners or field-level inline validation in the UI. No hard-coded coordinates; accessibility-clean color tokens with semantic naming. |
| Legacy Behavior | Adding a new error condition requires picking a free terminal position that doesn't collide with any existing one — effectively a manual layout-management overhead that scales linearly with error-message count. | Errors are surfaced contextually next to the field or operation that produced them. Layout is composable; no central registry of positions to maintain. |
| Recommendation |
✅ ELIMINATE CONSTRAINT — Retire the fixed-coordinate error display. Rationale: Two of three runs included this; the third implicitly retired it by retiring the terminal UI wholesale. Consensus is ELIMINATE. Implementation: Error contracts at the API layer; component-level rendering in the UI. The legacy coordinate registry is decommissioned. |
|
5.3.3 Credential Length Constraints (4-char password ceiling)
| Aspect | Legacy System (COBOL) | Target System (Python / PostgreSQL) |
|---|---|---|
| Discovery |
Concho Entity: CredentialLengthConstraints (confidence 0.95)Source: copybooks/wsmaps01.cob:10, :11Root Cause: Password field exactly 4 chars ( PIC X(4)); username 32 chars (PIC X(32)). The 4-char password ceiling exists because of a custom substitution-cipher encoding tied to the legacy authentication implementation — a platform constraint pretending to be a security policy.
|
OAuth2 / Cognito / OIDC with bcrypt or Argon2 hashing; modern minimum-length and entropy policies; no client-side substitution-cipher pseudo-encryption. |
| Legacy Behavior | A 4-character password has roughly 456,976 possible values for a 26-letter alphabet — brute-forceable in milliseconds. This is a security-grade failure of the legacy platform that has propagated through every authentication-touching program. | The modern identity provider is the system of truth for credentials; the application services never see plaintext passwords and never store password material. |
| Recommendation |
✅ ELIMINATE CONSTRAINT — Retire the 4-char password ceiling and the substitution cipher. Rationale: Two of three runs categorized this in 5.3 (UI/credential-form field); one categorized it in 5.4 (data-type rigidity). Majority consensus is 5.3, since the constraint manifests at the credential-form layer with terminal-bound field widths. All three runs scored ELIMINATE. Implementation: OAuth2 integration; bcrypt-hashed secrets at rest if any local credentials exist; minimum-length and entropy enforcement at the policy layer. |
|
5.4 Data Type Constraints
Data-type constraints define the shape of a field or record — precision, byte width, format string. The three entries below are platform artifacts of COBOL's PIC declarations and COMP-3 packed-decimal encoding. Modern strongly-typed Python plus rich PostgreSQL types retire them.
⚠️ SPECIAL CASE: 5.4.1 is BOTH platform-driven AND business-driven
The requirement for exact decimal precision (no floating-point drift on currency math) is a real audit-grade business rule. The COMP-3 packed-decimal encoding with an $99,999,999.99 ceiling is a platform artifact. The target preserves the precision invariant and eliminates the ceiling.
5.4.1 Financial Data Precision Requirement (COMP-3 s9(8)v99)
| Aspect | Legacy System (COBOL) | Target System (Python / PostgreSQL) |
|---|---|---|
| Platform Constraint (ELIMINATE) |
Concho Entity: FinancialDataPrecisionRequirement (confidence 0.93)Source: copybooks/fdbatch.cob:35, copybooks/glwspint.cob:8,:15, copybooks/glwspc.cob:10, copybooks/seledger.cob:9, copybooks/wssys4.cob:10, copybooks/fdpost.cob:8, general/gl071.cbl:107, general/gl080.cbl:152Mechanism: All money fields are COMP-3 packed decimal s9(8)v99, capping at $99,999,999.99. Some flavors use 9(9)v99 for higher-precision ops, introducing mixed precision across copybooks.
|
PostgreSQL NUMERIC(15,2) with Python decimal.Decimal. No 8-digit ceiling; mantissa/scale uniform across services. Single source of truth in Pydantic models. |
| Business Requirement (PRESERVE) | The exact-decimal invariant — never use IEEE-754 floats for currency math — is an audit-grade business rule that survives the modernization. | Decimal in Python and NUMERIC in PostgreSQL preserve exact decimal arithmetic. Pydantic validators enforce the no-float invariant at the API boundary; the type system enforces it inside the service. |
| Recommendation |
⚖️ HYBRID APPROACH — Eliminate the COMP-3 encoding and the $99,999,999.99 ceiling; preserve the exact-decimal invariant. Rationale: Full consensus across all three runs. The HYBRID call here is what makes the verdict defensible — calling this PRESERVE would lock the modern system into an artificial 8-digit ceiling; calling it ELIMINATE without the PRESERVE side risks introducing float drift into financial math. Implementation: NUMERIC(15,2) in PostgreSQL; Decimal in Python; Pydantic field validators reject any float inputs at the API boundary.
|
|
5.4.2 Date Format Standardization (binary-long yyyymmdd, century=20 hard-code)
| Aspect | Legacy System (COBOL) | Target System (Python / PostgreSQL) |
|---|---|---|
| Discovery |
Concho Entity: DateFormatStandardization (confidence 0.90)Source: irs/irs.cbl:555, stock/st000.cbl:167,:192, copybooks/wsmaps03.cob:7,:30, README.TXT:160Root Cause: All dates stored binary-long yyyymmdd; u-date field exactly 10 chars; Date-Form parameter 1/2/3 (UK/USA/International); century hard-coded 20 for years 2000–2099 (Y2K-era patch).
|
Python date/datetime with PostgreSQL DATE/TIMESTAMPTZ; ISO 8601 at API boundary; locale-aware rendering in the UI; no century hard-code; no Date-Form enum confusion. |
| Legacy Behavior | Every date comparison requires manual parsing; the century=20 hard-code will need re-patching in 2100; the regional Date-Form enum is the wrong abstraction layer (it conflates storage with display). |
Native date arithmetic; timezone-aware comparisons; ISO 8601 wire format with locale-driven display at the UI layer. The system is good through year 9999. |
| Recommendation |
✅ ELIMINATE CONSTRAINT — Retire the binary-long encoding, the century hard-code, and the regional Date-Form enum.Rationale: Two of three runs included this; the third subsumed it under broader data-type rigidity. Consensus is ELIMINATE. Implementation: Native types in Python and PostgreSQL; ISO 8601 at the wire; locale-aware UI rendering. |
|
5.4.3 Payment Record Structural Integrity (237-byte fixed Payment record, 113-byte sort record)
| Aspect | Legacy System (COBOL) | Target System (Python / PostgreSQL) |
|---|---|---|
| Discovery |
Concho Entity: PaymentRecordStructuralIntegrity (confidence 0.80; convergence-related to RecordStructureSizeValidation at 0.90)Source: copybooks/fdpay.cob:7,:16, purchase/pl090.cbl:206Root Cause: Payment proof sort records must be exactly 113 characters matching open-item-record-5; payment file records use a 237-byte fixed layout with COMP fields chosen specifically "for SQL compatibility instead of binary fields." A runtime FUNCTION LENGTH assertion halts processing on width mismatch.
|
payment and open_item tables in PostgreSQL; SQLAlchemy ORM models do not require byte-aligned width matching across operations. ALTER TABLE ADD COLUMN is a one-line migration with Alembic; no recompilation cascade. |
| Legacy Behavior | Adding a new field to the payment record requires touching every program that references the record, recompiling every copybook consumer, and re-asserting the 237-byte invariant. Schema evolution is a multi-program ripple. | Field addition is an Alembic migration; consumers re-pin their Pydantic models at their own pace; backward-compatible reads tolerate unknown columns. |
| Recommendation |
✅ ELIMINATE CONSTRAINT — Retire fixed-byte record layouts. Rationale: Two of three runs included this in Section 5; categorization split (Run 2 placed it in 5.1 capacity; Run 3 in 5.4 data type). Consensus category is 5.4 since the constraint is fundamentally about field-and-record shape, not about volume. All three included runs scored ELIMINATE. Implementation: Schema lives in Alembic migrations; field-level constraints in Pydantic; the 237/113-byte assertions are gone. |
|
Summary: Platform Affinity Decisions
The headline metric, sourced from Concho's implementation_constraints insight: of the 197 implementation constraints in the ACAS codebase, 197 (100%) are hard-coded as literals in copybooks or COBOL source — zero are externalized as configuration. The aggregate brittleness score is 0.49. The target stack changes that posture wholesale: type widths live in Pydantic and Alembic migrations, limits live in environment variables or per-tenant settings, formatting lives in service-owned templates. The 100%→low-percentage hard-coding shift is itself a major unlock independent of any single constraint.
| Constraint | Category | Driver | Decision | Modern Equivalent |
|---|---|---|---|---|
BatchSizeLimitation |
5.1 | Platform | ✅ ELIMINATE | batch_control table; nullable ceiling; per-tenant override. |
RemittanceAdviceFormatLimitations |
5.1 | Platform | ✅ ELIMINATE | Unbounded remittance_line rows; PDF pagination; UUID cheque IDs. |
PurchaseInvoiceLineItemLimit |
5.1 | Platform | ✅ ELIMINATE | open_item rows; no schema-level cap. |
MySQL Command Buffer Limitation |
5.1 | Platform | ✅ ELIMINATE | SQLAlchemy parameterized queries; no fixed buffer. |
DualStorageArchitecturalPattern |
5.2 | Platform | ✅ ELIMINATE | One SQLAlchemy repository per aggregate; SAROC handles coexistence. |
PaymentTransactionTypeFilter |
5.2 | Platform + Business | ⚖️ HYBRID | Python enum + repository predicate eligible_for_proof(). |
TerminalDimensionRequirements |
5.3 | Platform | ✅ ELIMINATE | Responsive web UI; no startup gate. |
TerminalDisplayPositioning |
5.3 | Platform | ✅ ELIMINATE | RFC 7807 Problem Details + component-level errors. |
CredentialLengthConstraints |
5.3 | Platform | ✅ ELIMINATE | OAuth2/Cognito + bcrypt; no field-length ceiling. |
FinancialDataPrecisionRequirement |
5.4 | Platform + Business | ⚖️ HYBRID | NUMERIC(15,2) + Decimal; preserve no-float invariant. |
DateFormatStandardization |
5.4 | Platform | ✅ ELIMINATE | Native DATE/TIMESTAMPTZ; ISO 8601 at API. |
PaymentRecordStructuralIntegrity |
5.4 | Platform | ✅ ELIMINATE | PostgreSQL row + Alembic migrations; no fixed byte layout. |
Distribution. 10 ELIMINATE / 2 HYBRID / 0 PRESERVE across 4 categories (5.1=4, 5.2=2, 5.3=3, 5.4=3). Every cited constraint is Concho-grounded: each row above is traceable to a named implementation_constraint entity in Concho cycle 8 with at least one file:line reference. The full per-constraint citation chain lives in the per-run handoffs and the reconciliation handoff (handoffs/modernization-handoff-platform-affinity-reconciler-012.md).
Key unlock framing. Roughly all the platform-driven constraints affecting Payment Processing are retired by the target stack. The two HYBRID entries split cleanly into a platform-imposed ceiling that is eliminated and a business invariant that the target preserves in a more flexible form (the exact-decimal precision rule for currency math; the type-set eligibility for payment-proof processing). No surveyed constraint required PRESERVE-only treatment within Section 5's scope.
6. Technical Debt Analysis
6.1 What This Section Looks Like for Other Modernizations
When the modernization target is a more recent stack — ASP.NET 4.7 on Windows Server 2012 R2, a Java 8 monolith on JBoss, a Rails 4 app on Ruby 2.4 — the analysis surfaces concrete artifacts:
- A row-per-dependency table of every
.csproj/pom.xml/Gemfileentry, the version pinned today, the upstream EOL date, the CVE backlog, and the modern replacement the target architecture imports. - Architecture anti-patterns the legacy code has accreted — God classes, distributed monoliths, shared mutable session state, RPC-over-HTTP — with a refactoring path per pattern.
- Dependency-graph hotspots: which libraries fan in to the most code, and which of those have no maintained successor (the "you must rewrite this" set).
- Operating-system / runtime end-of-life timelines that constrain how long the lift-and-shift option remains viable before vendor support expires.
Those reports run 10–30 pages and feed directly into the dependency-replacement steps of the artifact-generation workflow. They matter when the modernization is mostly a refresh — same language, same paradigm, modern infrastructure.
6.2 Why ACAS Is Different
ACAS is a fully-legacy COBOL system: dual ISAM/MySQL storage, terminal UI, packed-decimal data types, and a runtime (GnuCOBOL on Linux) that exists almost exclusively to keep applications like this one running. There is no library-by-library upgrade path because there are no libraries in the modern sense — the data structures are copybooks, the persistence layer is hand-rolled file I/O, and the UI is ACCEPT/DISPLAY statements writing directly to an 80×24 screen. The "tech debt" is the whole stack.
So instead of a dependency catalog, the case for the modernization compresses into a short list of motivations that anyone considering a project like this already knows:
- Vendor / runtime lock-in. Mainframe operators charge mainframe prices; even off-mainframe COBOL runtimes carry licensing and support overhead disproportionate to the code's business value. The platform vendor's roadmap is not your roadmap.
- Workforce attrition. The engineers who built and maintained ACAS-class systems through the 1980s and 1990s are retired, retiring, or no longer alive. The talent pipeline for COBOL maintenance is functionally closed; new hires arrive with Python, TypeScript, and Go on their résumés, not
PIC 9(8) COMP-3. - Integration friction. Modern business systems — CRM, BI, analytics, accounts-payable automation, regulatory reporting — speak REST, GraphQL, event streams, and OAuth. Every integration with the legacy stack is a custom adapter; every adapter is a fragile dependency. The cost is paid every quarter forever.
- User experience ceiling. An 80×24 terminal restricts what the business process itself can become. Cross-screen workflows, real-time validation, mobile access, and audit trails that span multiple actors are all available the moment the UI moves to the web; none are reachable while users are pressing function keys against a fixed grid.
- Platform stagnation. The legacy runtime receives security patches and not much else. Every new capability — observability, autoscaling, managed databases, AI/ML services, modern CI/CD — is something the modern stack ships with and the legacy stack will never have.
If you are reading this report you already know the above, which is why the list is short. The substantive analysis — what specifically gets built to replace the legacy stack — lives in Section 4: Target Architecture, Section 5: Platform Affinity Analysis, and the technical chapters that follow.
7. UI/UX Transformation Examples
TL;DR — Seven screens transformed, five legacy programs consolidated
This section transforms six legacy COBOL terminal screens (pl900 menu, pl080 entry, pl090 proof, pl100 cash post, pl940 cheque run, pl960 remittance) into modern React (TypeScript) components backed by the FastAPI payment-api / payment-batch / payment-reporting services. The seventh screen — the Payment Cycle Workbench — is a "Beyond 1:1" consolidated view that did not exist in the legacy because each data fragment lived in a different physical file managed by a different program. Modern wins: responsive layout, inline MOD-11 supplier validation (BR-PAY-002), live appropriation preview (BR-PAY-004), inline GL posting, S3-backed remittance PDFs.
targetPatterns. The Section 7.9 field inventory table that follows enumerates every user-facing field shown across these mockups.
7.1 UI Affinity Analysis
Before walking through individual screen transformations, the table below maps every salient legacy UI element to a modernization disposition. Every legacy element traces to actual COBOL source code discovered by Concho; the dispositions follow the platform-affinity reconciliation in Section 5.
| Legacy UI Element | Source Reference | Disposition | Modernization Notes |
|---|---|---|---|
| Numbered menu dispatch (6 options + X) | purchase/pl900.cbl lines 130–195 (menu-reply + CALL ws-called) |
Remove | Numbered menu-reply pic x entry and dynamic CALL ws-called dispatch eliminated. SPA sidebar with URL-based routing replaces the menu entirely. |
| Bordered "box" form layout | purchase/pl080.cbl lines 343–348 (the **** border DISPLAY literals) |
Remove | Hand-drawn asterisk borders at fixed row/col positions replaced by a responsive card layout with semantic HTML form sections. |
| 7-character supplier account entry (no live validation) | pl080.cbl line 372 (accept pay-customer at 0572) + common/maps09.cbl lines 90–145 (MOD-11) |
Modernize | Blank-prompt 7-character entry replaced by autocomplete with inline MOD-11 verification (BR-PAY-002 fires on each keystroke, not post-submit). Operator sees red ring + check-digit error before they leave the field. |
| Sequential per-invoice appropriation prompts | pl080.cbl lines 585–640 (accept-money2 per line) |
Modernize | One-by-one accept loop replaced by a single appropriation-preview table. The operator sees which invoices will be paid, what discount will be taken (BR-PAY-004), and any unapplied remainder before clicking Save — not as a sequence of after-the-fact prompts. |
| 9-element OCCURS on payment record | copybooks/fdpay.cob line 21 (filler occurs 9) |
Remove | The OCCURS 9 structural constraint propagates into pl960.cbl's 9-line remittance loop and limits each Pay-Record to 9 folios. PostgreSQL child rows on open_item and payment_lines remove the limit entirely. |
| 99-item batch ceiling | copybooks/fdbatch.cob line 18 (Items pic 99) |
Mitigate | 2-digit pic 99 field is a hard ceiling in COBOL. Target uses BIGINT on batch_control.items_count but keeps a feature-flag-gated soft 99-item cap during the SAROC coexistence window (BR-PAY-008) so the legacy bridge can translate batches 1-to-1. |
| Fixed 80×24 character grid | All pl080–pl960 programs (DISPLAY AT LLCC with hard-coded LL/CC) |
Remove | Fixed-position DISPLAY ... AT 0541 replaced by responsive grid. The Payment Cycle Workbench (7.7) shows ~10x more information density without horizontal scroll. |
| 3-character YES/NO confirm | purchase/pl100.cbl line 312 (accept wx-reply) expecting YES/NO literal |
Modernize | Text-typed YES/NO confirmation replaced by a confirmation modal that previews the GL journal entry (debit/credit pair) before commit. |
| Y/N unapplied-balance prompt | pl080.cbl lines 421–446 (accept-unappl-reqst) |
Modernize | BR-PAY-007 prompt preserved as a checkbox that conditionally appears when purch-unapplied > 0. Same 5/6 transaction-type sentinel values preserved server-side for SAROC compatibility. |
| Line-printer remittance output | pl960.cbl lines 14–16 (lpr -P command construction) |
Modernize | Fixed-width text print stream destined for line-printer hardware replaced by an S3-backed PDF per payment (BR-PAY-010). Content parity (supplier, invoice refs, Cheque n / BACS to your Bank text, total) preserved. |
| Escape-key cancellation | pl080.cbl (cob-crt-status = cob-scr-esc) |
Preserve | Escape still cancels the current action. Keyboard shortcuts (Ctrl+S to save, Ctrl+Enter to submit) added for power users who don't want the mouse. |
7.2 Payment Processing Menu (pl900) → SPA Sidebar Routing
The Payments Menu (pl900) is the legacy entry point: 6 numbered options plus an X-to-exit. In the modern system the menu disappears entirely — the operator lands on the Payment Cycle Workbench (7.7) and side-bar items expose direct access to the constituent screens. The legacy mockup below uses DISPLAY literals verified against purchase/pl900.cbl:130-195.
Legacy: 80×24 Terminal (pl900)
PL900 (3.02.05) Payment Menu 21/05/2026 Select one of the following by number :- [ ] (1) Generate Payments to be made (2) Amend Payments (3) Proof Payments (4) Generate Payments (5) Print Payment Register (6) Print remittance advices (X) Return to system menu
[1] menu-reply pic x accepted at row 5, col 43
[2] Dynamic CALL ws-called dispatch (pl910–pl960)
[3] No view of state until callee renders its own screen
Modern: React SPA sidebar + workbench-first landing
Routes: /payments/workbench, /payments/new, /payments/batches/:n, /payments/cheque-runs, /payments/remittances
Platform Affinity Wins
- Eliminated:
CALL ws-calleddispatch (Concho entity Payment Menu Navigation System, conf 0.67) - ~40% faster: deep-link to operation vs menu-traversal
- Persistent state: workbench remembers where the operator was in the cycle (no "what did I just do?" re-orientation)
- Accessible: ARIA navigation landmarks; keyboard sidebar traversal
7.3 Payment Data Entry (pl080) → Live Appropriation Preview
The Payment Data Entry program (pl080) is the heart of the payment cycle. The operator keys in a payment date, supplier account, and value, and the program then walks the supplier's outstanding invoices one by one, prompting the operator to accept or modify each appropriation. The modern interface collapses that imperative loop into one form with a live appropriation preview — the operator sees the full result before clicking Save.
Legacy: 80×24 Terminal (pl080)
PL080 (3.02.05) Payment Data Entry 21/05/2026 ACME ELECTRICAL SUPPLIES LTD 12 INDUSTRIAL ESTATE CRANBORNE ROAD POTTERS BAR HERTS EN6 3JN **************************************** *Date [21/05/2026]*A/C Nos [SUP0001]* *** *** *Value [ 1250.00 ]*Batch [00042/017]* **************************************** Current Balance - 8,742.55 Unapplied Balance - 25.00 Do you wish to allocate the unapplied Balance to this account? [Y] Inv# 00042001 21/04/2026 Net 1000.00 Disc 25.00 Apply 975.00 ....Fully Paid Inv# 00042002 18/04/2026 Net 250.00 Disc 0.00 Apply 250.00 ....Fully Paid Batch Total - 1,250.00
[1] accept pay-date at 0549, pay-customer at 0572, pay-value at 0749 — sequential keyed entry
[2] Per-invoice accept-money2 loop walks each outstanding invoice (pl080.cbl:585-640)
[3] Batch position display "00042/017" tracks bl-next-batch / k against the 99-item ceiling
Modern: React + FastAPI live preview
| Invoice | Date | Net | Disc | Apply |
|---|---|---|---|---|
| INV-00042001 | 2026-04-21 | $1,000.00 | $25.00 | $975.00 |
| INV-00042002 | 2026-04-18 | $250.00 | $0.00 | $250.00 |
| Batch total | $1,250.00 | $25.00 | $1,225.00 + $25.00 unapplied | |
Platform Affinity Wins
- BR-PAY-002 inline: MOD-11 check (
maps09.cbl:90-145) runs on each keystroke. Operator never submits a payment with a typo'd supplier code. - BR-PAY-004 visible: the $25.00 discount on INV-00042001 is shown before commit, not as an after-the-fact prompt.
- BR-PAY-007 contextual: the unapplied checkbox only appears when
purch-unapplied > 0— matches the legacy conditional display. - BR-PAY-008 visible: "17/99" indicator surfaces the batch-position constraint as a soft cap during coexistence.
- ~70% fewer keystrokes: two appropriation lines previously required ~14 keypresses each (accept-money2 loop); modern form is fill-once-submit-once.
7.4 Batch Dashboard / Proof Sort (pl090) → Sortable Batch Table
The proof sort program (pl090) reads open-item records, filters them by transaction type (BR-PAY-003 — only type 2 invoices and type 4 debits qualify), and emits a printed listing that the controller signs off. The modern interface materializes the sort output as an interactive table with status pills for the batch lifecycle (BR-PAY-006).
Legacy: 132-column proof listing
PAYMENT PROOF SORT - BATCH 00042 21/05/2026 SUPPLIER NAME BATCH/ITEM APPROP DEDUCT PAID TYPE ======== ============================ ========== ====== ====== ====== ===== SUP0001 ACME ELECTRICAL SUPPLIES LTD 00042/001 1000.00 25.00 975.00 Pay SUP0001 ACME ELECTRICAL SUPPLIES LTD 00042/002 250.00 0.00 250.00 Pay SUP0002 BAKER BUILDING SUPPLIES LTD 00042/003 500.00 0.00 500.00 Pay SUP0003 COMET COURIERS LIMITED 00042/004 750.00 15.00 735.00 Pay SUP0004 DELTA DEALS WHOLESALE 00042/005 1200.00 30.00 1170.00 Pay SUP0005 ECHO ENTERPRISES LLP 00042/006 400.00 0.00 400.00 Pay Payment Totals 4100.00 70.00 4030.00 Journal Totals 0.00 0.00 0.00
[1] Sort filter if oi-type = 1 or 3 ... skip (BR-PAY-003, sl090.cbl:262-267)
[2] Composite key oi-invoice = oi-b-nos * 1000 + oi-b-item (BR-PAY-008)
[3] No on-screen batch lifecycle indicator — the listing is the indicator
Modern: React batch dashboard
| Supplier | Batch/Item | Approp | Deduct | Paid | Status |
|---|---|---|---|---|---|
| SUP0001 ACME ELECTRICAL | 00042/001 | $1,000.00 | $25.00 | $975.00 | PROOFED |
| SUP0001 ACME ELECTRICAL | 00042/002 | $250.00 | $0.00 | $250.00 | PROOFED |
| SUP0002 BAKER BUILDING | 00042/003 | $500.00 | $0.00 | $500.00 | PROOFED |
| SUP0003 COMET COURIERS | 00042/004 | $750.00 | $15.00 | $735.00 | PROOFED |
| SUP0004 DELTA DEALS | 00042/005 | $1,200.00 | $30.00 | $1,170.00 | PROOFED |
| SUP0005 ECHO ENTERPRISES | 00042/006 | $400.00 | $0.00 | $400.00 | PROOFED |
Platform Affinity Wins
- BR-PAY-003 as a SQL filter:
WHERE type IN (2, 4) AND (batch_no <> 0 OR batch_item <> 0)— same predicate assl090.cbl:262-267, expressed once and indexed. - BR-PAY-006 visible: ENTERED → APPROPRIATED → PROOFED → POSTED pills with click-to-deep-link history.
- All 6 rows shown with sortable columns; no "next page" keystroke required.
- Sign-off in one click vs printing, signing, and re-keying.
7.5 Purchase Cash Posting (pl100) → Inline GL Preview & Commit
Cash posting (pl100) reads the proofed batch and applies each payment to the supplier's purch-current balance, writing inline GL journal entries via the BL-Write call. The legacy operator confirms with a 3-character YES/NO at pl100.cbl:312; the modern interface shows the GL debit/credit pair before commit.
Legacy: 80×24 Terminal (pl100)
PL100 (3.02.05) Purchase Cash Posting 21/05/2026
OK to post payment transactions (YES/NO) ? < > enter {CR}
[1] accept wx-reply at 1256 — expects literal YES or NO
[2] No batch summary; operator must remember which batch they proofed
[3] GL postings (BL-Write) happen inside the loop — no preview
Modern: GL preview modal
| Account | Description | Debit | Credit |
|---|---|---|---|
| 5000 Trade Creditors | Batch 42 payments | $4,030.00 | |
| 5050 Discounts received | Early-pay deductions | $70.00 | |
| 1200 Bank | Outflow | $4,100.00 | |
| Totals (debit = credit, OK) | $4,100.00 | $4,100.00 | |
UPDATE payment SET status='POSTED' + INSERT gl_posting; rollback both on any error.
Platform Affinity Wins
- Inline GL preserved: per General Ledger Posting Integration (Concho conf 0.88) the GL journal lives in the same DB transaction as the payment status update — identical semantics to
pl100.cbl:329+414, modern transactional substrate. - Preview before commit: the debit/credit pair is visible before the operator clicks Post; the legacy operator had to wait for the printed posting report.
- BR-PAY-001 surfaced: if
purchase_posting_state.flag ≠ 'posted'the Post button is disabled with a tooltip explaining why (legacy behaviour was to display PL121 and exit). - Audit hardened: every post writes an append-only row into
payment_auditwith the W3C trace context that originated at the operator's HTTP request.
7.6 Cheque / BACS Run (pl940) & Remittance Inbox (pl960) → Per-Payment PDF
After posting, pl940 inspects each payment's pay-sortcode field: zero means a cheque (the cheque-number counter increments), non-zero means a BACS payment (the c-cheque field is filled with the literal BACS). pl960 then reads the resulting cheque-file and emits one remittance per record — with the 9-line OCCURS limit on the invoice list (pl960.cbl:208-216) hard-coded by the COBOL record format. The modern flow splits cheque output from BACS (two pipelines, one per method), removes the 9-line ceiling, and materializes each remittance as an S3-backed PDF.
Legacy: line-printer remittance (pl960)
+--------------------------------------------------+
| REMITTANCE ADVICE |
+--------------------------------------------------+
To ACME ELECTRICAL SUPPLIES LTD From ACAS LTD
12 INDUSTRIAL ESTATE 12 HIGH STREET
CRANBORNE ROAD POTTERS BAR
POTTERS BAR
HERTS EN6 3JN
A/C: SUP0001 Date: 21/05/2026
----------------------------------------------
Invoice Folio Amount
----------------------------------------------
INV-00042001 F-000001 975.00
INV-00042002 F-000002 250.00
----------------------------------------------
Cheque 00012345 Total: $1,225.00
+ + + + + + + + + + + + + + + + + + + + + + +
To BAKER BUILDING SUPPLIES LTD From ACAS LTD
...
[1] Print stream sent to lpr -P queue (pl960.cbl:14-16)
[2] Hard 9-line invoice cap from perform varying z from 1 by 1 until z > 9 (pl960.cbl:208)
[3] Cheque vs BACS chosen via c-cheque not = "BACS" at line 281
Modern: Remittance Inbox
| Supplier | Method | Total | Status | Actions |
|---|---|---|---|---|
| SUP0001 ACME ELECTRICAL | Cheque 12345 | $1,225.00 | PDF ready | Download · Email |
| SUP0002 BAKER BUILDING | BACS 20-12-34 | $500.00 | Delivered | Download |
| SUP0003 COMET COURIERS | BACS 30-99-12 | $735.00 | Delivered | Download |
| SUP0004 DELTA DEALS | Cheque 12346 | $1,170.00 | PDF ready | Download · Email |
| SUP0005 ECHO ENTERPRISES | BACS 40-22-18 | $400.00 | Delivered | Download |
| SUP0006 FOX ENTERPRISES | BACS 50-15-77 | $0.00 (test) | Delivered | Download |
Platform Affinity Wins
- BR-PAY-005 dual pipeline: cheque payments produce a printable cheque PDF; BACS payments publish
acas.payment.issued.bacsevents for the banking-integration consumer. - BR-PAY-010 PDF parity: content (supplier, invoice refs, Cheque n vs BACS, total) is byte-for-byte equivalent to the legacy print stream — verified by a content-parity mitigation test.
- 9-line OCCURS limit removed: PDFs support arbitrary-length invoice lists; suppliers with 30 invoices no longer get truncated remittances.
- Self-service: suppliers see their own remittances in a portal; reduces support load.
7.7 Payment Cycle Workbench — "Beyond 1:1" Consolidated View
The legacy system spreads the payment lifecycle across five programs (pl080 entry, pl090 proof, pl100 post, pl940 cheque, pl960 remit) and four physical files (pay.dat, openitm5.dat, fdbatch records, GL batch records). The operator must remember the program names, the navigation sequence, and which file is authoritative at each step. The Payment Cycle Workbench collapses all five programs into one SPA page; every cell is one SQL JOIN away from payment, open_item, batch_control, gl_posting, and remittance_advice. This is the "Beyond 1:1" consolidation called out in the use-case storyboard — impossible on the 80×24 terminal, easy on the modern stack.
Payment Cycle Workbench · Batch #42 · Cycle Day 2026-05-21
payment)open_item)| Invoice | Net | Disc | Applied |
|---|---|---|---|
| INV-00042001 | $1,000.00 | $25.00 | $975.00 |
| INV-00042002 | $250.00 | $0.00 | $250.00 |
batch_control)Status: PROOFED · Controller sign-off pending
Entered: 2026-05-21 14:12 · Proofed: 14:38
Items in batch: 6 · Total gross: $4,100.00 · Total discount: $70.00
gl_posting on Post)| DR 5000 Trade Creditors | $4,030.00 |
| DR 5050 Discounts received | $70.00 |
| CR 1200 Bank | $4,100.00 |
Delivery: Email + supplier portal
Status: pending — will materialise after
acas.cheque-batch.generated event firesInvoice lines: 2 (no 9-line OCCURS limit)
Every cell on this workbench is a JOIN that was structurally impossible on the legacy terminal: each row lived in a different physical file managed by a different COBOL program. With the shared 5-table PostgreSQL schema and the FastAPI service surface, the Workbench is a single React route backed by a small set of read-side endpoints from payment-api and payment-reporting. This is the canonical demo screen: open the Workbench, click "Advance cycle" five times, and watch a payment travel from ENTERED to REMITTED with no program switches.
Scene 1: Three Screens Become One
The unified payment screen replaces three separate legacy programs — the entry → proof → post trio that the operator walked across three terminal sessions. The comparison below shows the three green-screen terminals that the modern single page replaces. These match the legacy mockups embedded in the generated SPA (Workbench Scene 1).
PL080 (3.02.05) Payment Data Entry ACME MANUFACTURING LTD **************************************** *Date [21/05/2026] *A/C Nos [ACME006 ] * *Value [4,250.00 ] *Batch [00042/017] * **************************************** Select option : [_] (1) Show outstanding invoices (2) Enter payment (3) Apply discount (4) Save and exit Enter further payments? (Y/N) [Y]
PL090 (3.02.05) Proof Sort Batch 00042 Items: 17 of 99 -- Released to sort stream -- ACME006 INV2026001 525.00 (2) ACME006 INV2026002 800.00 (2) ACME006 INV2026003 305.00 (2) ACME006 INV2026004 620.00 (2) ACME006 INV2026005 410.00 (2) ... Sign-off: [___] PROOF PRINTED
PL100 (3.02.05) Cash Posting Batch 00042 Posted GL pair (BL-Write inline): Dr 410000 Creditors 4,250.00 Cr 200100 Bank 4,225.00 Cr 4090 Discount 25.00 OK to post (YES/NO)? [YES]
Three separate programs. Three separate terminal sessions. A numbered menu (pl900) to navigate between them. In the legacy system, an operator entering a payment must invoke pl080 to capture the line items, exit to the menu, invoke pl090 to run proof, exit again, and invoke pl100 to post and write GL — with hand-offs between sessions and no shared view. The modern unified screen presents the entire entry → proof → post chain on a single page; the operator never leaves the view, and the GL pair lands inline as part of the post result.
Scene 2: Supplier Autocomplete
The operator types "ACM" in the supplier search field. An autocomplete dropdown appears showing matching suppliers with their account codes, outstanding balances, and open invoice counts — financial context that was invisible in the legacy system until after the supplier was selected. In the legacy PL080, the operator typed the exact 7-character account code (accept pay-customer at 0572) at a blank prompt with no search capability and no financial preview.
$16,847.50 outstanding • 6 open invoices
$0.00 outstanding • 0 open invoices
$4,220.00 outstanding • 2 open invoices
The autocomplete reveals the second platform affinity win: the same business need (find and select a supplier) with a completely modernized interaction model. Fuzzy search, financial preview, and immediate visual feedback replace the legacy's exact-code-or-nothing approach.
Scene 3: Allocation Preview — Pending State
After selecting ACME Corporation and entering a payment amount, the allocation preview table populates automatically. This is the pending state — nothing has been committed. The table shows how the payment will be applied to outstanding invoices, with BR-PAY-002 (Payment Appropriation Logic) calculating discount eligibility in real time. Invoices are sorted oldest-first, with estimated discount amounts prefixed by a tilde (~) to signal they are projections, not final values.
| ☑ | Invoice | Date | Amount | Est. Discount | Pending Allocation | Status |
|---|---|---|---|---|---|---|
| 48271 | 07 Jan 2026 | $3,250.00 | — | $3,250.00 | Will Pay | |
| 48356 | 15 Jan 2026 | $2,890.00 | — | $2,890.00 | Will Pay | |
| 48412 | 28 Jan 2026 | $4,125.50 | — | $4,125.50 | Will Pay | |
| 48503 | 10 Feb 2026 | $2,340.00 | ~$46.80 | $2,293.20 | Will Pay | |
| 48621 | 22 Feb 2026 | $1,895.00 | — | — | Excluded | |
| 48744 | 03 Mar 2026 | $2,347.00 | — | — | Excluded |
Selective Invoice Exclusion — Operators uncheck invoices to exclude them from the current payment run. Remaining checked invoices are still allocated oldest-first per BR-PAY-002. The legacy system (
pl080.cbl)
has no pre-selection mechanism — the only way to skip an invoice was to enter zero during line-by-line
terminal interaction. This is a net-new modernization enhancement, not a deviation from existing behavior.
The allocation preview shows BR-PAY-002 in action: invoices are allocated oldest-first, with discount eligibility calculated per the legacy algorithm (add 1 to oi-deduct-days, compare against payment date). Invoice 48503 has an early payment discount — it falls within the deduction terms window relative to the payment date. The first three invoices are too old for discount eligibility. The operator has exercised BR-PAY-008 (Selective Invoice Exclusion) by unchecking invoices 48621 and 48744 — these are greyed out with an "Excluded" badge and carry no allocation. The remaining four checked invoices are fully funded at $11,625.50, with an estimated discount of ~$46.80 on invoice 48503. In the legacy system, the only way to skip an invoice was to enter zero during line-by-line terminal interaction in the accept-money2 loop — there was no pre-selection mechanism.
Scene 4: Post Payment — Confirmed State
The operator clicks "Post Payment." The Payment API Service validates the batch (BR-PAY-001), the Payment Batch Service executes cash posting with GL integration, and the allocation table transitions from amber (pending) to green (confirmed). The same table structure is preserved — only colors, labels, and values change, making the state transition unmistakable.
| ☑ | Invoice | Date | Amount | Discount | Allocated | Status |
|---|---|---|---|---|---|---|
| 48271 | 07 Jan 2026 | $3,250.00 | — | $3,250.00 | Paid | |
| 48356 | 15 Jan 2026 | $2,890.00 | — | $2,890.00 | Paid | |
| 48412 | 28 Jan 2026 | $4,125.50 | — | $4,125.50 | Paid | |
| 48503 | 10 Feb 2026 | $2,340.00 | $46.80 | $2,293.20 | Paid |
The state transition is visible in every element: amber rows become green, "Will Pay" becomes "Paid," tilde-prefixed discount estimates become final amounts, column headers change from "Pending Allocation" to "Allocated" and from "Est. Discount" to "Discount," the amber summary bar becomes green, and the "Post Payment" button disappears because the action is complete. The two excluded invoices (48621 and 48744) do not appear in the confirmed state — they were filtered out before posting. The "View GL Postings" link on the success banner leads directly to Scene 5.
Scene 5: View GL Postings — Double-Entry Accounting
Clicking "View GL Postings" on the success banner navigates directly to the GL journal entries for this payment. No manual lookup, no switching programs, no re-entering the batch number. Three balanced journal entries are displayed with account names and descriptions alongside the codes — information that required memorization or a separate lookup in the legacy GL051 screen.
| Account | Description | Debit | Credit |
|---|---|---|---|
| 1200.00 | Cash — Operating | $11,625.50 | — |
| 6040.00 | Discount Allowed — Early payment discount | $46.80 | — |
| 2100.00 | Accounts Payable — Trade creditors | — | $11,672.30 |
| Totals | $11,672.30 | $11,672.30 | |
What took six separate program invocations, a numbered menu, and overnight batch latency for GL updates now happens on a single page with real-time feedback and full business rule transparency. The entire flow — from supplier search through allocation preview through posting through GL verification — completes in approximately 2 seconds of processing time, compared to the legacy's 12-hour gap between payment entry and GL visibility.
Scene 6: Reverse a Posted Payment — BR-PAY-009
Mistakes happen — a payment posted to the wrong supplier, a duplicated batch. In the legacy
system, correcting a posted payment meant a separate program (pl085, Payment
Amendment) and a hand-keyed compensating journal. The workbench surfaces it inline: a
Reverse action available on any posted payment.
| Compensating journal | Dr | Cr |
|---|---|---|
| GL 1200 Bank | 7,777.00 | — |
| GL 5000 Trade Creditors | — | 7,777.00 |
Clicking Reverse flips the payment POSTED → REVERSED and writes a
compensating double-entry — the original debit/credit pair mirrored — in the same
transaction, so the ledger nets to zero and stays balanced. This is BR-PAY-009 (Payment
Reversal Processing), which Concho extracted from the legacy reversal/amendment logic in
purchase/pl085.cbl. The affinity win: what was a separate green-screen program plus a
hand-keyed correcting journal is now one auditable action — the reversing entries written
automatically, and the original payment preserved (struck-through, never deleted) for the audit trail.
Show Storyboard Markdown
# UI Storyboard: Payment Cycle — Draft (Inferred from Concho Context Graph Analysis) **Source:** Concho Context Graph entities: `PaymentProcessingAndAllocation` (12 steps, conf 0.82), `PaymentProofProcessing` (7 steps, 0.77), `PaymentProcessingWorkflow` (7 steps, 0.64), `RemittanceAdviceGeneration` (8 steps, 0.70) **Programs Combined:** `pl080` (Payment Data Entry), `pl085` (Payment Amendment), `pl090` (Payment Proof Sort), `pl100` (Cash Posting + GL), `pl940` (Cheque/BACS Generation), `pl960` (Remittance Advice) **Spine:** `use-case-storyboard-012.md` — Use Case 1, "The Payment Cycle: Entry to Posted GL & Remittance" **Status:** DRAFT — inferred from `use-case-storyboard-012.md` and Concho MCP source extracts; review and refine before next-pass generation. **Inferred state machine (composite of four workflows):** ``` ENTRY -> VALIDATION -> APPROPRIATION -> BATCH_CONTROL -> PROOF_GENERATION -> POSTING -> CHEQUE_GENERATION -> REMITTANCE_GENERATION -> AUDIT_COMPLETE ``` **Storyboard rule applied:** every state transition has an explicit triggering user action, rendered as a real form element in the corresponding scene mockup. --- ## Scene 1: Three Screens Become One **User:** Sarah, an AP clerk (the payment operator) **User action:** Sarah opens the Payment Cycle Workbench from the sidebar nav (default route `/payments/cycle`) **Visible state:** the unified workbench renders in its empty initial state. The left-hand sidebar shows the legacy menu items (`Generate Payments to be made`, `Amend Payments`, `Proof Payments`, `Generate Payments` — pl940 cheque run, `Print Payment Register`, `Print remittance advices`) as deep-linked routes inside one application. A banner at the top of the page explains that five legacy programs (`pl080`, `pl090`, `pl100`, `pl940`, `pl960`) have been collapsed into one route, with the same business outcome. **Business rule fired:** none yet (no data entered). **Platform affinity win:** the `pl900` numbered-menu dispatch (`accept menu-reply at 0543` — one-character keypress drives `call pl910..pl960`) is replaced by URL routing. The operator's context (selected supplier, batch in progress, audit trail) survives between sections; the legacy `call ... goback` cycle reset the screen on every menu transition. --- ## Scene 2: Supplier Autocomplete **User:** Sarah (same session) **User action:** Sarah clicks the "New Payment" button on the workbench and starts typing `ACM` in the supplier search field **Visible state:** an autocomplete dropdown appears below the supplier field. Each row shows the supplier code (7-character, e.g. `ACME001`), the supplier name, the current outstanding balance, the count of open invoices, and a MOD-11 verification badge (green check — computed inline on every keystroke after 3 characters, with 200ms debounce). The legacy `pl080` "type the exact 7-character code at row 5, col 72" prompt has become a financial-context search. **Business rule fired:** `BR-PAY-002` Supplier Account Number Format (MOD-11) — the check-digit algorithm from `common/maps09.cbl:90-145` runs inline on each keystroke; supplier codes that fail check-digit are dimmed in the dropdown. **Platform affinity win:** the legacy `accept pay-customer at 0572` with `PIC X(7)` required the operator to know the exact code. The autocomplete with balance + open-invoice preview surfaces financial context that the 80x24 terminal could not show at the input stage. --- ## Scene 3: Allocation Preview — Pending State **User:** Sarah (same session); `ACME ELECTRICAL LIMITED` (`ACME001`) selected, current balance $4,521.00, 6 open invoices **User action:** Sarah enters the payment date `21/05/2026`, tabs to the Amount field, types `4,250.00`, and tabs out **Visible state:** the outstanding-invoices table loads below the form (oldest-first per the `pl080` pay-loop's `OTM5-Read-Next` order). All 6 invoice rows render with **amber row backgrounds** and **amber status badges** — the table is in *pending / preview* state, not yet committed. For each row the table shows: - Invoice number (`INV-00042001` .. `INV-00042006`) - Invoice date and due date - Outstanding net amount - Estimated discount (prefixed with `~`, shown for rows where today's date is inside the `oi-deduct-days` window per `BR-PAY-004`; zero for rows outside the window) - Amount that will be applied (`Will Pay` / `Partial (est.)` / `Skip` status) The running total at the bottom of the table shows "Pending allocation: $4,250.00" against a "Remaining balance: $0.00". An amber summary bar reads: "5 invoices fully paid, 1 partial, $25 early-payment discount projected. Click Post Payment to commit." **Business rule fired:** `BR-PAY-004` Early Payment Discount Calculation (per appropriated line; `purchase/pl080.cbl:585-605` — if `u-bin = oi-date + oi-deduct-days > pay-date` then `display-5 = oi-deduct-amt`); `BR-PAY-007` Unapplied Balance Allocation (preview if `purch-unapplied > 0`). **Platform affinity win:** the legacy `pl080` showed appropriation **after** submit, one line at a time, via `display display-inv at curs` inside the `pay-loop`. The modern view shows the full appropriation **before** commit, in a real-time pending state the operator can adjust. --- ## Scene 4: Post Payment — Confirmed State **User:** Sarah (same session) **User action:** Sarah reviews the pending allocation, then clicks the "Post Payment" button at the bottom of the table **Visible state:** a brief ~2-second processing indicator appears. Every row transitions from **amber to green** in unison. Status badges flip: "Will Pay" -> "Paid", "Partial (est.)" -> "Partial". The tilde-prefixed estimated discount values become final values (no tilde). Column headers update from "Pending Allocation" -> "Allocated", "Est. Discount" -> "Discount Taken". The amber summary bar turns green: "Allocated $4,225.00, discounts taken $25.00, batch 42 item 17 of 99 (BR-PAY-008 soft cap)." A new "View GL Postings" link appears in the summary footer. **Business rule fired:** `BR-PAY-001` Payment Posting Validation (the global posting-gate check from `pl080.cbl:288-293`, evaluated server-side as `purchase_posting_state = posted`); `BR-PAY-006` Batch Status Lifecycle (the batch transitions from `OPEN` to `OPEN-WITH-PAYMENTS` if first payment, stays `OPEN` otherwise; from `copybooks/fdbatch.cob:20-30`); `BR-PAY-008` Payment Batch Control Limits (99-item soft cap on `fdbatch.cob:23 Items pic 99`; `pl080.cbl:731-735` composite key `oi-invoice = oi-b-nos * 1000 + oi-b-item`). **Platform affinity win:** the legacy posting path was a 12-hour batch window: `pl080` wrote `OTM5` records, the day ended, `pl090` ran proof-sort overnight, `pl100` posted to supplier balances and the GL the next morning. The modern path is a single ~2-second synchronous commit with the appropriation result visible as a state transition the operator can see. --- ## Scene 5: View GL Postings — Double-Entry Accounting **User:** Sarah (same session) **User action:** Sarah clicks the "View GL Postings" link in the green summary footer **Visible state:** a GL journal panel slides in from the right (the same workbench page; no navigation). It shows the balanced double-entry pair that `pl100.cbl:329, 414` wrote inline in the same DB transaction as the payment row: | Account | Dr | Cr | |---|---:|---:| | 1100 — Bank (cash account) | | $4,225.00 | | 2000 — Accounts Payable — ACME001 | $4,250.00 | | | 4090 — Discount Received | | $25.00 | | **Totals** | **$4,250.00** | **$4,250.00** | A "Balance" pill reads **ZERO** with a green check. Each row links to the originating payment id (`PAY-2026-00042-017`) for traceability. **Business rule fired:** `BR-PAY-005` Payment Method Determination (BACS vs Cheque); General Ledger Posting Integration (the inline `BL-Write` from `pl100.cbl:414`, mapped to the modern `gl_posting` table written in the same transaction as `payment.status = POSTED`). **Platform affinity win:** the legacy GL visibility required exiting `pl100`, returning to the system menu, launching `gl051` (GL Transaction Enquiry) under the General Ledger module, and re-keying the batch number to see the posting. The modern view shows the GL pair without leaving the page, with per-line traceability back to the payment id. --- ## Scene 6: Reverse a Posted Payment (BR-PAY-009) **User:** the payment operator. **User action:** on a POSTED payment, the operator clicks **Reverse Payment**. **Visible state:** the payment row flips `POSTED → REVERSED`; the GL panel gains a *compensating* journal — the original debit/credit pair mirrored (DR Bank / CR Trade Creditors) — and the Balance pill still reads ZERO. The original payment is retained, struck-through, for the audit trail (never deleted). **Business rule fired:** `BR-PAY-009` Payment Reversal Processing — writes the mirrored journal in the same transaction so the ledger stays balanced. Concho extracted this from the legacy reversal/amendment logic in `purchase/pl085.cbl`. **Platform affinity win:** legacy ran reversal/amendment as a separate program (`pl085`) plus a hand-keyed correcting journal; the modern workbench makes it one inline, auditable action with the reversing entries written automatically. --- ## Scene 7 (optional): Side-by-Side Coexistence — SAROC Steady State **User:** Sarah (same session); operations manager peering over shoulder **User action:** Sarah switches to the "SAROC Bridge" tab in the workbench **Visible state:** two panels side-by-side. **Left** shows the legacy `pay.dat`/`openitm5.dat` files via the SAROC adapter — the same payment Sarah just posted appears as a record in the legacy file format, with the BACS-tagged record (`pay-sortcode != zero`) confirmed. **Right** shows the modern `payment` and `gl_posting` rows. A small status indicator reads: "SAROC bridge in dual-write mode — legacy and modern stores converged. Queue drain: 0 messages pending." **Business rule fired:** `BR-PAY-005` (the BACS vs cheque split that the SAROC bridge enforces during coexistence); `BR-PAY-008` (the 99-item soft cap that the SAROC translator depends on for 1-to-1 legacy translation). **Platform affinity win:** the dual-storage coexistence window lets the legacy users still run `pl080`/`pl100` against `pay.dat` while modern users post via the workbench, with the SAROC bridge keeping both stores in sync. There is no "cutover weekend" — both systems run side-by-side until the legacy ramp-down completes. --- ## Why these scenes (and not others) - **Scene 1** is the "consolidated view's empty state" canonical opener (per `ui-storyboard-transformation` SKILL Part 2 Step 5, scene-1 template). - **Scene 2** demonstrates the "exact code at blank prompt -> autocomplete with preview" affinity win (scene-2 template). - **Scene 3** shows the unified view in *pending* state with amber/yellow color treatment and per-row preview calculations (scene-3 template). - **Scene 4** is the commit transition — amber-to-green, will-to-did, estimated-to-final (scene-4 template). - **Scene 5** is the downstream-consequence view that previously required a different program/screen (scene-5 template — "everything visible without leaving the page"). - **Scene 6** is the SAROC coexistence story that the run-012 charter emphasizes (the optional scene-6 template for projects with a coexistence window). Each scene includes the five required elements per `ui-storyboard-transformation` SKILL Part 2 Step 5: user, user action, visible state, business rule fired, platform-affinity win. **End of draft storyboard.**
7.8 UI Pattern Library & Accessibility
| Pattern | Used in | Notes |
|---|---|---|
| Inline validation badge (green ring + tick / red ring + message) | 7.3 supplier autocomplete, 7.5 GL balance check | BR-PAY-002 MOD-11 runs on each keystroke; debounce 200ms. |
| Status pill (lifecycle indicator) | 7.4 batch dashboard, 7.7 workbench header | BR-PAY-006 lifecycle — ENTERED / APPROPRIATED / PROOFED / POSTED / REMITTED. |
| Live preview table | 7.3 appropriation, 7.5 GL journal | Computed on each form change; no submit required to see the result. BR-PAY-004 visible per row. |
| Confirm-and-commit modal | 7.5 cash posting, 7.6 cheque generation | Replaces 3-character YES/NO prompts; shows downstream consequences. |
| Document inbox / per-row download | 7.6 remittance inbox | S3 presigned URLs; supplier-portal delivery one click away. |
| Sidebar deep-link routing | 7.2 menu replacement | SPA routes /payments/...; replaces menu-reply + CALL ws-called. |
Accessibility improvements layered across every screen:
- Semantic HTML: form labels are wired to inputs with
for=; ARIAaria-required,aria-invalid,aria-describedbyon every input. - Keyboard navigation: Esc still cancels (legacy parity). Ctrl+S saves; Ctrl+Enter submits; Tab order matches visual order.
- Screen-reader announcements: appropriation preview updates use
aria-live="polite"; status pills are read as "PROOFED" not "yellow pill". - Colour contrast: all status pills tested at WCAG AA contrast on the report's brand palette.
- Focus indicators: 2px ring on every focusable element; never relying on browser default.
7.9 Field Inventory Reference
The field inventory table that follows is auto-generated from the storyboard JSON at inputs/ui-storyboards/payment-cycle.storyboard.json. This table enumerates every user-facing field shown across screens 7.2–7.7 — the ground truth that the mockups above, the modern React components in the artifact-generation workflow, and the Playwright drift-detection spec all derive from. Every row traces back to a real COBOL artifact (copybook PIC clause, accept statement, or display literal) via the evidence column.
(The table is rendered into a separate fragment, modernization-technical-examples-section-7-9-012.html, and inserted by the report-assembler at this point.)
7.9 Field Inventory Reference (full table)
This table enumerates every user-facing field across the Payment Processing flow as discovered in the legacy source — the ground truth that the mockups above, the modern components, and the Playwright drift-detection spec all derive from. Each row traces to a property on a real legacy ViewModel (see evidence column); the validate-storyboard.py gate re-checks every row against the cached legacy source before this section is assembled, so claimed fields cannot drift from what the legacy actually has.
Source profile: cobol · Target framework: React (TypeScript) + FastAPI (Python) · Screens: 8 · Business rules referenced: 10
| Step | Screen | Field id | Label | Kind | Type | Req? | Validation | Conditional | Evidence (legacy) |
|---|---|---|---|---|---|---|---|---|---|
| 1 | PaymentMenu | menuReply | Menu selection | text-input | char | ✓ | regex ^[1-6X]$ | purchase/pl900.cbl:155 accept menu-reply at 0543 with foreground-color 6 auto update UPPER | |
| 2 | PaymentEntry | payDate | Date | date-input | date | ✓ | regex ^\d{2}/\d{2}/\d{4}$ | purchase/pl080.cbl:351 accept ws-date at 0549 with foreground-color 3 update | |
| 2 | PaymentEntry | payCustomer | A/C Nos (Supplier Account) | text-input | string | ✓ | regex ^[A-Z0-9]{6}[0-9]$ | purchase/pl080.cbl:374 accept pay-customer at 0572; common/maps09.cbl:90-145 check-digit | |
| 2 | PaymentEntry | payValue | Value (Payment Amount) | currency-input | decimal | ✓ | regex ^-?\d{1,7}(\.\d{2})?$ | purchase/pl080.cbl:445-455 ws-amount-screen-accept; pay-value pic s9(7)v99 comp-3 | |
| 2 | PaymentEntry | allocateUnapplied | Allocate Unapplied Balance to this account? (Y/N) | radio-group | char | purch-unapplied > 0 | purchase/pl080.cbl:421-446 ws-reply / accept-unappl-reqst | ||
| 2 | PaymentEntry | unappliedAmount | Unapplied amount to allocate | currency-input | decimal | regex ^\d{1,7}(\.\d{2})?$ | allocateUnapplied = Y | purchase/pl080.cbl:447-455 accept-unappl-money; constrained `amt-ok not > purch-unapplied` | |
| 2 | PaymentEntry | moreData | Enter further payments? (Y/N) | radio-group | char | ✓ | purchase/pl080.cbl:494-503 more-data ws-reply at 1661 | ||
| 2 | PaymentEntry | supplierName | Supplier name | display | string | purchase/pl080.cbl:399 display purch-name at 0401 | |||
| 2 | PaymentEntry | supplierAddress1 | Supplier address line 1 | display | string | purchase/pl080.cbl:403-405 unstring address1 -> address-line at 0501 | |||
| 2 | PaymentEntry | supplierAddress2 | Supplier address line 2 | display | string | purchase/pl080.cbl:407-409 address-line at 0601 | |||
| 2 | PaymentEntry | supplierAddress3 | Supplier address line 3 | display | string | purchase/pl080.cbl:411-413 address-line at 0701 | |||
| 2 | PaymentEntry | supplierAddress4 | Supplier address line 4 | display | string | purchase/pl080.cbl:415-417 address-line at 0801 | |||
| 2 | PaymentEntry | currentBalance | Current Balance | display | decimal | purchase/pl080.cbl:420 purch-current displayed at 1019 | |||
| 2 | PaymentEntry | unappliedBalance | Unapplied Balance | display | decimal | purch-unapplied > 0 | purchase/pl080.cbl:424 purch-unapplied displayed at 0665 | ||
| 2 | PaymentEntry | batchNumber | Batch number | display | int32 | purchase/pl080.cbl:485 bl-next-batch displayed at 0770 | |||
| 2 | PaymentEntry | batchItem | Within-batch item k | display | int32 | purchase/pl080.cbl:486 k displayed at 0776 | |||
| 2 | PaymentEntry | batchTotal | Batch Total | display | decimal | purchase/pl080.cbl:466 batch-value displayed at 1055 | |||
| 2 | PaymentEntry › Invoice Appropriation Lines | oiInvoice | Invoice ref / folio | table-column | int64 | purchase/pl080.cbl:583 display-inv at curs; plwsoi.cob OI-Invoice pic 9(8) | |||
| 2 | PaymentEntry › Invoice Appropriation Lines | oiDate | Invoice date | table-column | date | purchase/pl080.cbl:586-587 u-date at curs; plwsoi.cob OI-Date binary-long | |||
| 2 | PaymentEntry › Invoice Appropriation Lines | workNet | O/S net (oldest first) | table-column | decimal | purchase/pl080.cbl:565-568 work-net = oi-net + oi-carriage + oi-vat + oi-c-vat | |||
| 2 | PaymentEntry › Invoice Appropriation Lines | deductionEligible | Deductible (discount window flag) | table-column | boolean | purchase/pl080.cbl:591-597 if u-bin > pay-date subtract work-2 from work-1 (within window) | |||
| 2 | PaymentEntry › Invoice Appropriation Lines | displayDiscount | Discount (display-5) | table-column | decimal | purchase/pl080.cbl:596-598 move work-2 to display-5 else move zero to display-5 | |||
| 2 | PaymentEntry › Invoice Appropriation Lines | payPaid | Applied amount (editable) | currency-input | decimal | regex ^\d{1,7}(\.\d{2})?$ | purchase/pl080.cbl:607-611 accept-money2 over pay-paid; user can override | ||
| 2 | PaymentEntry › Invoice Appropriation Lines | confirmPartial | Confirm partial payment line (Y/N) | radio-group | char | pay-paid != work-net (partial-payment branch) | purchase/pl080.cbl:660-668 ws-reply at curs for partial-payment confirmation | ||
| 2 | PaymentEntry › Invoice Appropriation Lines | lineStatus | Line status | table-column | enum | purchase/pl080.cbl:620-624 / 712-714 status text rendering | |||
| 3 | PaymentAmendment | amendDate | Date | date-input | date | ✓ | regex ^\d{2}/\d{2}/\d{4}$ | purchase/pl085.cbl:316-320 zz070-Convert-Date + display ws-date at 0549 | |
| 3 | PaymentAmendment | amendCustomer | Supplier (oi5-supplier) | text-input | string | ✓ | regex ^[A-Z0-9]{6}[0-9]$ | purchase/pl085.cbl:323-326 accept oi5-supplier at 0572 | |
| 3 | PaymentAmendment | amendBatchNumber | Batch number to amend | number-input | int32 | ✓ | regex ^[0-9]{1,5}$ | purchase/pl085.cbl:332-336 accept display-n at 0770 (batch-nos) | |
| 3 | PaymentAmendment | amendBatchItem | Item-within-batch (k) | number-input | int32 | ✓ | regex ^[0-9]{1,3}$ | purchase/pl085.cbl:338-341 accept k at 0776 | |
| 3 | PaymentAmendment | amendPayValue | Corrected payment value | currency-input | decimal | ✓ | regex ^\d{1,7}(\.\d{2})?$ | purchase/pl085.cbl:393-397 accept-money2 -> pay-value | |
| 3 | PaymentAmendment | amendCancelConfirm | Confirm No action? i.e. Request cancelled (Y/N) | radio-group | char | ✓ | amendPayValue = original oi-paid | purchase/pl085.cbl:399-409 accept ws-reply at 1251 (value-input-req) | |
| 3 | PaymentAmendment | amendMoreCorrections | Make further corrections? (Y/N) | radio-group | char | ✓ | purchase/pl085.cbl:436-442 more-data ws-reply at 1663 | ||
| 3 | PaymentAmendment | amendSupplierName | Supplier name | display | string | purchase/pl085.cbl:374 display purch-name at 0401 | |||
| 3 | PaymentAmendment | amendCurrentBalance | Current Balance | display | decimal | purchase/pl085.cbl:387 display-s at 1019 | |||
| 3 | PaymentAmendment | amendOriginalValue | Original payment value (oi-paid) | display | decimal | purchase/pl085.cbl:367 move oi-paid to pay-value hold-pay | |||
| 3 | PaymentAmendment | amendTransactionType | Transaction type (5=payment, 6=unapplied journal) | display | enum | purchase/pl085.cbl:359-363 oi-type 5 or 6; type-6 shows Warning Unapplied Balance Journal | |||
| 4 | BatchDashboard | batchSelectFromNumber | Batch range (from) | number-input | int32 | ✓ | regex ^[0-9]{1,5}$ | copybooks/fdbatch.cob:23 Batch-Nos pic 9(5) | |
| 4 | BatchDashboard | batchSelectToNumber | Batch range (to) | number-input | int32 | ✓ | regex ^[0-9]{1,5}$ | copybooks/fdbatch.cob:23 Batch-Nos pic 9(5); modern UI adds 'to' bound; legacy proofed all open batches | |
| 4 | BatchDashboard | proofTypeFilter | Type filter (proof eligibility) | dropdown | int32 | purchase/pl090.cbl proof-sort filter; only oi-type IN (2,4) released to sort stream | |||
| 4 | BatchDashboard | controllerSignOff | Controller sign-off (initials) | text-input | string | ✓ | regex ^[A-Z]{2,3}$ | purchase/pl090.cbl proof-print is signed by hand; modern UI captures initials inline | |
| 4 | BatchDashboard | itemCount | Items in batch (max 99 legacy) | display | int32 | copybooks/fdbatch.cob:23 Items pic 99 | |||
| 4 | BatchDashboard | batchStatus | Batch-Status (Open/Closed) | display | enum | copybooks/fdbatch.cob:20-22 Batch-Status pic 9 88-levels | |||
| 4 | BatchDashboard | clearedStatus | Cleared-Status | display | enum | copybooks/fdbatch.cob:24-27 88-levels | |||
| 4 | BatchDashboard | actualGross | Actual gross total | display | decimal | copybooks/fdbatch.cob:28 Actual-Gross pic 9(9)v99 | |||
| 4 | BatchDashboard | actualNet | Actual net total | display | decimal | copybooks/fdbatch.cob:29 Actual-Net pic 9(9)v99 | |||
| 4 | BatchDashboard › Proof-eligible items (type IN (2,4)) | oiType | Type | table-column | int32 | sales/sl090.cbl:262-267 type filter | |||
| 4 | BatchDashboard › Proof-eligible items (type IN (2,4)) | oiSupplier | Supplier | table-column | string | copybooks/plwsoi.cob OI-Supplier pic x(7) | |||
| 4 | BatchDashboard › Proof-eligible items (type IN (2,4)) | oiInvoiceProof | Invoice / folio | table-column | int64 | copybooks/plwsoi.cob OI-Invoice pic 9(8) | |||
| 4 | BatchDashboard › Proof-eligible items (type IN (2,4)) | oiAppropProof | Allocated amount | table-column | decimal | copybooks/plwsoi.cob OI-Approp pic s9(7)v99 comp-3 | |||
| 4 | BatchDashboard › Proof-eligible items (type IN (2,4)) | oiDeductProof | Discount taken | table-column | decimal | copybooks/plwsoi.cob OI-Deduct-Amt pic s9(7)v99 comp-3 | |||
| 4 | BatchDashboard › Proof-eligible items (type IN (2,4)) | oiBatchPair | Batch / Item | table-column | string | copybooks/plwsoi.cob OI-B-Nos pic 9(5) + OI-B-Item pic 999 composite | |||
| 5 | CashPosting | confirmPost | OK to post payment transactions (YES/NO)? | text-input | string | ✓ | regex ^(YES|NO)$ | purchase/pl100.cbl:312 accept wx-reply at 1256 (acpt-xrply) | |
| 5 | CashPosting | postingDate | Posting date (override default = today) | date-input | date | regex ^\d{2}/\d{2}/\d{4}$ | purchase/pl100.cbl:296 zz070-Convert-Date + display ws-date at 0171; modern UI surfaces override | ||
| 5 | CashPosting | glPostingFlag | Write inline GL journal entries (G-L flag) | checkbox | boolean | purchase/pl100.cbl:319 if G-L perform BL-Open; system-record G-L global | |||
| 5 | CashPosting | postSupplierName | Supplier (l5-name) | display | string | purchase/pl100.cbl:355-358 purch-name moved to l5-name | |||
| 5 | CashPosting | postBatchPair | Batch / Item (l5-batch/l5-item) | display | string | purchase/pl100.cbl:360-362 oi-b-nos to l5-batch + oi-b-item to l5-item | |||
| 5 | CashPosting | postOldBalance | Old balance (l5-old-bal) | display | decimal | purchase/pl100.cbl:371 subtract purch-unapplied from purch-current giving l5-old-bal | |||
| 5 | CashPosting | postNewBalance | New balance (l5-new-bal) | display | decimal | purchase/pl100.cbl:382 subtract purch-unapplied from purch-current giving l5-new-bal | |||
| 5 | CashPosting | batchPaid | Paid this batch (t-paid) | display | decimal | purchase/pl100.cbl:391 t-paid running total | |||
| 5 | CashPosting | batchApprop | Appropriations this batch (t-approp) | display | decimal | purchase/pl100.cbl:389 t-approp running total | |||
| 5 | CashPosting | batchDeduct | Deductions taken (t-deduct) | display | decimal | purchase/pl100.cbl:393 t-deduct running total | |||
| 5 | CashPosting | glJournalRef | GL journal entry id | display | string | purchase/pl100.cbl:414 BL-Write inline GL posting | |||
| 6 | ChequeRun | firstChequeNumber | First Cheque number | number-input | int64 | ✓ | regex ^[0-9]{1,8}$ | purchase/pl940.cbl:402-403 accept cheque-nos at 0634; auto-increments after each cheque | |
| 6 | ChequeRun | chequePaymentDate | Payment date (printed on cheque) | date-input | date | ✓ | regex ^\d{2}/\d{2}/\d{4}$ | purchase/pl940.cbl:405-410 accept ws-test-date at 0834 with foreground-color 3 update | |
| 6 | ChequeRun | minPaymentAmount | Minimum payment amount (pay-gross gate) | currency-input | decimal | regex ^\d{1,7}(\.\d{2})?$ | purchase/pl940.cbl:417 if pay-gross < .01 go to read-purchase; modern UI exposes the threshold | ||
| 6 | ChequeRun | chequeMethod | Method (Cheque / BACS) | display | enum | purchase/pl940.cbl:483-489 if pay-sortcode = zero move cheque-nos to c-cheque else move BACS to c-cheque-x | |||
| 6 | ChequeRun | chequeNumber | Cheque number (BACS shows 'BACS') | display | string | purchase/pl940.cbl:484 c-cheque field | |||
| 6 | ChequeRun | chequeAccount | Supplier sort code (decides Cheque vs BACS) | display | int32 | copybooks/fdpay.cob Pay-SortCode pic 9(6) comp | |||
| 6 | ChequeRun | chequeWordsLine1 | Amount in words (line 1) | display | string | purchase/pl940.cbl:432-450 c-words-1 built from wordn table | |||
| 6 | ChequeRun | chequeWordsLine2 | Amount in words (line 2) | display | string | purchase/pl940.cbl:451-475 c-words-2 + WS-Currency-Major/Minor | |||
| 6 | ChequeRun › Cheque/BACS line items (OCCURS 9 in legacy; unlimited in target) | cInv | Invoice (per line, max 9 legacy) | table-column | string | purchase/pl940.cbl:477 c-inv (z) from pay-invoice (z) | |||
| 6 | ChequeRun › Cheque/BACS line items (OCCURS 9 in legacy; unlimited in target) | cFolio | Folio (per line) | table-column | string | purchase/pl940.cbl:478 c-folio (z) from pay-folio (z) | |||
| 6 | ChequeRun › Cheque/BACS line items (OCCURS 9 in legacy; unlimited in target) | cValue | Amount (per line) | table-column | decimal | purchase/pl940.cbl:479 c-value (z) from pay-value (z) | |||
| 7 | RemittanceInbox | remittanceTo | Supplier (To) | display | string | purchase/pl960.cbl:239 l1-addr1 from c-name | |||
| 7 | RemittanceInbox | remittanceFrom | From address | display | string | purchase/pl960.cbl:240 l1-addr2 from usera | |||
| 7 | RemittanceInbox | remittanceAccount | Account number (c-account) | display | string | purchase/pl960.cbl:265 c-account moved to l2-ac | |||
| 7 | RemittanceInbox | remittanceDate | Payment date (c-date) | display | date | purchase/pl960.cbl:266 c-date moved to l2-date | |||
| 7 | RemittanceInbox | remittanceMethod | Cheque <n> or BACS to your Bank | display | string | purchase/pl960.cbl:281-287 l6-chq-bacs / l6-cheque | |||
| 7 | RemittanceInbox | remittanceTotal | Total (c-gross) | display | decimal | purchase/pl960.cbl:289 c-gross moved to l6-total | |||
| 7 | RemittanceInbox › Invoice lines (max 9 per OCCURS limit in legacy; unlimited in target) | lineInvoice | Invoice | table-column | string | purchase/pl960.cbl:274 l4-inv from c-inv(z) | |||
| 7 | RemittanceInbox › Invoice lines (max 9 per OCCURS limit in legacy; unlimited in target) | lineFolio | Folio | table-column | string | purchase/pl960.cbl:275 l4-folio from c-folio(z) | |||
| 7 | RemittanceInbox › Invoice lines (max 9 per OCCURS limit in legacy; unlimited in target) | lineAmount | Amount | table-column | decimal | purchase/pl960.cbl:276-277 l4-amount from c-value(z); skipped when spaces | |||
| 8 | PaymentCycleWorkbench | wbDateFrom | Filter: payment date from | date-input | date | regex ^\d{2}/\d{2}/\d{4}$ | Workbench-only synthetic field; modern UI exposes payment_date range filter over payment table | ||
| 8 | PaymentCycleWorkbench | wbDateTo | Filter: payment date to | date-input | date | regex ^\d{2}/\d{2}/\d{4}$ | Workbench-only synthetic field; modern UI exposes payment_date range filter over payment table | ||
| 8 | PaymentCycleWorkbench | wbStatusFilter | Filter: lifecycle status | dropdown | enum | Workbench filter wraps BR-PAY-006 state machine; copybooks/fdbatch.cob:20-30 88-level Batch-Status definitions | |||
| 8 | PaymentCycleWorkbench | wbMethodFilter | Filter: payment method | dropdown | enum | Workbench filter wraps BR-PAY-005 split; purchase/pl940.cbl:483-489 if pay-sortcode = zero | |||
| 8 | PaymentCycleWorkbench | wbSupplierFilter | Filter: supplier (autocomplete) | text-input | string | regex ^[A-Z0-9]{0,7}$ | Workbench filter wraps BR-PAY-002 (MOD-11 check applied if 7 chars entered); reuses pl080 supplier picker | ||
| 8 | PaymentCycleWorkbench | wbPaymentHeader | Payment header (supplier, amount, date, method) | display | object | JOIN of payment table columns | |||
| 8 | PaymentCycleWorkbench | wbAppropriation | Appropriation lines with discounts | display | array | JOIN of open_item rows | |||
| 8 | PaymentCycleWorkbench | wbBatchPosition | Batch position + lifecycle state | display | object | JOIN of batch_control row | |||
| 8 | PaymentCycleWorkbench | wbGlPreview | GL journal preview (debit/credit pair) | display | object | JOIN of gl_posting rows | |||
| 8 | PaymentCycleWorkbench | wbRemittance | Remittance PDF preview + delivery status | display | object | JOIN of payment + S3 metadata | |||
| 8 | PaymentCycleWorkbench | wbAuditTrail | Audit trail timeline (W3C trace correlated) | display | array | JOIN of payment_audit JSONB rows ordered by created_at |
Business rules referenced across these fields: BR-PAY-001 Payment Posting Validation, BR-PAY-002 Supplier Account Number Format (MOD-11), BR-PAY-003 Payment Type Filtering, BR-PAY-004 Early Payment Discount Calculation, BR-PAY-005 Payment Method Determination (BACS vs Cheque), BR-PAY-006 Batch Status Lifecycle, BR-PAY-007 Unapplied Balance Allocation, BR-PAY-008 Payment Batch Control Limits (99-item cap), BR-PAY-009 Payment Reversal Processing, BR-PAY-010 Remittance Advice Processing.
8. Code Translation Examples
TL;DR — Cross-language modernization (COBOL → Python/FastAPI)
This is a cross-language modernization — GnuCOBOL on Linux to Python 3.12 on FastAPI + SQLAlchemy 2.0 async + Pydantic v2 + structlog + OpenTelemetry. Six representative translation examples walk the Use Case 1 business-rule chain: MOD-11 supplier validation, posting gate guard, early-payment discount, unapplied-balance prompt, BACS-vs-cheque routing, and inline GL posting. Key patterns: imperative perform/go to → declarative async services; fdpay.cob COMP-3 packed decimals → Decimal; display ... accept terminal I/O → FastAPI request handlers; BL-Write + OTM5-Rewrite inline updates → SQLAlchemy unit-of-work with transactional GL posting.
get_file_content_in_range — file paths and line ranges appear in the Translation Notes column. BR-PAY-XXX references appear in Translation Notes only, never inside legacy code blocks.
8.1 Translation Principles
| Pattern | Legacy idiom | Target idiom |
|---|---|---|
| I/O surface | display ... at LLCC + accept ... at LLCC | FastAPI request handler with Pydantic request/response models |
| Control flow | perform <section> thru <exit> + go to <label> | Pure-function helpers; async def orchestration; no goto |
| Numeric precision | pic s9(7)v99 comp-3 (packed decimal) | decimal.Decimal via condecimal(max_digits=9, decimal_places=2) |
| Data access | OTM5-Read-Next / Purch-Rewrite via ISAM file handlers | SQLAlchemy 2.0 async ORM (AsyncSession, select(), update()) on PostgreSQL |
| Validation | Custom accept ... auto update loops + maps09 | Pydantic field_validator + reusable validators package |
| Logging | display to terminal; no structured trail | structlog.get_logger() with <entity>.<action> event names + W3C trace context |
| Errors | fs-reply ≠ 0 branches; "PL nnn" on-screen messages | Domain exceptions (ValidationError, NotFoundError, ConflictError) → RFC 7807 responses |
| Background work | Operator runs program directly; no scheduling | aio-pika consumer + Celery-style background jobs for batch posting / remittance |
| Observability | None (printed posting register is the audit) | OpenTelemetry spans on every business operation; OTLP → ADOT → X-Ray + CloudWatch Metrics |
8.2 Example 1 — Supplier MOD-11 Check Digit Validation
The maps09 utility implements the MOD-11 check digit algorithm shared between customer and supplier identifiers. Each of the first six characters is weighted by position via (8 - position), the weighted sum is divided by 11, and the remainder is subtracted from 11 to yield the check digit. The target translates this verbatim into a Pydantic validator that runs on every payment-entry submit.
Legacy — common/maps09.cbl
main.
move customer-nos to work-array.
move zero to suma.
perform addition-loop through addition-end
varying a from 1 by 1 until a > 6.
if suma = zero
move "N" to maps09-reply
go to main-exit.
divide suma by 11 giving z.
compute a = 11 - (suma - (11 * z)).
if maps09-reply = "C"
move a to check-digit
move "Y" to maps09-reply.
if maps09-reply = "V"
and a = check-digit
move "Y" to maps09-reply.
go to main-exit.
addition-loop.
set q to 1.
search ar1 at end go to addition-error
when ar1 (q) = array (a)
go to addition-do.
addition-do.
set y to q.
compute z = y * (8 - a).
add z to suma.
Target — payment_api/validators/supplier_id.py
from __future__ import annotations
from pydantic import BaseModel, field_validator
import structlog
logger = structlog.get_logger()
ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
def compute_mod11_check_digit(identity: str) -> int:
"""Return the MOD-11 check digit for the 6-char identity portion."""
if len(identity) != 6:
raise ValueError("identity must be 6 characters")
total = 0
for position, ch in enumerate(identity, start=1):
q = ALPHABET.index(ch) + 1 # 'set q to 1' + search ar1
total += q * (8 - position) # compute z = y * (8 - a)
if total == 0:
raise ValueError("invalid identity (zero weighted sum)")
remainder = total % 11
return 11 - remainder
class SupplierId(BaseModel):
value: str # 7 chars: 6 identity + 1 check digit
@field_validator("value")
@classmethod
def verify(cls, v: str) -> str:
if len(v) != 7:
raise ValueError("supplier id must be 7 chars")
identity, supplied = v[:6], v[6]
computed = compute_mod11_check_digit(identity)
if str(computed) != supplied:
logger.warning(
"supplier_id.check_digit_mismatch",
identity=identity,
supplied=supplied,
computed=computed,
)
raise ValueError("MOD-11 check digit mismatch")
return v
Translation Notes
- BR-PAY-002 — verbatim transfer per the catalog (
behavioralFidelity: verbatim). - COBOL
search ar1on the alphabet collation table → PythonALPHABET.index(ch).qis 1-indexed in both. - The COBOL formula
11 - (suma - (11 * z))simplifies to11 - (suma mod 11)— the target uses the cleaner form but produces identical output. - Mode 'V' (verify) is the default Pydantic path; mode 'C' (calculate) is exposed as the top-level helper
compute_mod11_check_digit()for supplier-master CRUD. - Logging:
structlog.get_logger()emitssupplier_id.check_digit_mismatchwith structured fields, replacing legacy on-screen-only feedback. - Source:
common/maps09.cbl:90-145.
8.3 Example 2 — Posting Gate Guard (P-Flag-I check)
The legacy posting gate at pl080.cbl:288-293 uses a global flag (P-Flag-I) to block payment entry whenever a prior purchase-ledger step left unposted transactions. The target replaces the global with an explicit state table and returns RFC 7807 409 Conflict from the FastAPI handler.
Legacy — purchase/pl080.cbl:288-293
if P-Flag-I = 1
display PL121 at 0501
display PL116 at 2401
accept ws-reply at 2440
go to main-exit.
PL121/PL116 are static error-message literals from wsmaps03.cob; main-exit exits the program. The gate is global — whatever set P-Flag-I = 1 elsewhere in the session also blocks payment entry here.
Target — payment_api/api/payments.py
from fastapi import APIRouter, Depends, status
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from opentelemetry import trace
import structlog
from .deps import get_session
from .models import PaymentCreate, PaymentRead
from .exceptions import PostingRequiredError
from .db import PurchasePostingState
router = APIRouter(prefix="/api/v1/payments", tags=["payments"])
logger = structlog.get_logger()
tracer = trace.get_tracer("payment-api")
@router.post("", response_model=PaymentRead,
status_code=status.HTTP_201_CREATED)
async def create_payment(
body: PaymentCreate,
session: AsyncSession = Depends(get_session),
) -> PaymentRead:
with tracer.start_as_current_span("payment.create") as span:
span.set_attribute("supplier.code", body.counterparty_code)
# BR-PAY-001 — posting gate guard (replaces P-Flag-I global)
state = (await session.execute(
select(PurchasePostingState).limit(1)
)).scalar_one()
if state.flag != "posted":
logger.warning(
"payment.create.blocked_posting_required",
state=state.flag,
)
raise PostingRequiredError(state=state.flag)
# ... appropriation, persistence, event emission elided ...
Translation Notes
- BR-PAY-001 — verbatim semantics; modern HTTP envelope.
- Global
P-Flag-Ireplaced by an explicitpurchase_posting_staterow — explicit state, no hidden control coupling. PostingRequiredErroris a domain exception (per resolvedtargetPatterns.errorHandling = domain-exceptions); a FastAPI exception handler converts it to RFC 7807 withcode=POSTING_REQUIREDand HTTP 409.- OpenTelemetry span
payment.createtags the supplier code and posting-state attribute so the gate hit can be queried in X-Ray. - structlog event
payment.create.blocked_posting_required— the legacy PL121/PL116 sequence emitted nothing to a log; the target emits a structured warning with embedded trace context. - Source:
purchase/pl080.cbl:288-293; target followsconfig/code-patterns/error-handling.py.
8.4 Example 3 — Early-Payment Discount Calculation
The most arithmetic-heavy of the catalog rules. Legacy computes u-bin = oi-date + oi-deduct-days, tests u-bin > pay-date, and subtracts the configured deduction from the outstanding net when the payment falls inside the discount window. Target translates the formula verbatim into a Decimal-based pure function with an OpenTelemetry span for traceability.
Legacy — purchase/pl080.cbl:585-605
move oi-date to u-bin.
perform zz060-Convert-Date.
move 11 to cole.
display u-date at curs with foreground-color 3.
move work-net to work-1.
move work-ded to work-2.
add 1 oi-deduct-days to u-bin.
if u-bin > pay-date
subtract work-2 from work-1
move work-2 to display-5
else
move zero to display-5.
subtract oi-paid from work-1.
move work-1 to pay-paid.
move work-1 to display-8.
Target — payment_api/domain/discounts.py
from __future__ import annotations
from dataclasses import dataclass
from datetime import date, timedelta
from decimal import Decimal
from opentelemetry import trace
import structlog
logger = structlog.get_logger()
tracer = trace.get_tracer("payment-api.discounts")
ZERO = Decimal("0.00")
@dataclass(frozen=True)
class Invoice:
invoice_ref: str
invoice_date: date
deduct_days: int # oi-deduct-days
work_net: Decimal # oi-net (outstanding net)
work_ded: Decimal # oi-deduct-amt (configured deduction)
oi_paid: Decimal # prior paid amount
@dataclass(frozen=True)
class AppropriationLine:
invoice_ref: str
new_outstanding: Decimal # work-1
discount_taken: Decimal # display-5
def compute_appropriation(inv: Invoice,
payment_date: date) -> AppropriationLine:
with tracer.start_as_current_span("discount.compute") as span:
span.set_attribute("invoice.ref", inv.invoice_ref)
# u-bin = oi-date + 1 + oi-deduct-days
cutoff = inv.invoice_date + timedelta(days=1 + inv.deduct_days)
work_1 = inv.work_net
if cutoff > payment_date: # within window
work_1 -= inv.work_ded # subtract deduction
discount = inv.work_ded
else:
discount = ZERO
work_1 -= inv.oi_paid
span.set_attribute("discount.taken", str(discount))
logger.info(
"discount.computed",
invoice_ref=inv.invoice_ref,
cutoff=cutoff.isoformat(),
within_window=bool(cutoff > payment_date),
discount_taken=str(discount),
new_outstanding=str(work_1),
)
return AppropriationLine(
invoice_ref=inv.invoice_ref,
new_outstanding=work_1,
discount_taken=discount,
)
Translation Notes
- BR-PAY-004 — verbatim transfer. The strict
cutoff > payment_datecomparator (matchingu-bin > pay-date) preserves the legacy boundary semantics: a payment on the discount-deadline date is treated as on-time (eligible). add 1 oi-deduct-days to u-bin→timedelta(days=1 + inv.deduct_days). The implicit "+1" in the COBOL idiom is preserved.Decimalfrom thedecimalmodule preserves COBOLcomp-3precision (pic s9(7)v99= max 9,999,999.99 with 2 decimal places, no float drift).- The display side-effects (
display display-5 at curs) are stripped from the pure function; rendering happens in the React appropriation-preview table (Section 7.3) from the function's return value. - OpenTelemetry span
discount.computetags the invoice ref and discount taken; a downstream BI dashboard can aggregate discount uptake by supplier without touching the source code. - Source:
purchase/pl080.cbl:585-605.
8.5 Example 4 — Unapplied-Balance Prompt
BR-PAY-007 surfaces a Y/N prompt when the supplier has a non-zero purch-unapplied balance. Answering Y flips transaction-type from 5 (plain payment) to 6 (unapplied allocation) and consumes the unapplied amount. The target represents the choice as a boolean field on the request body; the UI surfaces a conditional checkbox; the same 5/6 sentinel values are preserved server-side for SAROC bridge compatibility.
Legacy — purchase/pl080.cbl:421-446
move 5 to transaction-type.
if purch-unapplied = zero
go to value-input.
move purch-unapplied to display-8 amt-ok.
display "Unapplied Balance - " at 0645
with foreground-color 2.
display display-8 at 0665 with foreground-color 3.
display "Do you wish to allocate the unapplied Balance to this account? [Y]"
at 1601 with foreground-color 2.
move "Y" to ws-reply.
accept-unappl-reqst.
move zero to cob-crt-status.
accept ws-reply at 1666 with foreground-color 6 update.
if cob-crt-status = cob-scr-esc
go to new-payment-Main.
move function upper-case (ws-reply) to ws-reply.
if ws-reply not = "Y" and not = "N"
go to accept-unappl-reqst.
if ws-reply = "N"
display space at 1601 with erase eol
go to value-input.
move 6 to transaction-type.
Target — payment_api/api/models.py + payment_api/services/allocation.py
from decimal import Decimal
from enum import IntEnum
from pydantic import BaseModel, Field
import structlog
logger = structlog.get_logger()
class TransactionType(IntEnum):
PAYMENT = 5 # legacy oi-type sentinel
UNAPPLIED_ALLOCATION = 6
class PaymentCreate(BaseModel):
counterparty_code: str
payment_date: date
amount_total: Decimal
allocate_unapplied: bool = Field(
default=False,
description="BR-PAY-007: consume the supplier's "
"purch-unapplied balance.",
)
async def assign_transaction_type(
supplier_unapplied: Decimal,
allocate_unapplied: bool,
) -> TransactionType:
"""Replaces accept-unappl-reqst loop."""
if supplier_unapplied > Decimal("0") and allocate_unapplied:
logger.info("payment.transaction_type.unapplied_consumed",
amount=str(supplier_unapplied))
return TransactionType.UNAPPLIED_ALLOCATION
return TransactionType.PAYMENT
Translation Notes
- BR-PAY-007 — verbatim semantics. The 5/6 sentinel values from
plwsoi.cobsurvive as theTransactionTypeenum members so the SAROC translator can emit them straight into the legacyopen_item.typecolumn. - The COBOL
accept-unappl-reqstretry loop ("re-prompt on invalid reply") becomes Pydantic boolean validation at the request boundary — invalid inputs return RFC 7807422 Unprocessable Entitywith field-level details. - UI counterpart: the checkbox in Section 7.3 only renders when
supplier.unapplied_balance > 0, matching the legacy conditional display. - structlog event
payment.transaction_type.unapplied_consumedrecords each consumption with the amount, enabling unapplied-balance trend dashboards. - Source:
purchase/pl080.cbl:421-446.
8.6 Example 5 — Payment Method Determination (BACS vs Cheque)
BR-PAY-005 is the strongest cross-service example in the catalog. pl940.cbl:517-525 inspects the supplier sort-code, increments the cheque counter on zero, and tags the record BACS otherwise. The target splits this single physical file path into two pipelines: cheque payments produce a printable cheque PDF in payment-batch; BACS payments publish a payment.issued.bacs event consumed downstream. The mitigation is a smoke test asserting no payment is dropped between the legacy single-file path and the dual-path target.
Legacy — purchase/pl940.cbl:517-525
if pay-sortcode = zero
move cheque-nos to c-cheque
move cheque-nos to pay-cheque
add 1 to cheque-nos
else
move zero to pay-cheque
move "BACS" to c-cheque-x.
Target — payment_batch/services/cheque_run.py
from enum import StrEnum
from opentelemetry import trace
from aio_pika import Message, DeliveryMode
import structlog
logger = structlog.get_logger()
tracer = trace.get_tracer("payment-batch.cheque_run")
class PaymentMethod(StrEnum):
CHEQUE = "CHEQUE"
BACS = "BACS"
async def route_payment(payment, *, cheque_counter, channel):
"""Replaces pl940's single physical file with two pipelines."""
with tracer.start_as_current_span("payment.route") as span:
span.set_attribute("payment.id", str(payment.id))
if payment.sort_code == 0:
method = PaymentMethod.CHEQUE
cheque_no = cheque_counter.next()
payment.cheque_no = cheque_no
span.set_attribute("payment.method", "CHEQUE")
span.set_attribute("cheque.no", cheque_no)
logger.info("payment.routed.cheque",
payment_id=str(payment.id),
cheque_no=cheque_no)
else:
method = PaymentMethod.BACS
payment.cheque_no = None
span.set_attribute("payment.method", "BACS")
await channel.default_exchange.publish(
Message(
body=payment.to_event_bytes(method),
delivery_mode=DeliveryMode.PERSISTENT,
content_type="application/json",
),
routing_key="acas.payment.issued.bacs",
)
logger.info("payment.routed.bacs",
payment_id=str(payment.id),
sort_code=payment.sort_code)
payment.method = method
return method
Translation Notes
- BR-PAY-005 — delivery refactor per the catalog. Per-payment routing is preserved verbatim; only the materialization fans out from one file to two pipelines.
- The cheque-number sequence (
cheque-nos+ auto-increment) is owned by acheque_counterservice that uses a PostgreSQL sequence; cheque numbers are guaranteed unique across concurrent payment-batch tasks. - BACS payments publish to the
acas.payment.issued.bacsRabbitMQ topic withDeliveryMode.PERSISTENTanddead-letterenabled per the SAROC messaging contract. - Cross-service implications:
payment-reportingconsumesacas.cheque-batch.generatedto materialize cheque-method remittance PDFs, while a future banking-integration service consumesacas.payment.issued.bacsto file BACS submissions. - OpenTelemetry trace carries the W3C trace context from the operator's HTTP request through the RabbitMQ message into the downstream consumer, giving a single trace for the entire cheque-run.
- Source:
purchase/pl940.cbl:517-525.
8.7 Example 6 — Inline Cash Posting + GL Journal (single DB transaction)
pl100.cbl:329-414 walks open-item records, updates the supplier's purch-current balance, and inline-writes a GL batch row via BL-Write. The target collapses this into a single SQLAlchemy 2.0 async unit-of-work that updates the payment status, inserts the GL postings, and commits atomically — eliminating the legacy overnight-cycle latency.
Legacy — purchase/pl100.cbl:329, 414
loop.
perform OTM5-Read-Next.
if fs-reply = 10
go to main-end.
if oi-type = 2 and
oi-b-nos not = zero and
oi-b-item not = zero
perform compute-purch-pay thru csp-exit
move zeros to oi-b-nos oi-b-item oi-cr
perform OTM5-Rewrite
go to loop.
...
cust-update.
move oi-supplier to WS-Purch-Key.
perform Purch-Read-Indexed.
subtract oi-approp from purch-current.
subtract oi-deduct-amt from purch-current.
move oi-approp to oi-paid.
perform Purch-Rewrite.
if G-L
perform BL-Write. *> inline GL journal
move 1 to oi-status.
perform OTM5-Rewrite.
Target — payment_batch/services/posting.py
from decimal import Decimal
from sqlalchemy import select, update
from sqlalchemy.ext.asyncio import AsyncSession
from opentelemetry import trace
import structlog
from .db import Payment, OpenItem, GlPosting, PaymentAudit
from .exceptions import PostingError
logger = structlog.get_logger()
tracer = trace.get_tracer("payment-batch.posting")
async def post_batch(batch_number: int,
posted_by: str,
session: AsyncSession) -> int:
with tracer.start_as_current_span("batch.post") as span:
span.set_attribute("batch.number", batch_number)
# Single DB transaction: all writes commit or roll back together
items = (await session.execute(
select(OpenItem).where(
OpenItem.batch_number == batch_number,
OpenItem.type.in_([5, 6]), # BR-PAY-003 carry-through
)
)).scalars().all()
if not items:
raise PostingError("nothing to post")
gl_debit = Decimal("0.00")
gl_credit = Decimal("0.00")
for it in items:
# Mirror pl100 cust-update: subtract approp + deduct from balance
session.add(GlPosting(
payment_id=it.payment_id,
account="5000", # Trade Creditors (DR)
amount=it.approp_amount,
direction="DR",
))
session.add(GlPosting(
payment_id=it.payment_id,
account="1200", # Bank (CR)
amount=it.approp_amount,
direction="CR",
))
gl_debit += it.approp_amount
gl_credit += it.approp_amount
it.posting_reference = f"BATCH-{batch_number}"
await session.execute(
update(Payment)
.where(Payment.batch_number == batch_number)
.values(status="POSTED", posted_by=posted_by)
)
session.add(PaymentAudit(
batch_number=batch_number,
actor=posted_by,
event="batch.posted",
payload={"gl_debit": str(gl_debit),
"gl_credit": str(gl_credit),
"items_posted": len(items)},
))
await session.commit() # atomic: payment + GL together
span.set_attribute("payments.posted", len(items))
logger.info("batch.posted",
batch_number=batch_number,
items=len(items),
gl_total=str(gl_debit))
return len(items)
Translation Notes
- Implements the General Ledger Posting Integration boundary (Concho conf 0.88); the legacy "if G-L perform BL-Write" inside the per-record loop becomes a single SQLAlchemy unit-of-work where the payment status update and the GL postings commit together.
- BR-PAY-006 — the lifecycle transition to
POSTEDis applied in the same transaction; backward transitions are rejected at the API surface by a CHECK constraint plus an exception handler. - The audit row carries the W3C trace context (the OpenTelemetry baggage in the calling request); the legacy
OTM5-Rewritewrote no audit, and the printed posting register was the only paper trail. - Span
batch.postrecordsbatch.numberandpayments.postedso capacity dashboards can spot anomalies (e.g., a batch that posts 0 items). - Domain exception
PostingError→ 422 RFC 7807; the legacy program "completed" successfully when there was nothing to post. - Source:
purchase/pl100.cbl:300-460.
8.8 Data Access Patterns
The legacy code accesses data through the Payment File Handler Abstraction (Concho entity acas032, ~650 LOC, conf 0.80) — a dual-mode ISAM/MySQL switch driven by FA-RDBMS-Flat-Statuses. Every perform OTM5-Open / Read-Next / Rewrite / Close branches on the storage-mode flag and either calls the ISAM handler or the MySQL bridge. Modern code replaces this entirely with the SQLAlchemy repository pattern:
| Legacy access | Modern equivalent | Notes |
|---|---|---|
perform OTM5-Open | session = AsyncSession() | SQLAlchemy 2.0 async; connection from RDS pool (PostgreSQL). |
perform OTM5-Read-Next | await session.execute(select(OpenItem).order_by(...)) | Server-side cursor; can stream millions of rows without loading them all. |
perform Purch-Read-Indexed | await session.get(Purchase, supplier_code) | Indexed lookup; relationship loading driven by query options. |
perform OTM5-Rewrite | session.add(modified_open_item) + await session.commit() | Identity-map dirty-tracking; no explicit "rewrite" call. |
perform Purch-Rewrite | Same as above with Purchase entity | One unit-of-work per HTTP request or background-job step. |
Dual ISAM/MySQL switch via acas032 | Single PostgreSQL engine; SAROC translator handles legacy-side compatibility | The dual-mode complexity disappears entirely from application code; the bridge translator is the only place that still cares about ISAM. |
8.9 Error Handling Patterns
COBOL's classical error idiom is if fs-reply ≠ 0 after every file operation, with display "PLnnn"-style on-screen messages. The target uses an explicit domain-exception hierarchy mapped to RFC 7807 Problem Details responses by a single FastAPI exception handler. The legacy idiom and its target replacement, side by side:
Legacy — purchase/pl080.cbl
move pay-customer to WS-Purch-Key.
*>
perform Purch-Read-Indexed. *> read purchase-file
*> record invalid key
if fs-reply = 21
move zero to c-check.
*>
if not c-exists
go to customer-input.
*>
*> Every file op is followed by an inline fs-reply test;
*> status 21 = "record not found", 10 = end-of-file.
*> On-screen "PLnnn" messages report the error to the
*> operator — no structured error contract exists.
Target — payment_api/errors/handlers.py
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from opentelemetry import trace
import structlog
from .exceptions import (
DomainError, ValidationError, NotFoundError,
ConflictError, PostingRequiredError,
)
logger = structlog.get_logger()
def install_error_handlers(app: FastAPI) -> None:
@app.exception_handler(DomainError)
async def handle_domain_error(request: Request, exc: DomainError):
span = trace.get_current_span()
span.record_exception(exc)
span.set_attribute("error.code", exc.code)
logger.warning("api.domain_error",
code=exc.code,
path=request.url.path)
return JSONResponse(
status_code=exc.status_code,
content={
"type": f"https://acas.example/errors/{exc.code}",
"title": exc.title,
"status": exc.status_code,
"detail": exc.detail,
"instance": str(request.url),
},
media_type="application/problem+json",
)
Concrete subclasses:
ValidationError→ HTTP 422 (replaces COBOL "re-prompt on invalid reply" loops).NotFoundError→ HTTP 404 (replacesfs-reply = 21"record not found" branches).ConflictError→ HTTP 409 (replaces sentinel-flag blocking like BR-PAY-001 posting gate).PostingRequiredError→ HTTP 409 +code: POSTING_REQUIRED(specific to BR-PAY-001).DependencyError→ HTTP 503 (downstream RabbitMQ / S3 / RDS unavailable).
8.10 Testing Approach for Translated Code
This subsection covers verification at the translated-code layer (pytest + pytest-asyncio, 80% coverage floor per resolved targetPatterns.testing). The broader testing posture — Playwright behavior tests auto-derived from the Section 9 BR catalog + the Section 9.8 field-level rules, run inside the iterate-till-green loop in the artifact-generation workflow — is described as part of that downstream workflow and is the gate that decides whether artifacts ship; the pytest layers below are what those Playwright tests stand on.
- Unit tests against pure functions (e.g.,
compute_appropriation,compute_mod11_check_digit) using table-driven cases generated from the COBOL examples in the Concho Cycle 8 entities. Each business-rule entry in the catalog (behavioral-rules-catalog-012.md) yields at least one happy-path test and one boundary test. - Integration tests against an in-process FastAPI + an ephemeral PostgreSQL container (via
testcontainers); these exercise the SQLAlchemy unit-of-work, the GL inline posting, and the RabbitMQ event emission (with an in-memory broker). - Playwright behavior tests (handoff to the artifact workflow) — the
render-playwright-br-specandrender-playwright-specgenerators emit a.spec.tsper BR + per field-level rule + per user-journey scene. The artifact-generation workflow runs these in aniterate-till-greenloop driven bymig-adversarial-validator: a Playwright failure rejects the artifact, the validator emits a diff against the spec, the artifact agent regenerates, the loop repeats until 100% green. PASS-DEFERRED is forbidden. See the artifact workflow'sRUNTIME-VERIFICATION-RETROSPECTIVE.mdfor the per-defect log produced by this loop in run-012. - Adversarial / consensus tests — only the BR-PAY-008 cap-relaxation mitigation requires a SAROC dual-storage fixture comparing legacy COBOL output against modern output side-by-side, because it is the one rule whose per-payment behavior actually changes (hard cap → soft cap → no cap). BR-PAY-005 and BR-PAY-010 are delivery refactors — their per-payment behavior is identical, so they are covered by the unit/integration/Playwright layers above without needing a side-by-side comparison.
A future revision of this report will lift testing into its own top-level section so it is not buried inside “Code Translation Examples.” The content above is the code-translation slice; the artifact-workflow slice (iterate-till-green, Playwright auto-derivation, per-step validators) is owned by the companion code-generation workflow.
8.11 Code Translation Summary
| Pattern translated | Legacy LOC (representative) | Target LOC (representative) | Behavioural fidelity |
|---|---|---|---|
| MOD-11 check digit (BR-PAY-002) | ~55 | ~30 | Verbatim |
| Posting gate (BR-PAY-001) | ~5 | ~12 (incl. exception) | Verbatim |
| Early-payment discount (BR-PAY-004) | ~20 | ~45 | Verbatim |
| Unapplied-balance prompt (BR-PAY-007) | ~25 | ~25 | Verbatim |
| BACS vs Cheque routing (BR-PAY-005) | ~9 | ~40 | Delivery refactor |
| Cash posting + GL (BR-PAY-006 + GL integration) | ~130 | ~55 | Verbatim |
The artifact-generation workflow scales this pattern across the rest of the 30-strong file set: each COBOL program in purchase/pl08x-pl9xx and common/maps* maps to a FastAPI router, a service-layer module, and a SQLAlchemy data-access layer, with the resolved targetPatterns consistently applied.
9. Business Rules Analysis
This section documents the behavioral rules extracted from the ACAS Payment Processing subsystem, showing COBOL source implementation alongside Python (FastAPI) translations with formal Given-When-Then specifications. All 10 rules were discovered through Concho's Context Graph analysis (cycle 8) and verified against COBOL source code via the Concho MCP get_file_content_in_range tool. A companion subsection — Section 9.8: Field-Level Business Rules — extends this catalog with per-field validation and formatting rules auto-derived from the UI field inventory; those field-level rules feed directly into Playwright test generation for the artifact workflow.
9.1 Business Rules Overview
Concho cycle 8 catalogued 214 system-wide business rules across the ACAS codebase. Filtering to the Payment Processing subsystem and applying a confidence floor of 0.60 produced the 10 rules documented here. The rule extraction uses Concho's converged-entity model (the canonical merge of redundant rule statements across files) which is why a single rule like SupplierAccountNumberFormat consolidates four duplicate source statements into one BR. Concho confidence scores range from 0.60 to 0.88; the most confident rule (BR-PAY-002, MOD-11 check digit) has STRUCTURAL evidence from a 60-line algorithm in common/maps09.cbl, while the lowest-confidence rules (BR-PAY-007 Unapplied Balance, BR-PAY-009 Payment Reversal) are INFERRED from flow patterns rather than explicit declarations.
ACAS is a COBOL/GnuCOBOL codebase with no automated test suite cataloged by Concho (search_files for file_category=Test returned 0 results). Per the legacy-test-discovery skill, the COBOL short-circuit applies: every BR's testEvidence[] is empty and every GWT carries origin: code-inferred. This is expected for mainframe-shaped projects — absence of test corroboration is a fact about the source repository, not a quality flag against this catalog. The artifact-generation workflow that follows this report will generate Playwright behavior tests from each BR's GWT specification (combined with the field-level rules in Section 9.8), giving the modernized stack the regression coverage the legacy never had.
Rule Distribution by Category
| Category | Count | Description | BR IDs |
|---|---|---|---|
| Validation | 3 | Input validation, identity-format enforcement, eligibility filters | BR-PAY-001, BR-PAY-002, BR-PAY-003 |
| Calculation | 2 | Financial arithmetic and method-routing computations | BR-PAY-004, BR-PAY-005 |
| State Transition | 2 | Multi-stage status progression with audit-trail invariants | BR-PAY-006, BR-PAY-007 |
| Workflow | 3 | Multi-step processes spanning batch entry, correction, and document output | BR-PAY-008, BR-PAY-009, BR-PAY-010 |
| Authorization | 0 | No role-based authorization rules exist in the legacy — see note below | — |
CredentialLengthConstraints 4-character password is documented in Section 5.3 as a platform constraint), but the codebase contains no role-based authorization rules — no per-action permission checks, no user-role-to-operation mapping. Role-based access control will be introduced as part of the modernization target architecture (FastAPI dependency-injected auth, JWT bearer tokens), but that is a forward-architectural addition, not a behavioral rule being lifted-and-shifted from the legacy.
Rule Discovery Methodology
Concho's get_insight(insight_type='business_rules', detail_level='detailed') surfaced the system-wide rule inventory with confidence and evidence-quality distributions. get_entities_by_subject(subject_name='Payment Processing', entity_type='business_rule') and a series of search_entities queries (filtered by name patterns Payment / Batch / Supplier / Remittance and confidence_gte = 0.6 to 0.8) narrowed the catalog to the Payment Processing scope. For each candidate rule, get_entity retrieved the converged description, source-file list, and line references. Workflow-assigned BR-PAY-NNN identifiers were applied during this report-generation step — the BR-PAY-NNN tokens do NOT exist in the COBOL source code and never appear inside the legacy-code panels below.
For each cited line reference, get_file_content_in_range retrieved the actual COBOL implementation, which is the source for the COBOL panels shown below. The Given-When-Then specifications were derived from a combination of the Concho entity description (canonical statement of intent) and the verified COBOL implementation (literal facts about flow control, comparison operators, and field semantics). No COBOL code was fabricated; no line references were invented.
9.2 Validation Rules
BR-PAY-001: Payment Posting Validation
COBOL (purchase/pl080.cbl)
if P-Flag-I = 1
display PL121 at 0501
display PL116 at 2401
accept ws-reply at 2440
go to main-exit.
*>
if FS-Cobol-Files-Used *> code added to match sl080
call "CBL_CHECK_FILE_EXIST" using File-29 *> Open ITM5 file
File-Info
if return-code not = zero *> not = zero - No file found
display PL119 at 2301
display PL116 at 2401
accept ws-reply at 2440
go to main-exit
end-if
end-if.
Python (FastAPI) (payment_api/routers/payments.py)
@router.post("/payments", status_code=201)
async def create_payment(
body: NewPaymentRequest,
ledger: PurchaseLedgerState = Depends(get_ledger_state),
) -> PaymentCreated:
if ledger.posting_state != PostingState.POSTED:
raise HTTPException(
status_code=409,
detail={
"code": "POSTING_REQUIRED",
"message": (
"Purchase ledger has unposted transactions; "
"complete posting before recording payments."
),
},
)
# ... payment-entry path proceeds ...
Business Rule Analysis
Given: The global posting flag P-Flag-I has been set to 1 by an earlier purchase-ledger step that produced unposted transactions.
When: An operator launches pl080 (the cash-posting entry program).
Then: The program displays PL121 (posting-required notice) and PL116 (acknowledge prompt), accepts a keypress, and exits via main-exit without entering the payment-entry loop.
GWT origin: Code-inferred
Legacy Test Coverage: No legacy test coverage found
Source: purchase/pl080.cbl lines 288–293
Concho entity: PaymentPostingValidation
Confidence: 0.65 (INFERRED)
Behavioral Fidelity: Verbatim — same gate, modern HTTP semantics. Sentinel value 1 becomes a typed enum; flow-control via early-return becomes HTTPException(409).
BR-PAY-002: Supplier Account Number Format (MOD-11 Check Digit)
Reassigned from Section 5: The platform-affinity reconciler explicitly relocated SupplierAccountNumberFormat from the platform-constraints section to Section 9 because the MOD-11 check digit is a genuine business identity invariant, not a platform artifact. Concho's canonical entity name is CustomerNumberMOD11CheckDigitValidation — the algorithm is shared across customer and supplier identifiers via the maps09 utility.
COBOL (common/maps09.cbl)
main.
move customer-nos to work-array.
move zero to suma.
perform addition-loop through addition-end
varying a from 1 by 1 until a > 6.
*>
if suma = zero
move "N" to maps09-reply
go to main-exit.
*>
divide suma by 11 giving z.
compute a = 11 - (suma - (11 * z)).
*>
if maps09-reply = "C"
move a to check-digit
move "Y" to maps09-reply.
*>
if maps09-reply = "V"
and a = check-digit
move "Y" to maps09-reply.
*>
addition-do.
set y to q.
compute z = y * (8 - a).
add z to suma.
Python (FastAPI) (payment_api/domain/identifiers.py)
ACAS_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
def _mod11_sum(identifier: str) -> int:
"""Weighted sum: position p (1..6) uses weight (8 - p)."""
suma = 0
for position, ch in enumerate(identifier[:6], start=1):
weight = 8 - position
ordinal = ACAS_ALPHABET.index(ch) + 1
suma += ordinal * weight
return suma
def compute_check_digit(identifier_6: str) -> int | None:
suma = _mod11_sum(identifier_6)
if suma == 0:
return None
z = suma // 11
return 11 - (suma - (11 * z))
def verify_supplier_id(supplier_id: str) -> bool:
if len(supplier_id) != 7:
return False
expected = compute_check_digit(supplier_id[:6])
if expected is None:
return False
return str(expected) == supplier_id[6]
Business Rule Analysis
Given: A 7-character supplier account identifier (6 characters of identity + 1 check digit) is presented to the payment-entry workflow.
When: pl080 invokes maps09 with mode='V' (verify) before opening the supplier balance record.
Then: maps09 returns maps09-reply='Y' when the computed weighted-MOD-11 check digit matches the trailing character, or 'N' when it differs; an 'N' reply blocks payment-record creation against that supplier identifier.
GWT origin: Code-inferred
Legacy Test Coverage: No legacy test coverage found
Source: common/maps09.cbl lines 90–145
Concho entity: CustomerNumberMOD11CheckDigitValidation (4 source entities converged)
Confidence: 0.88 (STRUCTURAL — highest in catalog)
Behavioral Fidelity: Verbatim — algorithm preserved byte-for-byte; Pydantic validator runs the same arithmetic in calculation mode at supplier creation and verification mode on every inbound payment.
BR-PAY-003: Payment Type Filtering (Sort Eligibility)
COBOL (sales/sl090.cbl)
perform OTM3-Read-Next. *> read open-item-file-3 next record
if fs-reply = 10
go to end-of-input.
*>
if oi-type = 1 or 3 *> Ignore Receipts & Cr. Notes
go to process-input.
if zero = oi-b-nos and oi-b-item *> batch data zero
go to process-input.
*>
release sort-record from open-item-record-3.
go to process-input.
Python (FastAPI) (payment_api/services/payment_proof.py)
class OpenItemType(IntEnum):
RECEIPT = 1
INVOICE = 2
CREDIT_NOTE = 3
DEBIT = 4
# Eligible for payment-proof sort:
PROOF_ELIGIBLE_TYPES = {OpenItemType.INVOICE, OpenItemType.DEBIT}
def select_proof_candidates(session: Session) -> list[OpenItem]:
return (
session.query(OpenItem)
.filter(OpenItem.type.in_(PROOF_ELIGIBLE_TYPES))
.filter(or_(OpenItem.batch_no != 0,
OpenItem.batch_item != 0))
.order_by(OpenItem.batch_no, OpenItem.batch_item)
.all()
)
Business Rule Analysis
Given: Open-item records (OTM3) are being read in sequence for the payment-proof sort pass.
When: The current record's oi-type is 1 (receipt) or 3 (credit note), or both oi-b-nos and oi-b-item are zero.
Then: The record is skipped (no release to sort-record), and the read advances to the next OTM3 record; otherwise the record is released into the sort stream for payment-proof output.
GWT origin: Code-inferred
Legacy Test Coverage: No legacy test coverage found
Source: sales/sl090.cbl lines 262–267
Concho entity: PaymentTypeFilteringRule
Confidence: 0.79 (CATEGORICAL)
Behavioral Fidelity: Verbatim — eligibility logic moves from imperative skip-and-continue to a SQL WHERE clause; constants 1/2/3/4 preserved in OpenItemType enum for SAROC bridge compatibility.
9.3 Calculation Rules
BR-PAY-004: Early Payment Discount Calculation
COBOL (purchase/pl080.cbl)
move work-net to work-1.
move work-ded to work-2.
*>
add 1 oi-deduct-days to u-bin.
if u-bin > pay-date
subtract work-2 from work-1
move work-2 to display-5
else
move zero to display-5.
*>
subtract oi-paid from work-1.
move work-1 to pay-paid.
move work-1 to display-8.
Python (FastAPI) (payment_api/domain/discount.py)
from decimal import Decimal
from datetime import date, timedelta
from dataclasses import dataclass
@dataclass(frozen=True)
class DiscountResult:
discount_taken: Decimal # display-5 in legacy
appropriation: Decimal # work-1 / pay-paid in legacy
def compute_early_discount(
*, work_net: Decimal, work_ded: Decimal,
oi_deduct_days: int, oi_date: date, oi_paid: Decimal,
pay_date: date,
) -> DiscountResult:
discount_deadline = oi_date + timedelta(days=1 + oi_deduct_days)
if discount_deadline > pay_date:
appropriation = work_net - work_ded - oi_paid
return DiscountResult(work_ded, appropriation)
return DiscountResult(Decimal("0"), work_net - oi_paid)
Business Rule Analysis
Given: An open invoice has work-net (outstanding amount), work-ded (configured deduction), oi-deduct-days (discount-window length), oi-date (invoice date); the user has entered pay-date and pay-paid.
When: pl080 computes u-bin = oi-date + 1 + oi-deduct-days and tests u-bin > pay-date.
Then: If u-bin > pay-date (payment within window), subtract work-ded from work-net and emit the deduction into display-5; otherwise display-5 = 0. Resulting work-1 = work-net - work-ded - oi-paid is the new appropriation amount.
GWT origin: Code-inferred
Legacy Test Coverage: No legacy test coverage found
Source: purchase/pl080.cbl lines 585–605
Concho entity: PaymentAppropriationLogic
Confidence: 0.70 (INFERRED)
Behavioral Fidelity: Verbatim — Decimal arithmetic preserves COBOL comp-3 precision; boundary strict-greater-than semantics preserved (a payment on the exact deadline is eligible). The legacy add 1 oi-deduct-days off-by-one is faithfully reproduced.
BR-PAY-005: Payment Method Determination (BACS vs Cheque) delivery refactor
COBOL (purchase/pl940.cbl)
if pay-sortcode = zero
move cheque-nos to c-cheque pay-cheque
add 1 to cheque-nos
else
move zero to pay-cheque
move "BACS" to c-cheque-x
end-if
write cheque-record from cheque.
*>
move u-bin to pay-date.
perform Payments-Rewrite.
*>
*> now loop back for next item....
*>
go to read-purchase.
Python (FastAPI) (payment_api/services/payment_finalizer.py)
class PaymentMethod(str, Enum):
CHEQUE = "cheque"
BACS = "bacs"
def determine_method(supplier: Supplier) -> PaymentMethod:
"""pl940:517 — sort_code == 0 means physical cheque."""
if supplier.sort_code == 0:
return PaymentMethod.CHEQUE
return PaymentMethod.BACS
async def finalize_payment(
payment: Payment, supplier: Supplier,
cheque_seq: ChequeSequence, broker: MessageBroker,
) -> None:
method = determine_method(supplier)
if method is PaymentMethod.CHEQUE:
payment.cheque_no = await cheque_seq.next_value()
await ledger.persist(payment)
await broker.publish("payment.issued.cheque", payment.dict())
else:
payment.cheque_no = 0
await ledger.persist(payment)
await broker.publish("payment.issued.bacs", payment.dict())
Business Rule Analysis
Given: A pay-record is being processed by pl940 (payment-finalization) and has a pay-sortcode field populated from the supplier master.
When: pl940 tests if pay-sortcode = zero.
Then: If true, move cheque-nos to c-cheque and pay-cheque, then add 1 to cheque-nos (sequential cheque allocation); otherwise, move zero to pay-cheque and move "BACS" to c-cheque-x. The cheque-record is written either way.
GWT origin: Code-inferred
Legacy Test Coverage: No legacy test coverage found
Source: purchase/pl940.cbl lines 517–525
Concho entity: PaymentMethodDetermination (2 source entities converged)
Confidence: 0.79 (STRUCTURAL)
Behavioral Fidelity: Mitigation required. Legacy emits a single physical cheque-file containing mixed cheque + "BACS"-tagged records, all printed by pl960. Target fans out into two pipelines: cheque payments produce a printable PDF cheque-report; BACS payments publish to a SAROC topic for downstream banking. Per-payment semantics preserved; only the materialization branches. Mitigation: integration smoke test asserting no payment is dropped between the legacy single-file path and the dual-path target.
9.4 State Transition Rules
BR-PAY-006: Batch Status Lifecycle
COBOL (copybooks/fdbatch.cob)
88 PL-Batch value 2.
88 SL-Batch value 3.
05 Batch-Nos pic 9(5).
03 Items pic 99.
*>
03 Batch-Status pic 9.
88 Status-Open value 0.
88 Status-Closed value 1.
*>
03 Cleared-Status pic 9.
88 Waiting value 0.
88 Processed value 1.
88 Archived value 2.
*>
03 Bcycle pic 99.
03 Dates.
05 Entered binary-long.
05 Proofed binary-long.
05 Posted binary-long.
05 Stored binary-long.
Python (FastAPI) (payment_api/domain/batch.py)
class BatchStatus(IntEnum):
OPEN = 0
CLOSED = 1
class ClearedStatus(IntEnum):
WAITING = 0
PROCESSED = 1
ARCHIVED = 2
# Permitted transitions enforced at the DB layer via CHECK constraint
# and at the service layer via this map:
_BATCH_TRANSITIONS = {BatchStatus.OPEN: {BatchStatus.CLOSED}}
_CLEARED_TRANSITIONS = {
ClearedStatus.WAITING: {ClearedStatus.PROCESSED},
ClearedStatus.PROCESSED: {ClearedStatus.ARCHIVED},
}
def transition_batch(b: Batch, target: BatchStatus, posted: Decimal) -> None:
if target not in _BATCH_TRANSITIONS.get(b.status, set()):
raise InvalidTransition(b.status, target)
if target is BatchStatus.CLOSED and posted != b.header_total:
raise BatchTotalsMismatch(expected=b.header_total, actual=posted)
b.status = target
audit_log.record(b.id, "batch.status", b.status, target)
Business Rule Analysis
Given: A batch record exists with Batch-Status = 0 (Open) and Cleared-Status = 0 (Waiting), and item-level postings have been entered against it.
When: An operator runs the batch-close step after item totals reconcile against the batch header.
Then: Batch-Status moves 0 → 1 (Open → Closed); subsequent successful posting transitions Cleared-Status 0 → 1 (Waiting → Processed); subsequent archival transitions 1 → 2 (Processed → Archived). Backward transitions are not permitted.
GWT origin: Code-inferred
Legacy Test Coverage: No legacy test coverage found
Source: copybooks/fdbatch.cob lines 20–30; enforcement in general/gl070.cbl:307
Concho entity: BatchStatusLifecycleValidation (2 source entities converged)
Confidence: 0.87 (STRUCTURAL)
Behavioral Fidelity: Verbatim — sentinel values 0/1/2 preserved as enum integer values for SAROC bridge translation; PostgreSQL CHECK constraints enforce valid transitions at the storage layer in addition to the service-layer guard.
BR-PAY-007: Unapplied Balance Allocation
COBOL (purchase/pl080.cbl)
move 5 to transaction-type.
if purch-unapplied = zero
go to value-input.
*>
move purch-unapplied to display-8 amt-ok.
display "Unapplied Balance - " at 0645 with foreground-color 2.
display display-8 at 0665 with foreground-color 3.
display "Do you wish to allocate the unapplied Balance to this account? [Y]"
at 1601 with foreground-color 2.
move "Y" to ws-reply.
*>
accept-unappl-reqst.
move zero to cob-crt-status.
accept ws-reply at 1666 with foreground-color 6 update.
move function upper-case (ws-reply) to ws-reply.
if ws-reply not = "Y" and not = "N"
go to accept-unappl-reqst.
if ws-reply = "N"
display space at 1601 with erase eol
go to value-input.
*>
move 6 to transaction-type.
Python (FastAPI) (payment_api/routers/payments.py)
class TransactionType(IntEnum):
PAYMENT = 5
UNAPPLIED_ALLOCATION = 6
class NewPaymentRequest(BaseModel):
supplier_id: str
amount: Decimal
pay_date: date
# When the supplier has a non-zero unapplied balance, the UI
# surfaces a checkbox bound to this field:
allocate_unapplied: bool = False
@router.post("/payments", status_code=201)
async def create_payment(body: NewPaymentRequest, ...):
supplier = await suppliers.get(body.supplier_id)
if supplier.unapplied_balance > 0 and body.allocate_unapplied:
txn_type = TransactionType.UNAPPLIED_ALLOCATION # 6
supplier.unapplied_balance = Decimal("0")
else:
txn_type = TransactionType.PAYMENT # 5
# ... record payment with txn_type ...
Business Rule Analysis
Given: A new payment is being entered against a supplier; purch-unapplied is non-zero, and transaction-type defaults to 5 (payment).
When: pl080 detects purch-unapplied > 0 and prompts the operator: "Do you wish to allocate the unapplied Balance to this account?"
Then: If ws-reply = 'Y', transaction-type is set to 6 (unapplied-allocation); if 'N', the prompt is erased and processing continues with transaction-type = 5 (plain payment). Invalid replies re-prompt.
GWT origin: Code-inferred
Legacy Test Coverage: No legacy test coverage found
Source: purchase/pl080.cbl lines 421–446
Concho entity: UnappliedBalanceAllocation
Confidence: 0.60 (INFERRED)
Behavioral Fidelity: Verbatim — sentinel values 5 (payment) and 6 (unapplied-allocation) preserved in the open_items.type column for SAROC bridge compatibility; the operator's keyboard prompt becomes an explicit boolean field on the request body, surfaced as a checkbox in the web UI only when the supplier has a non-zero unapplied balance.
9.5 Workflow Rules
BR-PAY-008: Payment Batch Control Limits (99-item cap + composite key) mitigation required
COBOL (purchase/pl080.cbl + copybooks/fdbatch.cob)
*> copybooks/fdbatch.cob: header layout
03 Items pic 99. *> max 99 per batch
03 Batch-Status pic 9.
*>
*> purchase/pl080.cbl:731 — composite invoice key
move pay-customer to oi-supplier.
move pay-date to oi-date.
move transaction-type to oi-type.
move pay-value to oi-paid.
move approp-amount to oi-approp.
move deduct-taken to oi-deduct-amt.
move bl-next-batch to oi-b-nos.
move k to oi-b-item.
multiply oi-b-nos by 1000 giving oi-invoice.
add oi-b-item to oi-invoice.
*>
move OI-Header to WS-OTM5-Record.
perform OTM5-Write.
Python (FastAPI) (payment_api/domain/batch.py)
SAROC_COEXISTENCE_ITEM_CAP = 99 # feature-flag-gated soft cap
class Batch(SQLModel, table=True):
id: UUID = Field(default_factory=uuid4, primary_key=True)
batch_no: int = Field(index=True, unique=True)
items_count: int = Field(default=0) # PG BIGINT, no 99 ceiling
status: BatchStatus = BatchStatus.OPEN
header_total: Decimal = Decimal("0")
def add_item(batch: Batch, item: OpenItem,
coexistence_mode: bool) -> OpenItem:
if coexistence_mode and batch.items_count >= SAROC_COEXISTENCE_ITEM_CAP:
raise BatchFullDuringCoexistence(batch_no=batch.batch_no)
batch.items_count += 1
item.batch_no = batch.batch_no
item.batch_item = batch.items_count
# Composite legacy key emitted for SAROC bridge translation only:
item.legacy_invoice_key = batch.batch_no * 1000 + item.batch_item
return item
Business Rule Analysis
Given: A batch has been opened (bl-next-batch assigned, Items = 00); the operator is entering payment item #k against supplier pay-customer.
When: Item #k reaches main-end (the open-item record is being written to OTM5); the program computes oi-invoice = oi-b-nos * 1000 + oi-b-item.
Then: An OTM5 record is written with the composite oi-invoice key, the header Items field is incremented; if Items reaches 99 the next item-entry attempt is blocked (the pic 99 field overflow guard) and the operator must close the batch and open a new one.
GWT origin: Code-inferred
Legacy Test Coverage: No legacy test coverage found
Source: purchase/pl080.cbl lines 731–735 (composite key); copybooks/fdbatch.cob line 18 (Items pic 99)
Concho entity: PaymentBatchControlLimits
Confidence: 0.75 (STRUCTURAL)
Behavioral Fidelity: Mitigation required. Target uses PG BIGINT for items_count — the 99-item ceiling is preserved as a feature-flag-gated soft cap during SAROC coexistence so the bridge can translate batches 1-to-1; the cap is removed at cutover. The legacy composite key oi-b-nos * 1000 + oi-b-item is emitted as legacy_invoice_key alongside the new UUID for any record that needs to flow back to COBOL. Mitigation: feature-flag test fixture asserting the cap rejects 100-item batches in coexistence mode and accepts them post-cutover.
BR-PAY-009: Payment Reversal Processing
COBOL (purchase/pl085.cbl)
*>
if oi-approp = zero
go to skip-pay-approp.
move oi-deduct-amt to si-deduct-amt.
*>
data-input.
perform payment-appropriate.
*>
skip-pay-approp.
*>
if trans-unapplied
add hold-pay to purch-unapplied
perform Purch-Rewrite.
move 14 to lin.
perform erase-screen.
display "Make further corrections? (Y/N) [Y]"
at 1629 with foreground-color 2.
*>
more-data.
move "Y" to ws-reply.
accept ws-reply at 1663 with foreground-color 6 update.
move function upper-case (ws-reply) to ws-reply.
Python (FastAPI) (payment_api/routers/payments.py)
class PaymentCorrection(SQLModel, table=True):
id: UUID
original_id: UUID # FK to payments.id (immutable)
correction_id: UUID # FK to payments.id (the new amount)
delta_amount: Decimal
created_at: datetime
actor: str
@router.post("/payments/{payment_id}/reverse",
status_code=200,
response_model=PaymentReversed)
async def reverse_payment(
payment_id: UUID, body: ReversalRequest,
broker: MessageBroker, ...
) -> PaymentReversed:
original = await payments.require(payment_id)
if original.transaction_type == TransactionType.UNAPPLIED_ALLOCATION:
# Mirrors pl085:445 — trans-unapplied path
supplier = await suppliers.get(original.supplier_id)
supplier.unapplied_balance += original.amount
correction = await payments.create_correction(
original=original, new_amount=body.corrected_amount,
actor=body.actor,
)
await broker.publish("payment.reversed", correction.event())
return PaymentReversed.from_orm(correction)
Business Rule Analysis
Given: A payment was previously recorded with appropriations across one or more invoices; an operator launches pl085 to make corrections.
When: The operator selects the affected payment; pl085 detects trans-unapplied is true (original was an unapplied allocation), and the operator supplies a corrected amount.
Then: hold-pay is added back to purch-unapplied and the supplier record is rewritten; payment-appropriate is re-invoked to apply the corrected amount; original + corrective allocations both remain in OTM5 history.
GWT origin: Code-inferred
Legacy Test Coverage: No legacy test coverage found
Source: purchase/pl085.cbl lines 435–460
Concho entity: PaymentReversalProcessing
Confidence: 0.60 (INFERRED)
Behavioral Fidelity: Verbatim — original payment row is never deleted (legacy parity: immutable OTM5 history); a payment_corrections table links original ↔ correction with the delta amount. The unapplied-balance restoration is preserved bit-for-bit; the audit trail is materially stronger via the SAROC payment.reversed event.
BR-PAY-010: Remittance Advice Processing delivery refactor
COBOL (purchase/pl960.cbl)
perform varying z from 1 by 1 until z > 9
move c-inv (z) to l4-inv
move c-folio (z) to l4-folio
move c-value (z) to l4-amount
if l4-amount not equal spaces
write print-record from line-4 after 1
end-perform.
*>
write print-record from line-5 after 3 lines.
*>
if C-Cheque not = "BACS"
move "Cheque" to l6-chq-bacs
move C-Cheque to l6-cheque
else
move "BACS" to l6-chq-bacs
move "to your Bank" to l6-cheque.
move c-gross to l6-total.
*>
write print-record from line-6 after 1.
Python (FastAPI) (reporting_service/remittance.py)
async def render_remittance(payment: Payment, supplier: Supplier,
invoices: list[InvoiceLine]) -> bytes:
# Equivalent of pl960 zero-amount skip:
visible_lines = [
ln for ln in invoices
if ln.amount and ln.amount != Decimal("0")
]
method_text = (
f"Cheque {payment.cheque_no}"
if payment.method is PaymentMethod.CHEQUE
else "BACS to your Bank"
)
ctx = {
"supplier": supplier,
"lines": visible_lines,
"method_text": method_text,
"total": payment.amount,
}
pdf = await render_pdf_template("remittance.html.j2", ctx)
s3_key = f"remittance/{payment.id}.pdf"
await s3.put_object(bucket=REMITTANCE_BUCKET, key=s3_key, body=pdf)
return s3_key
Business Rule Analysis
Given: A cheque-file produced by pl940 contains payment records with up to 9 c-inv/c-folio/c-value invoice lines (a line-printer-driven OCCURS array — see Section 5 RemittanceAdviceFormatLimitations) and the c-cheque indicator.
When: pl960 prints the remittance: each non-zero invoice line is written to print-record; the c-cheque value is tested against "BACS".
Then: Zero-amount invoice lines are skipped silently; c-cheque != "BACS" renders Cheque <number>; c-cheque = "BACS" renders BACS to your Bank; first remittance per file forces a page break (the a=1 path); subsequent remittances continue on the same page until cheque-file EOF.
GWT origin: Code-inferred
Legacy Test Coverage: No legacy test coverage found
Source: purchase/pl960.cbl lines 208–290
Concho entity: RemittanceAdviceProcessingRules
Confidence: 0.65 (INFERRED)
Behavioral Fidelity: Mitigation required. Target produces a PDF per payment (no shared-page-stream behavior; each remittance is a self-contained document stored in S3 with a download URL). The 9-line OCCURS limit (a line-printer artifact enumerated in Section 5) is removed; PDF supports arbitrary-length invoice lists. Mitigation: byte-for-byte content parity test on the printable region — the line content (supplier name/address, invoice references, payment indicator text, totals) must match the legacy output even when the format wrapper changes from fixed-width text to PDF.
9.6 Test Cases (Representative)
The following table illustrates representative test cases for each rule category. The companion artifact-generation workflow auto-derives complete Playwright behavior tests from each BR's Given-When-Then specification and from the field-level rules in Section 9.8.
| Test Case | Input | Expected Output | Rule Verified |
|---|---|---|---|
| Payment blocked when posting required | POST /payments while purchase-posting-state = UNPOSTED |
409 Conflict with code: POSTING_REQUIRED |
BR-PAY-001 |
| Valid 7-char supplier ID accepted | supplier_id = "ABC1232" (correct MOD-11 check digit) |
verify_supplier_id == True |
BR-PAY-002 |
| Invalid MOD-11 rejected at payment entry | supplier_id = "ABC1239" (wrong check digit) |
422 Unprocessable Entity with code: INVALID_SUPPLIER_ID |
BR-PAY-002 |
| Credit notes excluded from payment proof | OTM3 record with oi-type = 3 |
Not included in sort output | BR-PAY-003 |
| Discount applied on early payment | Invoice with deduct-days = 30, paid on day 15 | Deduction amount subtracted; display-5 non-zero |
BR-PAY-004 |
| Discount not applied on late payment | Invoice with deduct-days = 30, paid on day 35 | No deduction; display-5 = 0 |
BR-PAY-004 |
| BACS routing for supplier with sort code | Supplier with sort_code = 401234 |
Method = BACS; payment.issued.bacs event published |
BR-PAY-005 |
| Cheque routing for supplier without sort code | Supplier with sort_code = 0 |
Method = CHEQUE; sequential cheque number assigned | BR-PAY-005 |
| Batch state-machine rejects backward transition | Batch with status = CLOSED, attempt to set status = OPEN | 409 Conflict with InvalidTransition |
BR-PAY-006 |
| Unapplied-balance allocation prompt surfaces | Supplier with unapplied_balance > 0 |
UI surfaces "allocate unapplied" checkbox | BR-PAY-007 |
| Batch cap enforced during coexistence | Attempt to add 100th item in coexistence mode | BatchFullDuringCoexistence raised |
BR-PAY-008 |
| Payment reversal preserves original row | POST /payments/{id}/reverse |
Original payment unchanged; correction row added; payment.reversed event published |
BR-PAY-009 |
| Remittance text matches legacy for cheque | Payment with method = CHEQUE, cheque_no = 12345 | PDF body contains "Cheque 12345" string | BR-PAY-010 |
9.7 Business Rule Traceability
| Rule ID | Rule Name | Source File | Lines | Confidence | Evidence | Category | Fidelity |
|---|---|---|---|---|---|---|---|
| BR-PAY-001 | Payment Posting Validation | purchase/pl080.cbl |
288–293 | 0.65 | INFERRED | Validation | Verbatim |
| BR-PAY-002 | Supplier Account Number Format (MOD-11) | common/maps09.cbl |
90–145 | 0.88 | STRUCTURAL | Validation | Verbatim |
| BR-PAY-003 | Payment Type Filtering | sales/sl090.cbl |
262–267 | 0.79 | CATEGORICAL | Validation | Verbatim |
| BR-PAY-004 | Early Payment Discount Calculation | purchase/pl080.cbl |
585–605 | 0.70 | INFERRED | Calculation | Verbatim |
| BR-PAY-005 | Payment Method Determination | purchase/pl940.cbl |
517–525 | 0.79 | STRUCTURAL | Calculation | Mitigation required |
| BR-PAY-006 | Batch Status Lifecycle | copybooks/fdbatch.cob, general/gl070.cbl |
20–30; 307 | 0.87 | STRUCTURAL | State Transition | Verbatim |
| BR-PAY-007 | Unapplied Balance Allocation | purchase/pl080.cbl |
421–446 | 0.60 | INFERRED | State Transition | Verbatim |
| BR-PAY-008 | Payment Batch Control Limits | purchase/pl080.cbl, copybooks/fdbatch.cob |
731–735; 23 | 0.75 | STRUCTURAL | Workflow | Mitigation required |
| BR-PAY-009 | Payment Reversal Processing | purchase/pl085.cbl |
435–460 | 0.60 | INFERRED | Workflow | Verbatim |
| BR-PAY-010 | Remittance Advice Processing | purchase/pl960.cbl |
208–290 | 0.65 | INFERRED | Workflow | Mitigation required |
For the testing approach that validates rule preservation (unit, integration, regression) at the artifact level, see Section 8.7: Testing Approach. The artifact-generation workflow auto-derives Playwright behavior tests from this section's Given-When-Then specifications and the field-level rules in Section 9.8 — that is where rule validation actually executes.
Behavioral Fidelity Summary
- 10 behavioral rules extracted from the ACAS Payment Processing subsystem via Concho cycle 8 (
get_insight(business_rules, detailed)→ 214 system-wide rules narrowed to 10 Payment-Processing rules at confidence ≥ 0.60). - 9 of 10 transfer with identical per-payment behavior — the modernized Python/FastAPI implementation preserves the exact arithmetic, sentinel values, and state-machine semantics, with type-safe enums and Pydantic validators carrying the COBOL field-level intent forward.
- 1 of 10 requires a behavioral mitigation: BR-PAY-008 (the 99-item batch cap, re-expressed as a feature-flag-gated soft cap during SAROC coexistence). Two of the nine carried-forward rules are additionally delivery refactors — BR-PAY-005 (single-file fan-out into cheque-PDF + BACS topic) and BR-PAY-010 (text print stream → PDF + S3): per-payment semantics are identical, only the output materialization changes.
- Confidence distribution: 1 rule at ≥ 0.85 (STRUCTURAL), 4 rules in 0.70–0.85, 5 rules in 0.60–0.70 (INFERRED, recommend domain-expert review at runtime).
- Test corroboration: 0 of 10 rules have legacy-test evidence (ACAS has no automated test suite in the cataloged source — standard for COBOL/mainframe projects). All GWTs carry
origin: code-inferred. The modernization stack will be the first to receive automated regression coverage, generated automatically from this section's GWT specifications by the artifact workflow. - Authorization-category rules: none in the legacy. Role-based access control will be added as part of the target architecture (FastAPI dependency-injected auth + JWT) and is documented in Section 4, not here.
9.8 Field-Level Business Rules (auto-derived from field inventory)
Each row below is a Given/When/Then scenario derived from a single field's metadata on the storyboard JSON (required flag, validation regex, conditional collection, display masking, enum source). These complement the workflow-level BR-PAY-NNN rules above by addressing the field level — the rules a Playwright spec would assert against the running modern UI and the API contract. They are not authored by hand; they are generated by the field-level specification step of the modernization-planning workflow from the same storyboard JSON that drives Section 7.9's field inventory table. When the legacy source changes (required flag added, validation tightened, conditional flipped), regenerating the storyboard updates both the inventory table and these scenarios in lockstep.
Subsystem: Payment Processing · Fields with derivable scenarios: 30 · Total scenarios: 52 · Scope coverage: rendering / validation / display / io
Test artifact: each row below corresponds to a Playwright test() block in acas-br.spec.ts (auto-generated by the test-generation step of the modernization-planning workflow). UI-testable rows (required, format, phone-mask) run live against the demo; rows that need backend support (conditional-hide, file upload, enum-validation) appear as test.skip() with the reason inline.
Step 1: Payment Processing Menu (pl900)
| Rule id | Scope | Given | When | Then |
|---|---|---|---|---|
BR-F-PAY-PaymentMenu-menuReply-required-emptyfield: menuReply | validation | the Menu selection field is required | the payment operator submits the form with the field left empty | client-side validation blocks submit with a required-field error And: if the client check is bypassed, the API returns HTTP 422 with a field-level error |
BR-F-PAY-PaymentMenu-menuReply-format-validationfield: menuReply | validation | the Menu selection field has regex constraint ^[1-6X]$ | the payment operator enters a value that does not match the regex | client-side validation blocks submit with a format error And: if the client check is bypassed, the API returns HTTP 422 with the same error |
Step 2: Payment Data Entry (pl080)
| Rule id | Scope | Given | When | Then |
|---|---|---|---|---|
BR-F-PAY-PaymentEntry-payDate-required-emptyfield: payDate | validation | the Date field is required | the payment operator submits the form with the field left empty | client-side validation blocks submit with a required-field error And: if the client check is bypassed, the API returns HTTP 422 with a field-level error |
BR-F-PAY-PaymentEntry-payDate-format-validationfield: payDate | validation | the Date field has regex constraint ^\d{2}/\d{2}/\d{4}$ | the payment operator enters a value that does not match the regex | client-side validation blocks submit with a format error And: if the client check is bypassed, the API returns HTTP 422 with the same error |
BR-F-PAY-PaymentEntry-payCustomer-required-emptyfield: payCustomer | validation | the A/C Nos (Supplier Account) field is required | the payment operator submits the form with the field left empty | client-side validation blocks submit with a required-field error And: if the client check is bypassed, the API returns HTTP 422 with a field-level error |
BR-F-PAY-PaymentEntry-payCustomer-format-validationfield: payCustomer | validation | the A/C Nos (Supplier Account) field has regex constraint ^[A-Z0-9]{6}[0-9]$ | the payment operator enters a value that does not match the regex | client-side validation blocks submit with a format error And: if the client check is bypassed, the API returns HTTP 422 with the same error |
BR-F-PAY-PaymentEntry-payValue-required-emptyfield: payValue | validation | the Value (Payment Amount) field is required | the payment operator submits the form with the field left empty | client-side validation blocks submit with a required-field error And: if the client check is bypassed, the API returns HTTP 422 with a field-level error |
BR-F-PAY-PaymentEntry-payValue-format-validationfield: payValue | validation | the Value (Payment Amount) field has regex constraint ^-?\d{1,7}(\.\d{2})?$ | the payment operator enters a value that does not match the regex | client-side validation blocks submit with a format error And: if the client check is bypassed, the API returns HTTP 422 with the same error |
BR-F-PAY-PaymentEntry-allocateUnapplied-conditional-hidefield: allocateUnapplied | rendering | the storyboard condition purch-unapplied > 0 evaluates to false (tenant config / form state) | the payment operator renders Step 2 (Payment Data Entry (pl080)) | the Allocate Unapplied Balance to this account? (Y/N) field is not rendered in the DOMAnd: submitting the form succeeds without this field being present in the payload |
BR-F-PAY-PaymentEntry-allocateUnapplied-enum-validationfield: allocateUnapplied | validation | the Allocate Unapplied Balance to this account? (Y/N) field draws its values from pl080.cbl literal Y/N reply | the API receives a value not in the enum set (replay attack / malformed client) | the API returns HTTP 422 with an 'enum.invalid' field-level error |
BR-F-PAY-PaymentEntry-unappliedAmount-conditional-hidefield: unappliedAmount | rendering | the storyboard condition allocateUnapplied = Y evaluates to false (tenant config / form state) | the payment operator renders Step 2 (Payment Data Entry (pl080)) | the Unapplied amount to allocate field is not rendered in the DOMAnd: submitting the form succeeds without this field being present in the payload |
BR-F-PAY-PaymentEntry-unappliedAmount-format-validationfield: unappliedAmount | validation | the Unapplied amount to allocate field has regex constraint ^\d{1,7}(\.\d{2})?$ | the payment operator enters a value that does not match the regex | client-side validation blocks submit with a format error And: if the client check is bypassed, the API returns HTTP 422 with the same error |
BR-F-PAY-PaymentEntry-moreData-required-emptyfield: moreData | validation | the Enter further payments? (Y/N) field is required | the payment operator submits the form with the field left empty | client-side validation blocks submit with a required-field error And: if the client check is bypassed, the API returns HTTP 422 with a field-level error |
BR-F-PAY-PaymentEntry-moreData-enum-validationfield: moreData | validation | the Enter further payments? (Y/N) field draws its values from pl080.cbl literal Y/N reply | the API receives a value not in the enum set (replay attack / malformed client) | the API returns HTTP 422 with an 'enum.invalid' field-level error |
BR-F-PAY-PaymentEntry-payPaid-format-validationfield: payPaid | validation | the Applied amount (editable) field has regex constraint ^\d{1,7}(\.\d{2})?$ | the payment operator enters a value that does not match the regex | client-side validation blocks submit with a format error And: if the client check is bypassed, the API returns HTTP 422 with the same error |
BR-F-PAY-PaymentEntry-confirmPartial-conditional-hidefield: confirmPartial | rendering | the storyboard condition pay-paid != work-net (partial-payment branch) evaluates to false (tenant config / form state) | the payment operator renders Step 2 (Payment Data Entry (pl080)) | the Confirm partial payment line (Y/N) field is not rendered in the DOMAnd: submitting the form succeeds without this field being present in the payload |
BR-F-PAY-PaymentEntry-confirmPartial-enum-validationfield: confirmPartial | validation | the Confirm partial payment line (Y/N) field draws its values from pl080.cbl literal Y/N reply | the API receives a value not in the enum set (replay attack / malformed client) | the API returns HTTP 422 with an 'enum.invalid' field-level error |
Step 3: Payment Transaction Amendment (pl085)
| Rule id | Scope | Given | When | Then |
|---|---|---|---|---|
BR-F-PAY-PaymentAmendment-amendDate-required-emptyfield: amendDate | validation | the Date field is required | the payment operator submits the form with the field left empty | client-side validation blocks submit with a required-field error And: if the client check is bypassed, the API returns HTTP 422 with a field-level error |
BR-F-PAY-PaymentAmendment-amendDate-format-validationfield: amendDate | validation | the Date field has regex constraint ^\d{2}/\d{2}/\d{4}$ | the payment operator enters a value that does not match the regex | client-side validation blocks submit with a format error And: if the client check is bypassed, the API returns HTTP 422 with the same error |
BR-F-PAY-PaymentAmendment-amendCustomer-required-emptyfield: amendCustomer | validation | the Supplier (oi5-supplier) field is required | the payment operator submits the form with the field left empty | client-side validation blocks submit with a required-field error And: if the client check is bypassed, the API returns HTTP 422 with a field-level error |
BR-F-PAY-PaymentAmendment-amendCustomer-format-validationfield: amendCustomer | validation | the Supplier (oi5-supplier) field has regex constraint ^[A-Z0-9]{6}[0-9]$ | the payment operator enters a value that does not match the regex | client-side validation blocks submit with a format error And: if the client check is bypassed, the API returns HTTP 422 with the same error |
BR-F-PAY-PaymentAmendment-amendBatchNumber-required-emptyfield: amendBatchNumber | validation | the Batch number to amend field is required | the payment operator submits the form with the field left empty | client-side validation blocks submit with a required-field error And: if the client check is bypassed, the API returns HTTP 422 with a field-level error |
BR-F-PAY-PaymentAmendment-amendBatchNumber-format-validationfield: amendBatchNumber | validation | the Batch number to amend field has regex constraint ^[0-9]{1,5}$ | the payment operator enters a value that does not match the regex | client-side validation blocks submit with a format error And: if the client check is bypassed, the API returns HTTP 422 with the same error |
BR-F-PAY-PaymentAmendment-amendBatchItem-required-emptyfield: amendBatchItem | validation | the Item-within-batch (k) field is required | the payment operator submits the form with the field left empty | client-side validation blocks submit with a required-field error And: if the client check is bypassed, the API returns HTTP 422 with a field-level error |
BR-F-PAY-PaymentAmendment-amendBatchItem-format-validationfield: amendBatchItem | validation | the Item-within-batch (k) field has regex constraint ^[0-9]{1,3}$ | the payment operator enters a value that does not match the regex | client-side validation blocks submit with a format error And: if the client check is bypassed, the API returns HTTP 422 with the same error |
BR-F-PAY-PaymentAmendment-amendPayValue-required-emptyfield: amendPayValue | validation | the Corrected payment value field is required | the payment operator submits the form with the field left empty | client-side validation blocks submit with a required-field error And: if the client check is bypassed, the API returns HTTP 422 with a field-level error |
BR-F-PAY-PaymentAmendment-amendPayValue-format-validationfield: amendPayValue | validation | the Corrected payment value field has regex constraint ^\d{1,7}(\.\d{2})?$ | the payment operator enters a value that does not match the regex | client-side validation blocks submit with a format error And: if the client check is bypassed, the API returns HTTP 422 with the same error |
BR-F-PAY-PaymentAmendment-amendCancelConfirm-conditional-hidefield: amendCancelConfirm | rendering | the storyboard condition amendPayValue = original oi-paid evaluates to false (tenant config / form state) | the payment operator renders Step 3 (Payment Transaction Amendment (pl085)) | the Confirm No action? i.e. Request cancelled (Y/N) field is not rendered in the DOMAnd: submitting the form succeeds without this field being present in the payload |
BR-F-PAY-PaymentAmendment-amendCancelConfirm-required-emptyfield: amendCancelConfirm | validation | the Confirm No action? i.e. Request cancelled (Y/N) field is required | the payment operator submits the form with the field left empty | client-side validation blocks submit with a required-field error And: if the client check is bypassed, the API returns HTTP 422 with a field-level error |
BR-F-PAY-PaymentAmendment-amendCancelConfirm-enum-validationfield: amendCancelConfirm | validation | the Confirm No action? i.e. Request cancelled (Y/N) field draws its values from pl085.cbl literal Y/N reply | the API receives a value not in the enum set (replay attack / malformed client) | the API returns HTTP 422 with an 'enum.invalid' field-level error |
BR-F-PAY-PaymentAmendment-amendMoreCorrections-required-emptyfield: amendMoreCorrections | validation | the Make further corrections? (Y/N) field is required | the payment operator submits the form with the field left empty | client-side validation blocks submit with a required-field error And: if the client check is bypassed, the API returns HTTP 422 with a field-level error |
BR-F-PAY-PaymentAmendment-amendMoreCorrections-enum-validationfield: amendMoreCorrections | validation | the Make further corrections? (Y/N) field draws its values from pl085.cbl literal Y/N reply | the API receives a value not in the enum set (replay attack / malformed client) | the API returns HTTP 422 with an 'enum.invalid' field-level error |
Step 4: Batch Dashboard / Proof Sort (pl090)
| Rule id | Scope | Given | When | Then |
|---|---|---|---|---|
BR-F-PAY-BatchDashboard-batchSelectFromNumber-required-emptyfield: batchSelectFromNumber | validation | the Batch range (from) field is required | the payment operator submits the form with the field left empty | client-side validation blocks submit with a required-field error And: if the client check is bypassed, the API returns HTTP 422 with a field-level error |
BR-F-PAY-BatchDashboard-batchSelectFromNumber-format-validationfield: batchSelectFromNumber | validation | the Batch range (from) field has regex constraint ^[0-9]{1,5}$ | the payment operator enters a value that does not match the regex | client-side validation blocks submit with a format error And: if the client check is bypassed, the API returns HTTP 422 with the same error |
BR-F-PAY-BatchDashboard-batchSelectToNumber-required-emptyfield: batchSelectToNumber | validation | the Batch range (to) field is required | the payment operator submits the form with the field left empty | client-side validation blocks submit with a required-field error And: if the client check is bypassed, the API returns HTTP 422 with a field-level error |
BR-F-PAY-BatchDashboard-batchSelectToNumber-format-validationfield: batchSelectToNumber | validation | the Batch range (to) field has regex constraint ^[0-9]{1,5}$ | the payment operator enters a value that does not match the regex | client-side validation blocks submit with a format error And: if the client check is bypassed, the API returns HTTP 422 with the same error |
BR-F-PAY-BatchDashboard-proofTypeFilter-enum-validationfield: proofTypeFilter | validation | the Type filter (proof eligibility) field draws its values from purchase/pl090.cbl proof filter; sales/sl090.cbl:262-267 | the API receives a value not in the enum set (replay attack / malformed client) | the API returns HTTP 422 with an 'enum.invalid' field-level error |
BR-F-PAY-BatchDashboard-controllerSignOff-required-emptyfield: controllerSignOff | validation | the Controller sign-off (initials) field is required | the payment operator submits the form with the field left empty | client-side validation blocks submit with a required-field error And: if the client check is bypassed, the API returns HTTP 422 with a field-level error |
BR-F-PAY-BatchDashboard-controllerSignOff-format-validationfield: controllerSignOff | validation | the Controller sign-off (initials) field has regex constraint ^[A-Z]{2,3}$ | the payment operator enters a value that does not match the regex | client-side validation blocks submit with a format error And: if the client check is bypassed, the API returns HTTP 422 with the same error |
Step 5: Purchase Cash Posting (pl100)
| Rule id | Scope | Given | When | Then |
|---|---|---|---|---|
BR-F-PAY-CashPosting-confirmPost-required-emptyfield: confirmPost | validation | the OK to post payment transactions (YES/NO)? field is required | the payment operator submits the form with the field left empty | client-side validation blocks submit with a required-field error And: if the client check is bypassed, the API returns HTTP 422 with a field-level error |
BR-F-PAY-CashPosting-confirmPost-format-validationfield: confirmPost | validation | the OK to post payment transactions (YES/NO)? field has regex constraint ^(YES|NO)$ | the payment operator enters a value that does not match the regex | client-side validation blocks submit with a format error And: if the client check is bypassed, the API returns HTTP 422 with the same error |
BR-F-PAY-CashPosting-postingDate-format-validationfield: postingDate | validation | the Posting date (override default = today) field has regex constraint ^\d{2}/\d{2}/\d{4}$ | the payment operator enters a value that does not match the regex | client-side validation blocks submit with a format error And: if the client check is bypassed, the API returns HTTP 422 with the same error |
Step 6: Cheque / BACS Generation (pl940)
| Rule id | Scope | Given | When | Then |
|---|---|---|---|---|
BR-F-PAY-ChequeRun-firstChequeNumber-required-emptyfield: firstChequeNumber | validation | the First Cheque number field is required | the payment operator submits the form with the field left empty | client-side validation blocks submit with a required-field error And: if the client check is bypassed, the API returns HTTP 422 with a field-level error |
BR-F-PAY-ChequeRun-firstChequeNumber-format-validationfield: firstChequeNumber | validation | the First Cheque number field has regex constraint ^[0-9]{1,8}$ | the payment operator enters a value that does not match the regex | client-side validation blocks submit with a format error And: if the client check is bypassed, the API returns HTTP 422 with the same error |
BR-F-PAY-ChequeRun-chequePaymentDate-required-emptyfield: chequePaymentDate | validation | the Payment date (printed on cheque) field is required | the payment operator submits the form with the field left empty | client-side validation blocks submit with a required-field error And: if the client check is bypassed, the API returns HTTP 422 with a field-level error |
BR-F-PAY-ChequeRun-chequePaymentDate-format-validationfield: chequePaymentDate | validation | the Payment date (printed on cheque) field has regex constraint ^\d{2}/\d{2}/\d{4}$ | the payment operator enters a value that does not match the regex | client-side validation blocks submit with a format error And: if the client check is bypassed, the API returns HTTP 422 with the same error |
BR-F-PAY-ChequeRun-minPaymentAmount-format-validationfield: minPaymentAmount | validation | the Minimum payment amount (pay-gross gate) field has regex constraint ^\d{1,7}(\.\d{2})?$ | the payment operator enters a value that does not match the regex | client-side validation blocks submit with a format error And: if the client check is bypassed, the API returns HTTP 422 with the same error |
Step 8: Payment Cycle Workbench (Beyond 1:1, consolidated view)
| Rule id | Scope | Given | When | Then |
|---|---|---|---|---|
BR-F-PAY-PaymentCycleWorkbench-wbDateFrom-format-validationfield: wbDateFrom | validation | the Filter: payment date from field has regex constraint ^\d{2}/\d{2}/\d{4}$ | the payment operator enters a value that does not match the regex | client-side validation blocks submit with a format error And: if the client check is bypassed, the API returns HTTP 422 with the same error |
BR-F-PAY-PaymentCycleWorkbench-wbDateTo-format-validationfield: wbDateTo | validation | the Filter: payment date to field has regex constraint ^\d{2}/\d{2}/\d{4}$ | the payment operator enters a value that does not match the regex | client-side validation blocks submit with a format error And: if the client check is bypassed, the API returns HTTP 422 with the same error |
BR-F-PAY-PaymentCycleWorkbench-wbStatusFilter-enum-validationfield: wbStatusFilter | validation | the Filter: lifecycle status field draws its values from BR-PAY-006 batch lifecycle states (copybooks/fdbatch.cob:20-30) | the API receives a value not in the enum set (replay attack / malformed client) | the API returns HTTP 422 with an 'enum.invalid' field-level error |
BR-F-PAY-PaymentCycleWorkbench-wbMethodFilter-enum-validationfield: wbMethodFilter | validation | the Filter: payment method field draws its values from BR-PAY-005 BACS vs Cheque (purchase/pl940.cbl:483-489) | the API receives a value not in the enum set (replay attack / malformed client) | the API returns HTTP 422 with an 'enum.invalid' field-level error |
BR-F-PAY-PaymentCycleWorkbench-wbSupplierFilter-format-validationfield: wbSupplierFilter | validation | the Filter: supplier (autocomplete) field has regex constraint ^[A-Z0-9]{0,7}$ | the payment operator enters a value that does not match the regex | client-side validation blocks submit with a format error And: if the client check is bypassed, the API returns HTTP 422 with the same error |
10. Data Mapping Strategy
TL;DR — 5 legacy files → 5 PostgreSQL tables, 3 worked examples
This section maps the five legacy payment-processing files (pay.dat, openitm5.dat, fdbatch, GL batch records, remittance print spool) to the five PostgreSQL tables introduced in Section 4.4 (payment, open_item, batch_control, gl_posting, payment_audit). Three worked entity examples below show the full COBOL copybook → PostgreSQL DDL → SQLAlchemy ORM model derivation. Zero "PRESERVED unchanged" blocks — this is a cross-platform modernization (ISAM/MySQL → PostgreSQL) so every column is rewritten, every COMP-3 packed decimal becomes NUMERIC(p,s), every OCCURS array becomes a child table. The sage-source-fidelity-check skill is not in scope for this section because no DDL block claims byte-faithful preservation.
get_file_content_in_range) — the legacy copybook is the only schema of record, so there is no pre-existing target schema to query; the PostgreSQL DDL and SQLAlchemy ORM rows are derived field-by-field from that copybook (not from each other). Each derived column traces back to a named copybook field, and the mapping is exercised downstream rather than taken on trust: the artifact-generation workflow's database-schema agent emits the actual schema.sql and seed data, and the generated test suite runs against that live schema before any code ships. Section 10 is the contract between this planning workflow and the artifact-generation workflow's database-schema and core-logic agents: the schema is the source of truth, and ORM models match it exactly.
For the complete target data model ER diagram showing all entity relationships, see Section 4.4: Data Model.
10.1 Legacy Data Analysis
The Payment-Processing subsystem reads/writes five physical files in the legacy GnuCOBOL stack. Each is defined by a COBOL FD (file descriptor) + working-storage copybook pair under copybooks/:
| Legacy file | Record | FD copybook | WS copybook | Bytes / record | Storage mode |
|---|---|---|---|---|---|
pay.dat | Pay-Record | copybooks/fdpay.cob | copybooks/wspay.cob | 237 | ISAM + MySQL (dual via acas032) |
openitm5.dat | open-item-record-5 | copybooks/fdoi4.cob (113 bytes — renamed from fdoi5 historically) | copybooks/plwsoi.cob | 113 | ISAM + MySQL |
| Batch records (within ledger) | Batch-Record | copybooks/fdbatch.cob | (same FD) | 96 / 98 (header comment shows revision) | ISAM + MySQL |
| GL batch lines | (written via BL-Write) | (general-ledger FDs) | (general-ledger working storage) | variable | ISAM + MySQL |
| Remittance print spool | (line-printer print records) | (none — ephemeral spool) | pl960 in-memory lines | 132-col print line | line-printer pipe |
Three files (pay.dat, openitm5.dat, purchled.dat) are the watched artifacts the SAROC file-watcher monitors (per report-plan.json.coexistencePattern.source.watchedArtifacts); the file-watcher emits diff events that the SAROC consumer in payment-batch applies to the PostgreSQL schema with natural-key idempotency.
10.2 Schema Mappings — 3 worked entity examples
For the complete target data model ER diagram showing all entity relationships, see Section 4.4: Data Model. The mappings below illustrate the per-entity 4-zone layout: 3 code columns on top (Legacy COBOL / PostgreSQL DDL / SQLAlchemy ORM) followed by full-width Schema Mapping Notes.
10.2.1 Payment — fdpay.cob → payment + payment_line
Column 1 — Legacy COBOL (copybooks/fdpay.cob)
fd Pay-File.
01 Pay-Record.
03 Pay-Key.
05 Pay-Supl-Key pic x(7).
05 Pay-Nos pic 99.
03 Pay-Cont pic x.
03 Pay-Date pic 9(8) comp.
03 Pay-Cheque pic 9(8) comp.
03 Pay-SortCode pic 9(6) comp.
03 Pay-Account pic 9(8) comp.
03 Pay-Gross pic s9(7)v99 comp-3.
03 filler occurs 9.
05 Pay-Folio pic 9(8) comp.
05 Pay-Period pic 99 comp.
05 Pay-Value pic s9(7)v99 comp-3.
05 Pay-Deduct pic s999v99 comp-3.
05 Pay-Invoice pic x(10).
237 bytes per record. The occurs 9 array forces a hard 9-line-item limit on every payment.
Column 2 — PostgreSQL DDL (target schema)
CREATE TABLE payment (
id UUID PRIMARY KEY
DEFAULT gen_random_uuid(),
legacy_natural_key VARCHAR(64)
UNIQUE NOT NULL,
supplier_code VARCHAR(7) NOT NULL,
payment_number SMALLINT NOT NULL,
payment_method VARCHAR(8) NOT NULL
CHECK (payment_method IN ('CHEQUE',
'BACS')),
payment_date DATE NOT NULL,
cheque_no BIGINT,
sort_code INTEGER,
bank_account BIGINT,
amount_total NUMERIC(9, 2) NOT NULL,
currency CHAR(3) NOT NULL DEFAULT 'USD',
batch_number INTEGER NOT NULL
REFERENCES batch_control(batch_number),
status VARCHAR(16) NOT NULL DEFAULT 'ENTERED',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
created_by VARCHAR(64),
updated_by VARCHAR(64),
posted_at TIMESTAMPTZ,
posted_by VARCHAR(64),
UNIQUE (supplier_code, payment_number)
);
CREATE INDEX idx_payment_batch ON payment(batch_number);
CREATE INDEX idx_payment_supplier ON payment(supplier_code);
CREATE TABLE payment_line (
id UUID PRIMARY KEY
DEFAULT gen_random_uuid(),
payment_id UUID NOT NULL
REFERENCES payment(id) ON DELETE CASCADE,
line_seq SMALLINT NOT NULL,
folio BIGINT,
period SMALLINT,
amount_applied NUMERIC(9, 2) NOT NULL,
discount_taken NUMERIC(5, 2) NOT NULL DEFAULT 0,
invoice_ref VARCHAR(10),
UNIQUE (payment_id, line_seq)
);
Column 3 — SQLAlchemy 2.0 ORM (derived from DDL)
from datetime import date, datetime
from decimal import Decimal
from uuid import UUID, uuid4
from sqlalchemy import (
String, Integer, SmallInteger, Numeric, Date,
DateTime, ForeignKey, CheckConstraint,
)
from sqlalchemy.orm import (
Mapped, mapped_column, relationship, DeclarativeBase,
)
class Base(DeclarativeBase):
pass
class Payment(Base):
__tablename__ = "payment"
id: Mapped[UUID] = mapped_column(primary_key=True,
default=uuid4)
legacy_natural_key: Mapped[str] = mapped_column(
String(64), unique=True)
supplier_code: Mapped[str] = mapped_column(String(7))
payment_number: Mapped[int] = mapped_column(SmallInteger)
payment_method: Mapped[str] = mapped_column(String(8))
payment_date: Mapped[date] = mapped_column(Date)
cheque_no: Mapped[int | None] = mapped_column(nullable=True)
sort_code: Mapped[int | None] = mapped_column(nullable=True)
bank_account: Mapped[int | None] = mapped_column(nullable=True)
amount_total: Mapped[Decimal] = mapped_column(Numeric(9, 2))
currency: Mapped[str] = mapped_column(String(3),
default="USD")
batch_number: Mapped[int] = mapped_column(
ForeignKey("batch_control.batch_number"))
status: Mapped[str] = mapped_column(String(16),
default="ENTERED")
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=datetime.utcnow)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=datetime.utcnow)
posted_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True)
lines: Mapped[list["PaymentLine"]] = relationship(
back_populates="payment", cascade="all, delete-orphan")
__table_args__ = (
CheckConstraint(
"payment_method IN ('CHEQUE','BACS')",
name="payment_method_valid"),
)
class PaymentLine(Base):
__tablename__ = "payment_line"
id: Mapped[UUID] = mapped_column(primary_key=True,
default=uuid4)
payment_id: Mapped[UUID] = mapped_column(
ForeignKey("payment.id", ondelete="CASCADE"))
line_seq: Mapped[int] = mapped_column(SmallInteger)
folio: Mapped[int | None] = mapped_column(nullable=True)
period: Mapped[int | None] = mapped_column(SmallInteger,
nullable=True)
amount_applied: Mapped[Decimal] = mapped_column(Numeric(9, 2))
discount_taken: Mapped[Decimal] = mapped_column(
Numeric(5, 2), default=Decimal("0"))
invoice_ref: Mapped[str | None] = mapped_column(String(10),
nullable=True)
payment: Mapped[Payment] = relationship(
back_populates="lines")
Schema Mapping Notes — Payment
| Legacy field | Legacy PIC | Target column | Target type | Notes |
|---|---|---|---|---|
Pay-Supl-Key | pic x(7) | supplier_code | VARCHAR(7) | Width preserved; MOD-11 check digit lives in the seventh character (BR-PAY-002). |
Pay-Nos | pic 99 | payment_number | SMALLINT | +2 digits beyond legacy ceiling per universal rule (10.9). Composite uniqueness via (supplier_code, payment_number). |
Pay-Date | pic 9(8) comp (CCYYMMDD) | payment_date | DATE | Native PostgreSQL DATE replaces 4-byte binary integer encoding. |
Pay-Cheque | pic 9(8) comp | cheque_no | BIGINT NULL | Nullable; BACS payments leave it NULL. |
Pay-SortCode | pic 9(6) comp | sort_code | INTEGER NULL | Drives BR-PAY-005 routing (zero → cheque; non-zero → BACS). |
Pay-Account | pic 9(8) comp | bank_account | BIGINT NULL | Free of legacy binary-long encoding artefacts. |
Pay-Gross | pic s9(7)v99 comp-3 | amount_total | NUMERIC(9, 2) | Exact decimal — never floating-point for monetary values (universal rule 10.9). |
filler occurs 9 (Pay-Folio/Period/Value/Deduct/Invoice) | (9-element array) | payment_line (child table) | (per-row columns) | OCCURS elimination: 9-row fixed array becomes an unbounded child table with FK and ON DELETE CASCADE. The 9-line ceiling is removed entirely (universal rule 10.9). |
| (none) | — | legacy_natural_key | VARCHAR(64) UNIQUE | NEW — SAROC idempotency anchor for natural-key dual-write per coexistencePattern.target.idempotencyStrategy. |
| (none) | — | status | VARCHAR(16) | NEW — lifecycle state (BR-PAY-006). Backed by a future CHECK constraint or enum once status values stabilize. |
| (none) | — | created_at, updated_at, created_by, updated_by, posted_at, posted_by | TIMESTAMPTZ / VARCHAR(64) | NEW — universal audit-trail columns (universal rule 10.9). |
Data volume: ~3,000 payment records/year per legacy customer site (typical mid-market AP volumes). Target sizing allows 50-year retention without partitioning.
Transformation approach: SAROC file-watcher reads pay.dat diffs every 1000 ms, translates each Pay-Record into a sage-domain-event-v1 envelope, publishes to RabbitMQ topic acas.legacy.payment; the payment-batch consumer applies it via INSERT ... ON CONFLICT (legacy_natural_key) DO UPDATE.
10.2.2 Open Item — plwsoi.cob → open_item
Column 1 — Legacy COBOL (copybooks/plwsoi.cob)
01 OI-Header.
03 OI-Key.
05 OI-Customer.
07 OI-Supplier.
09 OI-Nos Pic X(6).
09 OI-Check Pic 9.
05 OI-Invoice Pic 9(8).
03 OI-Date Binary-long.
03 OI-Batch Comp.
05 OI-B-Nos Pic 9(5).
05 OI-B-Item Pic 999.
03 OI-Type pic 9.
03 OI-ref pic x(10).
03 OI-order pic x(10).
03 OI-hold-flag pic x.
03 OI-unapl pic x.
03 filler comp-3.
05 OI-P-C pic s9(7)v99.
05 OI-Net pic s9(7)v99.
05 OI-Extra pic s9(7)v99.
05 OI-Carriage pic s9(7)v99.
05 OI-Vat pic s9(7)v99.
05 OI-Discount pic s9(7)v99.
05 OI-E-Vat pic s9(7)v99.
05 OI-C-Vat pic s9(7)v99.
05 OI-Paid pic s9(7)v99.
03 OI-Status pic 9.
03 OI-Deduct-Days binary-char.
03 OI-Deduct-Amt pic s999v99 comp.
03 OI-Deduct-Vat pic s999v99 comp.
03 OI-Days binary-char.
03 OI-CR binary-long.
03 OI-Applied pic x.
03 OI-Date-Cleared binary-long.
113 bytes. OI-Type sentinel values: 1 Receipt, 2 Invoice, 3 Credit Note, 4 Proforma, 5 Payment, 6 Journal-Unapplied (per the 88-level documentation in the copybook header).
Column 2 — PostgreSQL DDL
CREATE TABLE open_item (
id UUID PRIMARY KEY
DEFAULT gen_random_uuid(),
legacy_natural_key VARCHAR(64) UNIQUE NOT NULL,
supplier_code VARCHAR(7) NOT NULL,
invoice_ref VARCHAR(10) NOT NULL,
invoice_date DATE NOT NULL,
batch_number INTEGER NOT NULL
REFERENCES batch_control(batch_number),
batch_item SMALLINT NOT NULL,
payment_id UUID
REFERENCES payment(id) ON DELETE SET NULL,
type SMALLINT NOT NULL
CHECK (type BETWEEN 1 AND 9),
status SMALLINT NOT NULL DEFAULT 0,
hold_flag CHAR(1),
unapplied_flag CHAR(1),
amount_p_c NUMERIC(9, 2) DEFAULT 0,
amount_net NUMERIC(9, 2) DEFAULT 0,
amount_extra NUMERIC(9, 2) DEFAULT 0,
amount_carriage NUMERIC(9, 2) DEFAULT 0,
amount_vat NUMERIC(9, 2) DEFAULT 0,
amount_discount NUMERIC(9, 2) DEFAULT 0,
amount_extra_vat NUMERIC(9, 2) DEFAULT 0,
amount_carr_vat NUMERIC(9, 2) DEFAULT 0,
amount_paid NUMERIC(9, 2) DEFAULT 0,
deduct_days SMALLINT,
deduct_amount NUMERIC(5, 2) DEFAULT 0,
deduct_vat NUMERIC(5, 2) DEFAULT 0,
days_to_pay SMALLINT,
date_cleared DATE,
posting_reference VARCHAR(32),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (supplier_code, invoice_ref)
);
CREATE INDEX idx_open_item_batch
ON open_item(batch_number, batch_item);
CREATE INDEX idx_open_item_type_status
ON open_item(type, status)
WHERE status = 0; -- BR-PAY-003 hot path
Column 3 — SQLAlchemy 2.0 ORM
from datetime import date, datetime
from decimal import Decimal
from uuid import UUID, uuid4
from sqlalchemy import (
String, Integer, SmallInteger, Numeric, Date, DateTime,
ForeignKey, CheckConstraint, Index,
)
from sqlalchemy.orm import Mapped, mapped_column, relationship
class OpenItem(Base):
__tablename__ = "open_item"
id: Mapped[UUID] = mapped_column(primary_key=True,
default=uuid4)
legacy_natural_key: Mapped[str] = mapped_column(
String(64), unique=True)
supplier_code: Mapped[str] = mapped_column(String(7))
invoice_ref: Mapped[str] = mapped_column(String(10))
invoice_date: Mapped[date] = mapped_column(Date)
batch_number: Mapped[int] = mapped_column(
ForeignKey("batch_control.batch_number"))
batch_item: Mapped[int] = mapped_column(SmallInteger)
payment_id: Mapped[UUID | None] = mapped_column(
ForeignKey("payment.id", ondelete="SET NULL"),
nullable=True,
)
type: Mapped[int] = mapped_column(SmallInteger)
status: Mapped[int] = mapped_column(SmallInteger, default=0)
hold_flag: Mapped[str | None] = mapped_column(String(1),
nullable=True)
unapplied_flag: Mapped[str | None] = mapped_column(
String(1), nullable=True)
amount_net: Mapped[Decimal] = mapped_column(
Numeric(9, 2), default=Decimal("0"))
amount_paid: Mapped[Decimal] = mapped_column(
Numeric(9, 2), default=Decimal("0"))
amount_vat: Mapped[Decimal] = mapped_column(
Numeric(9, 2), default=Decimal("0"))
deduct_days: Mapped[int | None] = mapped_column(
SmallInteger, nullable=True)
deduct_amount: Mapped[Decimal] = mapped_column(
Numeric(5, 2), default=Decimal("0"))
date_cleared: Mapped[date | None] = mapped_column(
Date, nullable=True)
posting_reference: Mapped[str | None] = mapped_column(
String(32), nullable=True)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=datetime.utcnow)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=datetime.utcnow)
__table_args__ = (
CheckConstraint("type BETWEEN 1 AND 9", name="oi_type_range"),
Index("idx_open_item_batch", "batch_number", "batch_item"),
)
Schema Mapping Notes — Open Item
| Legacy field | Legacy PIC | Target column | Target type | Notes |
|---|---|---|---|---|
OI-Nos + OI-Check | x(6) + 9 | supplier_code | VARCHAR(7) | The MOD-11 check digit is the seventh char; same convention as payment.supplier_code. |
OI-Invoice | pic 9(8) | invoice_ref | VARCHAR(10) | Widened from 8 numeric to 10 alphanumeric (per Section 5 affinity win): legacy composite formula oi-invoice = oi-b-nos * 1000 + oi-b-item dropped in favour of natural invoice references; legacy key reconstructable via SAROC translator on the way out. |
OI-Date | binary-long | invoice_date | DATE | Native DATE; legacy binary integer encoding eliminated. |
OI-B-Nos + OI-B-Item | 9(5) + 999 | batch_number + batch_item | INTEGER + SMALLINT | Two columns instead of the multiplicative composite. The legacy oi-invoice = oi-b-nos * 1000 + oi-b-item formula (BR-PAY-008) is preserved by the SAROC translator on writes back to the COBOL side. |
OI-Type | pic 9 | type | SMALLINT + CHECK | Sentinel values 1-9 preserved (the 88-level enum in the copybook becomes a CHECK constraint). |
OI-Net, OI-Paid, OI-Vat, OI-Carriage, ... | pic s9(7)v99 comp-3 | 9 separate amount_* columns | NUMERIC(9, 2) | One COBOL COMP-3 field becomes one PostgreSQL NUMERIC column; total precision preserved exactly. |
OI-Deduct-Days | binary-char | deduct_days | SMALLINT | Drives BR-PAY-004 discount-window calculation. |
OI-Deduct-Amt | pic s999v99 comp | deduct_amount | NUMERIC(5, 2) | Narrower precision than other amounts — faithful to the 3-digit COBOL limit. |
| (none) | — | payment_id | UUID FK NULL | NEW — explicit FK linking the open-item allocation back to its parent payment. Legacy used the composite batch key as an implicit join. |
| (none) | — | posting_reference | VARCHAR(32) | NEW — written when the payment posts (BR-PAY-006); the legacy OI-Applied single-character flag was insufficient for audit. |
| (none) | — | created_at, updated_at | TIMESTAMPTZ | NEW — universal audit columns. |
Data volume: ~25,000 open-item rows/year (each payment touches 1-9 invoices, plus standalone invoice/credit-note rows). Indexed for BR-PAY-003 hot-path filter type IN (2, 4) AND status = 0.
Transformation approach: SAROC file-watcher monitors openitm5.dat; each diff produces acas.legacy.open-item.created/updated/deleted events; the consumer applies them with natural-key upsert.
10.2.3 Batch Control — fdbatch.cob → batch_control
Column 1 — Legacy COBOL (copybooks/fdbatch.cob)
fd Batch-File.
01 Batch-Record.
03 Batch-Key.
05 Ledger pic 9.
88 GL-Batch value 1.
88 PL-Batch value 2.
88 SL-Batch value 3.
05 Batch-Nos pic 9(5).
03 Items pic 99.
03 Batch-Status pic 9.
88 Status-Open value 0.
88 Status-Closed value 1.
03 Cleared-Status pic 9.
88 Waiting value 0.
88 Processed value 1.
88 Archived value 2.
03 Bcycle pic 99.
03 Dates.
05 Entered binary-long.
05 Proofed binary-long.
05 Posted binary-long.
05 Stored binary-long.
03 Amounts comp-3.
05 Input-Gross pic 9(9)v99.
05 Input-Vat pic 9(9)v99.
05 Actual-Gross pic 9(9)v99.
05 Actual-Vat pic 9(9)v99.
03 Description pic x(24).
03 posting-data.
05 bDefault pic 99.
05 Convention pic xx.
05 Batch-Def-AC pic 9(6).
05 Batch-Def-PC pic 99.
05 Batch-Def-Code pic xx.
05 Batch-Def-Vat pic x.
03 Batch-Start pic 9(5).
~96 bytes per record. Items pic 99 is the 99-item ceiling (BR-PAY-008).
Column 2 — PostgreSQL DDL
CREATE TABLE batch_control (
batch_number INTEGER PRIMARY KEY,
ledger CHAR(2) NOT NULL
CHECK (ledger IN ('GL', 'PL', 'SL')),
items_count BIGINT NOT NULL DEFAULT 0,
batch_status SMALLINT NOT NULL DEFAULT 0
CHECK (batch_status IN (0, 1)),
cleared_status SMALLINT NOT NULL DEFAULT 0
CHECK (cleared_status IN (0, 1, 2)),
bcycle SMALLINT,
entered_at TIMESTAMPTZ,
proofed_at TIMESTAMPTZ,
posted_at TIMESTAMPTZ,
stored_at TIMESTAMPTZ,
input_gross NUMERIC(11, 2),
input_vat NUMERIC(11, 2),
actual_gross NUMERIC(11, 2),
actual_vat NUMERIC(11, 2),
description VARCHAR(24),
default_ac INTEGER,
default_pc SMALLINT,
default_code CHAR(2),
default_vat CHAR(1),
convention CHAR(2),
batch_start INTEGER,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
-- BR-PAY-008 soft cap during SAROC coexistence; gated by
-- application feature flag, removed at cutover
CONSTRAINT items_soft_cap CHECK (items_count <= 999999)
);
CREATE INDEX idx_batch_status
ON batch_control(batch_status, cleared_status);
Column 3 — SQLAlchemy 2.0 ORM
from datetime import datetime
from decimal import Decimal
from sqlalchemy import (
String, Integer, SmallInteger, BigInteger, Numeric,
DateTime, CheckConstraint, Index,
)
from sqlalchemy.orm import Mapped, mapped_column
class BatchControl(Base):
__tablename__ = "batch_control"
batch_number: Mapped[int] = mapped_column(Integer,
primary_key=True)
ledger: Mapped[str] = mapped_column(String(2))
items_count: Mapped[int] = mapped_column(BigInteger,
default=0)
batch_status: Mapped[int] = mapped_column(SmallInteger,
default=0)
cleared_status: Mapped[int] = mapped_column(SmallInteger,
default=0)
entered_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True)
proofed_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True)
posted_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True)
stored_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True)
input_gross: Mapped[Decimal | None] = mapped_column(
Numeric(11, 2), nullable=True)
input_vat: Mapped[Decimal | None] = mapped_column(
Numeric(11, 2), nullable=True)
actual_gross: Mapped[Decimal | None] = mapped_column(
Numeric(11, 2), nullable=True)
actual_vat: Mapped[Decimal | None] = mapped_column(
Numeric(11, 2), nullable=True)
description: Mapped[str | None] = mapped_column(
String(24), nullable=True)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=datetime.utcnow)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=datetime.utcnow)
__table_args__ = (
CheckConstraint("ledger IN ('GL','PL','SL')",
name="batch_ledger_valid"),
CheckConstraint("batch_status IN (0, 1)",
name="batch_status_valid"),
CheckConstraint("cleared_status IN (0, 1, 2)",
name="cleared_status_valid"),
Index("idx_batch_status", "batch_status",
"cleared_status"),
)
Schema Mapping Notes — Batch Control
| Legacy field | Legacy PIC | Target column | Target type | Notes |
|---|---|---|---|---|
Ledger 88-levels | pic 9 (1=GL, 2=PL, 3=SL) | ledger | CHAR(2) + CHECK | Human-readable 'GL'/'PL'/'SL' replaces numeric sentinel. |
Batch-Nos | pic 9(5) | batch_number | INTEGER PK | Width widened (universal rule 10.9). |
Items | pic 99 (max 99) | items_count | BIGINT + soft cap CHECK | Hard 99 ceiling replaced by application-level feature-flag-gated soft cap (BR-PAY-008 mitigation); CHECK at 999,999 is sentinel-against-runaway only. |
Batch-Status 88-levels | pic 9 (0/1) | batch_status | SMALLINT + CHECK | 0=OPEN, 1=CLOSED preserved verbatim. Backward transitions rejected via the BR-PAY-006 service layer (a CHECK alone can't express temporal invariants). |
Cleared-Status 88-levels | pic 9 (0/1/2) | cleared_status | SMALLINT + CHECK | WAITING/PROCESSED/ARCHIVED preserved. |
Dates.Entered/Proofed/Posted/Stored | binary-long each | entered_at, proofed_at, posted_at, stored_at | TIMESTAMPTZ NULL | Upgraded from DATE-precision binary-long to TIMESTAMPTZ; preserves legacy NULL-when-not-yet semantics. |
Amounts (Input-Gross/Vat, Actual-Gross/Vat) | pic 9(9)v99 comp-3 | 4 NUMERIC columns | NUMERIC(11, 2) | +2 integer digits beyond legacy ceiling (universal rule 10.9). |
posting-data.* | (small group of defaults) | default_ac, default_pc, default_code, default_vat, convention | INTEGER / SMALLINT / CHAR | Per-field column names replace the COBOL group structure; "default" prefix added for readability. |
| (none) | — | created_at, updated_at | TIMESTAMPTZ | NEW — universal audit columns separate from the lifecycle timestamps. |
Data volume: ~600 batches/year (typical month-end batch + interim runs). Status indexes serve the dashboard hot path.
Transformation approach: SAROC consumer applies legacy batch-record diffs; the 99-item soft cap fires when the modern side accumulates items faster than legacy can produce batches.
10.3 Data Type Mappings
The cross-platform conversion table the artifact-generation workflow applies to every COBOL field across the subsystem:
| COBOL PIC clause | Bytes | PostgreSQL type | Python (SQLAlchemy) | Notes |
|---|---|---|---|---|
pic x(N) | N | VARCHAR(N) | String(N) | Width preserved; widen-with-comment if the affinity reconciliation indicated overflow. |
pic 9 | 1 | SMALLINT | SmallInteger | Single-digit sentinels often become enums + CHECK. |
pic 99 | 2 | SMALLINT or BIGINT | SmallInteger / BigInteger | Widened with explicit comment when the ceiling is being lifted (BR-PAY-008 case). |
pic 9(5) | 5 | INTEGER | Integer | +2 digits beyond legacy max (universal rule). |
pic 9(8) comp / binary-long | 4 | BIGINT or DATE if date-encoded | BigInteger / Date | Date-encoded binary-long (CCYYMMDD) becomes native DATE. |
pic s9(7)v99 comp-3 | 5 | NUMERIC(9, 2) | Numeric(9, 2) → Decimal | Exact decimal preservation; never float for money. |
pic s9(9)v99 comp-3 | 6 | NUMERIC(11, 2) | Numeric(11, 2) | +2 integer digits (universal rule). |
pic s999v99 | 4 (comp) | NUMERIC(5, 2) | Numeric(5, 2) | Smaller precision faithfully preserved. |
binary-char | 1 | SMALLINT | SmallInteger | Usually a small counter / day-count. |
OCCURS N | N × sub-record | Child table + FK + ON DELETE CASCADE | relationship(...) with back_populates | Always — never array columns. Universal rule 10.9. |
| 88-level enum sentinels | (implicit) | CHECK constraint or enum type | Python IntEnum | Preserves sentinel values exactly (5/6 for transaction-type, 0/1/2 for cleared-status, etc.). |
10.4 Data Transfer Procedures
ACAS uses the SAROC coexistence pattern (per report-plan.json.coexistencePattern.pattern = saroc), so the data-transfer mechanism during coexistence is the SAROC bridge itself rather than a one-shot ETL. Three procedure tracks:
-
Continuous CDC (Phase 1: SAROC active, legacy authoritative).
- File-watcher polls
pay.dat,openitm5.dat,purchled.datevery 1000ms. - Snapshot-diff against the prior pass; each changed record is encoded into the
sage-domain-event-v1envelope. - Published to RabbitMQ topic exchange (persistent, DLQ enabled) with routing keys
acas.legacy.payment.*,acas.legacy.open-item.*,acas.legacy.batch-control.*. payment-batchSAROC consumer applies withINSERT ... ON CONFLICT (legacy_natural_key) DO UPDATE— idempotent on re-delivery.
- File-watcher polls
-
Initial back-fill (one-time, before SAROC goes hot).
cobcrun-driven export programs walk each ISAM file, write CSV/Parquet snapshots to S3.- An Alembic-managed schema deploys the empty target tables; a one-shot
payment-schema-migrationsECS task runs the back-fill viaCOPY ... FROM. - Row-count + checksum reconciliation per file before SAROC is turned on.
-
Cutover (Phase 2: modern authoritative, legacy archived).
- Detailed cutover sequencing is out-of-scope for this section; see Section 11.
- Schema-side concern: at cutover, the
legacy_natural_keycolumn is retained for archive-trail purposes but no longer constrained asUNIQUE NOT NULL; future records get a NULL.
10.5 Data Validation Strategy
- Row-count parity: for every SAROC sync cycle, the consumer compares
SELECT count(*) FROM open_item WHERE created_at > ?against the file-watcher's emitted-event count for the same window. Mismatch fires an alert + halts apply until reconciled. - Checksum parity: per-record SHA-256 over the canonicalized field set; embedded in the SAROC envelope; consumer verifies before commit.
- BR-PAY-002 MOD-11 re-verification: every
supplier_codeinsert re-runs the check-digit calculation server-side, regardless of source. - Type+status filter (BR-PAY-003) negative test: nightly job asserts no records with
type IN (1, 3)ever made it into a posted-payment proof report. - Spot-sample reconciliation: for 1% of payments at random, the SAROC consumer fetches the legacy
purch-currentvia the bridge and compares against the target's running balance projection.
10.6 Rollback Procedures
- During SAROC dual-write: the modern side is non-authoritative, so a "rollback" is simply turning off SAROC consumer application and clearing the target tables. Legacy ISAM files are untouched.
- After cutover: the legacy COBOL system remains deployable in read-only mode for ~6 months. If a critical defect surfaces, SAROC reverses direction (modern is the source, legacy is the consumer) using a per-record export to ISAM via
cobcrunwriter programs. - Schema rollback: Alembic
downrevisions are tested before each deploy;payment-schema-migrationstask supports forward / backward. - Audit preservation: the immutable
payment_auditJSONB chain is preserved across any rollback; a "rollback" is itself an audit event.
10.7 Performance Considerations
- Indexes added beyond legacy keys:
idx_open_item_type_statuspartial index (WHERE status = 0) makes BR-PAY-003 proof-sort O(log N) on the active subset rather than O(N) full-scan;idx_payment_batchserves the batch-dashboard hot path. - Read replica routing:
payment-reportingservice reads from a PostgreSQL read replica for aged-creditor reports; writer is reserved for hot-path operations. - Connection pooling: SQLAlchemy application-side pool (per resolved
targetPatterns.connectionPooling = true); 20 connections per Fargate task, scales horizontally with task count. - Batch size: the SAROC consumer applies events in batches of 50 with one COMMIT per batch; reduces lock contention versus per-event commit.
10.8 Data Mapping Summary
| Legacy entity | Target table(s) | Notable transformation | SAROC watched? |
|---|---|---|---|
Pay-Record | payment + payment_line | OCCURS 9 → child table; +SAROC natural key; +audit columns; lifecycle status added | Yes (pay.dat) |
OI-Header | open_item | 9 packed-decimal amounts → 9 NUMERIC columns; composite batch key split; +FK to payment; +indexes for BR-PAY-003 | Yes (openitm5.dat) |
Batch-Record | batch_control | 99-item ceiling becomes soft cap; numeric-sentinel ledger becomes CHAR(2); 4 timestamps upgraded to TIMESTAMPTZ | Yes (purchled.dat) |
GL batch lines (BL-Write output) | gl_posting | Inline writes preserved; same DB transaction as payment.status update; 1 debit + 1 credit per payment | No (modern-side inline writes) |
Remittance print spool (pl960 output) | remittance_advice (sidecar columns + S3 URI) | 9-line OCCURS limit removed; PDF stored in S3 with presigned URL; line-printer pipe replaced with email/portal delivery | No (modern-side new artifact) |
| Amendment history (OTM5) | payment_audit (JSONB) | Immutable JSONB chain; per-event W3C trace context preserved | No (modern-side strengthened audit) |
10.9 Schema Migration Design Rules
Two-layer rule set: Universal Rules apply regardless of target database engine; Engine-Specific Rules show implementation details for PostgreSQL/Aurora, DynamoDB, and MongoDB/DocumentDB. For ACAS run-012 the target is PostgreSQL (per report-plan.json.architecture.databaseEngine = postgres), so the PostgreSQL column in the engine-specific table is the active one; the other columns document portability for future projects.
Universal Rules
| Decision | Rule | Rationale | Example |
|---|---|---|---|
| Field Naming | Expand COBOL abbreviations to readable snake_case | COBOL's 30-char ceiling and PL/SL/GL prefix convention produce cryptic names; readability matters more than terseness in modern code | OI-Deduct-Days → deduct_days (not deduct_dys); Pay-SortCode → sort_code |
| Numeric Precision | +2 integer digits beyond COBOL PIC clause; exact decimal preservation; never floating-point for financial data | Allows for 100x inflation over the legacy ceiling without future schema migration; eliminates COBOL "field overflow" defects | pic 9(7)v99 → NUMERIC(9, 2); pic 9(9)v99 → NUMERIC(11, 2); pic 99 on a counter → BIGINT |
| OCCURS Elimination | Always child entities with parent FK; never fixed-size arrays or list attributes | Legacy OCCURS N arrays propagate their N as a constraint into every consumer; child rows lift that constraint | filler occurs 9 (Pay-Folio/...) → payment_line child table with no ceiling |
| Audit Trail | created_at, updated_at, created_by, updated_by on all business entities | Legacy audit is per-program-discretion; universal columns close the gap | Every table in 10.2 carries the four columns regardless of source |
| New Operational Entities | Explicitly marked "NEW — no legacy equivalent" in mapping notes | Reviewers must distinguish modernization-introduced fields from legacy-equivalent fields | payment.legacy_natural_key, payment.status, payment_audit table |
| Reference Data | Read-only, minimal fields, owned by other subsystem | The Payment subsystem reads supplier-master and GL-chart-of-accounts data; it does not own them | Supplier records read via purchled.dat SAROC topic; GL accounts referenced by string code without FK during coexistence |
| String Field Widths | Map to legacy width; widen explicitly with rationale comment when an affinity win warrants | Width changes are silent breakages waiting to happen unless documented | OI-Invoice pic 9(8) → invoice_ref VARCHAR(10) with the widening rationale captured in the mapping table |
Engine-Specific Rules
| Decision | PostgreSQL/Aurora (run-012 target) | DynamoDB | MongoDB/DocumentDB |
|---|---|---|---|
| Primary Keys | UUID PRIMARY KEY DEFAULT gen_random_uuid() |
Composite PK (partition key + sort key) | _id: ObjectId or explicit UUID |
| Numeric Precision | NUMERIC(p, s) |
N type with documented precision constraint |
Decimal128 |
| Relationships | FK constraints with explicit ON DELETE semantics |
Denormalization or GSIs; no enforced FK | Embed when small & bounded; reference otherwise |
| Computed Values | GENERATED ALWAYS AS (...) STORED for immutable expressions; views or application properties for volatile values like CURRENT_DATE |
Compute-on-write at the application layer; never at the engine | Aggregation pipeline; on-read materialization |
| Audit Trail | Columns + trigger or application-layer write | Item attributes + DynamoDB Streams | Fields + Change Streams |
| Schema Artifact | schema.sql emitted by Alembic |
table-definitions.json + item-schemas/ per item kind |
validation-schemas.json (JSON Schema) |
| OCCURS → Children | Child table + FK + ON DELETE CASCADE | Child items (same PK, different SK) | Embedded array or separate collection (depends on size + access pattern) |
| String Widths | VARCHAR(N) or TEXT |
S type (document the max width) |
String with maxLength in JSON Schema |
mig-database-schema agent always runs first, producing the engine-appropriate schema artifact (schema.sql for run-012). The mig-core-logic agent then reads that artifact and generates SQLAlchemy ORM models that match it exactly. The orchestrator does not need to branch on engine — the same phase order works for all three engines above.
Section 10 is the contract between this planning workflow and the artifact-generation workflow. The migration-artifacts agents reference it for every data model decision.
For the overall modernization execution plan including phased cutover, dual-write implementation, and rollback procedures, see Section 11: Modernization Strategy.
11. Modernization Strategy
pay.dat, openitm5.dat, and purchled.dat; a copybook-aware translator emits JSON domain events into a persistent RabbitMQ topic exchange with a dead-letter queue; a background-thread consumer inside the modern FastAPI service drains the queue into PostgreSQL using natural-key idempotency. The legacy COBOL/Berkeley DB application is never modified, intercepted, or wrapped. Cutover from green screen to modern web UI is staged as a business decision — not a technical deadline — while both systems run side by side.
Sections 4 and 10 define what the target architecture looks like and how data maps from legacy ISAM into PostgreSQL. This section defines how the transition happens: the chosen coexistence pattern, the EIP building blocks of the bridge between legacy and modern, the cutover choreography, why ordering and idempotency are non-negotiable for a financial ledger, and the operational controls that govern the queue once it is running.
11.1 Why SAROC Fits ACAS Payment Processing
The legacy-coexistence skill defines three admissible coexistence patterns and one decision tree for picking among them:
Does the source have a request-routing surface (LB, API gateway, reverse proxy)?
├── No → SAROC
└── Yes → Are writes idempotent and is divergence acceptable for short windows?
├── No → Classical strangler fig (route reads first, then writes)
└── Yes → Symmetric dual-write
Applied to ACAS Payment Processing: the legacy interaction surface is a green-screen COBOL terminal application. Users interact directly with ACCEPT and DISPLAY statements in programs such as pl080.cbl, pl085.cbl, pl100.cbl, and pl960.cbl, presented through GnuCOBOL's screen runtime on an 80×24 terminal. There is no load balancer, no API gateway, no reverse proxy, and no HTTP request path of any kind. Both classical strangler-fig (which requires a request-routing surface to fork) and symmetric dual-write (which requires the legacy and modern systems to both accept writes through a common gateway) are structurally ruled out — not preferred against, but impossible to implement — because the routing layer they depend on does not exist.
SAROC is the remaining branch and the one that fits the system's actual shape. Its mechanism is to passively observe data-plane mutations on the legacy storage tier through Change Data Capture, translate them into ordered domain events, and replay them into the modern system. The legacy COBOL application is never modified, intercepted, or wrapped; the CDC adapter only reads. This is exactly the affordance required to modernize a green-screen system without rewriting the legacy code or coordinating a downtime window.
Pattern parameter declaration (from report-plan.json)
The full coexistencePattern block driving Sections 4 and 11 of this report — and the mig-coexistence-bridge agent in the artifacts workflow:
| Axis | Value | Source / rationale |
|---|---|---|
pattern | saroc | Decision tree above. Implementation status: full in v1 of the skill. |
source.kind | isam-berkeley-db | ACAS file handler acas032 writes Berkeley DB Btree indexed files when FA-RDBMS-Flat-Statuses selects ISAM mode. |
source.recordFormat | cobol-copybook | Records are laid out by COBOL copybooks (fdpay.cob, fdoi4.cob, plfdoi5.cob) using COMP-3 packed decimals and fixed-width text fields. |
source.formatDefinitionRef | config/formats/acas.json | Needs authoring. The path is reserved; the JSON layout must exist before mig-coexistence-bridge can emit the parser. Flagged in the handoff. |
source.watchedArtifacts | pay.dat, openitm5.dat, purchled.dat | The three files that carry Payment Processing aggregate state: payments, open items, purchase ledger join. |
cdc.mechanism | file-watcher | Skill default for isam-berkeley-db. The COBOL runtime writes through Berkeley DB which closes files on commit; a 1 s poll with snapshot diff is the cheapest and most robust observation mechanism. |
cdc.pollIntervalMs | 1000 | Skill default; one second is well under the response-time threshold users perceive between green-screen submit and modern UI confirmation. |
cdc.diffStrategy | snapshot-diff | Compares current snapshot of the watched files against the previous snapshot to detect added / modified / removed records. |
messaging.substrate | rabbitmq | Skill default for both docker-compose and aws-ecs-fargate target environments. |
messaging.exchangeType | topic | Routing keys partition by record type (payment.*, openitem.*) so per-aggregate ordering is preserved while readers can subscribe by topic. |
messaging.durability | persistent | Queues and messages survive broker restart; required for financial data. |
messaging.deadLetter | true | Run-012 default for new projects. (The pre-run-012 ACAS bridge ran without DLQ; this run promotes DLQ to the default per the skill's operational-hygiene policy.) |
translator.outputFormat | json | Skill v1 default. |
translator.eventEnvelope | sage-domain-event-v1 | Skill v1 envelope: schema-versioned, includes correlation ID, source artifact, and natural key. |
target.db | postgres | From Section 4. PostgreSQL 16+ on Amazon RDS. |
target.environment | docker-compose | Working hypothesis is dev / demo on Compose mirroring AWS ECS Fargate. The substrate (RabbitMQ) is identical in both deployments. |
target.consumerShape | background-thread | The Payment API service runs the consumer as a background thread in-process, matching the live ACAS reference bridge under migration-artifacts-acas-docker-006. |
target.idempotencyStrategy | natural-keys | The legacy schema has stable composite keys (supplier+invoice+date for payments; supplier+invoice+line for open items). PostgreSQL ON CONFLICT DO UPDATE against those keys makes replay safely idempotent. |
11.2 EIP Notation Diagram of the Bridge
Whatever the coexistence pattern, the bridge between legacy and modern always decomposes into four functional roles from Hohpe & Woolf's Enterprise Integration Patterns (2003). Figure 11.1 shows the four roles for the ACAS SAROC bridge in canonical EIP notation, with the legend at the bottom matching the iconography on every component.
Figure 11.1 — Reliable Message Queue Pattern (EIP component view). Each shape encodes pattern intent: the rounded-rectangle Channel Adapter reads the legacy ISAM files without modifying them; the chevron Message Translator decodes COBOL copybook records (COMP-3 packed decimal, PIC X fields, BINARY-LONG dates) into JSON domain events shaped per the sage-domain-event-v1 envelope; the pill Message Channel is a RabbitMQ topic exchange providing FIFO ordering, persistence across broker restart, and consumer-ACK at-least-once delivery, with a dead-letter queue catching poison messages; the rounded-rectangle Message Endpoint is the background-thread consumer inside the modern FastAPI service, applying each event idempotently to PostgreSQL.
Independence of the four components is the lever that makes the same pattern reusable across very different legacy estates. If a future modernization needs to bridge from a SQL Server source instead, the Channel Adapter becomes a Debezium connector and the Translator switches from copybook to row-shape decoding — the Channel and Endpoint shapes are unchanged. If the messaging substrate moves from RabbitMQ to AWS SQS FIFO for a Lambda-hosted modern stack, only the Channel changes. The pattern is the contract; the components are interchangeable.
11.3 Cutover Choreography
SAROC's defining property is that data flows from legacy to modern via two distinct paths — a one-shot bulk ETL of the historical snapshot, and an ongoing replay of the mutations that occur while that ETL is running and afterwards. Figure 11.2 shows the topology view (the parts and how data moves between them); Figure 11.3 shows the temporal view (when the steps happen and in what order); the step-details table after the diagrams makes the per-step state explicit.
Figure 11.2 — SAROC topology view. Path 1 (bulk ETL, red) loads the historical data snapshot — legacy continues to run while the modern consumer is OFF. Path 2 (change replay, green) drains the message queue after the ETL completes; idempotent application against the loaded snapshot brings the modern PostgreSQL store into convergence with legacy. The center callout is the trick that makes the pattern work: the queue accumulates every legacy mutation during the bulk load, then plays them back in order against the loaded state.
Figure 11.3 below shows the same flow on a time axis — who talks to whom, and when.
sequenceDiagram
participant U as Users
participant L as Legacy ACAS
participant CDC as CDC Adapter
participant Q as Ordered Message Queue
participant ETL as "ETL / Bulk Load"
participant M as Modern System
participant DB as PostgreSQL
Note over U,DB: Step 1 — Activate CDC Bridge
U->>L: Continue normal operations (green screen)
L->>CDC: CDC observes data mutations
CDC->>Q: Queue ordered domain events
Note right of Q: Queue begins accumulating
mutations from this point forward
Note over U,DB: Step 2 — Take Point-in-Time Snapshot
Note over L: Snapshot of legacy ISAM files
(consistent read / quiesce window)
L-->>ETL: Extract snapshot
Note over U,DB: Step 3 — Bulk Migration (consumer OFF)
Note over M: Consumer is OFF — messages accumulate
ETL->>DB: Load snapshot into PostgreSQL
Note over L: Legacy continues processing
New transactions queue in order
Note right of Q: Queue grows during
bulk load (minutes to hours)
Note over U,DB: Step 4 — Start Consumer, Replay Queue
M->>Q: Consumer connects
Q->>M: Replay queued events (FIFO order)
M->>DB: Apply each mutation sequentially
Note right of Q: Queue drains to zero
Note over U,DB: Step 5 — Verify Synchronization
Note right of DB: Reconciliation check:
record counts, checksums,
batch totals, GL balances
Note over U,DB: Step 5b — Side-by-Side Coexistence (indefinite)
U->>L: Users continue on COBOL / ISAM
L->>CDC: CDC captures every transaction
CDC->>Q: Ordered events (real-time)
Q->>M: Consumer applies to target
Note right of DB: Both systems in sync
Duration is a business decision
Note over U,DB: Step 6 — User Cutover (when business is ready)
U->>M: Users directed to modern web UI
Note over L: Legacy interface retired
Note over CDC: CDC bridge stopped
Note over Q: Queue drained and removed
Figure 11.3 — SAROC cutover sequence (temporal view). The seven labelled phases (Steps 1, 2, 3, 4, 5, 5b, 6) interleave actor interactions over a timeline that runs vertically. The two paths from Figure 11.2 show up here as Step 3 (Path 1, the ETL load) overlapping with Steps 1–3 (Path 2, the CDC adapter filling the queue), and Step 4 (Path 2 draining into the modern store) running after Path 1 completes.
Step Details
| Step | Action | Legacy Status | Modern Status | Queue State | Duration (ACAS) |
|---|---|---|---|---|---|
| 1 | Activate CDC bridge | Running (system of record) | Off | Accumulating | Instant |
| 2 | Point-in-time snapshot | Running (brief quiesce) | Off | Accumulating | Seconds (Berkeley DB consistent read) |
| 3 | Bulk migration (ISAM → PostgreSQL) | Running (new txns queue) | Consumer OFF | Growing (ordered backlog) | Tens of minutes at ACAS scale |
| 4 | Start consumer, replay queue | Running | Starting; draining backlog | Draining (FIFO) | Minutes |
| 5 | Verify synchronization | Running | Running (shadow mode) | Near-empty (real-time) | Hours to days (reconciliation cycle) |
| 5b | Side-by-side coexistence | Running (system of record) | Running (synchronized) | Real-time flow | Indefinite — business decides |
| 6 | User cutover | Retired | Primary | Removed | When user training & audit are complete |
11.4 Why Ordering and Idempotency Matter for ACAS
Two of the six verified Payment Processing business rules from Concho cycle 8 make ordering and idempotency non-negotiable for the SAROC bridge: PaymentBatchControlLimits (confidence 0.75, source purchase/pl080.cbl:733) and PaymentReversalProcessing (confidence 0.60, source pl085 / sl085).
Worked example: a 99-item batch followed by a reversal
PaymentBatchControlLimits caps each payment batch at 99 items with sequential numbering. A batch is opened, items are added with monotonically increasing sequence numbers within the batch, then the batch is proofed (P-Flag-I = 2, per PaymentPostingValidation) and posted. Posting writes payment records into pay.dat, open-item updates into openitm5.dat, and GL postings via the overnight cycle in pl100. PaymentReversalProcessing then allows an amendment to reverse the appropriations and redistribute amounts across related invoices with an audit trail in pl085.
Suppose an operator opens batch BATCH-0042, adds 99 payment items in sequence (BATCH-0042/001 through BATCH-0042/099), proofs the batch, and posts it. A few hours later, a single item — say BATCH-0042/057 — needs reversal because the supplier code was wrong. The operator runs the pl085 amendment flow, which reverses the appropriations for item 057 and redistributes the amount across the corrected supplier's open items.
Now consider what each delivery property guarantees about the bridge replay:
| Property | What goes wrong without it | Why SAROC provides it |
|---|---|---|
| FIFO ordering per aggregate | If the reversal event for item 057 arrives before the original-batch event that wrote item 057, the consumer tries to reverse an appropriation that does not yet exist in PostgreSQL. The reversal either fails outright (FK violation against a missing payment row) or silently no-ops, depending on the SQL handler. Either way the modern ledger now disagrees with legacy — the reversal never landed. | RabbitMQ topic exchange with FIFO per-queue delivery. Routing keys partition by payment record so events for the same payment travel through the same queue in source order. |
| Idempotent application | At-least-once delivery means the consumer can see the same event twice (broker retry, consumer crash before ACK during Step 4 drain). Without idempotency the second delivery double-posts — one $4,250 payment becomes two $4,250 payments, GL credits double, the batch total now reads $8,500 against a batch that only ever held $4,250 in legacy. | Natural-key idempotency: the legacy composite key (supplier+invoice+sequence) is the unique constraint in PostgreSQL. INSERT ... ON CONFLICT (legacy_natural_key) DO UPDATE turns the second delivery into a state-converging update rather than a duplicate row. |
| Batch atomicity | The 99-item batch limit is enforced at the legacy boundary — the modern batch_control entity must see the batch as a whole to validate the constraint. If items 1–50 of BATCH-0042 are visible but items 51–99 are still in flight, a concurrent query against modern reports the batch as half-full and a downstream actor (e.g., a reporting service) could draw the wrong conclusion. |
The translator emits batch-tagged events; the consumer applies all items of a batch in a single PostgreSQL transaction (begun on the batch-open event, committed on the batch-post event). Until commit, no reader sees a partial batch. |
| Sequence-number monotonicity | If items within BATCH-0042 are applied out of order — e.g., 099, 057, 014 — an audit query "show me items in posting order for BATCH-0042" returns a different result in modern than legacy. The financial trail diverges. | FIFO per-queue plus the source-order sequence number is preserved in the event envelope; consumer inserts in sequence order. |
Reversal compounds the problem because the pl085 amendment touches the same set of open items the original batch touched. If the original-batch events and the reversal events interleave incorrectly during replay, the open-item balances in modern PostgreSQL diverge from legacy ISAM — and at that point the SAROC contract is broken: modern is not legacy. A reconciliation check at Step 5 will catch the divergence, but the recovery is to re-run Path 1 (drop and reload), not a surgical fix.
11.5 Proof-of-Concept Results
A working SAROC bridge for ACAS has been implemented and exercised end to end. The implementation lives under projects/acas-payment-processing/strangler-bridge/ in the modernization repository, plus the per-target deployment under migration-artifacts/migration-artifacts-acas-docker-006/. Results below are qualitative — throughput, latency, and queue-depth measurements were not captured in a controlled benchmark and are not reported here.
What was built and verified
| EIP role | Implementation | Verified behavior |
|---|---|---|
| Channel Adapter (CDC) |
strangler-bridge/watcher.py — Python watchdog process polling Berkeley DB files at 1 s intervals, taking snapshot diffs. |
Detects CRUD changes to pay.dat, openitm5.dat, and purchled.dat within one poll cycle after the COBOL runtime closes the file on commit. |
| Message Translator | strangler-bridge/isam_parser.py — COBOL copybook decoder for COMP-3 packed decimal, BINARY-LONG, BINARY-SHORT, and PIC X fields. Copybook layouts are currently hardcoded; the v1 SAROC skill specifies externalization to config/formats/acas.json (gap flagged in the handoff). |
Records decoded for PLPAY-REC (payments), OTM5 (open items), and the purchase-ledger join records seen by the demo. Field widths match copybook specifications. |
| Message Channel | RabbitMQ 3 with management plugin; durable queues, persistent message delivery, manual consumer ACK, topic exchange acas.bridge. Configured in migration-artifacts-acas-docker-006/docker-compose.yml. |
FIFO ordering within a queue confirmed; messages survive broker restart (verified by stopping and restarting the RabbitMQ container while messages were queued). |
| Message Endpoint | Background-thread consumer inside the Payment API service; applies events through the FastAPI service’s payment-allocation endpoint, which performs natural-key upsert and inline GL posting in a single PostgreSQL transaction. | Payments entered in the ACAS green screen surface in the modern web UI shortly after the COBOL file is closed, including the allocated open items and the GL journal entries. |
End-to-end scenarios exercised
- Real-time bridge. Open ACAS in one terminal, the bridge watcher in another, the RabbitMQ management UI in a browser, and the modern web UI in another browser tab. Entering a payment through
pl080on the green screen produces a message in the RabbitMQ queue, the consumer ACKs it, and the payment with its allocations and GL postings appears in the modern UI — all without touching the legacy COBOL code. Walkthrough script:strangler-bridge/DEMO-INSTRUCTIONS.md; live demo script:strangler-bridge/saroc-live-demo.sh. - Deferred replay (SAROC Steps 3–4). The consumer was disconnected to simulate the bulk-migration window; payments entered in ACAS accumulated in the queue (visible as growing depth in the RabbitMQ management UI); the consumer was reconnected and drained the backlog; every payment was applied against the freshly loaded snapshot data in the correct sequence.
- Idempotent replay. The same message was re-delivered to the consumer; the second delivery upserted against the natural-key constraint without producing a duplicate payment row or duplicate GL postings.
- Demo reset script.
migration-artifacts-acas-docker-006/setup-demo.shresets both the Docker-side PostgreSQL and the legacy ACAS data, purges RabbitMQ queues, and reseeds a known supplier (ACME001) on both sides, providing a repeatable starting point for between-walkthrough runs.
messaging.deadLetter default for run-012 (true) is an improvement over the live ACAS bridge (which currently runs without a DLQ — the legacy-coexistence skill flags this as an operational-hygiene drift); promoting the live bridge to DLQ-enabled is a follow-up captured in the handoff. Likewise, externalizing the copybook layout into config/formats/acas.json remains to be done before the artifacts workflow can regenerate the parser from declaration.
11.6 Operational Considerations
Section 4’s observability declarations are the source of truth for thresholds; the values below align with those decisions (structlog JSON, OpenTelemetry traces and metrics, CloudWatch & X-Ray exporters via the ADOT sidecar, RFC 7807 error responses, exponential-backoff retries).
Dead-letter queue (DLQ) behavior
- Every bridge queue has a paired DLQ (
messaging.deadLetter = true). A message that exceeds its retry budget — default 5 retries with exponential backoff — is moved to the DLQ rather than blocking the live queue. - DLQ messages preserve the full
sage-domain-event-v1envelope plus anx-dead-letter-reasonannotation identifying the failure mode (parse error, FK violation, idempotency conflict). - The operator runbook for the DLQ is binary: fix the consumer or fix the data, then either replay from the DLQ back to the live queue or drop the messages after reconciling against legacy. A poisoned message is never silently discarded.
Queue-depth alert thresholds
Concrete thresholds are deliberately tied to the cutover phase the system is in — what is healthy during Step 3 (bulk-load backlog) is alarming during Step 5b (steady-state coexistence).
| Phase | Healthy queue depth | Warn (page during business hours) | Critical (page 24×7) |
|---|---|---|---|
| Step 3 (bulk load, consumer OFF) | Growing; bounded by ETL duration | n/a — expected to grow | n/a |
| Step 4 (initial drain) | Strictly decreasing | Plateau for >5 min | Increasing during drain |
| Step 5b (steady-state coexistence) | <10 messages | >100 messages | >1,000 messages or sustained growth |
| DLQ (any phase) | 0 | 1 (any DLQ message) | >10 DLQ messages or DLQ growth rate >1/min |
Consumer lag SLO
- Steady-state SLO: P50 consumer lag ≤ 2 s, P99 ≤ 10 s, measured from CDC detection timestamp on the event envelope to consumer-ACK timestamp.
- Drain SLO (Step 4): backlog drained within the smaller of (a) twice the bulk-load duration or (b) 30 minutes, whichever is smaller. Beyond that, the consumer is suspect — the runbook is to pause replay, root-cause via DLQ entries and consumer logs, and resume.
- Instrumentation: the consumer emits OpenTelemetry spans for every message processed (parent span carried via the event envelope’s correlation ID), and a gauge metric for current lag per queue. Both are routed through the ADOT sidecar to CloudWatch / X-Ray per Section 4’s observability stack.
Cutover rollback plan
| Phase reached | Rollback trigger | Rollback procedure | Data impact |
|---|---|---|---|
| Steps 1–3 | Bulk load errors, copybook mismatch, ETL timeout | Drop modern PostgreSQL schema. Stop CDC watcher. Legacy keeps running. | Zero. Legacy was never modified; CDC adapter only reads. |
| Step 4 | Replay errors, idempotency conflicts beyond DLQ tolerance | Stop consumer. Inspect DLQ. If failure is data-shape: fix translator, drop and re-run Path 1, restart consumer. If failure is consumer-code: redeploy fixed consumer image and let it resume from the queue. | Modern DB may hold partial replay. Safe to drop and reload. |
| Steps 5–5b | Reconciliation mismatch, user-reported divergence | Pause the consumer to halt forward writes to modern. Run reconciliation. Re-run Path 1 from a fresh legacy snapshot and re-drain the queue from that point. | Modern transactions entered via modern UI (if any) must be reverse-reconciled. |
| Step 6 (cutover) | User-reported business-rule regression after cutover | Re-point users at the legacy green screen. Re-activate CDC adapter pointing in the opposite direction is out of scope for SAROC v1; instead, hand-back is a controlled freeze of modern writes plus extraction of modern-only transactions to legacy. | Transactions captured only on modern between cutover and rollback must be re-keyed into legacy. |
12. How Concho Helped This Modernization Planning
TL;DR. Concho's Context Graph pre-analyzed 100 % of the 1.36 million-line ACAS codebase — surfacing 36 business functions, 56 aggregate roots, 37 bounded contexts, 308 enforced business rules, 136 integration points, and 197 implementation constraints in the catalog overview (149 of which are top-level direct entries) — before the planning agents read a single line of COBOL. That inverts the modernization workflow from "read code and hope you find what matters" to "know what matters, then verify it against the code." The same depth of analysis would take a senior architect 6–10 weeks of code archaeology — and Claude Code without Concho would be capped at roughly 5–15 % of the codebase by its context window. You don't know what you don't know — until Concho shows you.
This analysis demonstrates three distinct approaches: (1) Concho + Claude — Concho's pre-computed codebase intelligence consumed by Claude Code planning agents, (2) Claude Code Alone — analyzing the same codebase with only Read / Grep / Glob, without Concho's pre-analyzed knowledge graph, and (3) Traditional Manual Review — senior architects working through interviews, documentation, and selective code reading. Every comparison in this section includes all three columns so the value of Concho is visible relative to both AI-without-Concho and a human consulting baseline. The Applewood Computers Accounting System (ACAS) Payment Processing modernization plan was produced by an orchestrated workflow of 14 planning phases against Concho cycle 8; the numbers, file references, and confidence scores that appear throughout this report are quoted from that pre-analysis. This section explains what the Concho Analysis Engine made possible and what would have been infeasible without it.
12.1 Analysis Scope and Depth
ACAS is 1,358,687 lines of code across 449 COBOL files (plus copybooks and data files), organized into 36 business functions and 32 technology subjects. The Concho Analysis Engine processed the entire repository up front, producing a queryable knowledge graph of 56 aggregate roots, 37 bounded contexts, 308 enforced business rules, 197 implementation constraints in the catalog overview (149 direct top-level entries plus derived / cross-referenced constraints), and 136 integration points. Claude Code on its own — even with the same reasoning model — cannot read 1.36 M lines of COBOL in one session; it must sample (typically 5–15 % of files) and infer patterns from the sample. A senior architect cannot read 1.36 M lines at all; they must pick a slice and ask the customer to explain the rest. Only the pre-analyzed path reaches 100 % coverage on day one.
| Dimension | Concho + Claude (what we did) | Claude Code Alone (no Concho MCP) | Manual Review |
|---|---|---|---|
| Time to Insight | Minutes per phase — structured queries return pre-analyzed intelligence instantly; entire 14-phase plan produced in a single overnight run | Days to weeks per phase — must read sampled files sequentially, build mental model from scratch each time | Weeks to months — stakeholder interviews, code walkthroughs, documentation review, scoring workshops |
| Codebase Coverage | 100 % (all 449 COBOL files plus copybooks and data files cataloged, every line classified) | ~5–15 % (~25–65 files) before context limits force sampling decisions | ~1–5 % (~5–25 files in selective deep review) |
| Business Functions Evaluated | All 36 business functions discovered and scored across 3 independent agent runs for consensus | 3–5 sampled — constrained by time required to read each subsystem's files | 1–2 — typically the customer-suggested pilot plus one alternative |
| Aggregate Roots / Bounded Contexts | 56 aggregate roots, 37 bounded contexts with confidence scores (e.g., Payment at 0.90, PaymentProcessing at 0.90) |
Inferred from sampled file names and DATA DIVISION layouts; no quantified confidence; aggregates spanning unread files invisible | Drawn from interviews and ER-diagram archaeology; depends on whether the original architect is still at the company |
| Integration Points Detected | 136 integration points across the full codebase, including the GL Posting Integration (0.88), Payment File Handler Abstraction acas032 (0.80), Remittance Print Integration (0.73), and Menu Navigation (0.67) |
Explicit CALL and COPY statements in sampled files only — misses implicit coupling through shared copybooks (fdpay.cob, fdbatch.cob) and dual-storage handlers |
Stakeholder interviews plus selective code review — often incomplete; depends on institutional memory |
| Behavioral Rules Extracted | 308 system-wide rules pre-analyzed; 10 Payment-Processing-relevant rules with average confidence 0.77, source file and line references verified | Rules visible only in sampled files; sampling bias means rules in unread programs are missed entirely (e.g., the MOD-11 check digit in common/maps09.cbl, which is reached only via a CALL from pl080) |
Discovered through interviews and post-cutover UAT failures — often rules surface only when they break |
| Implementation Constraints Surveyed | 197 implementation constraints in the catalog overview (149 direct top-level entries; the remainder are derived / cross-referenced) with confidence scores; brittleness score 0.49; hard-coded constraint rate 100 % (zero externalized configuration) | Constraints visible only in sampled files; no rate calculation possible without full-codebase ground truth | Architect names the constraints they personally remember; the brittleness number is not produced at all |
| Repeatability | Deterministic — the same Concho cycle returns the same answers across the three independent consensus runs (3-of-3 unanimity on pilot selection) | Variable — depends on which files are sampled and in what order | Low — different reviewers reach different conclusions; multi-reviewer consensus is rare in practice |
Semantic Intelligence: Pre-Analyzed vs Inferred
The decisive difference is not the speed of the planning agents — it's whether they start from raw code or from a pre-built knowledge graph. With Concho's Context Graph, the planning agents start from semantic intelligence and use file reads only to verify specific assertions. Without it, every agent has to build its own mental model of ACAS from scratch — capped by context window and biased by whatever files happened to be read first.
- Business Function Classification. Concho returned all 36 documented business functions with file lists, LOC counts, and alternate-business-function cross-coupling in a single MCP call. Claude Code alone would need to grep, classify, and de-duplicate 281 COBOL programs across
common/,general/,sales/,purchase/,stock/, andirs/— days of work, with no canonical taxonomy to lean on (COBOL programs are namedpl080.cbl,sl100.cbl, etc., with no business-function labels). - Architecture Layer Mapping. Concho classified all files into a 9-layer architecture tree (Application, Data, Presentation, Cross-Cutting, Infrastructure, etc.) with 381,073 architectural lines. Claude Code alone can label individual files it reads, but cannot reach a system-wide layer distribution without reading every file. Manual review produces a layered architecture diagram from interviews — usually documenting the intended architecture rather than the actual one.
- Aggregate-Root Discovery. Concho surfaced 56 aggregate roots with relationship counts (e.g.,
Paymenthas 8 outbound relationships and owns 6 business rules). Claude Code alone would have to read everyfd*.cobcopybook, every*MT.cbldata-access program, and every transaction-posting program to derive the same picture. Manual review almost never reaches an explicit aggregate-root list at all. - Integration-Point Detection. Concho's analysis identified 63 bidirectional and 73 unidirectional integration points (136 total), with the dominant pattern (file-handler abstraction via
acas032) flagged explicitly as the SAROC bridge seam. Claude Code alone sees only the integrations that appear in its sampled files. Manual review depends on whether the integration was documented or remembered. - Confidence Scoring. Every Concho entity carries a 0.0–1.0 confidence score and an evidence tag (
STRUCTURAL,INFERRED, orCATEGORICAL). The planning agents use confidence to decide where to lean on Concho's conclusion and where to verify against source. Claude Code alone has no such instrument; manual review uses qualitative ("high/medium/low") labels.
This pre-analysis eliminates the typical 2–3 week "codebase inventory" phase at the front of every modernization engagement. The planning agents in run-012 started phase 1 already knowing what existed and where to verify it.
12.2 Business Rule Discovery
ACAS Payment Processing produced 10 behavioral rules in this run's catalog, with an average Concho confidence of 0.77 and a verbatim-transfer rate of 9 of 10 (the remaining one, BR-PAY-008, requires a mitigation test; BR-PAY-005 and BR-PAY-010 are delivery refactors that keep identical per-payment behavior while the output materialization shape changes). Every rule cites a Concho entity name plus a verified source file:line reference, and every source citation was verified by the planning agent re-opening the actual COBOL source span before the rule was included in the catalog. Without Concho's pre-analysis, an analyst would have to read every Payment-relevant COBOL program (~30 files, ~14,500 LOC) to find these rules — and without a pre-computed business-function index, would not even know which files to read.
| Rule Category | Rules Found (Run-012) | Concho + Claude | Claude Code Alone | Manual Review |
|---|---|---|---|---|
| Validation | 3 (BR-PAY-001, 002, 003) | Returned with confidence 0.65–0.88 and file:line refs (e.g., MOD-11 check digit at common/maps09.cbl:90–145) |
Discoverable only if maps09.cbl is sampled; visible from pl080 only as an opaque CALL |
Surfaced through interviews if the original author is reachable; otherwise discovered post-cutover when valid-looking IDs fail |
| Calculation | 2 (BR-PAY-004 discount, 005 method) | Discount window logic at purchase/pl080.cbl:585–605; BACS-vs-cheque routing at pl940.cbl:517–525 |
Requires reading multi-program flow (pl080 → pl940 → pl960); fragile in sampling |
Discoverable but slow; the boundary condition (strict > vs >= in the discount window) is the kind of detail that gets misremembered |
| State Transition | 2 (BR-PAY-006 batch lifecycle, 007 unapplied) | Lifecycle structurally evident from fdbatch.cob:20–30 at confidence 0.87 |
Requires reading the copybook plus every program that mutates the batch header | The "you can't reopen a closed batch" invariant tends to surface only when somebody tries |
| Workflow | 3 (BR-PAY-008 batch limits, 009 reversal, 010 remittance) | 99-item batch ceiling grounded in Items pic 99 at fdbatch.cob:18; composite-key formula at pl080.cbl:733 |
The 99-item ceiling is a 2-digit numeric field declaration; an LLM scanning for "MAXITEMS = 99" would miss it entirely | Architects know "there's a batch limit"; few remember the exact magic number or its source |
| Authorization | 0 (legacy has no role-based authorization) | Confirmed by absence: no business_rule entities in the Authorization category for Payment Processing |
Cannot prove a negative from sampling | Easy to assume role-based access exists when it doesn't — misleading the target-architecture phase |
Source Code Traceability: Pre-Linked vs Manual
Every rule in this run's behavioral catalog is anchored to a verified file:line citation from Concho's pre-analyzed entity store. The planning agent only needed to verify the source — not find it.
- Source File References. Concho's entity for
PaymentBatchControlLimitsships with both source files (purchase/pl080.cblandcopybooks/fdbatch.cob) pre-attached. Claude Code alone would have to grep across 449 files for a literal "99" and disambiguate the data-declaration site from arbitrary uses; manual review would skimpl080for "batch" and miss the copybook. - Line-Number Precision. Concho cites
pl080.cbl:731–735for the composite-key formula; the planning agent confirmed by re-reading the cited source range that the literalmultiply oi-b-nos by 1000 giving oi-invoiceis at those lines. Without Concho, the analyst's only signal is the search hit, with no priority among ~30 matches. - Code-Comment Mining. Concho's entity descriptions are converged from the COBOL comments, data-division text, and procedure-division evidence. The MOD-11 check-digit derivation is given in the entity description before the COBOL is even opened.
- Cross-Reference Validation. Concho knows that
BatchStatusLifecycleValidationis grounded in bothcopybooks/fdbatch.cob:20–30(the data structure) andgeneral/gl070.cbl:307(a usage site). Claude Code alone would have to read every program thatCOPY-sfdbatchto confirm the lifecycle is enforced consistently.
Example: How One Business Rule Surfaces in Three Approaches
BR-PAY-008 — PaymentBatchControlLimits (99-item batch cap)
Given-When-Then. Given that a batch has been opened with Items=00, when an item-entry attempt would cause Items to reach 99, then the next attempt is blocked (the 2-digit field overflow guard) and the operator must close the current batch and open a new one. The composite invoice key is computed as oi-invoice = oi-b-nos * 1000 + oi-b-item, guaranteeing uniqueness within a batch.
Concho + Claude (what we did).
- Single MCP call returns the rule with confidence 0.75, evidence type
STRUCTURAL, source filescopybooks/fdbatch.cobandpurchase/pl080.cbl, line range 731–735. - One verification call confirms the COBOL literal
multiply oi-b-nos by 1000 giving oi-invoiceatpl080.cbl:733and the field declarationItems pic 99atfdbatch.cob:18. - Target mapping written in minutes:
BIGINTfor batch.items_count, UUID + sequence for the composite key, feature-flag-gated 99-item soft cap during SAROC coexistence so the legacy bridge translates batches 1-to-1, ceiling relaxed at cutover. - Total agent time: under 5 minutes for this rule, including target-state design.
Claude Code Alone (no Concho MCP).
- Must first identify which programs implement Payment Processing — there is no business-function index, so this is an LOC-by-LOC archaeological dig.
- Must read
pl080.cbllooking for batch-control logic; the 99-item ceiling appears as a 2-digit field declaration, not as a comment or constant namedMAX_BATCH. - Must independently discover that
fdbatch.cobis the data definition driving the ceiling — possible only by reading every program thatCOPY-s the copybook. - Even if found, has no quantified confidence signal — cannot tell the planning agent how much to trust this finding versus another.
- Estimated wall-clock: 1–2 days for this one rule, with high probability of missing the copybook half.
Manual Review.
- Often surfaces during user interviews ("there's a batch limit, I think it's about 100") without the exact number or source.
- Once the team finds the magic 99, may or may not connect it to the 2-digit field declaration that produces it.
- The composite-key formula (
* 1000 + sequence) is the kind of detail consultants skip; it surfaces post-cutover when batch numbers above 999 collide. - Estimated wall-clock: 1–3 days of joint engineering + business-analyst time, with documentation that may go stale before the modernization completes.
Summary. Same rule, three timeframes: ~5 minutes (Concho + Claude) ⇐ 1–2 days (Claude alone) ⇐ 1–3 days (manual). And only Concho + Claude provides the quantified confidence (0.75) plus the structural-evidence tag.
Across the 10 rules in this run's catalog, the pattern holds: Concho's pre-analysis turns the requirements-extraction phase from days-per-rule into minutes-per-rule, and the resulting catalog is more complete than a manual pass would produce (because manual passes only find the rules someone remembered to mention).
12.3 Architecture Intelligence
ACAS Payment Processing's target architecture was decided through a multi-perspective service-architecture analysis (DDD, Technical, Business) with a tiebreaker scoring model. Every perspective cited the same Concho entities — the single PaymentProcessing bounded context (confidence 0.90), the single Payment aggregate root (confidence 0.90), the four integration boundaries (GL Posting at 0.88, File Handler at 0.80, Remittance Print at 0.73, Menu Navigation at 0.67), and the MultiKeySortProcessing pattern (0.70) — so the disagreement between perspectives was about shape (1 vs 3 vs 4 services), not about facts. Without Concho, the three perspectives would each have generated their own incompatible mental model of the system, and the tiebreaker would have collapsed into a debate about ground truth instead of a debate about decomposition strategy.
| Architecture Aspect | Concho + Claude | Claude Code Alone | Manual Review |
|---|---|---|---|
| Layer Distribution | 9-layer architecture tree with line counts (architectural LOC 381,073 of 1,358,687); Application 22.9 %, Data 53.3 %, Presentation 2.4 %, Documentation 28.6 % (files may belong to more than one architecture layer, so the four percentages sum above 100 %) | Inferred from file headers and directory naming on sampled files; cannot reach the system-wide percentage | Documented from architect interviews; usually reflects the intended not the actual layer split |
| Bounded Contexts | 37 contexts identified system-wide; 1 (PaymentProcessing, 0.90) for the pilot scope |
Inferred from naming and CALL patterns; high error rate on cross-cutting concerns like the *MT data-access pattern |
Reverse-engineered from organizational structure (Conway's law); often disagrees with the actual code seams |
| Integration Patterns | 136 integration points categorized (file-based, internal CALL, database); 63 bidirectional flagged as the architectural debt to unwind | Visible only as CALL and COPY in sampled files; bidirectionality determination requires reading both endpoints |
Captured incompletely; institutional memory loses fidelity quickly for systems older than the average tenure |
| Aggregate Boundary Verification | Aggregate root with relationship count (Payment at 0.90, 8 relationships, 6 owned rules) |
Aggregate boundaries can be guessed from sampled DATA DIVISION layouts; relationship counts unknown | Subjective; reviewers disagree about what counts as an aggregate |
| Architectural Diagrams | Concho-vended architecture-layer diagram (confidence 0.95) and data-flow diagram (confidence 0.90) used verbatim in Section 3 | Diagrams must be drawn from scratch based on whatever was sampled | Diagrams drawn from interviews; refreshed only as often as someone refreshes them |
| Pattern Discovery | The dual-storage FA-RDBMS-Flat-Statuses switch surfaced as the natural SAROC bridge seam — from Concho's architectural commentary, not from agent inference |
Could discover the pattern only if acas032.cbl, paymentsMT.cbl, and dummy-rdbmsMT.cbl all happen to be sampled together |
The dual-storage seam is the kind of design insight that gets lost when the original architect leaves; manual review may miss it entirely |
Architecture Example: Finding the SAROC Bridge Seam
The run-012 modernization plan adopts the SAROC (Snapshot and Replay Ordered Cutover) pattern for legacy/modern coexistence — a file-watcher CDC against the ISAM data path that observes data writes without intercepting the COBOL request path. The decision to use SAROC depends on the discovery that ACAS has no intercept-able request surface (no API gateway, no reverse proxy, no load balancer; users interact directly with green-screen ACCEPT / DISPLAY statements) and that the legacy file-handler abstraction acas032 (and the dual-storage routing through acas000–acas015) already inspects an FA-RDBMS-Flat-Statuses flag on every file operation. That flag is the natural insertion point for a CDC bridge.
Concho + Claude. The Concho entity catalog surfaced the file-handler abstraction as an integration boundary at confidence 0.80, with the explicit architectural-commentary note that the dual-storage switch is "already a clean abstraction seam." That single insight is what makes SAROC the right pattern for ACAS rather than the more invasive classical strangler-fig. Surfaced in seconds.
Claude Code Alone. Would need to read acas032.cbl, the entire acas000–acas015 family, plus at least one calling program from each subsystem to confirm the pattern is universally applied. Even with the right files in context, the leap from "this is a file-handler indirection" to "this is the right place to insert CDC" depends on architectural pattern-matching that an LLM can do but not without first reading those files. Estimated: half a week of file reading before the pattern is even visible.
Manual Review. An architect familiar with the codebase would describe the file-handler pattern, but might not connect it to the CDC-bridge use case until the modernization team explicitly asks "where should the bridge live?" The connection between the legacy seam and the modern pattern is the kind of insight that ships with a pre-computed knowledge graph but rarely with a stack of interview transcripts.
12.4 Time Savings and Completeness
The run-012 modernization plan moved through 14 phases (project-intel, candidate-selection 3-run consensus, candidate-assembly, target-architecture, service-architecture 3-perspective + tiebreaker, target-arch-reconciler, migration-execution, platform-affinity 3-run consensus + reconciler, behavioral-rules, use-case-discovery, plus validators) in an overnight automated session. The same end-state — a defensible target architecture, a 10-rule behavioral catalog, a platform-affinity verdict on 12 implementation constraints, and a strangler-fig sequencing plan — would take a senior architect 6–10 weeks of full-time work to produce, and would still be capped at perhaps 10 % codebase coverage.
| Analysis Activity | Manual Approach | Claude Code Alone | Concho + Claude (run-012) | Time Compression |
|---|---|---|---|---|
| Codebase Inventory (LOC, files, languages) | 1–3 days (cloc + manual subsystem classification across 449 COBOL files) |
Hours (must sample-read for classifications) | Single MCP call returns 1,358,687 LOC + technology breakdown | ~100× |
| Business-Function Inventory (36 subjects) | 2–3 weeks (interviews + program-comment archaeology; no canonical naming) | Days to weeks (must read program headers across the sample) | Single MCP call returns all 36 subjects with file lists | ~200× |
| Subsystem Scoring (8–10 candidates) | 5–8 person-days (single pass; consensus across multiple reviewers extends this) | 1–2 days for sampled candidates (cannot score what it hasn't read) | ~30 minutes of agent time per run, with 3 independent runs executed in parallel; 3-of-3 unanimous consensus on Payment Processing | ~50× |
| Business-Rule Extraction (10 rules + GWT specs) | 2–3 weeks of BA + senior engineer time (rule discovery, GWT authoring, source-link verification) | 3–5 days (extract from sampled files; will miss rules in unsampled files) | ~30 minutes — rules returned pre-extracted with confidence + file:line; GWT generated from converged descriptions | ~80× |
| Architecture-Layer Mapping | 1–2 weeks (interviews + diagram redraw + reviewer reconciliation) | 1–2 days for sampled layers; system-wide picture infeasible | Returned as a 9-layer tree with line counts; Concho-vended Mermaid diagram used verbatim | ~70× |
| Integration-Point Analysis (136 points) | 2–4 weeks (manual CALL/COPY trace across 449 files + integration-pattern classification) | Days (visible only in sampled files; bidirectionality requires both endpoints) | Returned as a categorized list with pattern classification; cross-coupling rows pre-computed | ~100× |
| Implementation-Constraint Catalog (197 entries) | 3–4 weeks of architect time, almost certainly incomplete | Days; constraint inferences from sampled files only | Returned in one MCP call with confidence scores, evidence tags, and the system-wide hard-coded rate (100 %) + brittleness score (0.49) | ~150× |
| Service Decomposition Decision (DDD vs Technical vs Business) | 2–3 weeks (workshops + architecture-review sessions to converge) | Possible but not faster than the workshop — LLM-without-Concho has to derive aggregates from sampled code | ~1 hour per perspective + tiebreaker; winning score 7.80 / 10; serviceCount = 3 agreed across perspectives by data, not by horse-trading |
~60× |
| Platform-Affinity Analysis (12 constraints, 3-run consensus) | 1–2 weeks (constraint discovery + retire/preserve disposition) | Days, sampling-bounded; verdict consistency across reruns unlikely | ~3 independent agent runs producing handoffs; reconciler converged on 12 consensus entries with zero unresolved 3-way splits | ~30× |
| Total — full plan | 6–10 weeks senior-architect equivalent | Multiple weeks; coverage capped at ~5–15 % | One overnight orchestrated run | ~50–100× |
Comprehensive vs Selective Coverage
Time compression matters, but the more important difference is completeness. Concho's Context Graph gives every planning phase 100 % coverage of the codebase from the first second. Sampling-based approaches cannot recover from the rules they miss.
- Every file is classified. Concho's catalog covers all 449 COBOL files and the surrounding copybook / data-file material. Claude Code alone is bounded by context window; manual review is bounded by attention span. Neither reaches 100 %.
- Every rule is reachable. The MOD-11 check digit (
common/maps09.cbl) and the 99-item batch ceiling (copybooks/fdbatch.cob) are both in files that a Payment-Processing-focused investigator might not sample — one is in a shared utility, the other is a copybook. Concho surfaces both at the rule-discovery step because the underlying entities are pre-linked to the Payment-Processing subject. - Every constraint is counted. The "100 % hard-coded constraint rate" finding (197 of 197) cannot be derived from sampling. A sample-based analysis would conclude "most constraints are hard-coded" without being able to quantify it — and would miss the brittleness-score signal that drives the platform-affinity verdict distribution.
- Cross-subsystem dependencies are visible. The Payment-Processing strong-file set has 17 distinct cross-references to other business functions; sampling-based approaches see only the ones the sample happened to capture.
Consistency Across Agents and Phases
Run-012 used 3 independent agents for candidate selection, 3 independent agents for platform affinity, and 3 independent perspective-agents for service architecture. Every one of those 9 agents cited the same Concho entities with the same names, the same confidence scores, and the same source file:line references. That cross-agent consistency is only possible because every agent is querying the same pre-computed knowledge graph. Without that shared ground truth, three agents would generate three subtly different mental models of ACAS, and the consensus step would have to reconcile data rather than opinion.
- Attention Consistency. Concho applies identical analytical depth to file #1 and file #449. Human attention degrades over multi-week analysis efforts; LLMs without Concho degrade in proportion to context-window usage.
- Pattern Recognition. Concho's pre-analysis identified the dual-storage abstraction, the file-handler indirection, and the bidirectional integration dominance — subtle patterns that humans might miss and LLMs without full-codebase visibility cannot prove.
- Cross-Referencing. Concho's catalog cross-references 308 business rules against 56 aggregate roots against 37 bounded contexts against 197 constraints simultaneously — impossible for human working memory or for a context-bounded LLM.
- No Confirmation Bias. The run-012 candidate-selection runs each independently arrived at Payment Processing from three different scoring emphases. That convergence is meaningful only because the underlying data is the same across runs; sample-based runs would converge on whatever each run happened to read.
12.5 How Concho Plugs Into the Modernization Workflow
This analysis was produced by the Concho Modernization Planning workflow — an orchestrator and a set of phase-specific planning agents running on Claude Code — consuming Concho's Context Graph through the Concho MCP server. The agents do not read raw COBOL as a first action; they query Concho for the semantic intelligence they need, and they reach for source code only to verify a specific assertion. This inverts the traditional workflow from "read code, hope you find what matters" to "know what matters, verify it against the code."
| Workflow Step | Concho + Claude (run-012) | Claude Code Alone (no Concho MCP) | Manual Review | Outcome Difference |
|---|---|---|---|---|
| Enumerate candidate subsystems | One query returns the 36 business functions with file lists and LOC counts; canonical taxonomy delivered in seconds | Grep + filename inference across 449 files; sample-bounded by context window; produces an ad-hoc taxonomy biased by which files the LLM happened to read | Architect walks the source tree, reads program comments, classifies by hand — days of work, error-prone naming because COBOL programs are named pl080, sl100, etc., with no business-function labels |
Days → seconds; canonical names; full coverage instead of sample bias or human guesswork |
| Score and rank candidates | 3 independent agents run in parallel against the same pre-computed file / LOC / coupling data; reproducible 3-of-3 unanimity on Payment Processing with composite scores +2.60 to +2.85 | Single-pass scoring against a sampled file set; rerunning the LLM yields a different ranking each time because the sample changes | Single-reviewer scoring with subjective inputs; multi-reviewer consensus is expensive (days of workshops) and rarely converges cleanly | Reproducible scoring; 3-of-3 unanimity instead of single-reviewer guess or run-to-run drift |
| Discover bounded contexts and aggregate roots | Concho returns 37 bounded contexts and 56 aggregate roots with confidence scores; the single PaymentProcessing context (0.90) and the Payment aggregate (0.90) are quoted across all 3 service-architecture perspectives |
Aggregates inferred from sampled DATA DIVISION layouts and CALL graphs; no quantified confidence; aggregates spanning unread programs are invisible |
Reverse-engineered from architect interviews and ER-diagram archaeology; depends on whether the original designer is still at the company; high reviewer-to-reviewer disagreement | Common ground truth across multi-perspective analyses; consistent naming across 3 agents instead of 3 different mental models |
| Find behavioral rules | Concho returns rules pre-extracted with file:line refs, confidence scores, and converged descriptions; agent re-opens the source at each cited range to confirm the literal | Must sample-read Payment-relevant programs to spot rule-shaped patterns; an LLM scanning for "MAXITEMS = 99" misses the 99-item ceiling because it surfaces as a 2-digit field declaration, not a constant | BA + senior engineer extract rules through interviews and selective code walkthrough; GWT specs authored from scratch; rules in shared utilities (e.g., MOD-11 in common/maps09.cbl) are easily missed |
Minutes-per-rule with file:line traceability instead of days-per-rule with discovery gaps |
| Catalog implementation constraints | Concho's catalog overview returns the full constraint set with the system-wide brittleness score (0.49) and the 100 % hard-coded rate; 3 agents independently filter to ~14–20 entries for Section 5; reconciler converges on 12 consensus entries | Constraint inference from sampled files only; no system-wide rate is provable from a sample; no brittleness score can be produced; 3 reruns of the same prompt yield 3 different lists | Architect names the constraints they personally remember (typically 10–20); the system-wide rate and brittleness score are not produced at all; coverage gaps surface post-cutover | Quantified posture replaces gut feel; 3-run consensus replaces single-reviewer judgment |
| Decide service decomposition | 3 perspectives (DDD, Technical, Business) each cite the same Concho entities; tiebreaker scores all three on the same rubric; Technical wins at 7.80 / 10 with serviceCount = 3 agreed by data |
Decomposition derived from sampled-file pattern inference; without shared aggregates, a second run argues a different decomposition; no quantified tiebreaker | Architecture workshop; verdict driven by whoever speaks loudest, not by the data; documentation is the slide deck, which goes stale | Decision driven by evidence, not by org politics or sampling luck |
Why the Workflow Is Possible
The most consequential capability is that multiple independent agents agree on the same set of facts. In run-012, the candidate-selection phase ran 3 times in parallel with 3 different scoring emphases — standard, technical-feasibility, business-value — and all 3 picked Payment Processing. The service-architecture phase ran 3 perspectives — DDD, Technical, Business — and all 3 worked from the same bounded-context / aggregate-root / integration-boundary set; the disagreement was about decomposition strategy, not about ground truth. The platform-affinity phase ran 3 independent reviewers and the reconciler converged on 12 entries with zero unresolved 3-way splits. None of this is possible without a shared, pre-computed semantic layer.
- Zero "Where do I start?" overhead. Every phase starts already knowing what exists.
- Structured data, not free-text search. Entity types, confidence scores, evidence tags, source references — queryable, not just searchable.
- Bidirectional traceability. Every conclusion in this report points back at a Concho entity name and a file:line citation; every Concho entity points back at the source code it was derived from. Hallucination detection becomes a check, not a hope.
- Cross-phase consistency. The Payment aggregate root's confidence (0.90) is the same number in Section 3, Section 4, Section 9, Appendix B, and this section. Without a shared knowledge graph, that consistency would have to be manually maintained.
- Reproducible audits. Re-running the modernization plan against the same Concho cycle produces the same numbers. Re-running against an updated Concho cycle shows the deltas explicitly.
Workflow Value Proposition
Concho's Context Graph transforms modernization analysis from a manual archaeological dig through 1.36 million lines of COBOL into a conversational query workflow against pre-computed semantic intelligence. The planning agents ask questions in natural-language form ("what business rules govern Payment Processing?"), and Concho returns structured data covering 100 % of the codebase. The result is more complete, more consistent, more reproducible, and roughly two orders of magnitude faster than the manual baseline — and it makes possible workflows (3-run consensus, 3-perspective architecture analysis) that are simply infeasible without a shared semantic ground truth.
12.6 Why Context Graph Matters
Claude Code alone: Fast AI reasoning + raw file access = rediscover patterns from scratch, bounded by context window
Claude Code + Concho MCP: Fast AI reasoning + pre-analyzed semantic intelligence = instant comprehensive analysis, 100 % coverage
Could Claude Code alone read a source file and determine legacy behavior? Sure — if it knew which file to read. But the fundamental problem is: you don't know what you don't know. Even if Claude Code is pointed at the right file and correctly understands the logic, it still cannot know whether that file is the only part of the system that touches the capability. The ACAS payment-batch ceiling is a perfect example: the constraint originates as a 2-digit numeric field declaration in copybooks/fdbatch.cob:18, propagates through the composite-key arithmetic at purchase/pl080.cbl:733, and surfaces as a behavioral guard that blocks payment-entry attempts past item 99. Three different files, three different architectural layers, one constraint. A tool analyzing one file at a time cannot see the full picture, and worse, it doesn't know to look for it.
The advantage of Concho's Context Graph is that the planning agent instantly has access to the full gamut of business flows, behavioral rules, architecture layers, and integration points — at both business and technology levels — across 100 % of the codebase. It doesn't start by reading files; it starts by knowing what exists. It retrieves actual source code if and only if needed to verify an assertion the Concho analysis has already made. This inverts the workflow from "read code and hope you find what matters" to "know what matters and verify it against the code."
The Coverage and Semantic Intelligence Gap
| Analysis Challenge | Claude Code Alone | Concho + Claude | Manual Review |
|---|---|---|---|
| Cross-file constraint propagation | Constraint visible only where it's declared; usage sites must be discovered by reading | Entity ships with all related source files attached (e.g., PaymentBatchControlLimits ships with both copybook and program references) |
Depends on whether the architect tracked the propagation; usually they tracked the symptom, not the origin |
| Discovering rules in shared utilities | A rule in common/maps09.cbl is invisible unless the utility is sampled (only reached via CALL from subsystem programs) |
Concho indexes rules by the subjects they affect, not by the file they live in — the MOD-11 check digit surfaces under Payment Processing because Concho knows the call graph | Sometimes discovered, often missed; depends on whether the utility's purpose is documented |
| Quantifying system-wide posture | Cannot reach a system-wide rate from samples (no "100 % hard-coded" finding is provable) | Brittleness score (0.49) and hard-coded constraint rate (100 %) computed across all 197 constraints | Quantification not produced at all |
| Consistency across multiple analyses | 3 independent runs would sample 3 different file sets and produce 3 inconsistent mental models | 3 independent runs against the same Concho cycle produce the same numbers; consensus is meaningful | Multi-reviewer consensus is expensive and rare; results often diverge |
| Disambiguating verified vs unverified claims | Every claim is equally confident (or equally hand-wavy); no quantified evidence signal | Every entity carries a 0.0–1.0 confidence + evidence tag (STRUCTURAL / INFERRED / CATEGORICAL); the run-012 catalog excludes the unverified 9-invoice allocation limit because Concho cycle 8 doesn't ground it | Verification depends on whether the source author is still reachable |
| Architectural-pattern surfacing | Patterns visible only if every file participating in the pattern is sampled together | Concho's architectural commentary surfaces the dual-storage abstraction and the file-handler indirection as the SAROC bridge seam — before any agent reads the code | Patterns surface only if the original architect documented them or is still on the team |
What Concho Caught That Sampling Would Have Missed
Three concrete examples from run-012 where Concho's pre-analysis caught something that sampling-based or interview-based approaches would have missed:
- The "9-invoice allocation limit" myth. Earlier ACAS notes mentioned a "9-invoice allocation limit" as a Payment Processing rule. Concho cycle 8's entity catalog does not ground this rule (the closest match is a 9-element OCCURS array in
pl960.cblfor remittance line printing — a UI-format constraint, not a business rule). The run-012 plan explicitly excludes this rule from the BR catalog because Concho's evidence does not support it. Without Concho, this misframing would have propagated into target-design. - The MOD-11 check-digit reassignment. The MOD-11 supplier-account check digit was initially classified as a platform constraint by one of the platform-affinity runs (because it lives in a common terminal-form utility). The platform-affinity-reconciler caught that this is a genuine business identity invariant and reassigned it from Section 5 to Section 9 as BR-PAY-002. Without the cross-agent reconciliation enabled by Concho's shared entity store, the rule would either have been duplicated in both sections or lost between them.
- Zero entanglement across 12 architectural elements. Concho's architectural insight surfaced that ACAS has zero entanglement findings across 12 major architectural elements despite 48+ years of evolution — evidence of "controlled architectural evolution rather than organic growth." That's a positive risk signal for strangler-fig modernization. A sample-based or interview-based approach simply cannot produce this kind of system-wide quality finding.
Key Insight. Concho doesn't just provide file access — it delivers the results of full-codebase semantic analysis in queryable form. Claude Code transforms that intelligence into modernization recommendations roughly two orders of magnitude faster than starting from raw source code, and it produces a more complete and more consistent plan because every phase shares the same ground truth.
12.7 Conclusion: From Partial Analysis to Full Deep Insight
The ACAS Payment Processing modernization plan in run-012 demonstrates a fundamental shift in legacy-modernization capability. Traditional approaches — whether manual or AI-assisted — are constrained to partial analysis: sampling representative files, inferring patterns from incomplete coverage, and accepting knowledge gaps as inevitable. Claude Code alone can accelerate this partial analysis but remains bound by the context window that forces sampling (~5–15 % of a 1.36 M-LOC codebase).
Claude Code + Concho MCP eliminates partial analysis entirely. By providing pre-analyzed semantic intelligence across 100 % of the codebase — the 36 business functions, 56 aggregate roots, 37 bounded contexts, 308 enforced business rules, 197 implementation constraints, and 136 integration points that anchor this report — Concho transforms modernization from educated guesswork into systematic engineering. The difference isn't just speed (one overnight run vs 6–10 weeks of senior-architect work); it's completeness, repeatability, and the discovery of critical business rules and architectural patterns that partial analysis would miss entirely.
In run-012 specifically, Concho's Context Graph made three things possible that aren't otherwise possible:
- A 3-of-3 candidate-selection consensus on Payment Processing (Standard / Technical-Feasibility / Business-Value emphases all converged because they queried the same data).
- A 3-perspective service-architecture analysis (DDD / Technical / Business) where all perspectives cited identical Concho entities and the tiebreaker scored on consensus quality, not on whose facts to believe.
- A platform-affinity verdict on 12 implementation constraints with zero unresolved 3-way splits across 3 independent reviewer agents — because the underlying entity catalog was the same for every reviewer.
Ready to Transform Your Legacy Modernization?
Experience the difference between partial sampling and full deep insight. The Concho Analysis Platform can analyze your legacy codebase — COBOL, Java, C++, .NET, AngularJS, or other languages — and deliver comprehensive modernization intelligence in hours, not weeks.
Visit concho.ai and sign up for a pilot project. Discover what you've been missing with partial analysis.
Appendices
Advanced analysis and deployment artifacts
Appendix A: Multi-Agent Subsystem Selection Methodology
This appendix documents the multi-agent consensus methodology Concho used to evaluate modernization candidates within ACAS. Payment Processing was customer-specified for this project; the methodology described here was run independently and is included to (a) document the analytic backing for the choice, and (b) demonstrate how Concho can assist organizations that need help selecting a modernization candidate when one has not been pre-specified.
Customer Pre-Selection
The customer's finance and IT leadership pre-specified Payment Processing as the modernization target. The multi-agent methodology was executed independently and arrived at the same conclusion across all three runs — full 3-of-3 consensus. The methodology in this appendix is therefore presented as alternative-methodology validation, not as a competing recommendation.
A.1 Methodology Overview
Single-analyst modernization-candidate selection is vulnerable to framing bias: an analyst who emphasizes technical neatness will land on different candidates than one who emphasizes business value, even given identical evidence. Concho's multi-agent consensus methodology mitigates this by running the same scoring rubric three times, with three different emphasis profiles, against the same Concho MCP–provided evidence base. Convergence across emphases is a strong signal; divergence forces an explicit tiebreaker discussion.
Each run independently queries Concho MCP for the project's full business-function inventory, gathers file counts, LOC, and cross-subsystem coupling for the candidate shortlist, applies the standard weighted-scoring formula from the modernization-scoring skill, and produces a top-3 ranking with rationale. The three runs do not read one another's handoffs — isolation is enforced at the agent-dispatch layer — so any agreement is genuine convergence, not echo.
A.2 Agent Perspectives
Run 1 — Standard Weighted Scoring
Applies the modernization-scoring rubric verbatim with no emphasis adjustment. Used as the baseline view; the other two runs are read against it. Focus is balanced across Risk, Feasibility, and Strategic Value with the standard 0.4/0.3/0.3 weighting.
Run 2 — Technical-Feasibility Emphasis
Same closed-form weights, but reads feasibility evidence (file count, LOC, dependency isolation, presence of clean Data Access Layer abstractions) more aggressively in setting the Feasibility dimension. Optimizes for "what pilot teaches us the most about migrating ACAS to the target architecture" rather than raw business return.
Run 3 — Business-Value Emphasis
Same closed-form weights, but reads strategic-value evidence (executive visibility, customer-touching outputs, operational drumbeat, pattern reuse across the rest of the roadmap) more aggressively in setting the Strategic Value dimension. Optimizes for the most credible proof-of-modernization to business stakeholders.
A.3 Scoring Criteria
The three runs share the rubric defined in the modernization-scoring skill:
| Dimension | Direction | Weight | Key Inputs |
|---|---|---|---|
| Risk | Lower is better | −0.40 | Criticality, integration coupling, data-migration surface, compliance exposure |
| Feasibility | Higher is better | +0.30 | Code volume, dependency containment, clean DAL abstraction, knowable scope |
| Strategic Value | Higher is better | +0.30 | Business impact, executive visibility, pattern reuse, stakeholder credibility |
Closed-form formula (applied verbatim by all three runs):
Score = (Risk × −0.40) + (Feasibility × 0.30) + (Strategic Value × 0.30)
A.4 Agent Analysis Results
Run 1 (Standard Weighting) — Recommendation: Payment Processing (+2.60)
Risk 5.5 · Feasibility 7.5 · Strategic Value 8.5 · Weighted Total +2.60
Key points from Run 1:
- ~30 strong-association files / ~14,300 LOC concentrated in three directories (
purchase/pl08x-pl9xx,sales/sl08x-sl1xx,common/payments*MT/RES/UNL). - Two cleanly-bounded value streams: PL cash-posting
pl080 → pl090 → pl095 → pl100 → pl940/pl950 → pl960and the matching SL receipts flowsl080 → sl090 → sl095 → sl100. - High strategic visibility: cheque/BACS payments and remittance advices (
pl960) are customer-touching artifacts a CFO sees daily. - Lower risk than mass-scale alternatives (GL, Stock Control, the Invoicing subsystems) which carry broader state-mutation and integration surfaces.
Run 2 (Feasibility Emphasis) — Recommendation: Payment Processing (+2.80)
Risk 5.0 · Feasibility 8.0 · Strategic Value 8.0 · Weighted Total +2.80
Key points from Run 2:
- Payment files (
pay.dat,cheque.dat,openitm5.dat) are well-encapsulated — file-watcher CDC is feasible on this boundary in a way it is not on, e.g., the General Ledger. - Second-smallest substantive financial subsystem and the most narrowly file-bounded. Clean separation between
purchase/pl0??/pl9??,sales/sl0??, and thecommon/Data Access Layer modules. - Dual-storage abstraction in
paymentsMT/paymentsLD/paymentsRES/paymentsUNLmeans the consumer-side shape is already discoverable from the legacy code. - Tiebreaker applied: smaller subsystems (Remittance Advices ~300 LOC; Supplier Management ~3,500 LOC) numerically score higher on the raw formula because they are tiny, but were demoted on Strategic Value — Remittance Advices is a report with no upstream without Payments; Supplier Management is master-data CRUD that does not exercise the SAROC coexistence pattern.
Run 3 (Business-Value Emphasis) — Recommendation: Payment Processing (+2.85)
Risk 4.5 · Feasibility 7.0 · Strategic Value 8.5 · Weighted Total +2.85
Key points from Run 3:
- Payments are the single most-scrutinized financial workflow in any AP/AR shop — late checks and missed BACS runs are CFO-escalation events. A successful pilot here is the most credible proof that modernization works on financially-material code.
- Self-contained business boundary: supplier payment + customer receipt allocation clusters cleanly around
pl08x/pl09x/pl9xxandsl08x/sl09x/sl100with no GL-internals contamination. - Reuse multiplier: the CDC pattern proven against
pay.datdirectly applies to ~30 other ACAS data files; the pilot pays back across the rest of the modernization roadmap. - Risk-value sweet spot: lower risk than General Ledger or Tax Processing, higher business-value than Stock Control or Customer Management.
- The SAROC
watchedArtifactslist inreport-plan.json(pay.dat,openitm5.dat,purchled.dat) maps one-to-one onto Payment Processing's strong-file set — independent corroboration that the coexistence pattern is already designed against this boundary.
A.5 Three-Run Consensus Table
Side-by-side comparison of the three independent runs:
| Run | Emphasis | Recommendation | Risk | Feasibility | Strategic Value | Weighted Total |
|---|---|---|---|---|---|---|
| 1 | Standard weighting | Payment Processing | 5.5 | 7.5 | 8.5 | +2.60 |
| 2 | Technical feasibility | Payment Processing | 5.0 | 8.0 | 8.0 | +2.80 |
| 3 | Business value | Payment Processing | 4.5 | 7.0 | 8.5 | +2.85 |
All three runs converged on Payment Processing as the recommended first modernization candidate. The slight score variance across runs (+2.60 to +2.85) reflects the different emphasis lenses — not disagreement about the candidate — with Strategic Value scored consistently in the 8.0–8.5 range and Risk consistently in the 4.5–5.5 range.
A.6 Full Subsystem Shortlist (Composite View)
Below is a composite of the subsystems evaluated across the three runs. Score ranges show the spread across runs; the rightmost column gives a brief reason none of the alternatives won. All file counts and LOC come from Concho MCP find_files_for_business_function_subject on cycle 8 — not estimated.
| Subsystem | Strong Files | Approx LOC | Weighted Score Range | Why Not First |
|---|---|---|---|---|
| Payment Processing | ~30–31 | ~14,300–14,800 | +2.60 to +2.85 | Selected. |
| Supplier Management | ~7–8 | ~3,500–3,600 | +2.40 to +2.90 | Master-data CRUD; doesn't exercise the SAROC coexistence pattern or GL integration. Good fast-follow, not lead. |
| Purchase Invoicing | ~14–28 | ~12,940–15,000 | +1.60 to +2.10 | plinvoiceMT (3,225 LOC) plus the autogen recurring-invoice subsystem raise risk and shrink feasibility advantage. |
| Sales Invoicing | ~12–30 | ~12,800–22,977 | +1.40 to +2.10 | Heavyweight programs (slinvoiceMT 3,258 LOC; sl910 3,342 LOC) plus back-order coupling to Stock Control widens the blast radius. |
| Customer Management | ~14–15 | ~8,000–12,000 | +1.85 to +2.00 | Largely read and master-file maintenance; the operational money-movement value lives in Payments, not in the master file. |
| Sales Order Processing | ~32–33 | ~12,600–18,000 | +0.90 to +1.65 | Overlaps Sales Invoicing; couples to Stock Control and Back Order Management — three subsystems on day one. |
| Purchase Order Processing | ~37–38 | ~16,500–17,000 | +0.95 to +1.45 | Broad supplier-management touch plus autogen complexity. |
| Stock Control | ~38 | ~16,000–24,500 | +0.45 to +1.50 | Largest single program (st010 2,264 LOC; stockMT 3,369 LOC); tight to PO/SO; lower CFO visibility. |
| General Ledger (GL) | ~36–38 | ~14,300–17,000 | +0.45 to +1.35 | Highest strategic ceiling AND highest risk — chart of accounts is referenced by every other module. Correct choice for end-state, wrong choice for pilot. |
| Financial Reporting | ~9 | ~6,000–7,000 | +1.85 to +2.10 | Read-only consumer of GL/SL/PL data; high cross-cutting dependency surface for a first pilot. |
| Tax Processing (IRS) | ~50–60 | ~15,000–20,000 (code only) | −0.80 to +0.85 | US-specific regulatory exposure; cross-cuts GL, PL, SL. Last to modernize, not first. |
A.7 Consensus Analysis
Areas of Unanimous Agreement
- Recommended candidate. All three runs converged on Payment Processing as the recommended first modernization candidate.
- Risk profile. All three runs placed Risk in the 4.5–5.5 band — financially material but well-bounded by clear file handlers and copybook contracts.
- Strategic Value profile. All three runs placed Strategic Value in the 8.0–8.5 band — cash-out is CFO-visible and the pattern reuses across the rest of the roadmap.
- Disqualification of GL and Tax Processing as first pilots. Both subsystems carry the highest risk and the broadest integration surface; all three runs ranked them in the bottom third.
- Watched-artifact corroboration. The SAROC
watchedArtifactslist inreport-plan.json(pay.dat,openitm5.dat,purchled.dat) maps one-to-one onto Payment Processing's strong-file set — independent confirmation that the coexistence pattern is already designed against this boundary.
Divergence Analysis
There was no candidate-level divergence across the three runs — all three named Payment Processing. The minor numerical spread (Risk 4.5 vs 5.0 vs 5.5; weighted totals +2.60 vs +2.80 vs +2.85) reflects the different emphasis lenses applied to the same evidence, not disagreement about the candidate. Run 2 (feasibility) saw the cleanest file boundaries; Run 3 (business value) gave the most generous Risk score because the boundary is narrow and CFO-visible; Run 1 (standard) sat in between.
Two near-miss candidates surfaced as runner-up tension and were resolved by tiebreaker:
- Supplier Management outscores Payment Processing on the raw formula in Runs 1 and 2 (+2.90 and +2.65) purely because it is smaller. Tiebreaker: it is master-data CRUD that does not exercise the SAROC file-watcher CDC pattern, the dual-storage adapter pattern, or GL integration — the very patterns the pilot is meant to validate. Demoted to fast-follow.
- Remittance Advices (a single 302-LOC program,
pl960) scores +3.00 in Run 2 because it is vanishingly small. Tiebreaker: it is a report derived from Payment Processing data — migrating it without payments produces a renderer with no upstream. Folded back into the Payment Processing scope.
A.8 Final Recommendation
The multi-agent consensus methodology recommends Payment Processing as the first modernization candidate for ACAS. This recommendation matches the customer's pre-selection, with full 3-of-3 consensus across emphasis profiles.
Concho's analysis also surfaces a natural follow-on sequence based on the spread in scores and the dependency graph — offered here as roadmap input, not as a commitment for this engagement:
- Payment Processing — this engagement.
- Supplier Management — smallest scope and closest dependency neighbor; fast-follow that reuses the supplier-balance update pattern.
- Purchase Invoicing — upstream of payments in the procure-to-pay chain; same dual-storage adapter and DAL patterns.
- Sales Invoicing — mirror of Purchase Invoicing; second validation of the invoicing pattern.
- General Ledger — deferred to last because it is the reconciliation root of every other module; modernize once the surrounding modules can absorb the change.
A.9 Concho MCP Tool Usage
Across the three runs, the methodology relied on the following Concho MCP tools against the ACAS project (cycle 8):
| Concho MCP Tool | Purpose |
|---|---|
get_project_metadata |
Project-level totals: 1,358,687 LOC across 449 COBOL files, complexity 7/10, business breadth 9/10, modularity 7/10. |
get_project_business_function_subjects |
Master list of all 36 documented business functions — the population from which candidates were drawn. |
find_files_for_business_function_subject |
For each candidate subsystem: strong-association file list with line counts, descriptions, and alternate-business-function coupling. |
get_subject_profile |
Per-subsystem profile data: complexity, coupling, file counts, supporting evidence for the Feasibility and Risk scoring. |
Total MCP-tool calls across the three runs: approximately 38 calls (project-level queries plus one file-by-subject query per shortlisted subsystem in each run). Every quantitative claim in the scoring tables is traceable to a Concho MCP response — no LOC, file counts, or coupling data were estimated.
A.10 Manual-Baseline Comparison
For context on what this methodology buys over a manual analyst pass:
| Activity | Manual (no Concho) | With Concho MCP |
|---|---|---|
| Enumerate every business subsystem in ACAS | Grep + source-tree walking + manual classification of 281 programs — 2–3 days for an engineer new to the codebase | One get_project_business_function_subjects call returns all 36 documented subjects in seconds |
| File-by-file LOC and description per subsystem | wc -l plus reading each file and writing summaries — ~1 day per subsystem to do well |
One find_files_for_business_function_subject call per subsystem returns LOC and Concho-generated descriptions in seconds |
| Identify cross-subsystem coupling | Read every file, infer relationships from CALL and COPY statements — days, high error rate |
Returned as the Alternate Business Functions column on every file |
| Defensible scored comparison across ~10 candidates | 5–8 person-days, one analyst, one pass | ~30 minutes of agent time per run; three runs in parallel for consensus |
| Reproducibility | Subjective; re-running with a different analyst gives different results | File-list snapshot is identical across runs; any disagreement is framework-driven, not data-driven |
A.11 Benefits of the Multi-Agent Approach
- Reduces single-analyst framing bias. Three independent emphasis profiles run against the same evidence; convergence is signal, divergence is a flag for explicit tiebreaker discussion.
- Validates customer pre-selections. When the customer has already chosen a candidate (as here), the methodology either corroborates the choice with quantified scoring or surfaces a defensible case for reconsideration before significant modernization investment.
- Quantified scoring enables objective comparison. A weighted-score range of +2.60–+2.85 for Payment Processing versus +0.45–+1.50 for Stock Control is something stakeholders can audit and discuss.
- Surface hidden risks via structured tiebreaker. The Supplier Management and Remittance Advices runner-up cases would have looked attractive on raw scoring; tiebreaker analysis revealed they don't exercise the patterns the pilot is meant to validate.
- Reproducible methodology. The same evidence base, the same rubric, and the same emphasis profiles can be reapplied at every modernization wave — not just the first pilot — producing a consistent prioritization signal across the full roadmap.
- Traceable to source-of-truth evidence. Every quantitative claim in the scoring tables maps back to a Concho MCP response on ACAS cycle 8, so the methodology output is auditable, not opinion.
Appendix B: Target Service Architecture Analysis
This appendix documents the detailed service architecture analysis for the Payment Processing subsystem of the Applewood Computers Accounting System (ACAS), evaluating service-decomposition options from three independent perspectives — domain-driven design, technical architecture, and business capability — to determine the optimal service-count and service-boundary configuration for the modernized stack on AWS ECS Fargate.
Service-decomposition decisions carry long-term consequences for operational complexity, team autonomy, and system maintainability. Rather than relying on a single viewpoint, this analysis applies three independent evaluation lenses, each developed against the same evidence base (single bounded context PaymentProcessing at confidence 0.90, single aggregate root Payment at confidence 0.90, six entry points, six business rules, four integration boundaries, two patterns). Each perspective produced an independent proposal; the three proposals were then scored against a weighted rubric and reconciled into the final recommendation.
B.1 Domain-Driven Design Analysis
Bounded Context Identification
Domain-entity analysis of the Payment Processing subject identified one bounded context — PaymentProcessing (confidence 0.90, STRUCTURAL evidence). The convergence step explicitly merged what could have been a separate Remittance Advices context into PaymentProcessing on the basis of 80% ubiquitous-language overlap and 83% core-entity overlap, with the merge rationale reading: "Both contexts focus on payment processing with identical core concepts: Payment, PaymentAllocation, RemittanceAdvice, and PaymentBatch. The responsibilities are complementary aspects of the same payment processing subdomain ... Both would naturally belong to the same payment-processing-service microservice and serve the same business stakeholders in accounts payable."
This convergence finding is load-bearing for the DDD perspective: a service architecture that splits remittance from the rest of payment processing would re-introduce the boundary the convergence step deliberately removed.
Aggregate Root Inventory
| Aggregate Root | Confidence | Cardinality | Owns | Depends On / Provides Data To |
|---|---|---|---|---|
| Payment | 0.90 | 1 Payment : N PaymentLineItems (max 9), 1 Payment : N OpenItemRecords, 1 Payment : 1 PaymentBatch, 1 Payment : 1 RemittanceAdvice | PaymentLineItems, OpenItemPaymentRecords, PaymentBatchStructure, RemittanceAdvices |
Depends on GeneralLedgerPosting; provides data to PurchaseLedger and SalesLedger; integrates with IRSCompliance |
Context Map
graph TB
subgraph PaymentProcessing["PaymentProcessing Bounded Context (conf 0.90)"]
P["Payment
(Aggregate Root, 0.90)"]
PLI["PaymentLineItems
(max 9 per Payment)"]
OI["OpenItemPaymentRecords
(appropriation tracking)"]
PB["PaymentBatchStructure
(99-item ceiling)"]
RA["RemittanceAdvices
(derived output)"]
end
subgraph Peers["External Bounded Contexts (Integration Peers)"]
GL["GeneralLedger
(0.85)"]
PL["PurchaseLedger
(0.84)"]
AR["AccountsReceivable
(0.73)"]
DO["DocumentOutput
(0.81)"]
DAL["DataAccessLayer
(0.84) - acas032"]
FTP["FinancialTransactionProcessing
(0.84)"]
end
P --> PLI
P --> OI
P --> PB
P --> RA
P -.->|"Customer/Supplier — posts journal entries"| GL
P -.->|"Customer/Supplier — supplier balances"| PL
P -.->|"Customer/Supplier — customer balances"| AR
RA -.->|"Open Host Service — remittance documents"| DO
DAL -.->|"Anti-Corruption Layer (SAROC bridge)"| P
P -.->|"Conformist — shared batch lifecycle"| FTP
classDef agg fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px
classDef entity fill:#e3f2fd,stroke:#1565c0,stroke-width:1px
classDef peer fill:#fff3e0,stroke:#e65100,stroke-width:1px,stroke-dasharray:5
class P agg
class PLI,OI,PB,RA entity
class GL,PL,AR,DO,DAL,FTP peer
DDD Recommendation
From a strict domain-driven-design perspective, one bounded context that owns one aggregate root is the textbook signal for a single microservice — named payment-processing-service. Splitting the Payment aggregate is not viable because its cardinality is hard (1:N with max 9, 1:1 with batch, 1:1 with remittance), and any sibling service that wrote to PaymentAllocation or RemittanceAdvice would violate aggregate ownership. The convergence-merge decision (described above) is itself an authoritative DDD finding that explicitly removed the only candidate sub-context boundary.
DDD proposal: 1 service.
B.2 Technical Architecture Analysis
Code-Coupling Findings
Coupling analysis of the 30 strong-association files (~14,800 LOC) identifies five distinct coupling-shape findings that drive the technical decomposition.
-
Loose coupling at the
pl900menu hub.purchase/pl900.cblis a dynamic-dispatch dispatcher (CALL ws-calledatpl900.cbl:194/:198) that invokespl910–pl960via linkage parameters only. Every leaf is already an independently invokable unit; ALB path routing replaces it natively. -
Tight coupling at the
paymentsMT/acas032data-access bottleneck. Three artifacts (common/acas032.cblat 650 LOC,common/paymentsMT.cblat 2,021 LOC, and the load/restore/unload utilities at ~950 LOC combined) form a single data-access fan-in. TheDualStorageArchitecturalPatternconstraint (confidence 0.77, sourced frompl090.cbl:199,acas032.cbl:303,paymentsLD.cbl:9) confirms one logical data store. This rules out per-service databases and converges the decomposition on a shared PostgreSQL schema. -
Purchase / sales mirror coupling.
Purchase (
pl08x–pl10x) and sales (sl08x–sl10x) chains are structural mirrors with no calls between them; both route through the same DAL and share the samePaymentaggregate. They collapse into the same service at each workflow stage via apayment_methoddiscriminator. -
Copybook coupling (data-shape lock-in).
copybooks/fdpay.cob,wspay.cob,plwspay.cob, andselpay.coball define the same 237-byte payment record (fdpay.cob:16); thePaymentRecordStructuralIntegrityconstraint (confidence 0.80) enforces this byte-for-byte. The whole record moves as one unit, ruling out a per-field service split. -
GL posting is an outbound integration, not an internal sibling.
The
General Ledger Posting Integration(confidence 0.88) is invoked frompl100.cbl:329/:414; in the target it becomes inline writes togl_postingin the same transaction as appropriation plus an outbound domain event. It does not become a sibling service in this phase.
Load-Profile Clusters
| Cluster | Entry Points | Load Shape | Scaling Axis |
|---|---|---|---|
| Cluster A — Interactive | pl080 (0.75), pl085 (0.92), pl900 (0.92) |
HTTP-shaped, sub-second response budget, low CPU, low memory, small connection count | Request count |
| Cluster B — Scheduled batch | pl090 (0.92), pl100 (0.77), plus pl095/pl940/pl950 |
Bursty, CPU-heavy during sort/posting, I/O-heavy at EOD/month-end, tolerates seconds-to-minutes response | Queue depth |
| Cluster C — Document / report | pl960 (0.70), plus pl910, pl930 |
Read-heavy, formatting-heavy, on-demand; read replica natural endpoint; S3 outputs | Read-replica capacity / request count |
Change-Velocity Clusters
Grouping by likely git-commit cohort (programs that change together) produces the same three clusters as the load-profile cut, which is a strong signal that the cluster boundary is real.
- Entry / Amendment cluster:
pl080/pl085+ sales mirror. Shared copybook chain. Changes ripple together. - Proof / Posting cluster:
pl090/pl095/pl100+pl940/pl950+ sales mirror. SharedPaymentTransactionTypeFilter(conf 0.85) andPaymentRecordStructuralIntegrity(conf 0.80) constraints span this whole chain. - Reporting / Remittance cluster:
pl910/pl930/pl960. All use outbound output channels; cluster on "output formatting" change cadence.
Technical proposal: 3 services — payment-api, payment-batch, payment-reporting.
B.3 Business Capability Analysis
Value-Stream Decomposition
The end-to-end payment value stream (money leaves the bank, books stay balanced, supplier reconciles) decomposes into four sub-streams, each owned by a different organizational archetype with a different cadence.
| Sub-stream | Owner | Cadence | Anchor workflow / entry point |
|---|---|---|---|
| Cash-Out Decision | AP Clerk | Daily | PaymentProcessingAndAllocation (conf 0.82); pl080, pl085 |
| Disbursement Execution | Treasurer / Cash Management | Weekly / biweekly | PaymentProcessingWorkflow (conf 0.64) and PaymentDocumentGeneration (conf 0.50); pl940 |
| Reconcile + Post to GL | Controller / GL Accountant | EOD + month-end | PaymentProofProcessing (conf 0.77); pl090, pl100 |
| Supplier Notification | AP Correspondent | Monthly (supplier cycle) | RemittanceAdviceGeneration (conf 0.70); pl960 |
Conway's-Law Mapping
The business perspective draws four service boundaries to match the four organizational archetypes. The legacy COBOL system's program-number progression (08x = entry, 09x = proof/post, 9x4–9x5 = disbursement, 9x6 = remittance) is itself evidence that the original developers grouped programs by who runs them; the business proposal makes that informal grouping first-class.
Business proposal: 4 services — Payment Entry, Payment Disbursement, Cash Posting & GL, Supplier Remittance.
B.4 Architecture Options Evaluated
Option A: 1-Service DDD Monolith (payment-processing-service)
One Python/FastAPI service owns the entire Payment Processing bounded context — supplier and customer payment lifecycle from data entry through validation, appropriation, batch control, proof generation, cash posting, GL integration, remittance advice generation, and amendment/reversal. The single service writes to all five tables.
- Pros: Simplest deployment topology. Zero cross-service writes to the
Paymentaggregate. Inline GL posting trivially preserved. SAROC consumer integrates as one in-process worker. Lowest cognitive load. Matches the convergence-merge decision exactly. - Cons: Bundles three load profiles (sync API, batch posting, read-heavy reporting) into one process — one task-shape sizing decision must cover all three. Single deploy unit blocks parallel feature streams. "Modular monolith" perception risk.
Option B: 3-Service Workload Split (payment-api + payment-batch + payment-reporting)
Three Python/FastAPI services on AWS ECS Fargate against a shared PostgreSQL schema and a shared payment-models Pydantic package. The decomposition is driven by three orthogonal cuts (data-coupling, load-profile, change-velocity) that all converge on the same three clusters. The SAROC consumer is hosted by payment-batch because the same async-worker shape serves both legacy-event consumption and batch job execution.
- Pros: Three independent scaling axes (request count, queue depth, replica capacity). Three independent deploy cadences mapped to the legacy code's natural change clusters. Inline GL posting preserved (lives in
payment-batch). SAROC consumer slots in naturally. Matchesreport-plan.jsonarchitecture.serviceCount = 3and the target-architecture working hypothesis. Aggregate-root integrity preserved via shared schema and shared models. - Cons: Shared-schema discipline required (per-service repository methods, Alembic coordination across three services). Shared Pydantic model package is a coordination point for breaking changes. Two operational owners (interactive ops + batch ops) must coordinate on the proof-then-post handoff.
Option C: 4-Service Business-Capability Split (Entry + Disbursement + Cash Posting & GL + Remittance)
Four Python/FastAPI services aligned to the four organizational archetypes (AP clerk, treasurer, controller, AP correspondent). Each service writes to a disjoint column set on the shared payment table; GL posting is moved to an outbox-driven domain event to preserve cross-service eventual consistency.
- Pros: Best Conway's-law alignment. Best change-velocity story (four independent release cadences). Clearest SOX/audit boundary (concentrated in Cash Posting & GL). Each service has one product owner and one user community.
- Cons: Four services write to the same
Paymentaggregate row (mitigated only by column-scoped repository methods). Requires an outbox pattern for GL posting, which contradicts the inline-GL decision already recorded in Section 4 / target-architecture handoff. Five deployable units (four services + SAROC consumer). Schema migrations now span four services.
B.5 Option Comparison
| Criteria | Weight | Option A (1-svc) | Option B (3-svc) | Option C (4-svc) |
|---|---|---|---|---|
| Operational Complexity | 20% | 9 / 10 | 7 / 10 | 5 / 10 |
| • Deployment complexity | 10 (single pipeline) | 7 (three pipelines) | 5 (four pipelines + SAROC) | |
| • Monitoring & observability burden | 9 (one service) | 7 (three services, shared ADOT pattern) | 5 (four services + consumer, shared ADOT pattern) | |
| • Cognitive load | 8 (one codebase, three modules) | 7 (clear workload-shape boundary) | 5 (four boundaries + outbox semantics) | |
| Business Alignment | 30% | 6 / 10 | 7 / 10 | 10 / 10 |
| • User-workflow mapping | 6 (all roles bundled) | 7 (load shape correlates with role) | 10 (1:1 role mapping) | |
| • Organizational alignment | 6 (one team handles everything) | 7 (interactive vs batch vs reporting teams) | 10 (four-team org structure) | |
| • Product-boundary clarity | 6 (one product) | 7 (clear sync / batch / read surface) | 10 (four distinct products) | |
| Technical Soundness | 30% | 8 / 10 | 9 / 10 | 5 / 10 |
| • Coupling analysis | 9 (no inter-service coupling) | 9 (three orthogonal cuts converge) | 5 (four services write to same aggregate row) | |
| • Scalability characteristics | 5 (one task-shape compromise) | 9 (three independent scaling axes) | 9 (four independent scaling axes) | |
| • Maintainability & testability | 9 (single codebase) | 8 (clear ownership boundaries) | 3 (outbox-driven GL contradicts Section 4 inline-GL decision) | |
| Change Velocity | 20% | 6 / 10 | 8 / 10 | 9 / 10 |
| • Independent deployment | 5 (all changes redeploy everything) | 8 (three deploy cadences) | 9 (four deploy cadences) | |
| • Team parallelization | 6 (single codebase bottleneck) | 8 (two-to-three concurrent streams) | 9 (four concurrent streams) | |
| • Feature delivery speed | 7 (no coordination overhead) | 8 (minimal coordination) | 9 (no coordination across most features) | |
| Weighted Total | 7.2 | 7.8 | 7.3 | |
Scoring formula: Score = (Operational Complexity × 0.20) + (Business Alignment × 0.30) + (Technical Soundness × 0.30) + (Change Velocity × 0.20)
Option B (3-service) scores 7.8 / 10, exceeding the 7.0 quality-gate threshold. Option A (1-service) scores 7.2 (penalized by scalability and change-velocity limitations under a single task-shape sizing). Option C (4-service) scores 7.3 (penalized by maintainability because the outbox-driven GL contradicts the inline-GL decision already recorded in Section 4, and by operational complexity from four services + SAROC consumer = five deployable units). The Technical → Business margin is 0.5 points, exactly at the tiebreaker boundary; the tiebreaker analysis ultimately preferred Option B because Option C would force a downstream rewrite of Section 4's inline-GL decision and would exceed the plan's declared service count.
B.6 Selected Architecture: 3 Services
The 3-service architecture (Option B) is selected based on the strongest combined score across all four evaluation dimensions and on two additional considerations:
-
Architectural decision lock-in. Option C's outbox-driven GL pattern would contradict the inline-GL-posting decision already recorded in Section 4 ("GL posting: Inline (same DB transaction as appropriation) — Replaces legacy overnight batch posting in
pl100."). Reversing that decision at the service-architecture phase would force a Section 4 rewrite cascade. Option B preserves the inline-GL decision (GL writes live inpayment-batch, in the same DB transaction as appropriation). -
Plan alignment. The approved
report-plan.jsondeclaresarchitecture.serviceCount = 3. Option B matches the plan exactly; Option A undershoots by two services; Option C overshoots by one service.
The DDD concern about aggregate-root integrity is mitigated because all three services share the same PostgreSQL schema and the same payment-models Pydantic package. Write paths are gated by service-specific repository methods so the shared schema does not become a shared-database anti-pattern, and the single Payment aggregate root is preserved at the data layer. The business-capability concern about Conway's-law alignment is partially addressed by the load-shape decomposition (sync / batch / read) which correlates with operational roles even if it does not map 1:1 to organizational archetypes.
B.7 Target Service Architecture Diagram
graph TB
subgraph Client["Client Layer"]
SPA["Modern SPA
(Angular / React)"]
end
subgraph AWS["AWS Cloud"]
ALB["Application Load Balancer
(path-based routing)"]
subgraph ECS["ECS Fargate Cluster"]
API["payment-api
(FastAPI, sync)
2-6 tasks"]
BATCH["payment-batch
(FastAPI + aio-pika
+ SAROC consumer)
1-4 tasks"]
REPORT["payment-reporting
(FastAPI + PDF)
1-4 tasks"]
end
subgraph Data["Data Layer"]
RDS_W[("PostgreSQL RDS
WRITER
payment, open_item,
batch_control,
gl_posting, payment_audit")]
RDS_R[("PostgreSQL RDS
READ REPLICA
(reports)")]
end
subgraph Messaging["Messaging"]
MQ["Amazon MQ
RabbitMQ topic
(persistent, DLQ)"]
end
subgraph Storage["Object Storage"]
S3["S3
(proof / cheque /
register / remittance PDFs)"]
end
subgraph Ops["Observability & Config"]
OTEL["ADOT collector
(sidecar per task)"]
SM["Secrets Manager
+ SSM Parameter Store"]
CW["CloudWatch Logs
+ X-Ray + Metrics"]
end
end
subgraph Bridge["SAROC Coexistence Bridge"]
CDC["File-Watcher CDC
(pay.dat,
openitm5.dat,
purchled.dat)"]
TR["Event Translator
(sage-domain-event-v1)"]
end
subgraph Legacy["Legacy ACAS (unchanged)"]
COBOL["COBOL Payment Programs
(pl08x-pl9xx + sl08x-sl1xx
+ paymentsMT / acas032)"]
LegacyDB[("ISAM / MySQL
via acas032")]
end
SPA --> ALB
ALB -->|"/api/v1/payments
/api/v1/batches
/api/v1/health"| API
ALB -->|"/api/v1/batches/*/proof
/api/v1/batches/*/post
/api/v1/jobs"| BATCH
ALB -->|"/api/v1/reports
/api/v1/remittances"| REPORT
API --> RDS_W
BATCH --> RDS_W
REPORT --> RDS_R
REPORT -.->|"fallback for fresh data"| RDS_W
API -.->|"emit"| MQ
BATCH -.->|"emit / consume"| MQ
REPORT -.->|"consume cheque-batch.generated"| MQ
BATCH --> S3
REPORT --> S3
COBOL --> LegacyDB
LegacyDB -.->|"snapshot-diff
1000ms poll"| CDC
CDC --> TR
TR -.->|"JSON events"| MQ
MQ -.->|"persistent subscribe
natural-key idempotency"| BATCH
API -.-> OTEL
BATCH -.-> OTEL
REPORT -.-> OTEL
OTEL --> CW
API -.-> SM
BATCH -.-> SM
REPORT -.-> SM
classDef svc fill:#e3f2fd,stroke:#1565c0,stroke-width:2px
classDef data fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px
classDef legacy fill:#fff3e0,stroke:#e65100,stroke-width:1px,stroke-dasharray:5
class API,BATCH,REPORT svc
class RDS_W,RDS_R,S3 data
class COBOL,LegacyDB legacy
B.8 Service Definitions
Service 1: payment-api
| Attribute | Detail |
|---|---|
| Purpose | Interactive payment lifecycle — entry, validation, amendment, lookup. Sub-second HTTP surface that replaces pl080/sl080/pl085/sl085 terminal programs and the pl900 menu dispatcher. |
| Ownership archetype | Interactive operations team. |
| Runtime | Python 3.12 + FastAPI + uvicorn + SQLAlchemy 2.0 async + Pydantic v2 on AWS ECS Fargate. |
| Task sizing | 512 MB / 0.25 vCPU per task; min 2 / max 6; autoscale on ALBRequestCountPerTarget @ 50 req/s/task. |
| Data ownership | Write: payment (ENTERED → APPROPRIATED → REVERSED transitions only), open_item (appropriation links), batch_control (open + increment), payment_audit (append-only). Read: all five tables. Does not write to gl_posting. |
| Business rules enforced | PaymentBatchControlLimits (conf 0.75), PaymentAppropriationLogic (conf 0.70), PaymentReversalProcessing (conf 0.60), PaymentTransactionTypeFilter (conf 0.85). |
| Events emitted | acas.payment.created, acas.payment.appropriated, acas.payment.amended, acas.payment.reversed (envelope sage-domain-event-v1). |
API Surface
| Method | Path | Purpose |
|---|---|---|
| GET | /api/v1/health/{live,ready,startup} | ECS lifecycle probes |
| POST | /api/v1/payments | Create payment with line items; auto-appropriate; assign to batch |
| GET | /api/v1/payments/{id} | Retrieve payment with allocations |
| GET | /api/v1/payments?supplier_code=&batch_number=&payment_date_from=&payment_date_to= | List / filter payments (paginated) |
| PUT | /api/v1/payments/{id} | Amend payment (triggers reversal-then-redistribute) |
| POST | /api/v1/payments/{id}/reverse | Reverse posted payment with audit trail |
| GET | /api/v1/payments/{id}/audit-trail | JSONB amendment history |
| POST | /api/v1/batches | Open new payment batch |
| GET | /api/v1/batches/{batch_number} | Batch status + contents |
| GET | /api/v1/suppliers/{code}/open-items | Outstanding invoices for appropriation (purchase side) |
| GET | /api/v1/customers/{code}/open-items | Outstanding receipts (sales side) |
Service 2: payment-batch
| Attribute | Detail |
|---|---|
| Purpose | Scheduled and on-demand batch operations — proof sort (pl090), proof report (pl095), cash posting with inline GL writes (pl100), cheque batch (pl940), payment register (pl950). Also hosts the SAROC consumer. |
| Ownership archetype | Batch operations team (covers treasury and controller workloads). |
| Runtime | Python 3.12 + FastAPI (HTTP) + aio-pika (RabbitMQ consumer, background thread) + asyncio job runner on AWS ECS Fargate. |
| Task sizing | 2 GB / 0.5 vCPU per task (4 GB / 1 vCPU month-end variant); min 1 / max 4; autoscale on RabbitMQ queue depth + in-process job-queue depth. |
| Data ownership | Write: payment (PROOFED / POSTED transitions), open_item (final posting reference), batch_control (proof / posted / cheque / register timestamps), gl_posting (sole writer), payment_audit (batch-shaped operations). SAROC consumer writes to all five tables with natural-key idempotency. |
| Business rules enforced | PaymentPostingValidation (conf 0.65, P-Flag-I = 2 required), UnappliedBalanceAllocation (conf 0.60), PaymentTransactionTypeFilter (conf 0.85), MultiKeySortProcessing pattern (conf 0.70). |
| Events emitted | acas.payment.proofed, acas.payment.posted, acas.gl.journal-entry-created, acas.cheque-batch.generated, acas.payment-register.generated. |
| Events consumed | acas.legacy.payment.*, acas.legacy.open-item.*, acas.legacy.batch-control.* (from SAROC bridge). |
API Surface
| Method | Path | Purpose |
|---|---|---|
| GET | /api/v1/health/{live,ready,startup} | ECS lifecycle probes |
| POST | /api/v1/batches/{batch_number}/proof | Run proof sort + report (returns job ID) |
| POST | /api/v1/batches/{batch_number}/post | Cash posting with inline GL writes |
| POST | /api/v1/batches/{batch_number}/cheques | Generate cheque file (BACS / printed) |
| POST | /api/v1/batches/{batch_number}/register | Payment register report |
| POST | /api/v1/batches/{batch_number}/sign-off | Controller approval gate |
| GET | /api/v1/jobs/{job_id} | Job status |
| GET | /api/v1/jobs/{job_id}/output | Job artifact (S3 presigned URL) |
Service 3: payment-reporting
| Attribute | Detail |
|---|---|
| Purpose | Read-mostly reporting and document generation — aged-creditor / aged-debtor analysis (pl910/pl930) and remittance advice generation (pl960) with S3-backed PDF output. Replaces the legacy lpr print integration. |
| Ownership archetype | Reporting / supplier-facing team. |
| Runtime | Python 3.12 + FastAPI + uvicorn + WeasyPrint / ReportLab on AWS ECS Fargate. |
| Task sizing | 1 GB / 0.5 vCPU per task; min 1 / max 4; autoscale on ALBRequestCountPerTarget + RabbitMQ queue depth for remittance jobs. |
| Data ownership | Read-only on payment, open_item, batch_control, gl_posting, payment_audit (routed to PostgreSQL read replica). Write: remittance-tracking columns / sidecar table only. |
| Business rules enforced | RemittanceAdviceProcessingRules (conf 0.65) — non-zero invoice lines, cheque vs BACS formatting. |
| Events emitted | acas.remittance.generated (S3 presigned URL in payload), acas.remittance.delivered. |
| Events consumed | acas.cheque-batch.generated (triggers remittance generation). |
API Surface
| Method | Path | Purpose |
|---|---|---|
| GET | /api/v1/health/{live,ready,startup} | ECS lifecycle probes |
| GET | /api/v1/reports/payments-due?as_of=&age_bucket=&supplier_code= | Aged creditor report (paginated) |
| GET | /api/v1/reports/payments-due/proof | Proof-of-due view |
| GET | /api/v1/reports/receipts-due | Aged debtor (sales side) |
| GET | /api/v1/reports/payments-due.csv | CSV export |
| POST | /api/v1/remittances | Generate remittance(s) for a cheque/BACS batch |
| GET | /api/v1/remittances/{id} | Remittance metadata (S3 URL, status) |
| GET | /api/v1/remittances/{id}/document | Download remittance PDF |
| POST | /api/v1/remittances/{id}/deliver | Trigger delivery (email / portal / print) |
| GET | /api/v1/suppliers/{code}/remittance-history | Supplier-facing reconciliation aid |
B.9 Inter-Service Communication
Synchronous Pattern: Operator-Initiated Batch Job
When an operator initiates a batch operation (e.g., posting a proofed batch), the request is routed through the ALB to payment-batch, which accepts the request, enqueues a job, and returns a job ID. The operator polls for completion. The interactive operator service (payment-api) is not on the critical path of batch jobs.
sequenceDiagram
participant SPA as Modern SPA
participant ALB as Application Load Balancer
participant BATCH as payment-batch
participant DB as "PostgreSQL (writer)"
participant MQ as RabbitMQ
SPA->>ALB: POST /api/v1/batches/42/post
ALB->>BATCH: Route to payment-batch
BATCH-->>ALB: 202 Accepted {job_id}
ALB-->>SPA: 202 Accepted {job_id}
Note over BATCH,MQ: Job runs asynchronously
BATCH->>DB: BEGIN
BATCH->>DB: UPDATE payment SET status='POSTED' WHERE batch_number=42
BATCH->>DB: INSERT INTO gl_posting (...) (inline)
BATCH->>DB: COMMIT
BATCH->>MQ: publish acas.payment.posted
BATCH->>MQ: publish acas.gl.journal-entry-created
SPA->>ALB: GET /api/v1/jobs/{job_id}
ALB->>BATCH: Route
BATCH-->>SPA: response {status=SUCCEEDED, summary=...}
Asynchronous Pattern: SAROC Bridge Coexistence
During the strangler-fig modernization, the legacy COBOL system remains authoritative. A file-watcher CDC observes the three watched artifacts (pay.dat, openitm5.dat, purchled.dat), the translator emits sage-domain-event-v1 JSON events, and the payment-batch SAROC consumer applies them to the modernized schema with natural-key idempotency. Each modern service can then serve modernized reads without ever modifying the legacy system.
sequenceDiagram
participant COBOL as Legacy COBOL
participant ISAM as "ISAM / MySQL"
participant CDC as File-Watcher CDC
participant TR as Translator
participant MQ as RabbitMQ
participant BATCH as "payment-batch (SAROC consumer)"
participant DB as "PostgreSQL (writer)"
participant API as payment-api
participant REPORT as payment-reporting
COBOL->>ISAM: WRITE pay.dat / openitm5.dat
ISAM-->>CDC: file change detected (1000 ms poll)
CDC->>TR: snapshot diff
TR->>MQ: publish acas.legacy.payment.created (sage-domain-event-v1)
MQ-->>BATCH: deliver event (persistent, DLQ-enabled)
BATCH->>DB: INSERT INTO payment ... ON CONFLICT (legacy_natural_key) DO UPDATE
Note over API,REPORT: Modern services serve reads from the modernized schema
API->>DB: SELECT FROM payment / open_item
REPORT->>DB: SELECT FROM payment / open_item (via read replica)
Asynchronous Pattern: Cheque-Batch → Remittance Generation
When payment-batch generates a cheque batch, it publishes acas.cheque-batch.generated. payment-reporting subscribes to this event and generates remittance documents per supplier, writing PDFs to S3 and emitting acas.remittance.generated for downstream delivery services (email, supplier portal).
sequenceDiagram
participant BATCH as payment-batch
participant MQ as RabbitMQ
participant REPORT as payment-reporting
participant DB as "PostgreSQL (read replica)"
participant S3 as S3
participant DELIVERY as "Delivery Service
(email / portal)"
BATCH->>MQ: publish acas.cheque-batch.generated {cheque_batch_id}
MQ-->>REPORT: deliver event
REPORT->>DB: SELECT cheques + open_items + supplier
DB-->>REPORT: cheque + line-item rows
REPORT->>REPORT: render PDF (WeasyPrint)
REPORT->>S3: PUT remittance-{id}.pdf
S3-->>REPORT: object URL
REPORT->>MQ: publish acas.remittance.generated {s3_url}
MQ-->>DELIVERY: deliver event
DELIVERY->>S3: GET PDF, send to supplier
DELIVERY->>MQ: publish acas.remittance.delivered
Data Consistency Approach
The architecture uses strong consistency within a service via single-database transactions, and eventual consistency across services via the RabbitMQ topic exchange. The single Payment aggregate root remains the unit of consistency — no cross-service transaction is needed to enforce the six business rules because each rule's write path is confined to one service. The SAROC consumer's natural-key idempotency (INSERT ... ON CONFLICT (legacy_natural_key) DO UPDATE) ensures that duplicate legacy events are safely no-ops, which is the load-bearing primitive that makes strangler-fig coexistence safe.
B.10 Architecture Decision Rationale
The 3-service architecture was selected as the strongest combined fit across all four evaluation dimensions, with the technical-soundness dimension being decisive. The key insight is that the Payment Processing subsystem has three orthogonal seams (data-coupling clusters, load-profile clusters, change-velocity clusters) that all converge on the same three boundaries. This is unusual — in most subsystems, those three cuts produce three different decompositions and force a trade-off. When they converge, the boundary is real, and the operational-shape split lets each service right-size for its actual workload without violating the single-aggregate-root constraint.
The key trade-off accepted is the shared PostgreSQL schema. A strict microservices-purist position would require per-service databases, but the legacy paymentsMT / acas032 data-access bottleneck (Coupling Finding 2) and the byte-for-byte copybook contracts (Coupling Finding 4) mean that any per-service-database split would either duplicate the DAL or require synchronous query fan-out. Both are worse than the shared-schema discipline (per-service repository methods + coordinated Alembic migrations).
The DDD perspective was honored in three ways: the single bounded context is preserved at the data layer (shared schema, shared models); the single aggregate root is preserved as the write contract (write paths gated by repository methods); the convergence-merge decision (which removed Remittance Advices as a candidate sub-context) is honored by keeping remittance generation in the same shared-schema fleet rather than spawning it into a separate-database service.
The business perspective was partially honored: the load-shape boundary correlates with operational role (interactive ops uses payment-api; batch ops uses payment-batch; reporting and supplier-facing ops use payment-reporting), even though it does not map 1:1 to the four organizational archetypes. The future option to split payment-batch further (treasury / controller) is preserved by the internal module structure of that service — cheque-writer logic (pl940 port) lives in a separate module from cash-posting logic (pl100 port), so a clean extraction is possible later if organizational evolution justifies it.
B.11 Implementation Roadmap
Service-architecture-scoped implementation roadmap (full strangler-fig sequencing is owned by Section 11):
- Foundation (week 1–2). Stand up the shared
payment-modelsPydantic package andpayment-schema-migrationsAlembic project. Provision RDS PostgreSQL writer + read replica, Amazon MQ broker, ECR repositories, VPC, ALB, IAM roles. - Service skeletons (week 2–3). Generate all three service repositories with the standard FastAPI / uvicorn template. Wire the ADOT sidecar. Verify the three health-check probes (
/health/live,/health/ready,/health/startup) pass against the empty schema. Deploy hello-world to ECS Fargate behind the ALB. - payment-api (week 3–5). Implement the synchronous endpoints; enforce the four business rules that fire on entry/amendment (
PaymentBatchControlLimits,PaymentAppropriationLogic,PaymentReversalProcessing,PaymentTransactionTypeFilter); full pytest coverage with 80% target. - payment-batch (week 5–8). Implement proof/post/cheque/register endpoints; implement inline GL writes; implement the SAROC background consumer; wire RabbitMQ end-to-end (file-watcher → translator → broker → consumer → DB) with natural-key idempotency verified against a curated set of legacy events.
- payment-reporting (week 8–10). Implement aged-creditor reports against the read replica; implement remittance generation with S3 PDF output; subscribe to
acas.cheque-batch.generatedfor the triggered remittance flow. - Cross-service validation (week 10–12). Adversarial-validator pass on all three services. Integration-test harness running against the live legacy COBOL system in coexistence mode. Consensus check on behavioral equivalence (modern service output vs legacy COBOL output for the same input).
The sequence (sync first, then batch, then reports) matches the load-shape priority for the modernization narrative: the interactive surface is the user-perceived modernization, the batch service unlocks the SAROC coexistence pattern, and the reporting service completes the workload coverage. Throughout, the legacy COBOL system is unmodified and remains authoritative — the strangler-fig sequencing in Section 11 details how reads and writes shift from legacy to modern over the cutover window.
Appendix C. Deployment and Infrastructure
This appendix provides deployable infrastructure-as-code for the three Payment-Processing services (payment-api, payment-batch, payment-reporting) on both deployment targets called out in report-plan.json.project.targetPlatform — Docker Compose for local development and AWS ECS Fargate for production. Both targets share the same multi-stage Docker images and the same observability contract from the resolved targetPatterns (structlog JSON logs, OpenTelemetry OTLP, three health probes, env-vars-only configuration).
C.1 Local Development — Docker Compose
The Compose file below brings up the full stack with PostgreSQL 16, RabbitMQ 3.13, MinIO (S3-compatible), and a Jaeger all-in-one for OTLP traces. The three service containers run identical images to production; only their environment variables differ.
# compose.yml — local dev stack for ACAS Payment Processing (run-012)
# Usage: docker compose up --build
# Smoke test: bash setup-demo.sh
services:
postgres:
image: postgres:16
container_name: acas-postgres
environment:
POSTGRES_DB: acas
POSTGRES_USER: acas
POSTGRES_PASSWORD: acas_dev_password
ports: ["5432:5432"]
volumes:
- acas-pgdata:/var/lib/postgresql/data
- ./schema:/docker-entrypoint-initdb.d:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U acas -d acas"]
interval: 5s
timeout: 3s
retries: 10
rabbitmq:
image: rabbitmq:3.13-management
container_name: acas-rabbitmq
environment:
RABBITMQ_DEFAULT_USER: acas
RABBITMQ_DEFAULT_PASS: acas_dev_password
ports: ["5672:5672", "15672:15672"]
healthcheck:
test: ["CMD", "rabbitmq-diagnostics", "ping"]
interval: 10s
timeout: 5s
retries: 5
minio:
image: minio/minio:RELEASE.2026-04-15T00-00-00Z
container_name: acas-minio
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: acas
MINIO_ROOT_PASSWORD: acas_dev_password
ports: ["9000:9000", "9001:9001"]
volumes:
- acas-miniodata:/data
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
interval: 10s
timeout: 5s
retries: 5
jaeger:
image: jaegertracing/all-in-one:1.57
container_name: acas-jaeger
environment:
COLLECTOR_OTLP_ENABLED: "true"
ports:
- "16686:16686" # UI
- "4317:4317" # OTLP gRPC
- "4318:4318" # OTLP HTTP
schema-migrations:
build:
context: ./services/payment-schema-migrations
container_name: acas-schema-migrations
depends_on:
postgres:
condition: service_healthy
environment:
DATABASE_URL: postgresql+asyncpg://acas:acas_dev_password@postgres:5432/acas
restart: "no"
payment-api:
build:
context: ./services/payment-api
target: runtime
container_name: acas-payment-api
depends_on:
schema-migrations:
condition: service_completed_successfully
rabbitmq:
condition: service_healthy
jaeger:
condition: service_started
environment:
ENVIRONMENT: local
LOG_LEVEL: INFO
DATABASE_URL: postgresql+asyncpg://acas:acas_dev_password@postgres:5432/acas
RABBITMQ_URL: amqp://acas:acas_dev_password@rabbitmq:5672/
OTEL_SERVICE_NAME: payment-api
OTEL_EXPORTER_OTLP_ENDPOINT: http://jaeger:4317
OTEL_EXPORTER_OTLP_PROTOCOL: grpc
OTEL_RESOURCE_ATTRIBUTES: deployment.environment=local,service.version=0.1.0
ports: ["8081:8080"]
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/api/v1/health/live"]
interval: 10s
timeout: 5s
retries: 5
start_period: 20s
payment-batch:
build:
context: ./services/payment-batch
target: runtime
container_name: acas-payment-batch
depends_on:
schema-migrations:
condition: service_completed_successfully
rabbitmq:
condition: service_healthy
payment-api:
condition: service_started
environment:
ENVIRONMENT: local
LOG_LEVEL: INFO
DATABASE_URL: postgresql+asyncpg://acas:acas_dev_password@postgres:5432/acas
RABBITMQ_URL: amqp://acas:acas_dev_password@rabbitmq:5672/
S3_ENDPOINT_URL: http://minio:9000
S3_ACCESS_KEY: acas
S3_SECRET_KEY: acas_dev_password
S3_BUCKET: acas-artifacts
OTEL_SERVICE_NAME: payment-batch
OTEL_EXPORTER_OTLP_ENDPOINT: http://jaeger:4317
OTEL_EXPORTER_OTLP_PROTOCOL: grpc
OTEL_RESOURCE_ATTRIBUTES: deployment.environment=local,service.version=0.1.0
ports: ["8082:8080"]
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/api/v1/health/live"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
payment-reporting:
build:
context: ./services/payment-reporting
target: runtime
container_name: acas-payment-reporting
depends_on:
schema-migrations:
condition: service_completed_successfully
minio:
condition: service_healthy
rabbitmq:
condition: service_healthy
environment:
ENVIRONMENT: local
LOG_LEVEL: INFO
DATABASE_URL: postgresql+asyncpg://acas:acas_dev_password@postgres:5432/acas
RABBITMQ_URL: amqp://acas:acas_dev_password@rabbitmq:5672/
S3_ENDPOINT_URL: http://minio:9000
S3_ACCESS_KEY: acas
S3_SECRET_KEY: acas_dev_password
S3_BUCKET: acas-remittances
OTEL_SERVICE_NAME: payment-reporting
OTEL_EXPORTER_OTLP_ENDPOINT: http://jaeger:4317
OTEL_EXPORTER_OTLP_PROTOCOL: grpc
OTEL_RESOURCE_ATTRIBUTES: deployment.environment=local,service.version=0.1.0
ports: ["8083:8080"]
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/api/v1/health/live"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
volumes:
acas-pgdata:
acas-miniodata:
C.2 Service Dockerfile (multi-stage; identical image for local + cloud)
Each Python service ships the same Dockerfile pattern: a slim builder layer to compile wheels, then a slim runtime layer with only the wheels installed. Image size lands at ~190 MB per service.
# services/payment-api/Dockerfile
# Same shape for payment-batch and payment-reporting
FROM python:3.12-slim AS builder
WORKDIR /build
RUN apt-get update \
&& apt-get install -y --no-install-recommends gcc libpq-dev \
&& rm -rf /var/lib/apt/lists/*
COPY pyproject.toml uv.lock ./
RUN pip install --no-cache-dir uv \
&& uv export --no-dev --format=requirements.txt > requirements.txt \
&& pip wheel --no-cache-dir --wheel-dir=/wheels -r requirements.txt
FROM python:3.12-slim AS runtime
WORKDIR /app
RUN apt-get update \
&& apt-get install -y --no-install-recommends libpq5 curl \
&& rm -rf /var/lib/apt/lists/* \
&& useradd --uid 10001 --no-create-home --shell /usr/sbin/nologin app
COPY --from=builder /wheels /wheels
RUN pip install --no-cache-dir --no-index --find-links=/wheels /wheels/*.whl \
&& rm -rf /wheels
COPY ./src /app/src
USER app
# Three health probes per resolved targetPatterns.observability.healthChecks
HEALTHCHECK --interval=15s --timeout=5s --start-period=20s --retries=3 \
CMD curl --fail http://localhost:8080/api/v1/health/live || exit 1
EXPOSE 8080
ENV PYTHONPATH=/app/src \
UVICORN_HOST=0.0.0.0 \
UVICORN_PORT=8080
CMD ["uvicorn", "payment_api.main:app", "--host", "0.0.0.0", "--port", "8080", "--workers", "1"]
C.3 Service requirements.txt (target-patterns compliant)
Pinned package set matching the resolved targetPatterns: FastAPI, Pydantic v2, SQLAlchemy 2.0 async, structlog, OpenTelemetry with OTLP/HTTP exporters, aio-pika, alembic.
# services/payment-api/requirements.txt fastapi==0.115.5 uvicorn[standard]==0.32.1 pydantic==2.10.3 pydantic-settings==2.7.0 # Database (async) sqlalchemy==2.0.36 asyncpg==0.30.0 alembic==1.14.0 # Messaging (SAROC consumer) aio-pika==9.5.4 # Object store (remittance PDFs / batch artifacts) boto3==1.35.84 # Observability structlog==24.4.0 opentelemetry-api==1.29.0 opentelemetry-sdk==1.29.0 opentelemetry-exporter-otlp-proto-grpc==1.29.0 opentelemetry-instrumentation-fastapi==0.50b0 opentelemetry-instrumentation-sqlalchemy==0.50b0 opentelemetry-instrumentation-aio-pika==0.50b0 opentelemetry-instrumentation-botocore==0.50b0 # Resilience tenacity==9.0.0 # RFC 7807 helpers problemdetails==0.4.0
C.4 PostgreSQL Schema Bootstrap
The schema-migrations one-shot container runs the Alembic head migration before the application containers start. The Compose depends_on with service_completed_successfully condition guarantees the order.
# services/payment-schema-migrations/Dockerfile FROM python:3.12-slim WORKDIR /app RUN apt-get update \ && apt-get install -y --no-install-recommends libpq5 \ && rm -rf /var/lib/apt/lists/* COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY alembic /app/alembic COPY alembic.ini /app/ CMD ["alembic", "upgrade", "head"]
C.5 AWS ECS Fargate — Service Task Definition (excerpt)
Same image, different environment surface. The ECS task definition below shows the payment-api service with the ADOT collector sidecar, AWS Secrets Manager secrets injection, and SSM Parameter Store config, all per the resolved targetPatterns. Health-check command pings /api/v1/health/live per the template's healthChecks.commandPath.
{
"family": "acas-payment-api",
"networkMode": "awsvpc",
"requiresCompatibilities": ["FARGATE"],
"cpu": "256",
"memory": "512",
"executionRoleArn": "arn:aws:iam::123456789012:role/acas-ecs-execution",
"taskRoleArn": "arn:aws:iam::123456789012:role/acas-payment-api-task",
"containerDefinitions": [
{
"name": "payment-api",
"image": "123456789012.dkr.ecr.us-east-1.amazonaws.com/acas/payment-api:0.1.0",
"essential": true,
"portMappings": [{"containerPort": 8080, "protocol": "tcp"}],
"environment": [
{"name": "ENVIRONMENT", "value": "prod"},
{"name": "LOG_LEVEL", "value": "INFO"},
{"name": "OTEL_SERVICE_NAME", "value": "payment-api"},
{"name": "OTEL_EXPORTER_OTLP_ENDPOINT",
"value": "http://localhost:4317"},
{"name": "OTEL_EXPORTER_OTLP_PROTOCOL", "value": "grpc"},
{"name": "OTEL_RESOURCE_ATTRIBUTES",
"value": "deployment.environment=prod,service.version=0.1.0"}
],
"secrets": [
{"name": "DATABASE_URL",
"valueFrom": "arn:aws:secretsmanager:us-east-1:123456789012:secret:acas/payment-api/db-url"},
{"name": "RABBITMQ_URL",
"valueFrom": "arn:aws:secretsmanager:us-east-1:123456789012:secret:acas/payment-api/rabbitmq-url"}
],
"healthCheck": {
"command": ["CMD-SHELL",
"curl -f http://localhost:8080/api/v1/health/live || exit 1"],
"interval": 15,
"timeout": 5,
"retries": 3,
"startPeriod": 30
},
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/aws/ecs/acas/payment-api",
"awslogs-region": "us-east-1",
"awslogs-stream-prefix": "payment-api",
"awslogs-create-group": "true"
}
},
"dependsOn": [
{"containerName": "adot-collector", "condition": "START"}
]
},
{
"name": "adot-collector",
"image": "public.ecr.aws/aws-observability/aws-otel-collector:v0.40.2",
"essential": true,
"command": ["--config=/etc/ecs/ecs-default-config.yaml"],
"environment": [
{"name": "AWS_REGION", "value": "us-east-1"}
],
"portMappings": [
{"containerPort": 4317, "protocol": "tcp"},
{"containerPort": 4318, "protocol": "tcp"}
],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/aws/ecs/acas/adot-collector",
"awslogs-region": "us-east-1",
"awslogs-stream-prefix": "adot",
"awslogs-create-group": "true"
}
}
}
]
}
The ADOT collector receives OTLP from the application container on localhost:4317 (sidecars share a network namespace in awsvpc mode), then exports traces to AWS X-Ray and metrics to CloudWatch Metrics. CloudWatch Logs receives structured JSON logs from the awslogs driver.
C.6 ALB + ECS Service Wiring (CDK Python excerpt)
AWS CDK in Python provisions the ALB, target groups, listener rules, ECS cluster, and the three Fargate services. The excerpt below shows the payment-api service; payment-batch and payment-reporting follow the same shape.
from aws_cdk import (
Stack, Duration,
aws_ec2 as ec2,
aws_ecs as ecs,
aws_ecs_patterns as ecs_patterns,
aws_elasticloadbalancingv2 as elbv2,
aws_rds as rds,
aws_amazonmq as amazon_mq,
aws_s3 as s3,
aws_secretsmanager as sm,
aws_ssm as ssm,
aws_logs as logs,
aws_iam as iam,
)
from constructs import Construct
class AcasPaymentProcessingStack(Stack):
def __init__(self, scope: Construct, id: str, **kwargs):
super().__init__(scope, id, **kwargs)
vpc = ec2.Vpc(self, "AcasVpc",
max_azs=2,
nat_gateways=1)
# RDS PostgreSQL 16 (shared 5-table schema)
db = rds.DatabaseInstance(self, "AcasDb",
engine=rds.DatabaseInstanceEngine.postgres(
version=rds.PostgresEngineVersion.VER_16_3),
instance_type=ec2.InstanceType.of(
ec2.InstanceClass.BURSTABLE3,
ec2.InstanceSize.SMALL),
vpc=vpc,
credentials=rds.Credentials.from_generated_secret("acas_admin"),
database_name="acas",
multi_az=True,
deletion_protection=True,
backup_retention=Duration.days(14),
)
# Amazon MQ for RabbitMQ (SAROC topic exchange)
broker = amazon_mq.CfnBroker(self, "AcasMq",
broker_name="acas-broker",
engine_type="RABBITMQ",
engine_version="3.13",
host_instance_type="mq.t3.micro",
deployment_mode="SINGLE_INSTANCE",
publicly_accessible=False,
users=[amazon_mq.CfnBroker.UserProperty(
username="acas_admin",
password=sm.Secret.from_secret_name_v2(
self, "MqPassword",
"acas/mq/admin-password"
).secret_value.unsafe_unwrap(),
)],
)
# S3 buckets
artifacts_bucket = s3.Bucket(self, "AcasArtifacts",
bucket_name="acas-payment-artifacts",
block_public_access=s3.BlockPublicAccess.BLOCK_ALL,
encryption=s3.BucketEncryption.S3_MANAGED,
versioned=True,
)
remittances_bucket = s3.Bucket(self, "AcasRemittances",
bucket_name="acas-remittances",
block_public_access=s3.BlockPublicAccess.BLOCK_ALL,
encryption=s3.BucketEncryption.S3_MANAGED,
versioned=True,
)
# ECS cluster
cluster = ecs.Cluster(self, "AcasCluster", vpc=vpc)
# payment-api service via the ApplicationLoadBalancedFargateService pattern
task_def = ecs.FargateTaskDefinition(self, "PaymentApiTaskDef",
cpu=256,
memory_limit_mib=512,
)
container = task_def.add_container("payment-api",
image=ecs.ContainerImage.from_registry(
"123456789012.dkr.ecr.us-east-1.amazonaws.com/acas/payment-api:0.1.0"),
essential=True,
port_mappings=[ecs.PortMapping(container_port=8080)],
environment={
"ENVIRONMENT": "prod",
"LOG_LEVEL": "INFO",
"OTEL_SERVICE_NAME": "payment-api",
"OTEL_EXPORTER_OTLP_ENDPOINT": "http://localhost:4317",
"OTEL_EXPORTER_OTLP_PROTOCOL": "grpc",
"OTEL_RESOURCE_ATTRIBUTES":
"deployment.environment=prod,service.version=0.1.0",
},
secrets={
"DATABASE_URL": ecs.Secret.from_secrets_manager(
sm.Secret.from_secret_name_v2(
self, "PaApiDbUrl",
"acas/payment-api/db-url"
)),
"RABBITMQ_URL": ecs.Secret.from_secrets_manager(
sm.Secret.from_secret_name_v2(
self, "PaApiMqUrl",
"acas/payment-api/rabbitmq-url"
)),
},
health_check=ecs.HealthCheck(
command=["CMD-SHELL",
"curl -f http://localhost:8080/api/v1/health/live || exit 1"],
interval=Duration.seconds(15),
timeout=Duration.seconds(5),
retries=3,
start_period=Duration.seconds(30),
),
logging=ecs.LogDriver.aws_logs(
stream_prefix="payment-api",
log_retention=logs.RetentionDays.ONE_MONTH,
),
)
# ADOT collector sidecar (OTLP -> X-Ray + CloudWatch)
task_def.add_container("adot-collector",
image=ecs.ContainerImage.from_registry(
"public.ecr.aws/aws-observability/aws-otel-collector:v0.40.2"),
essential=True,
command=["--config=/etc/ecs/ecs-default-config.yaml"],
environment={"AWS_REGION": "us-east-1"},
logging=ecs.LogDriver.aws_logs(
stream_prefix="adot",
log_retention=logs.RetentionDays.ONE_MONTH,
),
)
api_service = ecs_patterns.ApplicationLoadBalancedFargateService(
self, "PaymentApiService",
cluster=cluster,
task_definition=task_def,
desired_count=2,
public_load_balancer=False, # internal ALB
health_check_grace_period=Duration.seconds(60),
)
# ECS health-check path -> /api/v1/health/ready (NOT /live)
api_service.target_group.configure_health_check(
path="/api/v1/health/ready",
healthy_http_codes="200",
interval=Duration.seconds(15),
timeout=Duration.seconds(5),
healthy_threshold_count=2,
unhealthy_threshold_count=3,
)
# IAM permissions for OTLP, Secrets Manager, S3
task_def.task_role.add_to_policy(iam.PolicyStatement(
actions=["xray:PutTraceSegments",
"xray:PutTelemetryRecords",
"cloudwatch:PutMetricData"],
resources=["*"],
))
artifacts_bucket.grant_read_write(task_def.task_role)
# Autoscaling: 2-6 tasks on ALB request count per target
scaling = api_service.service.auto_scale_task_count(
min_capacity=2, max_capacity=6)
scaling.scale_on_request_count("AlbScaling",
requests_per_target=50,
target_group=api_service.target_group,
)
# Allow API tasks to talk to RDS + Amazon MQ
db.connections.allow_default_port_from(api_service.service)
# Amazon MQ security group allow rule omitted for brevity
C.7 Health Probes — Three Endpoints, Each with a Distinct Job
Per the resolved targetPatterns.observability.healthChecks (enabled = true, three endpoints), every Python service ships the same probe surface. The endpoints are wired into both Compose (healthcheck) and ECS (container healthCheck + ALB target group health check).
| Endpoint | Purpose | Returns 200 when | Used by |
|---|---|---|---|
/api/v1/health/live | Liveness — "the process is responsive" | The FastAPI event loop is running | Container runtime (Docker / ECS) liveness check |
/api/v1/health/ready | Readiness — "I can serve requests" | DB connection pool reaches the DB and RabbitMQ broker is reachable | ALB target-group health check (only "ready" tasks receive traffic) |
/api/v1/health/startup | Startup — "I have finished initialising" | Alembic schema version matches the binary's expected version | ECS task start-period grace window; protects readiness during cold-start |
The implementation lives in payment_api/api/health.py and follows the same shape across all three services:
from fastapi import APIRouter, Depends, status, Response
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import text
import aio_pika
import structlog
from .deps import get_session, get_rabbit_channel
from .config import settings
router = APIRouter(prefix="/api/v1/health", tags=["health"])
logger = structlog.get_logger()
@router.get("/live")
async def live() -> dict:
return {"status": "live"}
@router.get("/ready")
async def ready(
session: AsyncSession = Depends(get_session),
channel: aio_pika.abc.AbstractChannel = Depends(get_rabbit_channel),
response: Response = None,
) -> dict:
checks = {}
try:
await session.execute(text("SELECT 1"))
checks["database"] = "ok"
except Exception as e:
checks["database"] = f"failing: {type(e).__name__}"
try:
if channel.is_closed:
checks["rabbitmq"] = "closed"
else:
checks["rabbitmq"] = "ok"
except Exception as e:
checks["rabbitmq"] = f"failing: {type(e).__name__}"
overall = "ready" if all(v == "ok" for v in checks.values()) else "not-ready"
if overall != "ready":
response.status_code = status.HTTP_503_SERVICE_UNAVAILABLE
return {"status": overall, "checks": checks}
@router.get("/startup")
async def startup(session: AsyncSession = Depends(get_session)) -> dict:
row = (await session.execute(
text("SELECT version_num FROM alembic_version"))).scalar_one()
expected = settings.expected_schema_version
return {
"status": "started" if row == expected else "schema-mismatch",
"current_schema": row,
"expected_schema": expected,
}
C.8 Smoke Test — End-to-End Payment Cycle
The smoke test below proves the deployment by running the Use Case 1 flow end-to-end: create a payment, run the proof, post the batch, verify the inline GL writes, verify the remittance PDF lands in S3. Both Compose and ECS can run this script (different BASE_URL).
#!/usr/bin/env bash
# scripts/smoke-test.sh — end-to-end Use Case 1 verification
set -euo pipefail
BASE_API="${BASE_API:-http://localhost:8081}"
BASE_BATCH="${BASE_BATCH:-http://localhost:8082}"
BASE_REPORT="${BASE_REPORT:-http://localhost:8083}"
echo "==> Probing health endpoints..."
curl -fsS "${BASE_API}/api/v1/health/live" >/dev/null
curl -fsS "${BASE_API}/api/v1/health/ready" >/dev/null
curl -fsS "${BASE_BATCH}/api/v1/health/ready" >/dev/null
curl -fsS "${BASE_REPORT}/api/v1/health/ready" >/dev/null
echo " all three services are ready."
echo "==> Creating a payment via payment-api..."
PAYMENT_JSON=$(curl -fsS -X POST "${BASE_API}/api/v1/payments" \
-H "Content-Type: application/json" \
--data '{
"payment_method": "BACS",
"ledger": "PURCHASE",
"counterparty_code": "SUP0001",
"payment_date": "2026-05-21",
"amount_total": 1250.00,
"currency": "USD",
"batch_number": 42,
"legacy_natural_key": "PL|42|2026-05-21|SUP0001|1250.00"
}')
PAYMENT_ID=$(echo "$PAYMENT_JSON" | jq -r '.id')
echo " payment id: ${PAYMENT_ID}"
echo "==> Triggering batch proof + post..."
curl -fsS -X POST "${BASE_BATCH}/api/v1/batches/42/proof" \
-H "Content-Type: application/json" \
--data '{}' >/dev/null
curl -fsS -X POST "${BASE_BATCH}/api/v1/batches/42/sign-off" \
-H "Content-Type: application/json" \
--data '{"controller": "smoke-test"}' >/dev/null
POST_RESULT=$(curl -fsS -X POST "${BASE_BATCH}/api/v1/batches/42/post" \
-H "Content-Type: application/json" \
--data '{"posted_by": "smoke-test", "posting_date": "2026-05-21"}')
echo " batch posted: $(echo "$POST_RESULT" | jq -c '.summary')"
echo "==> Generating remittance..."
REMIT_JSON=$(curl -fsS -X POST "${BASE_REPORT}/api/v1/remittances" \
-H "Content-Type: application/json" \
--data '{"cheque_batch_id": "42", "delivery_modes": ["portal"]}')
JOB_ID=$(echo "$REMIT_JSON" | jq -r '.job_id')
echo " remittance job: ${JOB_ID}"
echo "==> Polling for remittance completion..."
for i in $(seq 1 20); do
STATUS=$(curl -fsS "${BASE_REPORT}/api/v1/jobs/${JOB_ID}" | jq -r '.status')
if [ "$STATUS" = "SUCCEEDED" ]; then
echo " remittance generation SUCCEEDED."
break
fi
sleep 1
done
[ "$STATUS" = "SUCCEEDED" ] || { echo "FAIL: remittance never completed"; exit 1; }
echo "==> All checks passed."
C.9 Operational Notes
- Image builds: CI builds one multi-arch image per service per commit on
main; ECR stores immutable tags;:latestis never used in task definitions. - Secrets rotation: AWS Secrets Manager rotates RDS credentials every 30 days; ECS task definitions resolve secrets at task-start, so a rotation requires a service redeploy (handled by EventBridge → CodeBuild trigger).
- Backups: RDS 14-day point-in-time recovery; S3 buckets versioned; RabbitMQ persistent queues + DLQ enabled per
coexistencePattern.messaging. - Cost envelope: Cost estimation is out of scope for this stakeholder review; see the future operations runbook for sizing guidance. Burstable instance classes are nevertheless an intentional choice for the steady-state fleet because the workload is sub-second-p95 with batch-job spikes.
- Local-to-cloud parity: the only Compose-to-ECS deltas are (a) secrets injection (Compose env vars vs ECS secrets), (b) OTLP destination (Jaeger vs ADOT → X-Ray), (c) S3 endpoint (MinIO vs AWS), and (d) image registry (local build vs ECR). The application code and Dockerfiles are identical.