diff --git a/api/src/form_schema/forms/sf424c/1/0/form_json.py b/api/src/form_schema/forms/sf424c/1/0/form_json.py
index 38fbff3fd7..498e15fb1c 100644
--- a/api/src/form_schema/forms/sf424c/1/0/form_json.py
+++ b/api/src/form_schema/forms/sf424c/1/0/form_json.py
@@ -39,6 +39,7 @@
},
"construction": {
"allOf": [{"$ref": "#/$defs/budget_row"}],
+ "required": ["total_cost"],
},
"equipment": {
"allOf": [{"$ref": "#/$defs/budget_row"}],
@@ -145,12 +146,403 @@
"name": "Table1",
"type": "section",
"label": "Budget Information for Construction Programs",
+ "description": "NOTE: Certain Federal assistance programs require additional computations to arrive at the Federal share of project costs eligible for participation. If such is the case, you will be notified.",
"children": [
{
"type": "multiField",
- "name": "Budget424cTable1",
- "widget": "Budget424cTable1",
+ "name": "budget_424c_table_1",
+ "widget": "Table",
"definition": ["/properties/budget_information"],
+ "children": {
+ "columns": [
+ {
+ "columnHeader": "COST CLASSIFICATION",
+ "width": 40,
+ },
+ {
+ "columnHeader": "a. Total Cost",
+ "width": 20,
+ },
+ {
+ "columnHeader": "b. Costs Not Allowable \nfor Participation",
+ "width": 20,
+ },
+ {
+ "columnHeader": "c. Total Allowable Costs \n(Columns a - b)",
+ "width": 20,
+ },
+ ],
+ "rows": [
+ {
+ "cells": [
+ {
+ "type": "plainText",
+ "staticContent": "1. Administrative and legal expenses",
+ },
+ {
+ "type": "input",
+ "definition": "/properties/administrative_and_legal_expenses/properties/total_cost",
+ "format": "dollar",
+ },
+ {
+ "type": "input",
+ "definition": "/properties/administrative_and_legal_expenses/properties/non_allowable_cost",
+ "format": "dollar",
+ },
+ {
+ "type": "readOnly",
+ "definition": "/properties/administrative_and_legal_expenses/properties/total_allowable_cost",
+ "format": "dollar",
+ },
+ ],
+ },
+ {
+ "cells": [
+ {
+ "type": "plainText",
+ "staticContent": "2. Land, structures, rights-of-way, appraisals, etc.",
+ },
+ {
+ "type": "input",
+ "definition": "/properties/land_structures_rights_of_way/properties/total_cost",
+ "format": "dollar",
+ },
+ {
+ "type": "input",
+ "definition": "/properties/land_structures_rights_of_way/properties/non_allowable_cost",
+ "format": "dollar",
+ },
+ {
+ "type": "readOnly",
+ "definition": "/properties/land_structures_rights_of_way/properties/total_allowable_cost",
+ "format": "dollar",
+ },
+ ],
+ },
+ {
+ "cells": [
+ {
+ "type": "plainText",
+ "staticContent": "3. Relocation expenses and payments",
+ },
+ {
+ "type": "input",
+ "definition": "/properties/relocation_expenses/properties/total_cost",
+ "format": "dollar",
+ },
+ {
+ "type": "input",
+ "definition": "/properties/relocation_expenses/properties/non_allowable_cost",
+ "format": "dollar",
+ },
+ {
+ "type": "readOnly",
+ "definition": "/properties/relocation_expenses/properties/total_allowable_cost",
+ "format": "dollar",
+ },
+ ],
+ },
+ {
+ "cells": [
+ {
+ "type": "plainText",
+ "staticContent": "4. Architectural and engineering fees",
+ },
+ {
+ "type": "input",
+ "definition": "/properties/architectural_engineering_fees/properties/total_cost",
+ "format": "dollar",
+ },
+ {
+ "type": "input",
+ "definition": "/properties/architectural_engineering_fees/properties/non_allowable_cost",
+ "format": "dollar",
+ },
+ {
+ "type": "readOnly",
+ "definition": "/properties/architectural_engineering_fees/properties/total_allowable_cost",
+ "format": "dollar",
+ },
+ ],
+ },
+ {
+ "cells": [
+ {
+ "type": "plainText",
+ "staticContent": "5. Other architectural and engineering fees",
+ },
+ {
+ "type": "input",
+ "definition": "/properties/other_architectural_engineering_fees/properties/total_cost",
+ "format": "dollar",
+ },
+ {
+ "type": "input",
+ "definition": "/properties/other_architectural_engineering_fees/properties/non_allowable_cost",
+ "format": "dollar",
+ },
+ {
+ "type": "readOnly",
+ "definition": "/properties/other_architectural_engineering_fees/properties/total_allowable_cost",
+ "format": "dollar",
+ },
+ ],
+ },
+ {
+ "cells": [
+ {
+ "type": "plainText",
+ "staticContent": "6. Project inspection fees",
+ },
+ {
+ "type": "input",
+ "definition": "/properties/project_inspection_fees/properties/total_cost",
+ "format": "dollar",
+ },
+ {
+ "type": "input",
+ "definition": "/properties/project_inspection_fees/properties/non_allowable_cost",
+ "format": "dollar",
+ },
+ {
+ "type": "readOnly",
+ "definition": "/properties/project_inspection_fees/properties/total_allowable_cost",
+ "format": "dollar",
+ },
+ ],
+ },
+ {
+ "cells": [
+ {
+ "type": "plainText",
+ "staticContent": "7. Site work",
+ },
+ {
+ "type": "input",
+ "definition": "/properties/site_work/properties/total_cost",
+ "format": "dollar",
+ },
+ {
+ "type": "input",
+ "definition": "/properties/site_work/properties/non_allowable_cost",
+ "format": "dollar",
+ },
+ {
+ "type": "readOnly",
+ "definition": "/properties/site_work/properties/total_allowable_cost",
+ "format": "dollar",
+ },
+ ],
+ },
+ {
+ "cells": [
+ {
+ "type": "plainText",
+ "staticContent": "8. Demolition and removal",
+ },
+ {
+ "type": "input",
+ "definition": "/properties/demolition_and_removal/properties/total_cost",
+ "format": "dollar",
+ },
+ {
+ "type": "input",
+ "definition": "/properties/demolition_and_removal/properties/non_allowable_cost",
+ "format": "dollar",
+ },
+ {
+ "type": "readOnly",
+ "definition": "/properties/demolition_and_removal/properties/total_allowable_cost",
+ "format": "dollar",
+ },
+ ],
+ },
+ {
+ "cells": [
+ {
+ "type": "plainText",
+ "staticContent": "9. Construction",
+ },
+ {
+ "type": "input",
+ "definition": "/properties/construction/properties/total_cost",
+ "format": "dollar",
+ },
+ {
+ "type": "input",
+ "definition": "/properties/construction/properties/non_allowable_cost",
+ "format": "dollar",
+ },
+ {
+ "type": "readOnly",
+ "definition": "/properties/construction/properties/total_allowable_cost",
+ "format": "dollar",
+ },
+ ],
+ },
+ {
+ "cells": [
+ {
+ "type": "plainText",
+ "staticContent": "10. Equipment",
+ },
+ {
+ "type": "input",
+ "definition": "/properties/equipment/properties/total_cost",
+ "format": "dollar",
+ },
+ {
+ "type": "input",
+ "definition": "/properties/equipment/properties/non_allowable_cost",
+ "format": "dollar",
+ },
+ {
+ "type": "readOnly",
+ "definition": "/properties/equipment/properties/total_allowable_cost",
+ "format": "dollar",
+ },
+ ],
+ },
+ {
+ "cells": [
+ {
+ "type": "plainText",
+ "staticContent": "11. Miscellaneous",
+ },
+ {
+ "type": "input",
+ "definition": "/properties/miscellaneous/properties/total_cost",
+ "format": "dollar",
+ },
+ {
+ "type": "input",
+ "definition": "/properties/miscellaneous/properties/non_allowable_cost",
+ "format": "dollar",
+ },
+ {
+ "type": "readOnly",
+ "definition": "/properties/miscellaneous/properties/total_allowable_cost",
+ "format": "dollar",
+ },
+ ],
+ },
+ {
+ "cells": [
+ {
+ "type": "plainText",
+ "staticContent": "12. SUBTOTAL (sum of lines 1-11)",
+ },
+ {
+ "type": "readOnly",
+ "definition": "/properties/subtotal_1/properties/total_cost",
+ "format": "dollar",
+ },
+ {
+ "type": "readOnly",
+ "definition": "/properties/subtotal_1/properties/non_allowable_cost",
+ "format": "dollar",
+ },
+ {
+ "type": "readOnly",
+ "definition": "/properties/subtotal_1/properties/total_allowable_cost",
+ "format": "dollar",
+ },
+ ],
+ },
+ {
+ "cells": [
+ {
+ "type": "plainText",
+ "staticContent": "13. Contingencies",
+ },
+ {
+ "type": "input",
+ "definition": "/properties/contingencies/properties/total_cost",
+ "format": "dollar",
+ },
+ {
+ "type": "input",
+ "definition": "/properties/contingencies/properties/non_allowable_cost",
+ "format": "dollar",
+ },
+ {
+ "type": "readOnly",
+ "definition": "/properties/contingencies/properties/total_allowable_cost",
+ "format": "dollar",
+ },
+ ],
+ },
+ {
+ "cells": [
+ {
+ "type": "plainText",
+ "staticContent": "14. SUBTOTAL",
+ },
+ {
+ "type": "readOnly",
+ "definition": "/properties/subtotal_2/properties/total_cost",
+ "format": "dollar",
+ },
+ {
+ "type": "readOnly",
+ "definition": "/properties/subtotal_2/properties/non_allowable_cost",
+ "format": "dollar",
+ },
+ {
+ "type": "readOnly",
+ "definition": "/properties/subtotal_2/properties/total_allowable_cost",
+ "format": "dollar",
+ },
+ ],
+ },
+ {
+ "cells": [
+ {
+ "type": "plainText",
+ "staticContent": "15. Project (program) income",
+ },
+ {
+ "type": "input",
+ "definition": "/properties/project_income/properties/total_cost",
+ "format": "dollar",
+ },
+ {
+ "type": "input",
+ "definition": "/properties/project_income/properties/non_allowable_cost",
+ "format": "dollar",
+ },
+ {
+ "type": "readOnly",
+ "definition": "/properties/project_income/properties/total_allowable_cost",
+ "format": "dollar",
+ },
+ ],
+ },
+ {
+ "cells": [
+ {
+ "type": "plainText",
+ "staticContent": "16. TOTAL PROJECT COSTS (subtract 15 from 14)",
+ },
+ {
+ "type": "readOnly",
+ "definition": "/properties/total_project_costs/properties/total_cost",
+ "format": "dollar",
+ },
+ {
+ "type": "readOnly",
+ "definition": "/properties/total_project_costs/properties/non_allowable_cost",
+ "format": "dollar",
+ },
+ {
+ "type": "readOnly",
+ "definition": "/properties/total_project_costs/properties/total_allowable_cost",
+ "format": "dollar",
+ },
+ ],
+ },
+ ],
+ },
}
],
},
@@ -158,17 +550,70 @@
"name": "Table2",
"type": "section",
"label": "Federal Funding",
+ "description": "Federal assistance requested, calculate as follows.",
"children": [
{
"type": "multiField",
- "name": "Budget424cTable2",
- "widget": "Budget424cTable2",
+ "name": "budget_424c_table_2",
+ "widget": "Table",
"definition": ["/properties/federal_funding"],
+ "children": {
+ "columns": [
+ {
+ "columnHeader": "Field",
+ "width": 60,
+ },
+ {
+ "columnHeader": "Value",
+ "width": 40,
+ },
+ ],
+ "rows": [
+ {
+ "cells": [
+ {
+ "type": "plainText",
+ "staticContent": "Total project costs",
+ },
+ {
+ "type": "readOnly",
+ "definition": "/properties/total_project_costs",
+ "format": "dollar",
+ },
+ ],
+ },
+ {
+ "cells": [
+ {
+ "type": "plainText",
+ "staticContent": "Federal percentage share \n(Consult Federal agency for Federal percentage share.)",
+ },
+ {
+ "type": "input",
+ "definition": "/properties/federal_percentage_share",
+ "format": "percentage",
+ },
+ ],
+ },
+ {
+ "cells": [
+ {
+ "type": "plainText",
+ "staticContent": "Federal funding share",
+ },
+ {
+ "type": "readOnly",
+ "definition": "/properties/federal_funding_share",
+ "format": "dollar",
+ },
+ ],
+ },
+ ],
+ },
}
],
},
]
-
FORM_RULE_SCHEMA = {
##### PRE-POPULATION RULES
"budget_information": {
diff --git a/api/tests/src/form_schema/forms/test_sf424c.py b/api/tests/src/form_schema/forms/test_sf424c.py
index 1c8428e40a..2ee5a6f7f0 100644
--- a/api/tests/src/form_schema/forms/test_sf424c.py
+++ b/api/tests/src/form_schema/forms/test_sf424c.py
@@ -131,6 +131,35 @@ def test_sf424c_v2_0_percentage_negative(sf424c_v2_0):
assert validation_issues[0].field == "$.federal_funding.federal_percentage_share"
+def test_sf424c_v2_0_construction_total_cost_required(sf424c_v2_0):
+ """construction.total_cost is required — missing it fails schema validation,
+ unlike other budget rows (e.g. site_work) which stay fully optional."""
+ data = {
+ "budget_information": {
+ "construction": {
+ "non_allowable_cost": "5000.00",
+ }
+ }
+ }
+ validation_issues = validate_json_schema_for_form(data, sf424c_v2_0)
+ assert len(validation_issues) == 1
+ assert validation_issues[0].type == "required"
+ assert validation_issues[0].field == "$.budget_information.construction.total_cost"
+
+
+def test_sf424c_v2_0_other_rows_total_cost_not_required(sf424c_v2_0):
+ """Unlike construction, other budget rows (e.g. site_work) don't require total_cost."""
+ data = {
+ "budget_information": {
+ "site_work": {
+ "non_allowable_cost": "5000.00",
+ }
+ }
+ }
+ validation_issues = validate_json_schema_for_form(data, sf424c_v2_0)
+ assert len(validation_issues) == 0
+
+
def test_sf424c_v2_0_rules_empty_state(
enable_factory_create, verify_no_warning_error_logs, sf424c_v2_0
):
diff --git a/frontend/src/components/apply-form/widgets/TableWidget/TableCell.test.tsx b/frontend/src/components/apply-form/widgets/TableWidget/TableCell.test.tsx
index f1a8c864ae..fc1fafec58 100644
--- a/frontend/src/components/apply-form/widgets/TableWidget/TableCell.test.tsx
+++ b/frontend/src/components/apply-form/widgets/TableWidget/TableCell.test.tsx
@@ -341,4 +341,24 @@ describe("TableCell", () => {
expect(input).toHaveAttribute("aria-invalid", "false");
expect(input).not.toHaveAttribute("aria-describedby");
});
+ it("wraps long error text without breaking words mid-way", () => {
+ render(
+