From 588b20f8fd7530a7897582bff640d2d6220331a6 Mon Sep 17 00:00:00 2001 From: Madjo Diapena Date: Wed, 25 Mar 2026 00:59:41 -0400 Subject: [PATCH 1/2] fix elgr step 6 last month ended orders --- .../sync/eligibility/eligibility.queries.ts | 8 +-- .../steps/step6-order-payment-check.spec.ts | 59 ++++++++++++++++++- .../rules/steps/step6-order-payment-check.ts | 25 +++++--- 3 files changed, 80 insertions(+), 12 deletions(-) diff --git a/backend/src/sync/eligibility/eligibility.queries.ts b/backend/src/sync/eligibility/eligibility.queries.ts index 4c84c75a..a61e68da 100644 --- a/backend/src/sync/eligibility/eligibility.queries.ts +++ b/backend/src/sync/eligibility/eligibility.queries.ts @@ -108,12 +108,12 @@ const CHANGED_CONTACTS_CTE = ` * - changed_contacts (incremental only): contacts with recently changed data * - eligible_cases: all case rows from staging (filtered by change detection in incremental mode) * - latest_legal_auth: most recent legal authority per person (DISTINCT ON X_CONTACT_NUM) - * - icm_placements_agg: active/interrupted ICM placements grouped by contact + * - icm_placements_agg: active/interrupted/ended/closed ICM placements grouped by contact * - icm_orders_agg: ICM orders grouped by contact (via placement -> agreement) * - icm_agreements_agg: ICM agreements grouped by contact (via placement) * - mis_payments_agg: MIS payments grouped by contact (via contract -> placement -> PERSON_ID_MIS) * - mis_contracts_agg: MIS contracts grouped by contact (via placement -> PERSON_ID_MIS) - * - mis_placements_agg: active/interrupted/ended MIS placements grouped by contact (via PERSON_ID_MIS) + * - mis_placements_agg: active/interrupted/ended/closed MIS placements grouped by contact (via PERSON_ID_MIS) * * Join keys: * - ICM data joins on CONTACT_ROW_ID (table-to-table), aggregated by X_CONTACT_NUM (person) @@ -190,7 +190,7 @@ export function buildLoadContactProfilesSql( )) AS data FROM stg_icm_placements icm_plc INNER JOIN eligible_cases ON eligible_cases.ROW_ID = icm_plc.CASE_ROW_ID - WHERE UPPER(TRIM(icm_plc.X_STATUS)) IN ('ACTIVE', 'INTERRUPTED') + WHERE UPPER(TRIM(icm_plc.X_STATUS)) IN ('ACTIVE', 'INTERRUPTED', 'ENDED', 'CLOSED') GROUP BY eligible_cases.X_CONTACT_NUM ), @@ -339,7 +339,7 @@ export function buildLoadContactProfilesSql( FROM stg_mis_placements mis_plc INNER JOIN eligible_cases ON mis_plc.person_id_mis = eligible_cases.PERSON_ID_MIS - WHERE UPPER(TRIM(mis_plc.status)) IN ('ACTIVE', 'INTERRUPTED', 'ENDED') + WHERE UPPER(TRIM(mis_plc.status)) IN ('ACTIVE', 'INTERRUPTED', 'ENDED', 'CLOSED') GROUP BY eligible_cases.X_CONTACT_NUM ) diff --git a/backend/src/sync/eligibility/rules/steps/step6-order-payment-check.spec.ts b/backend/src/sync/eligibility/rules/steps/step6-order-payment-check.spec.ts index c273eaa1..ad6621c2 100644 --- a/backend/src/sync/eligibility/rules/steps/step6-order-payment-check.spec.ts +++ b/backend/src/sync/eligibility/rules/steps/step6-order-payment-check.spec.ts @@ -115,7 +115,6 @@ describe('step6_OrderPaymentCheck', () => { }) it('should check order effective start date is in previous month', () => { - // REF_DATE is Feb 2026, so previous month is Jan 2026 const ctx = makeCtx({ orders: [makeOrder({ effectiveStartDate: new Date('2025-12-15') })], }) @@ -169,4 +168,62 @@ describe('step6_OrderPaymentCheck', () => { const result = step6_OrderPaymentCheck.evaluate(ctx) expect(result!.step).toBe(8) }) + + describe('ICM precedence over MIS', () => { + it('should use ICM orders when ICM has prev-month orders, ignoring MIS', () => { + const ctx = makeCtx({ + orders: [ + makeOrder({ source: 'ICM', amount: 1000 }), + makeOrder({ source: 'MIS', amount: 1600 }), + ], + }) + const result = step6_OrderPaymentCheck.evaluate(ctx) + expect(result!.step).toBe(8) + }) + + it('should fall back to MIS when no ICM orders in previous month', () => { + const ctx = makeCtx({ + orders: [ + makeOrder({ source: 'ICM', effectiveStartDate: new Date('2025-12-15') }), + makeOrder({ source: 'MIS', amount: 1600 }), + ], + }) + const result = step6_OrderPaymentCheck.evaluate(ctx) + expect(result!.step).toBe(7) + }) + + it('should fall back to MIS when no ICM orders exist at all', () => { + const ctx = makeCtx({ + orders: [makeOrder({ source: 'MIS', amount: 1600 })], + }) + const result = step6_OrderPaymentCheck.evaluate(ctx) + expect(result!.step).toBe(7) + }) + + it('should only use prev-month ICM orders when mixed with non-prev-month ICM orders', () => { + const ctx = makeCtx({ + orders: [ + makeOrder({ source: 'ICM', effectiveStartDate: new Date('2025-12-15'), amount: 2000 }), + makeOrder({ source: 'ICM', amount: 1000 }), + makeOrder({ source: 'MIS', amount: 1600 }), + ], + }) + const result = step6_OrderPaymentCheck.evaluate(ctx) + expect(result!.step).toBe(8) + }) + + it('should use ICM even when ICM fails and MIS would succeed', () => { + const ctx = makeCtx( + { + orders: [ + makeOrder({ source: 'ICM', orderType: 'Invalid Type' }), + makeOrder({ source: 'MIS', amount: 2000 }), + ], + }, + { hasNonPlacement: false }, + ) + const result = step6_OrderPaymentCheck.evaluate(ctx) + expect(result!.step).toBe(9) + }) + }) }) diff --git a/backend/src/sync/eligibility/rules/steps/step6-order-payment-check.ts b/backend/src/sync/eligibility/rules/steps/step6-order-payment-check.ts index 1ae489c5..f1709327 100644 --- a/backend/src/sync/eligibility/rules/steps/step6-order-payment-check.ts +++ b/backend/src/sync/eligibility/rules/steps/step6-order-payment-check.ts @@ -9,12 +9,14 @@ import { step9_UpdateNotEligible } from './step9-update-not-eligible' /** * STEP 6: Order (ICM) / Payment (MIS) check * - * From orders linked to valid placements via contractNumber (MIS) or agreementRowId (ICM): - * 1. Filter to previous month orders only - * 2. Check ALL orders against the 4 criteria (type, status, date, amount) + * ICM takes precedence over MIS (per FDD): + * 1. Link orders to eligible placements via contractNumber or agreementRowId + * 2. Check ICM orders for previous month first + * 3. Only fall back to MIS payments if no ICM orders found in previous month + * 4. Evaluate against 4 criteria (type, status, date, amount) * * Any order matches all 4 -> Step 7 (eligible) - * Best match only fails on amount (or no order found) -> Step 8 (eligible_tbd) + * Best match only fails on amount -> Step 8 (eligible_tbd) * More than one criterion fails -> Step 8 if hasNonPlacement, Step 9 otherwise * No matching orders at all -> Step 8 (eligible_tbd) */ @@ -38,9 +40,18 @@ export const step6_OrderPaymentCheck: EligibilityRule = { } const prevMonth = getPreviousMonth(ctx.referenceDate) - const previousMonthOrders = matchingOrders.filter((order) => - isInMonth(order.effectiveStartDate, prevMonth), - ) + + // ICM first: check ICM orders in previous month + const icmOrders = matchingOrders.filter((order) => order.source === 'ICM') + const icmPrevMonth = icmOrders.filter((order) => isInMonth(order.effectiveStartDate, prevMonth)) + + // Fall back to MIS only if no ICM orders in previous month + const previousMonthOrders = + icmPrevMonth.length > 0 + ? icmPrevMonth + : matchingOrders + .filter((order) => order.source === 'MIS') + .filter((order) => isInMonth(order.effectiveStartDate, prevMonth)) if (previousMonthOrders.length === 0) { return hasNonPlacement From ddbf91e8172c0880884e583247d042fc115c16ad Mon Sep 17 00:00:00 2001 From: Madjo Diapena Date: Wed, 25 Mar 2026 01:24:52 -0400 Subject: [PATCH 2/2] add var rate label to order type --- backend/src/sync/eligibility/eligibility.config.ts | 1 + .../rules/steps/step6-order-payment-check.spec.ts | 14 +++++++++----- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/backend/src/sync/eligibility/eligibility.config.ts b/backend/src/sync/eligibility/eligibility.config.ts index b4b21957..76a6614d 100644 --- a/backend/src/sync/eligibility/eligibility.config.ts +++ b/backend/src/sync/eligibility/eligibility.config.ts @@ -28,6 +28,7 @@ export const ELIGIBILITY_CONFIG = { 'MAINTENANCE PAYMENT', 'FIXED RATE', 'VARIABLE RATE', + 'VAR RATE', ] as readonly string[], ELIGIBLE_ORDER_STATUSES: ['CLOSED', 'PROCESSED'] as readonly string[], MIN_ORDER_AMOUNT: 1549.2, diff --git a/backend/src/sync/eligibility/rules/steps/step6-order-payment-check.spec.ts b/backend/src/sync/eligibility/rules/steps/step6-order-payment-check.spec.ts index ad6621c2..953d2a0b 100644 --- a/backend/src/sync/eligibility/rules/steps/step6-order-payment-check.spec.ts +++ b/backend/src/sync/eligibility/rules/steps/step6-order-payment-check.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import { ContactProfile, OrderRecord } from '../../eligibility.types' -import { makeContact, makeOrder as makeBaseOrder } from '../../test-helpers' +import { makeOrder as makeBaseOrder, makeContact } from '../../test-helpers' import { EligibilityContext } from '../rule.interface' import { step6_OrderPaymentCheck } from './step6-order-payment-check' @@ -90,12 +90,16 @@ describe('step6_OrderPaymentCheck', () => { expect(result!.step).toBe(7) }) - it('should accept MIS order types (Fixed Rate, Variable Rate)', () => { - const ctx = makeCtx({ + it('should accept MIS order types (Fixed Rate, Variable Rate, Var Rate)', () => { + const fixedRate = makeCtx({ orders: [makeOrder({ orderType: 'Fixed Rate', source: 'MIS' })], }) - const result = step6_OrderPaymentCheck.evaluate(ctx) - expect(result!.step).toBe(7) + expect(step6_OrderPaymentCheck.evaluate(fixedRate)!.step).toBe(7) + + const varRate = makeCtx({ + orders: [makeOrder({ orderType: 'Var Rate', source: 'MIS' })], + }) + expect(step6_OrderPaymentCheck.evaluate(varRate)!.step).toBe(7) }) it('should handle variant casing and whitespace in order type and status', () => {