Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/logical_data_model.encoded

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/logical_data_model.puml
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ class Citations{
* updatedAt : timestamp with time zone : now()
active : boolean
active_through : date
calculated_category : text
calculated_finding_type : text
calculated_status : text
citation : text
Expand Down
7 changes: 4 additions & 3 deletions docs/monitoring-fact-tables.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@ One row per monitoring Finding (which links to a "citation" in `MonitoringStanda
| `citation` | TEXT | Citation text from `MonitoringStandards` (e.g., "1302.3") |
| `standard_text` | TEXT | Full standard text from `MonitoringStandards.text` |
| `guidance_category` | TEXT | Guidance text from `MonitoringStandards.guidance` |
| `findingCategoryId` | INTEGER | FK to `FindingCategories.id` — the normalized category row for this `guidance_category` value |
| `calculated_category` | TEXT | Effective category for display: `source_category` (`MonitoringFindings.source`) when present, falling back to `guidance_category` (`MonitoringStandards.guidance`). This is the value used by services and widgets. |
| `findingCategoryId` | INTEGER | FK to `FindingCategories.id` — the normalized category row matching `calculated_category` |
| `initial_review_uuid` | TEXT | Review UUID of the earliest delivered review where this finding appeared |
| `initial_narrative` | TEXT | Finding narrative from `MonitoringFindingHistories` linking to the initial review |
| `initial_determination` | TEXT | Determination from `MonitoringFindingHistories` linking to the initial review |
Expand All @@ -88,7 +89,7 @@ One row per monitoring Finding (which links to a "citation" in `MonitoringStanda

### FindingCategories

Mostly to keep a list of active category values that can be referenced by TTA Hub, FindingCategories is maintained as a dimensional table of unique `guidance_category` values observed across all Citations. It contains one row per distinct `MonitoringStandards.guidance` value seen in reports delivered since the monitoring start date. Populated and maintained by the same update script that manages Citations; a row is soft-deleted when no non-deleted Citation references that category name.
Mostly to keep a list of active category values that can be referenced by TTA Hub, FindingCategories is maintained as a dimensional table of unique `calculated_category` values observed across all Citations. It contains one row per distinct `calculated_category` value (`source_category` when present, otherwise `guidance_category`). Populated and maintained by the same update script that manages Citations; a row is soft-deleted when no non-deleted Citation references that category name.

| Column | Type | Description |
|---|---|---|
Expand Down Expand Up @@ -244,4 +245,4 @@ Updates are scoped to grants that are `Active` or became inactive within the las
- **Live value view models**: `src/models/citationsLiveValues.js`, `src/models/deliveredReviewsLiveValues.js`
- **Update script**: `src/tools/updateMonitoringFactTables.ts` (also recreates live value views nightly)
- **CLI wrapper**: `src/tools/updateMonitoringFactTablesCLI.ts`
- **Migrations**: `src/migrations/20260219034204-create-monitoring-fact-tables.js`, `src/migrations/20260421000000-create_finding_categories_table.js`, `src/migrations/20260424000000-create_live_values_views.js`, `src/migrations/20260429220319-expand_monitoring_fact_table_columns.js`, `src/migrations/20260521000000-add_calculated_review_finding_type.js`, `src/migrations/20260528063939-add_latest_monitoring_review_to_grants.js`
- **Migrations**: `src/migrations/20260219034204-create-monitoring-fact-tables.js`, `src/migrations/20260421000000-create_finding_categories_table.js`, `src/migrations/20260424000000-create_live_values_views.js`, `src/migrations/20260429220319-expand_monitoring_fact_table_columns.js`, `src/migrations/20260521000000-add_calculated_review_finding_type.js`, `src/migrations/20260528063939-add_latest_monitoring_review_to_grants.js`, `src/migrations/20260602001049-add_calculated_category_to_citations.js`
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module.exports = {
async up(queryInterface) {
// IF NOT EXISTS guard: updateMonitoringFactTables may have already added this column
// when called from an earlier migration before this one runs.
// TODO(TTAHUB-5287): Remove guard once updateMonitoringFactTables runs after all migrations.
await queryInterface.sequelize.query(`
ALTER TABLE "Citations"
ADD COLUMN IF NOT EXISTS calculated_category TEXT;
`);
},

async down(queryInterface) {
await queryInterface.removeColumn('Citations', 'calculated_category');
},
};
4 changes: 4 additions & 0 deletions src/models/citation.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,10 @@ export default (sequelize, DataTypes) => {
type: DataTypes.TEXT,
allowNull: true,
},
calculated_category: {
type: DataTypes.TEXT,
allowNull: true,
},
findingCategoryId: {
type: DataTypes.INTEGER,
allowNull: true,
Expand Down
2 changes: 1 addition & 1 deletion src/services/citations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ export async function getCitationsByGrantIds(
'reviewName', dr.review_name,
'reportDeliveryDate', dr.report_delivery_date,
'findingType', c.calculated_finding_type,
'findingSource', c.source_category,
'findingSource', c.calculated_category,
'monitoringFindingStatusName', c.raw_status,
'citation', c.citation,
'severity', CASE
Expand Down
2 changes: 1 addition & 1 deletion src/services/monitoring.testHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,7 @@ async function createReportAndCitationData(
calculated_status: 'Complete',
raw_finding_type: 'Noncompliance',
calculated_finding_type: 'Deficiency',
source_category: 'source',
calculated_category: 'source',
active: true,
last_review_delivered: true,
},
Expand Down
16 changes: 8 additions & 8 deletions src/services/monitoring.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ interface IFactCitationRow {
calculated_status: string | null;
raw_finding_type: string | null;
calculated_finding_type: string | null;
source_category: string | null;
calculated_category: string | null;
}

interface IGrantCitationRow {
Expand Down Expand Up @@ -364,7 +364,7 @@ interface IFactCitationForReviewRow {
calculated_status: string | null;
raw_finding_type: string | null;
calculated_finding_type: string | null;
source_category: string | null;
calculated_category: string | null;
finding_deadline: string | null;
}

Expand Down Expand Up @@ -442,7 +442,7 @@ function toGrantCitationRow(
calculated_status: optionalString(citation.calculated_status),
raw_finding_type: optionalString(citation.raw_finding_type),
calculated_finding_type: optionalString(citation.calculated_finding_type),
source_category: optionalString(citation.source_category),
calculated_category: optionalString(citation.calculated_category),
},
};
}
Expand Down Expand Up @@ -572,7 +572,7 @@ function toDeliveredReviewCitationWithCitationRow(
calculated_status: optionalString(citation.calculated_status),
raw_finding_type: optionalString(citation.raw_finding_type),
calculated_finding_type: optionalString(citation.calculated_finding_type),
source_category: optionalString(citation.source_category),
calculated_category: optionalString(citation.calculated_category),
finding_deadline: optionalString(citation.finding_deadline),
},
};
Expand Down Expand Up @@ -607,7 +607,7 @@ async function ttaByCitationsFromFactTables(
'calculated_status',
'raw_finding_type',
'calculated_finding_type',
'source_category',
'calculated_category',
],
},
],
Expand Down Expand Up @@ -638,7 +638,7 @@ async function ttaByCitationsFromFactTables(
citationNumber: citationData.citation,
status: citationData.calculated_status || citationData.raw_status || '',
findingType: citationData.calculated_finding_type || '',
category: citationData.source_category || '',
category: citationData.calculated_category || '',
grantNumbers: [],
grantNumbersSeen: new Set<string>(),
lastTTADateMoment: null,
Expand Down Expand Up @@ -904,7 +904,7 @@ async function ttaByReviewsFromFactTables(
'calculated_status',
'raw_finding_type',
'calculated_finding_type',
'source_category',
'calculated_category',
'finding_deadline',
],
},
Expand Down Expand Up @@ -996,7 +996,7 @@ async function ttaByReviewsFromFactTables(
correctionDeadline: citation.finding_deadline
? moment(citation.finding_deadline, 'YYYY-MM-DD').format('MM/DD/YYYY')
: '',
category: citation.source_category || '',
category: citation.calculated_category || '',
objectives,
},
];
Expand Down
2 changes: 0 additions & 2 deletions src/services/standardGoalsData.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,6 @@ describe('standardGoals with Data', () => {
citation: 'Citation on approved report',
raw_finding_type: 'Type 1',
calculated_finding_type: 'Type 1',
source_category: 'Source 1',
});

normalizedCitationOnNonApprovedReport = await Citation.create({
Expand All @@ -176,7 +175,6 @@ describe('standardGoals with Data', () => {
citation: 'Citation on non-approved report',
raw_finding_type: 'Type 2',
calculated_finding_type: 'Type 2',
source_category: 'Source 2',
});

await GrantCitation.create({
Expand Down
2 changes: 1 addition & 1 deletion src/services/ttaByCitation.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ describe('ttaByCitations', () => {
calculated_status: 'Active',
raw_finding_type: 'Deficiency',
calculated_finding_type: 'Deficiency',
source_category: 'mismatch-source',
calculated_category: 'mismatch-source',
active: true,
last_review_delivered: true,
},
Expand Down
4 changes: 2 additions & 2 deletions src/services/ttaByReview.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ describe('ttaByReviews', () => {
raw_finding_type: 'Deficiency',
// Citation-level calculated type differs from the review-specific value below
calculated_finding_type: 'Deficiency',
source_category: 'crft-source',
calculated_category: 'crft-source',
active: true,
last_review_delivered: true,
},
Expand Down Expand Up @@ -369,7 +369,7 @@ describe('ttaByReviews', () => {
calculated_status: 'Active',
raw_finding_type: 'Deficiency',
calculated_finding_type: 'Deficiency',
source_category: 'mismatch-source',
calculated_category: 'mismatch-source',
active: true,
last_review_delivered: true,
},
Expand Down
1 change: 0 additions & 1 deletion src/tools/processData.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -487,7 +487,6 @@ describe('processData', () => {
citation: 'Citation',
raw_finding_type: 'Deficiency',
calculated_finding_type: 'Deficiency',
source_category: 'Monitoring',
});
const citation = await ActivityReportObjectiveCitation.create({
activityReportObjectiveId: aro.id,
Expand Down
14 changes: 9 additions & 5 deletions src/tools/updateMonitoringFactTables.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -640,7 +640,7 @@ describe('updateMonitoringFactTables', () => {
findingId: findingIdB,
statusId: FINDING_STATUS_CORRECTED_ID,
findingType: 'Deficiency',
source: 'FA-1',
source: null,
name: 'Finding B',
hash: `hash-${uuidv4()}`,
...timestamps,
Expand Down Expand Up @@ -1392,6 +1392,7 @@ describe('updateMonitoringFactTables', () => {
expect(citation.standard_text).toBe('Standard text for testing');
expect(citation.guidance_category).toBe('Fiscal');
expect(citation.source_category).toBe('FA-1');
expect(citation.calculated_category).toBe('FA-1'); // source_category takes precedence over guidance_category
});

it('links the Citation to the correct Category row', async () => {
Expand All @@ -1400,7 +1401,8 @@ describe('updateMonitoringFactTables', () => {

const category = await FindingCategory.findByPk(citation.findingCategoryId);
expect(category).not.toBeNull();
expect(category.name).toBe('Fiscal');
// source_category is 'FA-1' so calculated_category = 'FA-1', which drives findingCategoryId
expect(category.name).toBe('FA-1');
expect(category.deletedAt).toBeNull();
});

Expand Down Expand Up @@ -1487,6 +1489,8 @@ describe('updateMonitoringFactTables', () => {
it('links the Citation to a different Category than Scenario A', async () => {
const citation = await Citation.findOne({ where: { finding_uuid: findingIdB } });
expect(citation.findingCategoryId).not.toBeNull();
// source is null for finding B, so calculated_category falls back to guidance_category ('Health')
expect(citation.calculated_category).toBe('Health');

const category = await FindingCategory.findByPk(citation.findingCategoryId);
expect(category).not.toBeNull();
Expand Down Expand Up @@ -2303,9 +2307,9 @@ describe('updateMonitoringFactTables', () => {
// Soft delete
// =====================
describe('soft delete', () => {
it('soft-deletes a Category when no non-deleted Citation references its guidance_category', async () => {
// Scenario B's finding (findingIdB) uses STANDARD_ID_2 → guidance: 'Health'
// Temporarily source-delete the standard so findingIdB drops out of full_citations
it('soft-deletes a Category when no non-deleted Citation references its calculated_category', async () => {
// Scenario B's finding (findingIdB) has null source, so calculated_category = guidance_category = 'Health'
// (from STANDARD_ID_2). Temporarily source-delete the standard so findingIdB drops out of full_citations
await MonitoringStandard.update(
{ sourceDeletedAt: new Date() },
{ where: { standardId: STANDARD_ID_2 } }
Expand Down
36 changes: 30 additions & 6 deletions src/tools/updateMonitoringFactTables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,8 @@ const updateMonitoringFactTables = async () => {
ms."standardId" standard_id,
ms.citation,
ms.text standard_text,
NULLIF(TRIM(ms.guidance),'') guidance_category
NULLIF(TRIM(ms.guidance),'') guidance_category,
COALESCE(NULLIF(TRIM(mf.source),''), NULLIF(TRIM(ms.guidance),'')) calculated_category
FROM all_reviews
JOIN "MonitoringFindingHistories" mfh
ON review_uuid = mfh."reviewId"
Expand Down Expand Up @@ -337,6 +338,7 @@ const updateMonitoringFactTables = async () => {
citation,
standard_text,
guidance_category,
calculated_category,
review_uuid latest_review_uuid,
mfh.narrative latest_narrative,
mfh.determination latest_determination,
Expand Down Expand Up @@ -381,6 +383,7 @@ const updateMonitoringFactTables = async () => {
citation,
standard_text,
guidance_category,
calculated_category,
latest_review_uuid,
latest_narrative,
latest_determination,
Expand Down Expand Up @@ -421,6 +424,7 @@ const updateMonitoringFactTables = async () => {
citation,
standard_text,
guidance_category,
calculated_category,
review_uuid initial_review_uuid,
mfh.narrative initial_narrative,
mfh.determination initial_determination,
Expand Down Expand Up @@ -569,12 +573,28 @@ const updateMonitoringFactTables = async () => {
)
;

-- TODO(TTAHUB-5287): Remove once updateMonitoringFactTables is called after all migrations
-- run rather than within them. This guard is required because migration
-- 20260429220319-expand_monitoring_fact_table_columns calls this function before
-- 20260602001049-add_calculated_category_to_citations runs.
DO $add_calculated_category$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'Citations' AND column_name = 'calculated_category'
) THEN
ALTER TABLE "Citations"
ADD COLUMN IF NOT EXISTS calculated_category TEXT;
END IF;
END
$add_calculated_category$;

-- Categories upsert
-- One row per unique guidance_category value seen across all active Citations
-- One row per unique calculated_category value seen across all active Citations
INSERT INTO "FindingCategories" (name, "createdAt")
SELECT DISTINCT guidance_category, NOW()
SELECT DISTINCT calculated_category, NOW()
FROM full_citations
WHERE guidance_category IS NOT NULL
WHERE calculated_category IS NOT NULL
ON CONFLICT (name)
DO UPDATE SET
"updatedAt" = NOW(),
Expand All @@ -589,7 +609,7 @@ const updateMonitoringFactTables = async () => {
WHERE "deletedAt" IS NULL
AND NOT EXISTS (
SELECT 1 FROM full_citations fc
WHERE fc.guidance_category = "FindingCategories".name
WHERE fc.calculated_category = "FindingCategories".name
);

-- Citations upsert
Expand All @@ -610,6 +630,7 @@ const updateMonitoringFactTables = async () => {
citation,
standard_text,
guidance_category,
calculated_category,
"findingCategoryId",
initial_review_uuid,
initial_narrative,
Expand Down Expand Up @@ -640,6 +661,7 @@ const updateMonitoringFactTables = async () => {
fc.citation,
fc.standard_text,
fc.guidance_category,
fc.calculated_category,
cat.id,
fc.initial_review_uuid,
fc.initial_narrative,
Expand All @@ -654,7 +676,7 @@ const updateMonitoringFactTables = async () => {
NOW()
FROM full_citations fc
LEFT JOIN "FindingCategories" cat
ON fc.guidance_category = cat.name
ON fc.calculated_category = cat.name
AND cat."deletedAt" IS NULL
ON CONFLICT (finding_uuid)
DO UPDATE SET
Expand All @@ -673,6 +695,7 @@ const updateMonitoringFactTables = async () => {
citation = EXCLUDED.citation,
standard_text = EXCLUDED.standard_text,
guidance_category = EXCLUDED.guidance_category,
calculated_category = EXCLUDED.calculated_category,
"findingCategoryId" = EXCLUDED."findingCategoryId",
initial_review_uuid = EXCLUDED.initial_review_uuid,
initial_narrative = EXCLUDED.initial_narrative,
Expand Down Expand Up @@ -702,6 +725,7 @@ const updateMonitoringFactTables = async () => {
OR "Citations".citation IS DISTINCT FROM EXCLUDED.citation
OR "Citations".standard_text IS DISTINCT FROM EXCLUDED.standard_text
OR "Citations".guidance_category IS DISTINCT FROM EXCLUDED.guidance_category
OR "Citations".calculated_category IS DISTINCT FROM EXCLUDED.calculated_category
OR "Citations"."findingCategoryId" IS DISTINCT FROM EXCLUDED."findingCategoryId"
OR "Citations".initial_review_uuid IS DISTINCT FROM EXCLUDED.initial_review_uuid
OR "Citations".initial_narrative IS DISTINCT FROM EXCLUDED.initial_narrative
Expand Down
5 changes: 2 additions & 3 deletions src/widgets/monitoring/monitoringTta.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,7 @@ describe('monitoringTta', () => {
calculated_status: status,
raw_finding_type: findingType,
calculated_finding_type: findingType,
guidance_category: category,
source_category: category,
calculated_category: category,
active,
last_review_delivered: true,
});
Expand Down Expand Up @@ -1700,7 +1699,7 @@ describe('monitoringTta', () => {
citation: '1302.50',
calculated_status: 'Active',
calculated_finding_type: 'Deficiency',
guidance_category: 'Health',
calculated_category: 'Health',
grantCitations: [
{
grantId: 77,
Expand Down
Loading
Loading