Skip to content
All work

Government|2024 – Present

Catalis Tax

One tax platform, fifty states. I build the API and the screen that calls it.

Catalis Tax parcel record: account header with bankruptcy statuses, current balance, an expanded solid-waste invoice, the summary and payment history
The parcel record, redrawn from the live screen.
Client
Catalis
Through
Programming.com
Role
Full-stack developer

Overview

A multi-tenant SaaS for county tax collectors: parcels, bills, cash register, solid-waste billing, work orders and reporting. A .NET 10 API on PostgreSQL behind a React 19 admin portal, shipped to AWS ECS through four environments.

239
commits by me
135
pull requests merged
74
Jira tickets delivered
22
months on the platform

Catalis Tax is the second-generation, multi-state version of a county tax system that previously shipped one codebase per state (the "Multi-County Tax Solution v1.0", known internally as Refactored LandNav / PrimeTSR). Version 2.0 rebuilt it as one cloud tenant per customer on a shared .NET and React codebase, with state-specific behaviour isolated behind configuration and per-state modules.

Users are tax collectors and their staff. They look up a parcel, see every bill and payment against it, take money at a cash register, print receipts, run year-end rollovers, import appraiser data and run statutory reports. Mistakes are measured in dollars and audit findings, so correctness is the product.

My role

I joined the team in December 2024 as a full-stack developer working through the outsourced engineering partner, and have shipped across both repositories every month since: the ASP.NET Core API and the React admin portal.

  • Full-stack developer in both repositories, December 2024 to present.
  • Own tickets end to end: database function, API, UI, pull request, QA follow-up.
  • Customers shipped for: Palm Beach County Solid Waste Authority (FL) and Jackson County (MS).
  • Team of about fourteen: product owner, BA and SME, lead dev manager, three to five developers, QA, PMO.

The platform in numbers

649
HTTP endpoints
in 95 controllers
450
EF Core entities
168 schema scripts
127
routes in the portal
70 feature folders
59
Combit report templates
51 Florida, 13 Mississippi folders
44
Quartz jobs per tenant
517
xUnit tests
145 files, fully offline
8,172
commits across both repos
since October 2022
4
environments, dev to prod

Impact

Six changes county staff and taxpayers feel every day. Each row shows who it helped, what the screen did before and after, the figure that proves it, and which layers of the stack the work went through.

  1. 4

    critical defects closed

    Receipts that never change after printing

    Jackson County, MS · cashiers and taxpayers

    Before
    A receipt was re-derived from the live account, so a later payment silently changed one the taxpayer already held.
    After
    Each payment stores its invoice amount and the lines it paid. The receipt renders from that snapshot, the same way forever.
    Receipts you can trustTCCT-3645, 3773, 3839, 3315, 3507, 3885
    • Database (yes)
    • API (yes)
    • Portal (yes)
  2. 12

    tickets, data model to grid

    Inspections recorded inside the tax system

    Palm Beach County SWA · field inspectors and billing

    Before
    Inspector visits to verify units, building use and service area lived outside the tax system.
    After
    Tasks, notes, status history, attachments and per-building findings sit on the parcel, and corrections reach billing with an audit trail.
    Work Order moduleTCCT-1239, 1253, 1254, 1638, 3097, 3100
    • Database (yes)
    • API (yes)
    • Portal (yes)
  3. 2

    reports off spreadsheets

    Statutory reports in one click

    Palm Beach County SWA · finance

    Before
    The account balance summary and the property address listing were built by hand in spreadsheets.
    After
    Both run as PostgreSQL functions behind the Combit report server, and the understated interest column was fixed before year-end close.
    Reports and importsTCCT-3281, 3411, 3481, 3295
    • Database (yes)
    • API (yes)
    • Portal (no)
  4. 6

    validators on every row

    Failed imports became a worked queue

    County staff loading appraiser, hauler and CO files

    Before
    One bad row in a fixed-width file stopped the whole import.
    After
    Failing rows are tagged with a reason, and staff correct, reject or reprocess them on screen while the rest of the file completes.
    Import Exception screenTCCT-1550, 1478, 1683, 1802
    • Database (no)
    • API (yes)
    • Portal (yes)
  5. 13

    WCAG tickets closed

    Accessible to every clerk and taxpayer

    Both tenants · audited against WCAG 2.1 AA

    Before
    WAVE reported errors on the Home page and menus, the Cart, the Parcel Record and its drawers, and the user screens.
    After
    Every WAVE finding raised against those screens is closed for both tenants, with labels, names, landmarks and contrast fixed.
    AccessibilityTCCT-2515, 2637, 2859, 2865, 2992, 2592, 2595
    • Database (no)
    • API (no)
    • Portal (yes)
  6. $100

    minimum-payment threshold

    Money rules the cart and the API agree on

    Jackson County, MS · cash register

    Before
    Under $100 the minimum could exceed the balance, and the cart flagged a legitimate tender as an overpayment.
    After
    The API is the source of truth for the minimum, per item subtype, and the cart mirrors it.
    Money rulesTCCT-2718, 2793, 2871, 3553
    • Database (no)
    • API (yes)
    • Portal (yes)

Five of the six changes went through at least two layers of the stack, and two went through all three: the SQL, the service and controller over it, and the screen that calls them, each ticket owned end to end.

The challenge

Property tax in the United States is administered by counties, under state law. Florida bills solid-waste assessments through the tax roll and sells tax certificates. Mississippi levies privilege licences by month. Virginia taxes vehicles. South Carolina runs tax sales. Each state has its own penalty and interest schedule, discount periods, installment plans, exemptions and statutory report formats.

The previous product line solved this by forking: one deployment per customer, drifting apart over years. Catalis Tax 2.0 set out to replace that with a single multi-tenant SaaS where a county is a tenant with its own PostgreSQL database and configuration, and the application code is shared. That shape creates the engineering problems this case study is about.

Correctness under variation
A receipt must show the right invoice amount whether the bill was paid in full, in installments, partially, reversed or refunded, and the rules differ by item type and by state.
Reference data at scale
Dozens of lookup tables (DOR codes, hauler codes, boundary tables, rates, rules) that staff maintain themselves and that roll over every tax year.
Files in and out
Appraiser rolls, hauler and CO files, bill-print exports: fixed-width formats from legacy mainframe systems, with an exception workflow when rows fail validation.
Public-sector obligations
ADA and WCAG accessibility, auditability, role and permission control per tenant.

Customers I shipped for

Palm Beach County SWA, FL
Solid Waste Authority billing through the tax roll: boundary tables, hauler codes, installments, work orders and inspections, CO and NAV file imports. Three-year agreement, kickoff August 2024.
Jackson County, MS
Full tax collection: cash register, receipts, privilege licences, bill-print export, supplemental bills, multi-owner parcels.

Vocabulary used on this page

Parcel / PCN
A taxable property and its control number.
Roll
The year's list of parcels, owners and values from the appraiser.
Bill, invoice, installment
A bill becomes an invoice with line values; it may be split into installments.
Millage
Tax rate per thousand dollars of value, per taxing district.
SWA
Solid Waste Authority: collection, disposal and recycling units billed on the roll.
Bump file
The SWA's nightly change file that updates billable items.
Unallocated cash
Money received but not yet applied to a bill, including overpayments.
TRIM
Florida's Truth in Millage notice; the platform exports its data.

System architecture

A browser SPA, a stateless API, a database per tenant, and workers around the edge. The accent line is the path a request takes from a click in the portal to a row in PostgreSQL.

  1. Client

    • React 19 admin portal

      Vite, MUI v5, Zustand, router v7

      oidc-client-ts: code flow, silent renew

      127 routes, 70 feature folders

    • Public pages

      Rates and rules tables

      public cart and search, no sign-in

    • Identity server (OIDC)

      Code flow, JWT bearer tokens

      refresh grant at /connect/token

      roles per tenant, linked tenants

  2. Edge and runtime, AWS us-east-1

    • nginx container, ECS

      Static build, SPA fallback, HSTS

      envsubst injects config at start

      /healthr for the target group

    • ASP.NET Core 10 API, ECS

      95 controllers, 649 endpoints

      Controller, Service, UoW, Repo

      FluentValidation, AutoMapper, NLog

    • Background jobs, EC2

      Quartz.NET, 44 jobs per tenant

      imports, exports, bills, reports

      SFTP via SSH.NET

  3. Data and integrations

    • PostgreSQL, one DB per tenant

      EF Core 10 and Npgsql, 450 entities

      row-level security via SET LOCAL

      fxcat_report_* SQL functions

    • AWS S3

      Documents and attachments

      import and report files

    • Combit report server

      59 templates (FL, MS)

      backed by SQL functions, to S3

Stateless API
Every request carries a JWT. The tenant is resolved from the token and the request is bound to that tenant's connection string, so the same container image serves every county.
Envelope contract
All endpoints return { Succeeded, Data, Message }. The React API client unwraps it, returning Data or throwing Message, so screens never branch on HTTP shape.
Same image, four stages
Dev builds the image; QA, Stage and Prod promote the exact digest. Configuration is injected at container start, never baked in.

The request pipeline

What happens between a click in the portal and a row in PostgreSQL. The tenant is never a parameter: it comes from the token, binds the connection, and reaches the database as a session setting that row-level security policies can read.

  1. JWT bearer

    IdentityServer token

  2. SecurityCheck

    user from token, verb to permission

  3. Model validate

    DataAnnotations to ValidationException

  4. Controller

    MiddlewareController base

  5. Service

    new XService(TenantId, UsNumb)

  6. UnitOfWork

    ConnectToTenant, lazy repositories

  7. Npgsql

    SET LOCAL tenant and user

  • The security filter reads the user from the token and places TenantId, UsNumb, user name and IP into the request context.
  • The unit of work looks up the tenant in the global tenant master and builds that tenant's connection string from a template plus the tenant's own database credentials.
  • A connection interceptor runs SET LOCAL request.tennant_id and request.user_id on open, which PostgreSQL row-level security policies can read.
  • Outbound, InvokeHandlerMiddleware catches every exception, maps it to 400, 404 or 500 inside Response<T>, and logs the request and response pair to the API call log table.

Backend

Twenty projects, one request pipeline, and a database per county. The solution is cattax.sln on .NET 10: EF Core 10 with Npgsql, AutoMapper, FluentValidation, NLog, Quartz, Swashbuckle, the AWS S3 SDK, SSH.NET, FileHelpers, EPPlus, OpenXML, DinkToPdf and EFCore.BulkExtensions.

  1. CTX.Services398 files44k
  2. CTX.Entities450 entities, 168 SQL scripts17k
  3. CTX.ImportExportFilesparsers, exporters, SFTP16k
  4. Cattax (Web API host)95 controllers13k
  5. CTX.Tests145 files, 517 tests12k
  6. CTX.Calculationspenalty, interest, discount, bills8.5k
  7. CTX.CartManagerCart.cs, Payments.cs4.8k
  8. PciToCatalisMigrationlegacy ETL4.6k
  9. CTX.RepositoriesGenericRepository<T>4.1k
  10. BackgroundProcessorQuartz, 44 jobs3.1k
  11. CTX.UnitOfWorkFactorylazy repositories2.9k
  12. CTX.ReportCombit REST client1.1k
  13. CTX.Configuration761
  14. CTX.AWSS3377
  15. CTX.IdentityServer317
  16. CTX.Encryption268
Lines of C# per project in cattax.sln, .NET 10. Eight small libraries under 1,000 lines are listed last.Where I worked
  1. GET284
  2. POST149
  3. PUT124
  4. DELETE91
  5. PATCH1
649 endpoints by HTTP verb, across 95 controllers. 32 of the controllers are FLSWA lookups.
Schema as scripts
The DbContext is scaffolded, with no EF migrations. Every schema change ships as an ordered SQL script in the Entities project, and release folders hold the migration sequence for each tenant.
Services own their scope
A controller action creates its service with the caller's tenant and user, and disposes it at the end. Each service opens its own unit of work, so there is no shared DbContext between requests.
Global versus tenant
A separate non-tenant unit of work serves global tables: tenant master, process jobs, permissions and API call logs. Everything else is read through the tenant's own connection.
How the states differ
Almost entirely through data: per-tenant settings, process-job rows, item types and rate tables. The exceptions are deliberate modules: 32 FL SWA lookup controllers and the AG, CO, NAV and BUMP imports for Palm Beach; Patriot imports, privilege licences and a mobile-home penalty branch for Jackson County; three Virginia import jobs.

Background processor

A Quartz.NET console service with 44 job classes, deployed per environment and state on Windows EC2. On start it reads every enabled process-job row for every tenant, resolves the class by reflection and schedules its cron expression. Work arrives as process-queue rows written by the API; a base job claims the next row, heartbeats every ten minutes, marks it complete or complete-with-errors and emails on failure. Stuck rows reset on restart. Jobs cover appraiser, hauler, CO, bump and Patriot imports; bump, TRIM, bill-print, escrow and tax-sale exports; bill calculations; report rendering; bill PDFs; the SWA new-year roll; and bump rollback.

Calculations

Tiered penalty and interest rates with a cumulative cap and minimum amount; tiered discounts by effective date; bill-run to invoice; prorated mobile homes; privilege licences by month with sliding or flat rates; SWA non-government and government bills; and minimum payment, where any item under $100 is due in full.

Frontend

A React 19 portal with 127 routes, one auth singleton and six stores. Seventy feature folders in total; beyond the largest, most are reference-data maintenance screens (DOR codes, hauler codes, boundary codes, SWA class codes) sharing one table-and-drawer CRUD pattern.

  1. ParcelRecord62 files31k
  2. ParcelRecordVA66-file Virginia fork27k
  3. Account7.9k
  4. PrintFiles (receipts)11 templates7.5k
  5. AddItem5.0k
  6. CustomUI (table, tabs, top bar)4.2k
  7. Home (search)4.1k
  8. Cartthe cash register3.8k
  9. Users3.3k
  10. MillageRate3.2k
  11. NAVFileData2.5k
  12. MotorVehicle2.2k
Lines of JS and JSX per feature folder; seventy folders in total, most of them reference-data screens on one table-and-drawer pattern.Where I worked

Provider tree, App.jsx

  1. ThemeProviderMUI, light or dark per user
  2. BrowserRouter
  3. I18nProvideren / es scaffold
  4. PermissionsProvider/userpermissions
  5. SettingsProvider/GetAllSettings
  6. CartProviderpayment cart
  7. RouterMain127 routes
  8. AuthRoute, Layout, Outlet

Stores, Zustand

userStore
auth user, persisted to session storage
GlobalStore
posting date, unallocated cash, payment types, rows per page per table
cartStore, paymentStore
tender, partial payment, payee; cleared on logout
searchStore, transactionSearchStore
criteria, paging, sort
Auth and token refresh
Login runs the authorization-code flow with oidc-client-ts. Between twenty minutes after sign-in and two minutes before expiry, each call exchanges the refresh token at /connect/token and writes the new token back to the store. An expired token logs the user out.
Tenancy in the UI
Tenancy is per user. The top bar offers "Change my tenant" from the user's linked tenants; a server call switches and the app reloads. Tenant and client-type settings gate menu items, item types and the appraiser link.
Accessibility baseline
626 aria attributes across 135 files, keyboard-enabled rows in search and the top bar, focus styles in the MUI overrides. The WAVE tickets I worked added most of the labels and names.
Known debt
Plain JSX without type checking, a near-copy ParcelRecordVA fork, i18n scaffolding with seven keys, no unit tests, no skip links. Listed because it shaped my decisions.

The screens that matter

Parcel Record is the product. Sixty-two files and 30,581 lines sit behind that one screen: invoice actions (void with reason, undelete with a required note), installment and split-bill panels, supplemental bill drawers for real estate, personal property and SWA, payment history, S3-backed document maintenance and the change log.

The cart is where the money moves. Cash over the total due opens a Change Due modal. Check over the total due offers a refund check or "keep in unallocated cash". Paying below the minimum needs a supervisor override. Receipts switch template by item type: real estate, personal property, mobile home, solid waste, government, privilege licence, aircraft, transient business.

  1. Add bills

    POST /Cart

  2. Edit lines

    amount, dates

  3. Tenders

    cash, check, card

  4. Complete

    POST /CompletePayment

  5. Receipt

    @react-pdf/renderer

Receipts render in the browser with @react-pdf/renderer: eleven templates chosen by item type, with the tenant's watermark and seal from the global store. Work orders export to PDF with html2pdf, the account page uses jsPDF with autotable, and bills are rendered server-side by DinkToPdf.

Screen by screen

Eighteen screens of the portal, redrawn in the product's own theme. These are faithful re-creations of the real layouts, not screenshots of customer data; every value shown is illustrative. Pick a screen, then hover, focus or tap a callout to light the part of the screen it explains.

Screen 3 of 18

Parcel record: invoices

Parcel record: invoices: The hub for one account: header, tabs, invoices, summary and payment history.

The hub for one account: header, tabs, invoices, summary and payment history.

The postmark date chip changes interest and discount; the tab set follows the item type; each invoice expands to its lines and line values; the row menu holds supplemental bills, chargebacks, bankruptcy, printing, removal and undelete; the payment history panel opens receipts and starts voids and reversals.

Contributions

Every month since December 2024, in both repositories. Front end in the accent, back end in ink.

04812Dec 24: 3 front end, 12 back endDec 24Jan 25: 0 front end, 0 back endFeb 25: 3 front end, 2 back endFebMar 25: 4 front end, 6 back endApr 25: 6 front end, 5 back endAprMay 25: 4 front end, 2 back endJun 25: 1 front end, 5 back endJunJul 25: 6 front end, 9 back endAug 25: 2 front end, 2 back endAugSep 25: 12 front end, 4 back endOct 25: 4 front end, 0 back endOctNov 25: 3 front end, 0 back endDec 25: 5 front end, 1 back endDec 25Jan 26: 9 front end, 2 back endFeb 26: 9 front end, 2 back endFebMar 26: 10 front end, 6 back endApr 26: 12 front end, 2 back endAprMay 26: 5 front end, 3 back endJun 26: 8 front end, 4 back endJunJul 26: 6 front end, 11 back endAug 26: 12 front end, 8 back endAugSep 26: 9 front end, 6 back endOct 26: 7 front end, 7 back endOct
Front end, 140 commitsBack end, 99 commitsJanuary 2025 had no commits.
Commits authored per month
MonthFront endBack end
Dec 24312
Jan 2500
Feb 2532
Mar 2546
Apr 2565
May 2542
Jun 2515
Jul 2569
Aug 2522
Sep 25124
Oct 2540
Nov 2530
Dec 2551
Jan 2692
Feb 2692
Mar 26106
Apr 26122
May 2653
Jun 2684
Jul 26611
Aug 26128
Sep 2696
Oct 2677

Where the tickets went

  1. Bugs, defects and hotfixes56
  2. Work orders, service requests40
  3. Payments, cart, installments38
  4. Accessibility (WCAG / WAVE)34
  5. Receipts and print files30
  6. Imports, processes, exceptions16
  7. Reports and SQL functions12
Commit messages tagged by theme. A commit can carry more than one theme, so the bars overlap. The first counts every commit whose message names a bug, defect, hotfix or fix.
RepositoryPRs mergedLines addedLines removed
FrontEnd8114,1536,008
BackendAPI5435,26629,156
Total13549,41935,164

Branch per ticket into dev, reviewed PRs, promoted through QA and Stage.

Files I touched most

Front end, commits per file

  1. PrintFiles/Invoice/index.jsxreceipt template switch11
  2. CustomUI/EnhancedDataTable.jsxshared data table11
  3. Cart/index.jsxcash register10
  4. PrintFiles/Invoice/REReceipt.jsxreal-estate receipt9
  5. ParcelRecord/BillsTabV2/index.jsxitems and assessments8
  6. ParcelRecord/WorkOrdersTab/BuildingDetails.jsxbuilding details grid7

Back end, commits per file

  1. CTX.Services/TaskMasterService.cswork orders9
  2. CTX.Services/InvoiceService.csinvoices and receipts9
  3. Controllers/TaskMasterController.cs7
  4. CTX.Services/ReportsService.csreport wiring6
  5. CTX.Cart/Cart.csminimum payment, tender rules5
  6. Combit/FL/…/fn_ihsumm.sqlIHSUMM report function4

Timeline

From lookup-table CRUD to the money paths.

  1. Dec 2024

    Onboarding on FL SWA lookup tables

    SWA owner codes, models, DTOs, repositories registered in the unit of work. First merged backend PR on 31 December.

  2. Feb to Mar 2025

    NAV appraiser import, rollover rates, Changelog tab

    NAV appraiser file import (TCCT-394), millage rate rollover (TCCT-407), installment group fields, a Changelog tab on the Parcel Record, a payment-calculation fix for Jackson MS.

  3. Apr to Jun 2025

    Boundary rollover, building cards, supplemental bills

    Roll the SWA boundary table into the new tax year (TCCT-899), redesigned building-card records, supplemental bill drawers (TCCT-836), initial installment creation (TCCT-1006), unallocated cash (TCCT-434), DR-409A generation.

  4. Jul 2025

    Work order data model

    Database and backend for TaskMaster, TaskNotes and TaskStatus (TCCT-1239, 1253, 1254); work order fields, notes sorting, tab sorting.

  5. Aug to Sep 2025

    Import Exception worker screen

    Backend (TCCT-1550) and frontend (TCCT-1478, 1683) for reviewing, correcting, rejecting and reprocessing rows that failed import. Also the MC-2 account header and MC-4 sub-navigation.

  6. Oct to Dec 2025

    Accessibility and user admin

    WCAG fixes from WAVE reports on checkboxes, empty buttons, Parcel Record and Cart; user tenant management; receipt after void or reversal; multiple owners on parcels.

  7. Jan to Apr 2026

    Service requests, WCAG, minimum payment

    Service Request tab (WO-01) back end and front end; Service Request Area (M-1); WCAG passes on Home and menus for two tenants; minimum payment under $100 for real estate, personal property and mobile home; database name shown in Swagger.

  8. May to Jul 2026

    Building details and statutory reports

    Building Details grid (MC-4) with add-row "Added by Inspector", edit, save and discard with status auto-population, soft delete with reason and undelete. In-House Account Balance Summary (IHSUMM) and Property Address Listing reports as SQL functions.

  9. Aug to Oct 2026

    Receipt integrity for Jackson MS

    Four critical receipt defects: dynamic adjustment, wrong invoice amount after paying a partial balance, edit-payor not saving, duplicated ZIP. Interest missing from IHSUMM. Cart minimum-payment flag. Hotfix: invoice lines paid up to the payment on receipts.

Spotlights

Work Order module

Twelve tickets, from three new tables to an inspector's editable grid. The Palm Beach SWA sends inspectors to properties to verify what is being billed: how many units, what building use, which service area. Those visits had lived outside the tax system. The Work Order module puts the task, its notes, its status history, its attachments and the per-building findings on the parcel itself.

SR-2026-0418 · Building detailsIn progressDiscardSave
BuildingUse codeUnitsStatusSource
BLDG 10100 Single family1VerifiedRoll
BLDG 20400 Multi-family4 to 6ModifiedRoll
BLDG 32300 Commercial1Added by InspectorInspector
BLDG 4Shed, not billable0Deleted, duplicateRoll
Inspection date
2026-06-18
Service area
Area 3 · Hauler 12
Notes
Unit count corrected on site

Interface fragment drawn from the real screen's structure. No customer data.

What I built

  • Tables and services for TaskMaster, TaskNotes and TaskStatus: paged listing with include-disabled and hide-completed filters, notes and files CRUD, soft delete.
  • New work-order fields, attachment validation, empty-state fixes (no more 12/31/1969 dates).
  • Service Request tab (WO-01) and Service Request Area (M-1): back end and front end.
  • Building Details grid (WO-02.4): add row as "Added by Inspector", inline edit, save or discard, status auto-populates on modified rows.
  • Soft delete with a required reason (demolition, lot change, reclassification, duplicate, other with notes), undelete, and a show-deleted toggle; deleted rows keep who, when and why.
  • Permission gating through the portal's permission context.

Decisions worth noting

  • Status history is its own table rather than a column, so an auditor can see who changed what and when.
  • Delete is a soft delete with a reason because inspectors' findings are evidence; nothing billed is ever lost.
  • Row history is kept per building, with revert gated by its own permission and the current tax year.

Receipts you can trust

A receipt is a legal document. It must not change after it is printed. Jackson County went live on the cash register and QA found a family of defects that all came from one design flaw: receipts were rendered from the current state of the account, not from the state at the moment of payment. Pay the remaining balance of a partially paid invoice and the old receipt silently changed. Edit the payor and the receipt did not update. Pay in two steps and the invoice amount shown was wrong.

Try it. Post the second payment under each rendering and watch what receipt 1 says.

Account activity

  1. Invoice 2026 RE 00412-A1,000.00
  2. Payment 1 · receipt printed600.00
  3. Payment 2 not yet posted—

Receipt 1 as printed at the register.

Payment receipt · Real estateR-26-018842
Paid2026-10-02 14:37:11
Payoras entered at the register
Invoice 2026 RE 00412-A1,000.00
Paid up to this payment600.00
This payment600.00
Total due at time of payment400.00
Balance after payment400.00

Receipt fragment drawn for this page. Values are illustrative.

Snapshot at payment
The receipt now carries the invoice lines paid up to that payment and an InvoiceAmount captured when the tender posts, returned by the API and stored with the payment.
Single-invoice rule
The receipt uses the captured invoice amount as Total Due only for single-invoice receipts; multi-invoice receipts sum lines, and Total Due is clamped at zero.
Partial payloads preserve values
The update-details endpoint merges rather than overwrites, so editing a payor cannot blank other fields.
Template fixes
Paid-date timestamp, mailing address, ZIP field duplication, Owner Name 2 hidden when blank, across the real-estate, personal-property, mobile-home and general receipt templates.
  • TCCT-3645Receipts should not adjust dynamically
  • TCCT-3773Incorrect invoice amount after paying remaining balance
  • TCCT-3839Invoice lines paid up to the payment on receipts (hotfix)
  • TCCT-3315Edit payor does not save; receipt not updated
  • TCCT-3507Paid date missing timestamp
  • TCCT-3885ZIP field duplicated on refund receipt

Reports and imports

The two edges of the system where the mainframe era still lives.

In-House Account Balance Summary (IHSUMM). A finance report the SWA runs to reconcile receivables. Delivered as a PostgreSQL function, fn_ihsumm, registered in the report menu tables and rendered by the Combit report server. First version shipped June 2026 (TCCT-3281), then a defect pass (TCCT-3411). The August fix: interest from the paid section was not captured, so the INTEREST column read 0.00 and the total balance was understated (TCCT-3481). Total due now includes payment history.

Property Address Listing (report 129). A roll listing by property address. SQL function plus a report-number guard, CSV only, and the "All" filter requirement removed after customer feedback (TCCT-3295). Also CO report updates (TCCT-1764) and the DR-409A certificate.

-- fn_ihsumm.sql (excerpt) · Combit/FL/IHSUMM
CREATE OR REPLACE FUNCTION public.fn_ihsumm(p_tax_year integer)
RETURNS TABLE (pcn varchar, owner_name varchar,
               bill_amount numeric, interest numeric,
               payments numeric, balance numeric)
LANGUAGE plpgsql AS $BODY$
BEGIN
  RETURN QUERY
  -- IH bill-installment lines for the year
  WITH ih_line AS (
    SELECT ald."ArLKey", inv."ArVKey", inv."AcKey"
    FROM "ArInvoiceMaster" inv
    JOIN "ArLineData" ald ON ald."ArVKey" = inv."ArVKey"
    JOIN "BillInstallmentData" bid ON bid."BidKey" = ald."BidKey"
    WHERE inv."ArVBillYear" = p_tax_year::SMALLINT
      AND bid."BsKey" = 'IH'
  ),
  -- line values, canonical type, IH payments …
END $BODY$;

Import Exception worker screen. Appraiser (NAV), hauler and CO files arrive as fixed-width or delimited text. Rows that fail validation used to stop the whole file. The exception screen, specified by the BA in Confluence and built by me on both sides, turns that into a workflow: six pluggable validation workers (account number, amount range, missing column, data type, required field, duplicate) tag each failing row with a reason.

  1. File uploaded

    to S3, queued as a process row

  2. Parsed row by row

    Quartz job, six validators

  3. Completed with exceptions

    each failing row tagged with a reason

  4. Process Exceptions screen

    view, edit record, reject, save with confirmation

  5. Reprocess

    resolved rows become records

  6. Completed

    rejected rows read-only afterwards

Also on the file side: the NAV appraiser import (TCCT-394) with its header and detail tables and the process-queue model; boundary table rollover into the new year, parts 1 and 2 (TCCT-899); report queue time display and date format for the SWA (TCCT-3515); report 129 restricted to CSV and All Residential Collection Units (CIS-089) handed to Combit.

Accessibility

Government customers audit against ADA and WCAG 2.1 AA with the WAVE browser extension, and every error on a page is a finding. Over ten tickets I cleared the reports on the Home page and menus for both tenants, the Cart, the Parcel Record and its drawers, global search, and the user admin screens.

626
aria attributes in the portal today
13
WCAG tickets I closed
2
tenants audited page by page
  1. Home and menus (2 tenants)5
  2. Parcel Record and drawers4
  3. Cart2
  4. Global search1
  5. Users and tenants1
WCAG tickets closed, by screen.
  • Form labels and aria-label on every input in the Edit Item Attributes and Update Item Details drawers (16 missing labels in one drawer alone).
  • Empty buttons given accessible names; icon-only controls described.
  • Checkbox and label association in tables; contrast fixed on status text.
  • Landmarks and heading order on Home so screen readers can navigate.

Money rules

Mississippi lets a taxpayer pay a minimum amount on real estate, personal property and mobile home bills. The rule misbehaved when the amount due was under $100: the "minimum" exceeded the balance, and the cart flagged a legitimate tender as an overpayment.

  • Backend: effective minimum becomes the balance when the balance is under $100, per item subtype (TCCT-2871).
  • Frontend: cart mirrors the rule and the Minimum Payment button no longer flags overpayment when tender exceeds the minimum but not the total due (TCCT-2718, 2793, 3553).
  • Follow-on in progress: underpayment over the threshold (TCCT-3968).

Other correctness work

  • Voiding a reversal re-applies the original payment (TCCT-670).
  • Discount removal and interest and penalty application on account and invoice (TCCT-1053).
  • Interest and discount displayed for every item type on the Parcel Record (TCCT-2342).
  • Implicit integer division corrected in a calculation path.
  • Unallocated cash: ability to add to it from the register (TCCT-434).
  • Initial installment creation for the SWA (TCCT-1006).

Delivery pipeline

Build once, promote the same image through four environments. Azure DevOps pipelines, AWS ECS, Terraform.

  1. Branch per ticket

    TCCT-nnnn-slug off dev

  2. Pull request

    AI review task plus human review

  3. Dev build gate

    xUnit suite must pass

  4. Dev deploy

    image to ECR to ECS, circuit-breaker rollback

  5. QA promote

    same digest, retagged

  6. Stage copy

    to the prod AWS account

  7. Prod promote

    behind an approval

# Dockerfile, build stage
RUN jq 'to_entries | map({(.key): ("$"+.key)}) | from_entries' \
    src/config.json > src/config.tmp.json
RUN npm run build
# runtime stage: nginx:alpine, non-root, 8080
ENTRYPOINT ["start-nginx.sh"]   # envsubst at start
Runtime configuration without rebuilds
The React app imports a JSON config module at build time. The Dockerfile rewrites every value to a $KEY placeholder before vite build; at container start, start-nginx.sh runs envsubst over the bundled JS. The same image therefore serves dev, QA, stage and prod with different API hosts and OIDC clients.
Hardening in the edge container
HSTS with preload, nosniff, frame-ancestors same-origin, strict referrer policy. nginx runs as the nginx user with server tokens off and gzip on text assets. try_files $uri /index.html keeps deep links alive on refresh; /healthr answers the ECS target group.
Backend quality gate
xUnit v3, NSubstitute and EF Core InMemory: 517 tests in 145 files, fully offline. Tests exercise controller to service to unit of work to repository against an in-memory database, plus the fixed-width parsers. The dev pipeline runs the suite before the image is built; SonarQube runs it with coverage weekly on main. Swagger shows the connected database name so nobody tests against the wrong tenant (my TCCT-2642).
What Terraform provisions for the API
ECS task definition and service, security group, ALB target group and listener rule, CloudWatch log group and the IAM task role. Database credentials come from SSM parameters; state is remote; dev and prod have their own variable files. The API image is aspnet 10 on port 5211 with the Chicago time zone.
Background processor deploys separately
One Windows EC2 service per environment and state (FL, MS). A PowerShell script stops the process, unzips the build and copies everything except the environment's settings file. Restart is a deliberate manual step so an overnight import is never interrupted mid-file.

Reflection

What changed in how I work, after twenty-two months on the platform.

What I would do differently

  • Snapshot receipts from day one. Every receipt defect traced back to rendering from live state; the fix was obvious in hindsight and expensive in sequence.
  • Push for a Vitest suite in the front end. The backend has a real gate; the portal's only automated check is an AI review.
  • Name fields once. Casing mismatches between DTOs and JSX (UsNumb, ArYTKey) cost real hours; a shared contract or TypeScript would have paid for itself.

What I learned

Own the whole slice.
The tickets I am proudest of touch a SQL function, a service, a controller, a store and a drawer. Being the one person across that path is why they shipped in one sprint.
Money logic belongs in one place.
Minimum payment lived in the cart and in the API; keeping them in step was the bug. The backend is now the source of truth and the UI mirrors it.
Accessibility is a checklist, not a vibe.
WAVE made it measurable, and measurable work gets finished.
Read the Confluence page first.
The exception-screen spec answered every edge case before I wrote a line.
239
commits by me, both repositories
135
pull requests merged
74
Jira tickets delivered

Feature catalogue

Part one told the story. This part is the reference: what each screen shows, which endpoints it calls, what rules the backend enforces, and where I worked on it. Facts were read from the code on 14 October 2026.

Search and accounts: /home, /SearchComponent, /allResults, /parcelRecord, /parcelRecordVA, /paymentHistory, /addItem, /newItem, /mv, /districtDetails.

Cash register and work: /cart, /workorders, /ServiceRequestDetails. Processes and files: /processes, /ProcessDetails, /MaintainProcess, /NAVFileData, /AgFileData, /COFileData, /BUMPImport.

Rates and billing setup, around thirty SWA reference-data screens, attributes and configuration, and administration (/users, /user, /reporting).

The side navigation is not hard-coded. It comes from the API per role, so a Mississippi tenant never sees the Florida SWA lookup screens, and a cashier does not see administration. Public pages render without sign-in.

Where I worked. Global search accessibility (TCCT-2992), home-page WCAG passes for both tenants, transaction search filter fixes, and the link from a parcel to the Property Appraiser site by PCN (TCCT-1434).

Invoice search is the default tab: it finds bills to pay and adds them to the cart, with status filters from Active and Unpaid through Bankruptcy, Tax Sale and Tax Deed Transfer, and a warning when older unpaid invoices exist.

Transaction search filters by date presets, cashiers, payor, tender reference and amount range; rows expand to tenders and lines with receipt numbers, and support void and reverse.

Global search in the top bar hits name, description, address, parcel, account and legacy account through one endpoint, shows ten results inline and remembers history per user.

Where I worked. Global search accessibility errors (TCCT-2992), WCAG passes on Home for both tenants (TCCT-2515, 2637, 2859, 2865).

Header: account type and key, owner and attention name, mailing address with an edit pencil, unallocated cash, inactive banner and bankruptcy chip; status chips sorted by severity; a postmark chip that turns red when it is not today.

Tabs follow the item type: Invoices, Items and Assessments, Property Details or Details, Notes, History, Document Maintenance, Change Logs and Service Requests, so one component serves every state.

Invoices tab: cart split button (add, pay in full, pay minimum), expandable lines and line values, and a row menu for chargebacks, supplemental bills, bankruptcy, print invoice, undelete with a note and licence regeneration. Cart gates run in order: read-only users, take-note statuses, prior-year tax-sale and collections invoices, newer-before-older warnings.

Payment history with void and reverse through one modal, a payment details drawer, S3-backed document maintenance, and a change log: services write field-level before and after values with user and IP, so the tab is a per-account audit trail without a separate audit product.

Where I worked. Changelog tab (TCCT-688), show all owner names (TCCT-841), interest and discount for all item types (TCCT-2342), WCAG labels in the item drawers (TCCT-2592, 2595, 2094), redesigned building cards (TCCT-994), supplemental bills (TCCT-836), the receipt snapshot series (TCCT-3645, 3773, 3839).

Status warning list, account header, sub-navigation account controls, transfer history and invoices stack without a tab strip. Collapsible Critical and Warning cards carry counts and refresh when a status changes.

Virginia on the server: three import jobs, storm water (a real-estate fee in two installments), solid waste and nuisance abatement. Each runs the general importer first and creates invoices only when there are zero exceptions, within a fiscal-year window. Vehicle taxation is specified in Confluence and prototyped in the motor-vehicle screen.

Where I worked. Account header and tools base page (TCCT-1776), sub-navigation account controls (TCCT-1778), restore checkout cart in the VA wrapper (TCCT-1618, 1927).

Posting and postmark dates each behind a confirm, a running total, Minimum Payment, Clear Cart and Remove Selected. Lines expand to sub-lines, each with its own minimum. Tenders: cash, check, credit or debit, charitable credit, write off and internal transfer.

Rules: minimum payment only when no RE, PP or MH item is under $100; a line below its minimum asks for confirmation; a partial payment locks both dates; posting-date changes and write-offs need supervisor credentials by permission; charitable credit is first tender only, once, at most half of tax and fees.

Server side the cart manager is a 2,860-line class: add invoice, add and remove tender, change line amount and dates, complete payment with overpayment or change to unallocated cash, void, reverse (a whole cart, one invoice or specific lines, inserting a reversal cart and tender linked as corrections), refund check and transfer. Unallocated cash is a signed ledger: refunds post negative entries and cannot exceed the balance.

Where I worked. Minimum-payment rule on both sides (TCCT-2718, 2793, 2871, 3553, 3968), unallocated cash from the register (TCCT-434), receipt after void or reversal (TCCT-1815), VA wrapper checkout (TCCT-1927).

The invoice router loads the invoice or payment and the grouped account attributes, then picks a template by tax type: real estate, personal property, mobile home, solid waste (with government and certificate-of-correction variants), privilege licence, transient business, aircraft and mobile-home registrations, and a general receipt.

Shared anatomy: tenant header, account and receipt numbers, a tax-district table, assessed and exemption values, county, city and school breakdown, a watermark logo and VOIDED, REVERSED or DELETED overlays.

Where I worked. Invoice router and the RE, PP, MH and General templates (TCCT-2695, 3315, 3507, 3773, 3839, 3885), print invoice and view receipt modal fix (TCCT-2387), DR-409A certificate of correction.

Tabs for all reports, favourites, a queue under thirty days and completed runs with status chips. Parameters load per report with cascading children; validate first, stream small reports back as a file, queue the rest and poll every ten seconds.

A report is rows in the menu master, menu group and parameter-control tables (caption, type, range, default, required, child parameter, dropdown SQL) plus role and favourite links. The Combit template calls its own fxcat_report_* or fn_* function; the API calls the report server's export endpoint with the template id and writes the output to S3. A direct request falls back to the queue after twenty seconds. Templates live in the repo as .rsxfer files next to their SQL.

Florida SWA has 51 folders, from Account Status and the six bump reports to TRIM notice totals and unallocated cash history; Mississippi has 13, including settlement reports and privilege licence by month.

Where I worked. IHSUMM function and its defect passes (TCCT-3281, 3411, 3481), Property Address Listing report 129 (TCCT-3295), CO report updates (TCCT-1764), report queue time and date display (TCCT-3515), CIS-089 hand-off to Combit (TCCT-3833).

Users: sortable, searchable, show-disabled toggle; a drawer with a role checklist filtered to the user's tenant; enable and disable as a soft delete. Roles: permissions by item type and sub type with create, read, update, delete and supervisor-required flags; menu items and reports per role.

Every request passes a security filter that finds the user from the JWT and maps the HTTP verb to read, create, modify or delete on the permission named by the endpoint's tag; 60 distinct permission names exist in code. Supervisor-required permissions prompt for credentials and post them to the override endpoint.

Where I worked. User tenant management (TCCT-2013), roles hotfix and groups per tenant (TCCT-2112, 2116, 2107), disabled users visually distinguished (TCCT-3576, 3607), WCAG labels on the user drawers.

Each parcel carries buildings, and each building carries billing units with modifiers. Unit counts come from the appraiser roll, the nightly bump file, certificates of occupancy and inspector findings. Two engines: non-government reprices every unbilled installment from the current rates table (year, type, district, CSF code, PA range) and bills in batches; government tonnage is rate × square feet, or rate × units × modifier, billed as an HTML letter rendered to PDF from a SQL function. Finalize recalculates the installments, syncs bills, creates invoices and can apply unallocated cash.

Thirty-two FLSWA controllers and about thirty-three maintenance pages: boundary, hauler, DOR, CSF, compliance, exempt, PZB, property and municipality codes, service days, rates and rules tables, discount periods and note types. A background job creates next year's bill run and rolls each installment forward; the boundary, rates and rules tables and discount periods are copied from their own screens.

Closed gap: 1,808 SWA parcels (about $2.6M) had accounts but no billable item because the bump change branch never created one. The add branch now creates items for accounts with history but no active item.

Where I worked. SWA owner codes and the first lookup screens (TCCT-176), boundary rollover (TCCT-899), initial installment (TCCT-1006), items and assessments details (TCCT-739, 3595), tax-year dropdown blank page (TCCT-3679), SWA refresh issues (TCCT-1415).

Millage rate: a tax-year chip menu grouped future, current and previous; a year is editable only above the current billed year, and "Add year" rolls every element and district forward when the next year has no rates. Districts lock for closed years and a supervisor can unlock one until the session ends. A district group rate is the sum of its linked element rates; gross tax is taxable value × group rate ÷ rate base (1,000 for mills).

Mobile home rates by single-wide, multi-wide and Katrina cottage; mobile-home tax is width × length × 0.15 × rate × depreciation × (13 − month) ÷ 12. SWA discount periods apply as tiered discounts by effective date. The SWA rates table is keyed by year, type, district, CSF code and PA range, and a new year copies the latest; it and the rules table are public routes a property owner can read without signing in.

Where I worked. Rollover rates (TCCT-407), roll the SWA boundary table to the new year, parts 1 and 2 (TCCT-899), "unable to add additional mill rate" fix (TCCT-1955), rates table controller and service changes.

NAV is the non-ad-valorem roll from the Property Appraiser: 1,200-character fixed-width records, one per parcel, edited through a 1,644-line drawer. The table is staging only; separate passes rewrite legal descriptions and owner and mailing addresses on the parcels, paged by key (a 662k-row file once took over seven hours). AG carries agricultural classification and refuses a repeated file. CO certificates of occupancy arrive pipe-delimited; rows under 33 columns are rejected, and a missing PZB code, PA category or boundary becomes an exception in a CSV on S3.

None of the four screens uploads a file. Files go from the Processes screen into S3, become a process-queue row, and are picked up by the matching Quartz job. The bump job stages two fixed-width files (parcel master and buildings) and flags each parcel add, change or delete; seven Excel reports match the legacy R187 output. Because the import commits across about thirty saves with no transaction, a rollback snapshots twelve tables before and after, compares rows three ways, reverts only rows unchanged since the run, reports conflicts, and blocks a new bump while a rollback is pending.

Where I worked. NAV import file for appraiser data (TCCT-394) with its header and detail tables and the process-queue model, SWA refresh issues (TCCT-1415), bump reports hand-off (CIS-089).

TaskMaster, TaskNotes and TaskStatus with paged listing, include-disabled and hide-completed filters, notes and file CRUD and soft delete. The Service Requests tab reads the county's external QAlert system through the API and shows field-service and file-change approvals as status chips.

The Building Details grid: add a row as "Added by Inspector", inline edit with save or discard, status auto-population on modified rows, soft delete with a required reason, undelete and a show-deleted toggle.

Where I worked. Work order data model (TCCT-1239, 1253, 1254), Service Request tab (WO-01), Service Request Area (M-1), Building Details grid (WO-02.4).

Stack

  • .NET 10
  • ASP.NET Core
  • C#
  • EF Core
  • PostgreSQL
  • React 19
  • MUI v5
  • Zustand
  • xUnit v3
  • Docker
  • AWS ECS
  • Terraform
  • Azure DevOps
  • WCAG 2.1 AA