From 687bfe9c2b2118900485cb4b497a1ecbac3e2061 Mon Sep 17 00:00:00 2001 From: Preston Cabe Date: Tue, 6 Oct 2026 13:23:14 -0400 Subject: [PATCH 01/13] Add remaining Philadelphia property tax benefits and eligibility checks Implement Low-Income Tax Freeze, LOOP, and RETIP with reusable income, age, ownership, assessment, abatement, and payment-plan checks. Publish builder-compatible compositions for all six benefits, document policy inputs and sources, and cover eligibility boundaries with Bruno tests. Treat an explicitly empty enrollment list as no enrollments while preserving unknown for unanswered inputs. --- library-api/PROPERTY_TAX_BENEFITS.md | 64 ++ library-api/README.md | 4 + library-api/src/main/resources/BDT.dmn | 30 + .../main/resources/benefits/pa/phl/loop.dmn | 593 ++++++++++++++++++ .../benefits/pa/phl/low-income-tax-freeze.dmn | 313 +++++++++ .../main/resources/benefits/pa/phl/retip.dmn | 333 ++++++++++ .../age/applicant-or-spouse-min-age.dmn | 163 +++++ .../enrollment/person-enrolled-in-benefit.dmn | 4 +- .../main/resources/checks/income/Income.dmn | 5 + .../resources/checks/income/loop-income.dmn | 68 ++ .../resources/checks/income/retip-income.dmn | 68 ++ .../checks/income/tax-freeze-income.dmn | 72 +++ .../retip-age-or-income-requirement.dmn | 126 ++++ .../residence/min-years-owner-occupant.dmn | 85 +++ .../no-disqualifying-tax-abatement.dmn | 69 ++ .../property-assessment-increase.dmn | 122 ++++ ...property-taxes-current-or-payment-plan.dmn | 87 +++ .../api/DynamicEndpointPatternTest.java | 5 +- .../library/api/OpenAPISchemaPatternTest.java | 46 ++ .../pa/phl/PhlLoop/Already enrolled.bru | 43 ++ .../Eligible with five year increase.bru | 43 ++ .../PhlLoop/Eligible with payment plan.bru | 44 ++ .../bdt/benefits/pa/phl/PhlLoop/Eligible.bru | 42 ++ .../phl/PhlLoop/Fails AssessmentIncrease.bru | 43 ++ .../PhlLoop/Fails MeetsIncomeRequirement.bru | 43 ++ .../Fails NoDisqualifyingTaxAbatement.bru | 43 ++ .../pa/phl/PhlLoop/Fails NotOnHomestead.bru | 43 ++ .../Fails TaxesCurrentOrPaymentPlan.bru | 44 ++ .../PhlLoop/Fails TenYearsOwnerOccupant.bru | 43 ++ .../benefits/pa/phl/PhlLoop/Ineligible.bru | 43 ++ .../benefits/pa/phl/PhlLoop/Smoke Test.bru | 27 + .../bdt/benefits/pa/phl/PhlLoop/folder.bru | 8 + .../Already enrolled.bru | 37 ++ .../pa/phl/PhlLowIncomeTaxFreeze/Eligible.bru | 36 ++ .../Fails MeetsIncomeRequirement.bru | 37 ++ .../phl/PhlLowIncomeTaxFreeze/Ineligible.bru | 37 ++ .../Married at limit.bru | 36 ++ .../phl/PhlLowIncomeTaxFreeze/Smoke Test.bru | 27 + .../pa/phl/PhlLowIncomeTaxFreeze/folder.bru | 8 + .../pa/phl/PhlRetip/Already enrolled.bru | 41 ++ .../pa/phl/PhlRetip/Eligible by income.bru | 44 ++ .../Eligible by live in spouse age.bru | 55 ++ .../bdt/benefits/pa/phl/PhlRetip/Eligible.bru | 40 ++ .../Fails MeetsAgeOrIncomeRequirement.bru | 44 ++ .../benefits/pa/phl/PhlRetip/Ineligible.bru | 41 ++ .../benefits/pa/phl/PhlRetip/Smoke Test.bru | 27 + .../bdt/benefits/pa/phl/PhlRetip/folder.bru | 8 + .../age/ApplicantOrSpouseMinAge/Fail.bru | 34 + .../Live in spouse.bru | 47 ++ .../age/ApplicantOrSpouseMinAge/Null.bru | 27 + .../age/ApplicantOrSpouseMinAge/Pass.bru | 33 + .../Spouse lives elsewhere.bru | 47 ++ .../Spouse residency unknown.bru | 47 ++ .../Spouse unknown.bru | 33 + .../Unrelated senior.bru | 38 ++ .../age/ApplicantOrSpouseMinAge/folder.bru | 8 + .../Enrollments empty.bru | 2 +- .../Enrollments empty.bru | 2 +- .../bdt/checks/income/LoopIncome/Fail.bru | 25 + .../income/LoopIncome/Invalid income.bru | 25 + .../bdt/checks/income/LoopIncome/Null.bru | 24 + .../bdt/checks/income/LoopIncome/Pass.bru | 25 + .../income/LoopIncome/Size 1 at cap.bru | 25 + .../income/LoopIncome/Size 1 below cap.bru | 25 + .../income/LoopIncome/Size 10 at cap.bru | 25 + .../income/LoopIncome/Size 10 below cap.bru | 25 + .../income/LoopIncome/Size 2 at cap.bru | 25 + .../income/LoopIncome/Size 2 below cap.bru | 25 + .../income/LoopIncome/Size 3 at cap.bru | 25 + .../income/LoopIncome/Size 3 below cap.bru | 25 + .../income/LoopIncome/Size 4 at cap.bru | 25 + .../income/LoopIncome/Size 4 below cap.bru | 25 + .../income/LoopIncome/Size 5 at cap.bru | 25 + .../income/LoopIncome/Size 5 below cap.bru | 25 + .../income/LoopIncome/Size 6 at cap.bru | 25 + .../income/LoopIncome/Size 6 below cap.bru | 25 + .../income/LoopIncome/Size 7 at cap.bru | 25 + .../income/LoopIncome/Size 7 below cap.bru | 25 + .../income/LoopIncome/Size 8 at cap.bru | 25 + .../income/LoopIncome/Size 8 below cap.bru | 25 + .../income/LoopIncome/Size 9 at cap.bru | 25 + .../income/LoopIncome/Size 9 below cap.bru | 25 + .../Unknown or unsupported size -1.bru | 25 + .../Unknown or unsupported size 0.bru | 25 + .../Unknown or unsupported size 1.5.bru | 25 + .../Unknown or unsupported size 11.bru | 25 + .../Unknown or unsupported size None.bru | 25 + .../bdt/checks/income/LoopIncome/folder.bru | 8 + .../bdt/checks/income/RetipIncome/Fail.bru | 25 + .../income/RetipIncome/Invalid income.bru | 25 + .../bdt/checks/income/RetipIncome/Null.bru | 24 + .../bdt/checks/income/RetipIncome/Pass.bru | 25 + .../income/RetipIncome/Size 1 at cap.bru | 25 + .../income/RetipIncome/Size 1 below cap.bru | 25 + .../income/RetipIncome/Size 2 at cap.bru | 25 + .../income/RetipIncome/Size 2 below cap.bru | 25 + .../income/RetipIncome/Size 3 at cap.bru | 25 + .../income/RetipIncome/Size 3 below cap.bru | 25 + .../income/RetipIncome/Size 4 at cap.bru | 25 + .../income/RetipIncome/Size 4 below cap.bru | 25 + .../income/RetipIncome/Size 5 at cap.bru | 25 + .../income/RetipIncome/Size 5 below cap.bru | 25 + .../income/RetipIncome/Size 6 at cap.bru | 25 + .../income/RetipIncome/Size 6 below cap.bru | 25 + .../income/RetipIncome/Size 7 at cap.bru | 25 + .../income/RetipIncome/Size 7 below cap.bru | 25 + .../income/RetipIncome/Size 8 at cap.bru | 25 + .../income/RetipIncome/Size 8 below cap.bru | 25 + .../Unknown or unsupported size -1.bru | 25 + .../Unknown or unsupported size 0.bru | 25 + .../Unknown or unsupported size 1.5.bru | 25 + .../Unknown or unsupported size 9.bru | 25 + .../Unknown or unsupported size None.bru | 25 + .../bdt/checks/income/RetipIncome/folder.bru | 8 + .../checks/income/TaxFreezeIncome/Fail.bru | 27 + .../income/TaxFreezeIncome/Income unknown.bru | 26 + .../income/TaxFreezeIncome/Invalid income.bru | 27 + .../TaxFreezeIncome/Married at limit.bru | 27 + .../TaxFreezeIncome/Married over limit.bru | 27 + .../checks/income/TaxFreezeIncome/Null.bru | 27 + .../checks/income/TaxFreezeIncome/Pass.bru | 27 + .../income/TaxFreezeIncome/Zero income.bru | 27 + .../checks/income/TaxFreezeIncome/folder.bru | 8 + library-api/test/bdt/checks/income/folder.bru | 8 + .../RetipAgeOrIncomeRequirement/Fail.bru | 33 + .../Late spouse does not qualify.bru | 36 ++ .../Live in spouse.bru | 44 ++ .../Low income without age.bru | 25 + .../RetipAgeOrIncomeRequirement/Null.bru | 22 + .../RetipAgeOrIncomeRequirement/Pass.bru | 30 + .../Senior above income limit.bru | 32 + .../RetipAgeOrIncomeRequirement/folder.bru | 8 + .../residence/MinYearsOwnerOccupant/Fail.bru | 27 + .../MinYearsOwnerOccupant/Invalid years.bru | 27 + .../Longer residency.bru | 27 + .../residence/MinYearsOwnerOccupant/Null.bru | 25 + .../residence/MinYearsOwnerOccupant/Pass.bru | 27 + .../MinYearsOwnerOccupant/folder.bru | 8 + .../NoDisqualifyingTaxAbatement/Fail.bru | 26 + .../NoDisqualifyingTaxAbatement/Null.bru | 26 + .../NoDisqualifyingTaxAbatement/Pass.bru | 26 + .../NoDisqualifyingTaxAbatement/folder.bru | 8 + .../PropertyAssessmentIncrease/Fail.bru | 30 + .../Five year only.bru | 29 + .../Five year path.bru | 30 + .../PropertyAssessmentIncrease/Null.bru | 26 + .../PropertyAssessmentIncrease/Pass.bru | 29 + .../Unknown five year history.bru | 29 + .../Zero baseline.bru | 30 + .../PropertyAssessmentIncrease/folder.bru | 8 + .../Delinquent with unknown plan.bru | 26 + .../Fail.bru | 27 + .../Null.bru | 27 + .../Pass.bru | 26 + .../Payment plan.bru | 27 + .../Plan with unknown delinquency.bru | 26 + .../folder.bru | 8 + 157 files changed, 6027 insertions(+), 5 deletions(-) create mode 100644 library-api/PROPERTY_TAX_BENEFITS.md create mode 100644 library-api/src/main/resources/benefits/pa/phl/loop.dmn create mode 100644 library-api/src/main/resources/benefits/pa/phl/low-income-tax-freeze.dmn create mode 100644 library-api/src/main/resources/benefits/pa/phl/retip.dmn create mode 100644 library-api/src/main/resources/checks/age/applicant-or-spouse-min-age.dmn create mode 100644 library-api/src/main/resources/checks/income/Income.dmn create mode 100644 library-api/src/main/resources/checks/income/loop-income.dmn create mode 100644 library-api/src/main/resources/checks/income/retip-income.dmn create mode 100644 library-api/src/main/resources/checks/income/tax-freeze-income.dmn create mode 100644 library-api/src/main/resources/checks/internal/retip-age-or-income-requirement.dmn create mode 100644 library-api/src/main/resources/checks/residence/min-years-owner-occupant.dmn create mode 100644 library-api/src/main/resources/checks/residence/no-disqualifying-tax-abatement.dmn create mode 100644 library-api/src/main/resources/checks/residence/property-assessment-increase.dmn create mode 100644 library-api/src/main/resources/checks/residence/property-taxes-current-or-payment-plan.dmn create mode 100644 library-api/test/bdt/benefits/pa/phl/PhlLoop/Already enrolled.bru create mode 100644 library-api/test/bdt/benefits/pa/phl/PhlLoop/Eligible with five year increase.bru create mode 100644 library-api/test/bdt/benefits/pa/phl/PhlLoop/Eligible with payment plan.bru create mode 100644 library-api/test/bdt/benefits/pa/phl/PhlLoop/Eligible.bru create mode 100644 library-api/test/bdt/benefits/pa/phl/PhlLoop/Fails AssessmentIncrease.bru create mode 100644 library-api/test/bdt/benefits/pa/phl/PhlLoop/Fails MeetsIncomeRequirement.bru create mode 100644 library-api/test/bdt/benefits/pa/phl/PhlLoop/Fails NoDisqualifyingTaxAbatement.bru create mode 100644 library-api/test/bdt/benefits/pa/phl/PhlLoop/Fails NotOnHomestead.bru create mode 100644 library-api/test/bdt/benefits/pa/phl/PhlLoop/Fails TaxesCurrentOrPaymentPlan.bru create mode 100644 library-api/test/bdt/benefits/pa/phl/PhlLoop/Fails TenYearsOwnerOccupant.bru create mode 100644 library-api/test/bdt/benefits/pa/phl/PhlLoop/Ineligible.bru create mode 100644 library-api/test/bdt/benefits/pa/phl/PhlLoop/Smoke Test.bru create mode 100644 library-api/test/bdt/benefits/pa/phl/PhlLoop/folder.bru create mode 100644 library-api/test/bdt/benefits/pa/phl/PhlLowIncomeTaxFreeze/Already enrolled.bru create mode 100644 library-api/test/bdt/benefits/pa/phl/PhlLowIncomeTaxFreeze/Eligible.bru create mode 100644 library-api/test/bdt/benefits/pa/phl/PhlLowIncomeTaxFreeze/Fails MeetsIncomeRequirement.bru create mode 100644 library-api/test/bdt/benefits/pa/phl/PhlLowIncomeTaxFreeze/Ineligible.bru create mode 100644 library-api/test/bdt/benefits/pa/phl/PhlLowIncomeTaxFreeze/Married at limit.bru create mode 100644 library-api/test/bdt/benefits/pa/phl/PhlLowIncomeTaxFreeze/Smoke Test.bru create mode 100644 library-api/test/bdt/benefits/pa/phl/PhlLowIncomeTaxFreeze/folder.bru create mode 100644 library-api/test/bdt/benefits/pa/phl/PhlRetip/Already enrolled.bru create mode 100644 library-api/test/bdt/benefits/pa/phl/PhlRetip/Eligible by income.bru create mode 100644 library-api/test/bdt/benefits/pa/phl/PhlRetip/Eligible by live in spouse age.bru create mode 100644 library-api/test/bdt/benefits/pa/phl/PhlRetip/Eligible.bru create mode 100644 library-api/test/bdt/benefits/pa/phl/PhlRetip/Fails MeetsAgeOrIncomeRequirement.bru create mode 100644 library-api/test/bdt/benefits/pa/phl/PhlRetip/Ineligible.bru create mode 100644 library-api/test/bdt/benefits/pa/phl/PhlRetip/Smoke Test.bru create mode 100644 library-api/test/bdt/benefits/pa/phl/PhlRetip/folder.bru create mode 100644 library-api/test/bdt/checks/age/ApplicantOrSpouseMinAge/Fail.bru create mode 100644 library-api/test/bdt/checks/age/ApplicantOrSpouseMinAge/Live in spouse.bru create mode 100644 library-api/test/bdt/checks/age/ApplicantOrSpouseMinAge/Null.bru create mode 100644 library-api/test/bdt/checks/age/ApplicantOrSpouseMinAge/Pass.bru create mode 100644 library-api/test/bdt/checks/age/ApplicantOrSpouseMinAge/Spouse lives elsewhere.bru create mode 100644 library-api/test/bdt/checks/age/ApplicantOrSpouseMinAge/Spouse residency unknown.bru create mode 100644 library-api/test/bdt/checks/age/ApplicantOrSpouseMinAge/Spouse unknown.bru create mode 100644 library-api/test/bdt/checks/age/ApplicantOrSpouseMinAge/Unrelated senior.bru create mode 100644 library-api/test/bdt/checks/age/ApplicantOrSpouseMinAge/folder.bru create mode 100644 library-api/test/bdt/checks/income/LoopIncome/Fail.bru create mode 100644 library-api/test/bdt/checks/income/LoopIncome/Invalid income.bru create mode 100644 library-api/test/bdt/checks/income/LoopIncome/Null.bru create mode 100644 library-api/test/bdt/checks/income/LoopIncome/Pass.bru create mode 100644 library-api/test/bdt/checks/income/LoopIncome/Size 1 at cap.bru create mode 100644 library-api/test/bdt/checks/income/LoopIncome/Size 1 below cap.bru create mode 100644 library-api/test/bdt/checks/income/LoopIncome/Size 10 at cap.bru create mode 100644 library-api/test/bdt/checks/income/LoopIncome/Size 10 below cap.bru create mode 100644 library-api/test/bdt/checks/income/LoopIncome/Size 2 at cap.bru create mode 100644 library-api/test/bdt/checks/income/LoopIncome/Size 2 below cap.bru create mode 100644 library-api/test/bdt/checks/income/LoopIncome/Size 3 at cap.bru create mode 100644 library-api/test/bdt/checks/income/LoopIncome/Size 3 below cap.bru create mode 100644 library-api/test/bdt/checks/income/LoopIncome/Size 4 at cap.bru create mode 100644 library-api/test/bdt/checks/income/LoopIncome/Size 4 below cap.bru create mode 100644 library-api/test/bdt/checks/income/LoopIncome/Size 5 at cap.bru create mode 100644 library-api/test/bdt/checks/income/LoopIncome/Size 5 below cap.bru create mode 100644 library-api/test/bdt/checks/income/LoopIncome/Size 6 at cap.bru create mode 100644 library-api/test/bdt/checks/income/LoopIncome/Size 6 below cap.bru create mode 100644 library-api/test/bdt/checks/income/LoopIncome/Size 7 at cap.bru create mode 100644 library-api/test/bdt/checks/income/LoopIncome/Size 7 below cap.bru create mode 100644 library-api/test/bdt/checks/income/LoopIncome/Size 8 at cap.bru create mode 100644 library-api/test/bdt/checks/income/LoopIncome/Size 8 below cap.bru create mode 100644 library-api/test/bdt/checks/income/LoopIncome/Size 9 at cap.bru create mode 100644 library-api/test/bdt/checks/income/LoopIncome/Size 9 below cap.bru create mode 100644 library-api/test/bdt/checks/income/LoopIncome/Unknown or unsupported size -1.bru create mode 100644 library-api/test/bdt/checks/income/LoopIncome/Unknown or unsupported size 0.bru create mode 100644 library-api/test/bdt/checks/income/LoopIncome/Unknown or unsupported size 1.5.bru create mode 100644 library-api/test/bdt/checks/income/LoopIncome/Unknown or unsupported size 11.bru create mode 100644 library-api/test/bdt/checks/income/LoopIncome/Unknown or unsupported size None.bru create mode 100644 library-api/test/bdt/checks/income/LoopIncome/folder.bru create mode 100644 library-api/test/bdt/checks/income/RetipIncome/Fail.bru create mode 100644 library-api/test/bdt/checks/income/RetipIncome/Invalid income.bru create mode 100644 library-api/test/bdt/checks/income/RetipIncome/Null.bru create mode 100644 library-api/test/bdt/checks/income/RetipIncome/Pass.bru create mode 100644 library-api/test/bdt/checks/income/RetipIncome/Size 1 at cap.bru create mode 100644 library-api/test/bdt/checks/income/RetipIncome/Size 1 below cap.bru create mode 100644 library-api/test/bdt/checks/income/RetipIncome/Size 2 at cap.bru create mode 100644 library-api/test/bdt/checks/income/RetipIncome/Size 2 below cap.bru create mode 100644 library-api/test/bdt/checks/income/RetipIncome/Size 3 at cap.bru create mode 100644 library-api/test/bdt/checks/income/RetipIncome/Size 3 below cap.bru create mode 100644 library-api/test/bdt/checks/income/RetipIncome/Size 4 at cap.bru create mode 100644 library-api/test/bdt/checks/income/RetipIncome/Size 4 below cap.bru create mode 100644 library-api/test/bdt/checks/income/RetipIncome/Size 5 at cap.bru create mode 100644 library-api/test/bdt/checks/income/RetipIncome/Size 5 below cap.bru create mode 100644 library-api/test/bdt/checks/income/RetipIncome/Size 6 at cap.bru create mode 100644 library-api/test/bdt/checks/income/RetipIncome/Size 6 below cap.bru create mode 100644 library-api/test/bdt/checks/income/RetipIncome/Size 7 at cap.bru create mode 100644 library-api/test/bdt/checks/income/RetipIncome/Size 7 below cap.bru create mode 100644 library-api/test/bdt/checks/income/RetipIncome/Size 8 at cap.bru create mode 100644 library-api/test/bdt/checks/income/RetipIncome/Size 8 below cap.bru create mode 100644 library-api/test/bdt/checks/income/RetipIncome/Unknown or unsupported size -1.bru create mode 100644 library-api/test/bdt/checks/income/RetipIncome/Unknown or unsupported size 0.bru create mode 100644 library-api/test/bdt/checks/income/RetipIncome/Unknown or unsupported size 1.5.bru create mode 100644 library-api/test/bdt/checks/income/RetipIncome/Unknown or unsupported size 9.bru create mode 100644 library-api/test/bdt/checks/income/RetipIncome/Unknown or unsupported size None.bru create mode 100644 library-api/test/bdt/checks/income/RetipIncome/folder.bru create mode 100644 library-api/test/bdt/checks/income/TaxFreezeIncome/Fail.bru create mode 100644 library-api/test/bdt/checks/income/TaxFreezeIncome/Income unknown.bru create mode 100644 library-api/test/bdt/checks/income/TaxFreezeIncome/Invalid income.bru create mode 100644 library-api/test/bdt/checks/income/TaxFreezeIncome/Married at limit.bru create mode 100644 library-api/test/bdt/checks/income/TaxFreezeIncome/Married over limit.bru create mode 100644 library-api/test/bdt/checks/income/TaxFreezeIncome/Null.bru create mode 100644 library-api/test/bdt/checks/income/TaxFreezeIncome/Pass.bru create mode 100644 library-api/test/bdt/checks/income/TaxFreezeIncome/Zero income.bru create mode 100644 library-api/test/bdt/checks/income/TaxFreezeIncome/folder.bru create mode 100644 library-api/test/bdt/checks/income/folder.bru create mode 100644 library-api/test/bdt/checks/internal/RetipAgeOrIncomeRequirement/Fail.bru create mode 100644 library-api/test/bdt/checks/internal/RetipAgeOrIncomeRequirement/Late spouse does not qualify.bru create mode 100644 library-api/test/bdt/checks/internal/RetipAgeOrIncomeRequirement/Live in spouse.bru create mode 100644 library-api/test/bdt/checks/internal/RetipAgeOrIncomeRequirement/Low income without age.bru create mode 100644 library-api/test/bdt/checks/internal/RetipAgeOrIncomeRequirement/Null.bru create mode 100644 library-api/test/bdt/checks/internal/RetipAgeOrIncomeRequirement/Pass.bru create mode 100644 library-api/test/bdt/checks/internal/RetipAgeOrIncomeRequirement/Senior above income limit.bru create mode 100644 library-api/test/bdt/checks/internal/RetipAgeOrIncomeRequirement/folder.bru create mode 100644 library-api/test/bdt/checks/residence/MinYearsOwnerOccupant/Fail.bru create mode 100644 library-api/test/bdt/checks/residence/MinYearsOwnerOccupant/Invalid years.bru create mode 100644 library-api/test/bdt/checks/residence/MinYearsOwnerOccupant/Longer residency.bru create mode 100644 library-api/test/bdt/checks/residence/MinYearsOwnerOccupant/Null.bru create mode 100644 library-api/test/bdt/checks/residence/MinYearsOwnerOccupant/Pass.bru create mode 100644 library-api/test/bdt/checks/residence/MinYearsOwnerOccupant/folder.bru create mode 100644 library-api/test/bdt/checks/residence/NoDisqualifyingTaxAbatement/Fail.bru create mode 100644 library-api/test/bdt/checks/residence/NoDisqualifyingTaxAbatement/Null.bru create mode 100644 library-api/test/bdt/checks/residence/NoDisqualifyingTaxAbatement/Pass.bru create mode 100644 library-api/test/bdt/checks/residence/NoDisqualifyingTaxAbatement/folder.bru create mode 100644 library-api/test/bdt/checks/residence/PropertyAssessmentIncrease/Fail.bru create mode 100644 library-api/test/bdt/checks/residence/PropertyAssessmentIncrease/Five year only.bru create mode 100644 library-api/test/bdt/checks/residence/PropertyAssessmentIncrease/Five year path.bru create mode 100644 library-api/test/bdt/checks/residence/PropertyAssessmentIncrease/Null.bru create mode 100644 library-api/test/bdt/checks/residence/PropertyAssessmentIncrease/Pass.bru create mode 100644 library-api/test/bdt/checks/residence/PropertyAssessmentIncrease/Unknown five year history.bru create mode 100644 library-api/test/bdt/checks/residence/PropertyAssessmentIncrease/Zero baseline.bru create mode 100644 library-api/test/bdt/checks/residence/PropertyAssessmentIncrease/folder.bru create mode 100644 library-api/test/bdt/checks/residence/PropertyTaxesCurrentOrPaymentPlan/Delinquent with unknown plan.bru create mode 100644 library-api/test/bdt/checks/residence/PropertyTaxesCurrentOrPaymentPlan/Fail.bru create mode 100644 library-api/test/bdt/checks/residence/PropertyTaxesCurrentOrPaymentPlan/Null.bru create mode 100644 library-api/test/bdt/checks/residence/PropertyTaxesCurrentOrPaymentPlan/Pass.bru create mode 100644 library-api/test/bdt/checks/residence/PropertyTaxesCurrentOrPaymentPlan/Payment plan.bru create mode 100644 library-api/test/bdt/checks/residence/PropertyTaxesCurrentOrPaymentPlan/Plan with unknown delinquency.bru create mode 100644 library-api/test/bdt/checks/residence/PropertyTaxesCurrentOrPaymentPlan/folder.bru diff --git a/library-api/PROPERTY_TAX_BENEFITS.md b/library-api/PROPERTY_TAX_BENEFITS.md new file mode 100644 index 00000000..85308dde --- /dev/null +++ b/library-api/PROPERTY_TAX_BENEFITS.md @@ -0,0 +1,64 @@ +# Philadelphia property tax benefits + +All six benefits are exposed at `POST /api/v1/benefits/pa/phl/{slug}`. +Their OpenAPI operations publish `x-bdt-benefit.checks`, including check +operation IDs, aliases, fixed parameters, and primary-person bindings, so the +builder can import their compositions. Computed alternatives are encapsulated +in checks under `checks/internal/`. + +| Benefit | Slug | Composed checks | +| --- | --- | --- | +| Homestead Exemption | `homestead-exemption` | Not already enrolled, owner-occupant, Philadelphia residence, no ten-year abatement | +| Owner-Occupied Payment Agreement | `owner-occupied-payment-agreement` | Not already enrolled, owner-occupant, Philadelphia residence, delinquent taxes | +| Senior Citizen Tax Freeze | `senior-citizen-tax-freeze` | Not already enrolled, owner-occupant, Philadelphia residence, existing applicant/spouse/late-spouse age requirement | +| Low-Income Tax Freeze | `low-income-tax-freeze` | Not already enrolled, owner-occupant, Philadelphia residence, single/married income limit | +| Longtime Owner Occupants Program (LOOP) | `loop` | Not already enrolled, owner-occupant, Philadelphia residence, ten years of ownership and occupancy, assessment increase, taxes current or payment plan, household income limit, no Homestead enrollment, no disqualifying abatement | +| Real Estate Tax Installment Plan (RETIP) | `retip` | Not already enrolled, owner-occupant, Philadelphia residence, applicant/live-in spouse age **or** household income limit | + +## New inputs + +Each model projects only the fields it uses from `BDT.tSituation`. + +| Situation field | Meaning | +| --- | --- | +| `householdIncome` | Annual income in dollars, calculated under the applicable program's income rules; the freeze uses the applicant's gross income plus the spouse's when married, while LOOP/RETIP use household income | +| `householdSize` | Family size for LOOP/RETIP income tables; a positive integer | +| `yearsOwnerOccupied` | Completed duration of ownership and occupancy of the primary residence, in years | +| `propertyAssessment` | Current assessment before exemption reductions | +| `previousYearPropertyAssessment` | Prior year's assessment before exemption reductions | +| `lowestPropertyAssessmentLastFiveYears` | Lowest assessment in the preceding five years, before exemption reductions | +| `simpleChecks.isMarried` | Whether the applicant is married; selects the freeze income cap | +| `simpleChecks.spouseLivesInHousehold` | Whether the current spouse lives with the applicant; required for the RETIP spouse age pathway | +| `simpleChecks.propertyTaxPaymentPlan` | Whether property taxes are covered by an active OOPA or installment plan | +| `simpleChecks.disqualifyingTaxAbatement` | Whether the current owner received an abatement on this property, or a previous owner who received one is a close relative; includes expired abatements | + +RETIP matches the spouse relationship to `primaryPersonId`; an unrelated senior +household member does not qualify the applicant. An empty `relationships` list +means no spouse, while an omitted list means unknown. RETIP checks age as of +today and does not use SCTF's late-spouse pathway. + +Missing information returns `null`. An alternative that is known to pass can +pass even when the other path is unknown. A failed required check makes the +benefit ineligible. Unsupported family sizes, negative income, and invalid +assessment baselines return unknown rather than extrapolating a policy rule. +An empty enrollment list means the applicant is enrolled in none of the +listed benefits; an omitted or null list means unanswered. + +## Policy sources and maintenance + +The three added benefits implement new-applicant eligibility from the city +guidance linked in issues #387, #389, and #537, verified October 5, 2026: + +- [Low-Income Tax Freeze](https://www.phila.gov/services/payments-assistance-taxes/taxes/property-and-real-estate-taxes/get-real-estate-tax-relief/tax-freeze/apply-for-the-low-income-real-estate-tax-freeze/): income at or below $33,500 single or $41,500 married, without an age restriction. +- [LOOP](https://www.phila.gov/services/payments-assistance-taxes/taxes/property-and-real-estate-taxes/get-real-estate-tax-relief/apply-for-the-longtime-owner-occupants-program-loop/): at least ten years of occupancy and a 50% one-year **or** 75% five-year assessment increase. New applicants use the 120% AMI table for family sizes 1–10, with income strictly below the cap. The 150% table for participants enrolled before 2023 is a renewal rule and is not used here. Homestead recipients must remove that exemption to enroll in LOOP. The historical owner/relative abatement rule differs from Homestead's current ten-year abatement check. +- [RETIP](https://www.phila.gov/services/payments-assistance-taxes/payment-plans-and-assistance-programs/income-based-programs-for-residents/set-up-real-estate-tax-installment-plan/): applicant or live-in spouse at least 65, regardless of income, **or** household income strictly below the annual family-size cap for sizes 1–8. The [July 2026 application](https://www.phila.gov/media/20260710071917/Real-Estate-Tax-Installment-Plan-application-form-revised.pdf) also describes income as below the cap. + +Income tables live in `checks/income/loop-income.dmn` and +`checks/income/retip-income.dmn`; the single/married caps live in +`checks/income/tax-freeze-income.dmn`. Update their boundary scenarios under +`test/bdt/checks/income/` when city limits change. Do not extrapolate larger +family sizes without a published rule. + +These screeners evaluate eligibility conditions, not application deadlines, +document submission, bill amounts, or administrative approval. The three +previously implemented benefits retain their existing compositions. diff --git a/library-api/README.md b/library-api/README.md index 87ea7af7..fad5ad98 100644 --- a/library-api/README.md +++ b/library-api/README.md @@ -61,6 +61,10 @@ File: src/main/resources/checks/age/person-min-age.dmn ### Checks + Benefits Architecture +The library includes all six Philadelphia property-tax benefits. See +[Property tax benefits](PROPERTY_TAX_BENEFITS.md) for their compositions, +input fields, policy sources, and income-table maintenance. + **Checks** = Reusable eligibility logic (e.g., "is person 65+?") - Location: `src/main/resources/checks/{category}/` - Example: `checks/age/person-min-age.dmn` → `/api/v1/checks/age/person-min-age` diff --git a/library-api/src/main/resources/BDT.dmn b/library-api/src/main/resources/BDT.dmn index 5aca4418..4882416f 100644 --- a/library-api/src/main/resources/BDT.dmn +++ b/library-api/src/main/resources/BDT.dmn @@ -19,6 +19,24 @@ tSimpleChecks + + number + + + number + + + number + + + number + + + number + + + number + @@ -98,6 +116,18 @@ boolean + + boolean + + + boolean + + + boolean + + + boolean + diff --git a/library-api/src/main/resources/benefits/pa/phl/loop.dmn b/library-api/src/main/resources/benefits/pa/phl/loop.dmn new file mode 100644 index 00000000..5d080884 --- /dev/null +++ b/library-api/src/main/resources/benefits/pa/phl/loop.dmn @@ -0,0 +1,593 @@ + + + Philadelphia Longtime Owner Occupants Program (LOOP) for new applicants. Homestead recipients must remove that exemption to enroll; historical owner/close-relative abatements are excluded. + + + + + + + + + + + + + + + Enrollment.tEnrollmentList + + + tSimpleChecks + + + number + + + number + + + number + + + number + + + number + + + number + + + string + + + + + boolean + + + boolean + + + boolean + + + boolean + + + boolean + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PersonNotEnrolledInBenefit.PersonNotEnrolledInBenefitService + + + + + situation + + + + + + + + + situation.primaryPersonId + + + + + + "PhlLoop" + + + + + + + + + + + OwnerOccupant.OwnerOccupantService + + + + + situation + + + + + + + + + LivesInPhiladelphiaPa.LivesInPhiladelphiaPaService + + + + + situation + + + + + + + + + MinYearsOwnerOccupant.MinYearsOwnerOccupantService + + + + + situation + + + + + + + + + 10 + + + + + + + + + + + PropertyAssessmentIncrease.PropertyAssessmentIncreaseService + + + + + situation + + + + + + + + + 50 + + + + + + 75 + + + + + + + + + + + PropertyTaxesCurrentOrPaymentPlan.PropertyTaxesCurrentOrPaymentPlanService + + + + + situation + + + + + + + + + LoopIncome.LoopIncomeService + + + + + situation + + + + + + + + + PersonNotEnrolledInBenefit.PersonNotEnrolledInBenefitService + + + + + situation + + + + + + + + + situation.primaryPersonId + + + + + + "PhlHomesteadExemption" + + + + + + + + + + + NoDisqualifyingTaxAbatement.NoDisqualifyingTaxAbatementService + + + + + situation + + + + + + + + + + + + + + + + + for check in get entries(checks) return check.value + + + + + all(checksAsList) + + + + + + + + + + + + + + 50 + 376 + 599 + + + 50 + 239 + 290 + + + + + 290 + + + 50 + 120 + 100 + + + 100 + + + 100 + + + 50 + 239 + 290 + + + + + 290 + + + 50 + 239 + 290 + + + + + 290 + + + 50 + 239 + 290 + + + + + 290 + + + 50 + 120 + 100 + + + 100 + + + 50 + 239 + 290 + + + + + 290 + + + 50 + 120 + 100 + + + 100 + + + 100 + + + 50 + 239 + 290 + + + + + 290 + + + 50 + 239 + 290 + + + + + 290 + + + 50 + 239 + 290 + + + + + 290 + + + 50 + 120 + 100 + + + 100 + + + 100 + + + 50 + 239 + 290 + + + + + 290 + + + 50 + 222 + 504 + + + 504 + + + 504 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/library-api/src/main/resources/benefits/pa/phl/low-income-tax-freeze.dmn b/library-api/src/main/resources/benefits/pa/phl/low-income-tax-freeze.dmn new file mode 100644 index 00000000..da2048bc --- /dev/null +++ b/library-api/src/main/resources/benefits/pa/phl/low-income-tax-freeze.dmn @@ -0,0 +1,313 @@ + + + Philadelphia Low-Income Real Estate Tax Freeze for owner-occupants meeting single/married gross annual income limits. + + + + + + + + + + + Enrollment.tEnrollmentList + + + tSimpleChecks + + + number + + + string + + + + + boolean + + + boolean + + + boolean + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PersonNotEnrolledInBenefit.PersonNotEnrolledInBenefitService + + + + + situation + + + + + + + + + situation.primaryPersonId + + + + + + "PhlLowIncomeTaxFreeze" + + + + + + + + + + + OwnerOccupant.OwnerOccupantService + + + + + situation + + + + + + + + + LivesInPhiladelphiaPa.LivesInPhiladelphiaPaService + + + + + situation + + + + + + + + + TaxFreezeIncome.TaxFreezeIncomeService + + + + + situation + + + + + + + + + + + + + + + + + for check in get entries(checks) return check.value + + + + + all(checksAsList) + + + + + + + + + + + + + + 50 + 376 + 599 + + + 50 + 239 + 290 + + + + + 290 + + + 50 + 120 + 100 + + + 100 + + + 100 + + + 50 + 239 + 290 + + + + + 290 + + + 50 + 239 + 290 + + + + + 290 + + + 50 + 239 + 290 + + + + + 290 + + + 50 + 222 + 504 + + + 504 + + + 504 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/library-api/src/main/resources/benefits/pa/phl/retip.dmn b/library-api/src/main/resources/benefits/pa/phl/retip.dmn new file mode 100644 index 00000000..50091528 --- /dev/null +++ b/library-api/src/main/resources/benefits/pa/phl/retip.dmn @@ -0,0 +1,333 @@ + + + Philadelphia Real Estate Tax Installment Plan (RETIP) for owner-occupants qualifying by applicant/live-in spouse age or household income. + + + + + + + + + + + Enrollment.tEnrollmentList + + + tSimpleChecks + + + string + + + tPersonList + + + BDT.tRelationshipList + + + number + + + number + + + + + boolean + + + boolean + + + boolean + + + + + string + + + date + + + + tPerson + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PersonNotEnrolledInBenefit.PersonNotEnrolledInBenefitService + + + + + situation + + + + + + + + + situation.primaryPersonId + + + + + + "PhlRetip" + + + + + + + + + + + OwnerOccupant.OwnerOccupantService + + + + + situation + + + + + + + + + LivesInPhiladelphiaPa.LivesInPhiladelphiaPaService + + + + + situation + + + + + + + + + RetipAgeOrIncomeRequirement.RetipAgeOrIncomeRequirementService + + + + + situation + + + + + + + + + + + + + + + + + for check in get entries(checks) return check.value + + + + + all(checksAsList) + + + + + + + + + + + + + + 50 + 376 + 599 + + + 50 + 239 + 290 + + + + + 290 + + + 50 + 120 + 100 + + + 100 + + + 100 + + + 50 + 239 + 290 + + + + + 290 + + + 50 + 239 + 290 + + + + + 290 + + + 50 + 239 + 290 + + + + + 290 + + + 50 + 222 + 504 + + + 504 + + + 504 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/library-api/src/main/resources/checks/age/applicant-or-spouse-min-age.dmn b/library-api/src/main/resources/checks/age/applicant-or-spouse-min-age.dmn new file mode 100644 index 00000000..7a860b19 --- /dev/null +++ b/library-api/src/main/resources/checks/age/applicant-or-spouse-min-age.dmn @@ -0,0 +1,163 @@ + + + Checks the applicant or live-in spouse against a minimum age as of today. An empty relationships list means no spouse; omitted relationships mean unknown. + + + + + + + string + + + tPersonList + + + BDT.tRelationshipList + + + tSimpleChecks + + + + + boolean + + + + + string + + + date + + + + tPerson + + + + number + + + + + + + + + + + + + + + + + + + + + + + + + + if situation.primaryPersonId = null then null else PersonMinAge.PersonMinAgeService(situation: situation, parameters: {personId: situation.primaryPersonId, minAge: parameters.minAge, asOfDate: today()}) + + + + + + if situation.relationships = null or situation.primaryPersonId = null then null else situation.relationships[personId = situation.primaryPersonId and type = "spouse"][1].relatedPersonId + + + + + + if situation.relationships = null or situation.primaryPersonId = null then null +else if SpouseId = null then false +else situation.simpleChecks.spouseLivesInHousehold and PersonMinAge.PersonMinAgeService(situation: situation, parameters: {personId: SpouseId, minAge: parameters.minAge, asOfDate: today()}) + + + + + PrimaryPersonMeetsAge or SpouseMeetsAge + + + + + + + + + + + + + + + + + + 50 + 120 + 917 + + + 917 + + + 917 + + + 917 + + + 917 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/library-api/src/main/resources/checks/enrollment/person-enrolled-in-benefit.dmn b/library-api/src/main/resources/checks/enrollment/person-enrolled-in-benefit.dmn index cf760918..ac41770d 100644 --- a/library-api/src/main/resources/checks/enrollment/person-enrolled-in-benefit.dmn +++ b/library-api/src/main/resources/checks/enrollment/person-enrolled-in-benefit.dmn @@ -34,7 +34,7 @@ - if situation.enrollments != null and count(situation.enrollments) > 0 and parameters.personId != null and parameters.benefit != null then + if situation.enrollments != null and parameters.personId != null and parameters.benefit != null then some enrollment in situation.enrollments satisfies enrollment.personId = parameters.personId and enrollment.benefit = parameters.benefit else null @@ -107,4 +107,4 @@ else - \ No newline at end of file + diff --git a/library-api/src/main/resources/checks/income/Income.dmn b/library-api/src/main/resources/checks/income/Income.dmn new file mode 100644 index 00000000..8c40f793 --- /dev/null +++ b/library-api/src/main/resources/checks/income/Income.dmn @@ -0,0 +1,5 @@ + + + Shared definitions for income eligibility checks. + + \ No newline at end of file diff --git a/library-api/src/main/resources/checks/income/loop-income.dmn b/library-api/src/main/resources/checks/income/loop-income.dmn new file mode 100644 index 00000000..94bde142 --- /dev/null +++ b/library-api/src/main/resources/checks/income/loop-income.dmn @@ -0,0 +1,68 @@ + + + New LOOP applicants: household annual income below the 120% AMI cap; pre-2023 renewals use different limits. City income table verified 2026-10-05. + + + + + + number + + + number + + + + + + + + + + + + + + + + if situation.householdIncome = null or situation.householdIncome < 0 or situation.householdSize = null then null +else if situation.householdSize < 1 or situation.householdSize > 10 or floor(situation.householdSize) != situation.householdSize then null +else situation.householdIncome < [103050, 117800, 132500, 147200, 159000, 170800, 182550, 194350, 206100, 217900][situation.householdSize] + + + + + + + + + + + + 917 + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/library-api/src/main/resources/checks/income/retip-income.dmn b/library-api/src/main/resources/checks/income/retip-income.dmn new file mode 100644 index 00000000..1cd7b201 --- /dev/null +++ b/library-api/src/main/resources/checks/income/retip-income.dmn @@ -0,0 +1,68 @@ + + + RETIP non-senior applicants: household annual income below the family-size cap. City income table verified 2026-10-05. + + + + + + number + + + number + + + + + + + + + + + + + + + + if situation.householdIncome = null or situation.householdIncome < 0 or situation.householdSize = null then null +else if situation.householdSize < 1 or situation.householdSize > 8 or floor(situation.householdSize) != situation.householdSize then null +else situation.householdIncome < [42950, 49100, 55250, 61350, 66300, 71200, 76100, 81000][situation.householdSize] + + + + + + + + + + + + 917 + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/library-api/src/main/resources/checks/income/tax-freeze-income.dmn b/library-api/src/main/resources/checks/income/tax-freeze-income.dmn new file mode 100644 index 00000000..028592f4 --- /dev/null +++ b/library-api/src/main/resources/checks/income/tax-freeze-income.dmn @@ -0,0 +1,72 @@ + + + Philadelphia tax freeze gross annual income: at most $33,500 single or $41,500 married (city guidance verified 2026-10-05). Married income includes both spouses. + + + + + + number + + + tSimpleChecks + + + + + boolean + + + + + + + + + + + + + + + + if situation.householdIncome = null or situation.householdIncome < 0 or situation.simpleChecks.isMarried = null then null +else situation.householdIncome <= (if situation.simpleChecks.isMarried then 41500 else 33500) + + + + + + + + + + + + 917 + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/library-api/src/main/resources/checks/internal/retip-age-or-income-requirement.dmn b/library-api/src/main/resources/checks/internal/retip-age-or-income-requirement.dmn new file mode 100644 index 00000000..f46eea25 --- /dev/null +++ b/library-api/src/main/resources/checks/internal/retip-age-or-income-requirement.dmn @@ -0,0 +1,126 @@ + + + RETIP eligibility through applicant/live-in spouse age 65 or low household income. No late-spouse age pathway. + + + + + + + + string + + + tPersonList + + + BDT.tRelationshipList + + + tSimpleChecks + + + number + + + number + + + + + boolean + + + + + string + + + date + + + + tPerson + + + + + + + + + + + + + + + + + + + + + ApplicantOrSpouseMinAge.ApplicantOrSpouseMinAgeService(situation: situation, parameters: {minAge: 65}) or RetipIncome.RetipIncomeService(situation: situation) + + + + + + + + + + + + 917 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/library-api/src/main/resources/checks/residence/min-years-owner-occupant.dmn b/library-api/src/main/resources/checks/residence/min-years-owner-occupant.dmn new file mode 100644 index 00000000..50a0c42e --- /dev/null +++ b/library-api/src/main/resources/checks/residence/min-years-owner-occupant.dmn @@ -0,0 +1,85 @@ + + + Checks that the applicant has owned and occupied the primary residence for at least the specified number of years. + + + + + + number + + + + + number + + + + + + + + + + + + + + + + + + + + if situation.yearsOwnerOccupied = null or situation.yearsOwnerOccupied < 0 or parameters.minYears = null or parameters.minYears < 0 then null +else situation.yearsOwnerOccupied >= parameters.minYears + + + + + + + + + + + + + + + + 917 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/library-api/src/main/resources/checks/residence/no-disqualifying-tax-abatement.dmn b/library-api/src/main/resources/checks/residence/no-disqualifying-tax-abatement.dmn new file mode 100644 index 00000000..5ec50b8a --- /dev/null +++ b/library-api/src/main/resources/checks/residence/no-disqualifying-tax-abatement.dmn @@ -0,0 +1,69 @@ + + + LOOP excludes an abatement received on this property by the current owner or a previous owner who is a close relative; includes historical abatements. + + + + + + tSimpleChecks + + + + + boolean + + + + + + + + + + + + + + + + if situation.simpleChecks.disqualifyingTaxAbatement = null then null +else not(situation.simpleChecks.disqualifyingTaxAbatement) + + + + + + + + + + + + 917 + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/library-api/src/main/resources/checks/residence/property-assessment-increase.dmn b/library-api/src/main/resources/checks/residence/property-assessment-increase.dmn new file mode 100644 index 00000000..d2a035f3 --- /dev/null +++ b/library-api/src/main/resources/checks/residence/property-assessment-increase.dmn @@ -0,0 +1,122 @@ + + + Checks either the one-year or five-year assessment increase; use assessments before Homestead reductions. + + + + + + number + + + number + + + number + + + + + number + + + number + + + + + + + + + + + + + + + + + + + + + + + if situation.propertyAssessment = null or situation.propertyAssessment < 0 or situation.previousYearPropertyAssessment = null or situation.previousYearPropertyAssessment <= 0 or parameters.oneYearPercent = null then null +else situation.propertyAssessment >= situation.previousYearPropertyAssessment * (1 + parameters.oneYearPercent / 100) + + + + + + if situation.propertyAssessment = null or situation.propertyAssessment < 0 or situation.lowestPropertyAssessmentLastFiveYears = null or situation.lowestPropertyAssessmentLastFiveYears <= 0 or parameters.fiveYearPercent = null then null +else situation.propertyAssessment >= situation.lowestPropertyAssessmentLastFiveYears * (1 + parameters.fiveYearPercent / 100) + + + + + OneYearIncrease or FiveYearIncrease + + + + + + + + + + + + + + + + + + 50 + 120 + 917 + + + 917 + + + 917 + + + 917 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/library-api/src/main/resources/checks/residence/property-taxes-current-or-payment-plan.dmn b/library-api/src/main/resources/checks/residence/property-taxes-current-or-payment-plan.dmn new file mode 100644 index 00000000..614fb1db --- /dev/null +++ b/library-api/src/main/resources/checks/residence/property-taxes-current-or-payment-plan.dmn @@ -0,0 +1,87 @@ + + + Checks whether property taxes are current or covered by an active OOPA or Real Estate Tax installment plan. + + + + + + + tSimpleChecks + + + + + boolean + + + boolean + + + + + + + + + + + + + + + + + + + not(TaxDelinquent.TaxDelinquentService(situation: situation)) or situation.simpleChecks.propertyTaxPaymentPlan + + + + + + + + + + + + 917 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/library-api/src/test/java/org/codeforphilly/bdt/library/api/DynamicEndpointPatternTest.java b/library-api/src/test/java/org/codeforphilly/bdt/library/api/DynamicEndpointPatternTest.java index eb16c353..ae41173e 100644 --- a/library-api/src/test/java/org/codeforphilly/bdt/library/api/DynamicEndpointPatternTest.java +++ b/library-api/src/test/java/org/codeforphilly/bdt/library/api/DynamicEndpointPatternTest.java @@ -13,6 +13,7 @@ import java.util.stream.Collectors; import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.hasKey; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; import static org.junit.jupiter.api.Assertions.*; @@ -54,7 +55,9 @@ public void testAllCheckEndpointsReturnCheckResult() { .post(path) .then() .statusCode(200) - .body("checkResult", notNullValue()) + // Generated examples are structural samples; unknown or unsupported values + // may legitimately return null under the check's three-valued contract. + .body("$", hasKey("checkResult")) .body("situation", notNullValue()); // 'parameters' is only echoed back for checks that declare a parameters input } diff --git a/library-api/src/test/java/org/codeforphilly/bdt/library/api/OpenAPISchemaPatternTest.java b/library-api/src/test/java/org/codeforphilly/bdt/library/api/OpenAPISchemaPatternTest.java index 48056fef..dacbeff4 100644 --- a/library-api/src/test/java/org/codeforphilly/bdt/library/api/OpenAPISchemaPatternTest.java +++ b/library-api/src/test/java/org/codeforphilly/bdt/library/api/OpenAPISchemaPatternTest.java @@ -111,6 +111,52 @@ public void testBenefitCompositionIsPublishedFromDmn() { && "MeetsAgeRequirement".equals(check.get("alias")))); } + @Test + public void testAllSixPropertyTaxBenefitsPublishResolvableComposition() { + Map benefits = Map.of( + "homestead-exemption", "PhlHomesteadExemptionService", + "owner-occupied-payment-agreement", "PhlOwnerOccupiedPaymentAgreementService", + "senior-citizen-tax-freeze", "PhlSeniorCitizenTaxFreezeService", + "low-income-tax-freeze", "PhlLowIncomeTaxFreezeService", + "loop", "PhlLoopService", + "retip", "PhlRetipService"); + + Map> paths = openApiSpec.getMap("paths"); + Map checkPathsByService = modelRegistry.getAllModels().values().stream() + .filter(model -> model.getPath().startsWith("checks/")) + .filter(model -> model.getDecisionServices().contains(model.getModelName() + "Service")) + .collect(Collectors.toMap(model -> model.getModelName() + "Service", ModelInfo::getPath)); + + benefits.forEach((slug, service) -> { + String operation = "paths.'/api/v1/benefits/pa/phl/" + slug + "'.post"; + assertEquals(service, openApiSpec.getString(operation + ".operationId")); + List> checks = openApiSpec.getList(operation + ".'x-bdt-benefit'.checks"); + assertNotNull(checks, slug + " must expose its composition to the builder"); + assertFalse(checks.isEmpty(), slug + " must have checks"); + for (Map check : checks) { + String checkService = (String) check.get("operationId"); + String checkPath = checkPathsByService.get(checkService); + assertNotNull(checkPath, slug + " references an undiscovered check: " + checkService); + assertTrue(paths.containsKey("/api/v1/" + checkPath), checkService + " must be callable"); + Map bindings = (Map) check.get("parameterBindings"); + assertTrue(bindings.values().stream().allMatch("primaryPersonId"::equals), + slug + " must use parameter bindings the builder can resolve"); + } + }); + + List> loopChecks = openApiSpec.getList( + "paths.'/api/v1/benefits/pa/phl/loop'.post.'x-bdt-benefit'.checks"); + assertEquals(9, loopChecks.size()); + Map years = loopChecks.stream() + .filter(check -> "MinYearsOwnerOccupantService".equals(check.get("operationId"))) + .findFirst().orElseThrow(); + assertEquals(Map.of("minYears", 10), years.get("parameters")); + Map assessment = loopChecks.stream() + .filter(check -> "PropertyAssessmentIncreaseService".equals(check.get("operationId"))) + .findFirst().orElseThrow(); + assertEquals(Map.of("oneYearPercent", 50, "fiveYearPercent", 75), assessment.get("parameters")); + } + @Test public void testAllCheckEndpointsHaveCheckResultInExamples() { Map allModels = modelRegistry.getAllModels(); diff --git a/library-api/test/bdt/benefits/pa/phl/PhlLoop/Already enrolled.bru b/library-api/test/bdt/benefits/pa/phl/PhlLoop/Already enrolled.bru new file mode 100644 index 00000000..5b7a8e47 --- /dev/null +++ b/library-api/test/bdt/benefits/pa/phl/PhlLoop/Already enrolled.bru @@ -0,0 +1,43 @@ +meta { + name: Already enrolled + type: http + seq: 4 +} + +post { + url: {{host}}/benefits/pa/phl/loop + body: json + auth: inherit +} + +body:json { + { + "situation": { + "primaryPersonId": "p1", + "enrollments": [ + { + "personId": "p1", + "benefit": "PhlLoop" + } + ], + "simpleChecks": { + "ownerOccupant": true, + "livesInPhiladelphiaPa": true, + "taxDelinquent": false, + "disqualifyingTaxAbatement": false + }, + "householdIncome": 100000, + "householdSize": 1, + "yearsOwnerOccupied": 10, + "propertyAssessment": 150000, + "previousYearPropertyAssessment": 100000, + "lowestPropertyAssessmentLastFiveYears": 100000 + } + } +} + +assert { + res.status: eq 200 + res.body.isEligible: eq false + res.body.checks.NotAlreadyEnrolled: eq false +} diff --git a/library-api/test/bdt/benefits/pa/phl/PhlLoop/Eligible with five year increase.bru b/library-api/test/bdt/benefits/pa/phl/PhlLoop/Eligible with five year increase.bru new file mode 100644 index 00000000..0a19494b --- /dev/null +++ b/library-api/test/bdt/benefits/pa/phl/PhlLoop/Eligible with five year increase.bru @@ -0,0 +1,43 @@ +meta { + name: Eligible with five year increase + type: http + seq: 13 +} + +post { + url: {{host}}/benefits/pa/phl/loop + body: json + auth: inherit +} + +body:json { + { + "situation": { + "primaryPersonId": "p1", + "enrollments": [ + { + "personId": "p1", + "benefit": "OtherBenefit" + } + ], + "simpleChecks": { + "ownerOccupant": true, + "livesInPhiladelphiaPa": true, + "taxDelinquent": false, + "disqualifyingTaxAbatement": false + }, + "householdIncome": 100000, + "householdSize": 1, + "yearsOwnerOccupied": 10, + "propertyAssessment": 175000, + "previousYearPropertyAssessment": 175000, + "lowestPropertyAssessmentLastFiveYears": 100000 + } + } +} + +assert { + res.status: eq 200 + res.body.isEligible: eq true + res.body.checks.AssessmentIncrease: eq true +} diff --git a/library-api/test/bdt/benefits/pa/phl/PhlLoop/Eligible with payment plan.bru b/library-api/test/bdt/benefits/pa/phl/PhlLoop/Eligible with payment plan.bru new file mode 100644 index 00000000..2d40ef2e --- /dev/null +++ b/library-api/test/bdt/benefits/pa/phl/PhlLoop/Eligible with payment plan.bru @@ -0,0 +1,44 @@ +meta { + name: Eligible with payment plan + type: http + seq: 12 +} + +post { + url: {{host}}/benefits/pa/phl/loop + body: json + auth: inherit +} + +body:json { + { + "situation": { + "primaryPersonId": "p1", + "enrollments": [ + { + "personId": "p1", + "benefit": "OtherBenefit" + } + ], + "simpleChecks": { + "ownerOccupant": true, + "livesInPhiladelphiaPa": true, + "taxDelinquent": true, + "disqualifyingTaxAbatement": false, + "propertyTaxPaymentPlan": true + }, + "householdIncome": 100000, + "householdSize": 1, + "yearsOwnerOccupied": 10, + "propertyAssessment": 150000, + "previousYearPropertyAssessment": 100000, + "lowestPropertyAssessmentLastFiveYears": 100000 + } + } +} + +assert { + res.status: eq 200 + res.body.isEligible: eq true + res.body.checks.TaxesCurrentOrPaymentPlan: eq true +} diff --git a/library-api/test/bdt/benefits/pa/phl/PhlLoop/Eligible.bru b/library-api/test/bdt/benefits/pa/phl/PhlLoop/Eligible.bru new file mode 100644 index 00000000..fe03ffc5 --- /dev/null +++ b/library-api/test/bdt/benefits/pa/phl/PhlLoop/Eligible.bru @@ -0,0 +1,42 @@ +meta { + name: Eligible + type: http + seq: 2 +} + +post { + url: {{host}}/benefits/pa/phl/loop + body: json + auth: inherit +} + +body:json { + { + "situation": { + "primaryPersonId": "p1", + "enrollments": [ + { + "personId": "p1", + "benefit": "OtherBenefit" + } + ], + "simpleChecks": { + "ownerOccupant": true, + "livesInPhiladelphiaPa": true, + "taxDelinquent": false, + "disqualifyingTaxAbatement": false + }, + "householdIncome": 100000, + "householdSize": 1, + "yearsOwnerOccupied": 10, + "propertyAssessment": 150000, + "previousYearPropertyAssessment": 100000, + "lowestPropertyAssessmentLastFiveYears": 100000 + } + } +} + +assert { + res.status: eq 200 + res.body.isEligible: eq true +} diff --git a/library-api/test/bdt/benefits/pa/phl/PhlLoop/Fails AssessmentIncrease.bru b/library-api/test/bdt/benefits/pa/phl/PhlLoop/Fails AssessmentIncrease.bru new file mode 100644 index 00000000..6cd7e187 --- /dev/null +++ b/library-api/test/bdt/benefits/pa/phl/PhlLoop/Fails AssessmentIncrease.bru @@ -0,0 +1,43 @@ +meta { + name: Fails AssessmentIncrease + type: http + seq: 6 +} + +post { + url: {{host}}/benefits/pa/phl/loop + body: json + auth: inherit +} + +body:json { + { + "situation": { + "primaryPersonId": "p1", + "enrollments": [ + { + "personId": "p1", + "benefit": "OtherBenefit" + } + ], + "simpleChecks": { + "ownerOccupant": true, + "livesInPhiladelphiaPa": true, + "taxDelinquent": false, + "disqualifyingTaxAbatement": false + }, + "householdIncome": 100000, + "householdSize": 1, + "yearsOwnerOccupied": 10, + "propertyAssessment": 149999, + "previousYearPropertyAssessment": 100000, + "lowestPropertyAssessmentLastFiveYears": 100000 + } + } +} + +assert { + res.status: eq 200 + res.body.isEligible: eq false + res.body.checks.AssessmentIncrease: eq false +} diff --git a/library-api/test/bdt/benefits/pa/phl/PhlLoop/Fails MeetsIncomeRequirement.bru b/library-api/test/bdt/benefits/pa/phl/PhlLoop/Fails MeetsIncomeRequirement.bru new file mode 100644 index 00000000..1018f49f --- /dev/null +++ b/library-api/test/bdt/benefits/pa/phl/PhlLoop/Fails MeetsIncomeRequirement.bru @@ -0,0 +1,43 @@ +meta { + name: Fails MeetsIncomeRequirement + type: http + seq: 8 +} + +post { + url: {{host}}/benefits/pa/phl/loop + body: json + auth: inherit +} + +body:json { + { + "situation": { + "primaryPersonId": "p1", + "enrollments": [ + { + "personId": "p1", + "benefit": "OtherBenefit" + } + ], + "simpleChecks": { + "ownerOccupant": true, + "livesInPhiladelphiaPa": true, + "taxDelinquent": false, + "disqualifyingTaxAbatement": false + }, + "householdIncome": 103050, + "householdSize": 1, + "yearsOwnerOccupied": 10, + "propertyAssessment": 150000, + "previousYearPropertyAssessment": 100000, + "lowestPropertyAssessmentLastFiveYears": 100000 + } + } +} + +assert { + res.status: eq 200 + res.body.isEligible: eq false + res.body.checks.MeetsIncomeRequirement: eq false +} diff --git a/library-api/test/bdt/benefits/pa/phl/PhlLoop/Fails NoDisqualifyingTaxAbatement.bru b/library-api/test/bdt/benefits/pa/phl/PhlLoop/Fails NoDisqualifyingTaxAbatement.bru new file mode 100644 index 00000000..5aabfbea --- /dev/null +++ b/library-api/test/bdt/benefits/pa/phl/PhlLoop/Fails NoDisqualifyingTaxAbatement.bru @@ -0,0 +1,43 @@ +meta { + name: Fails NoDisqualifyingTaxAbatement + type: http + seq: 10 +} + +post { + url: {{host}}/benefits/pa/phl/loop + body: json + auth: inherit +} + +body:json { + { + "situation": { + "primaryPersonId": "p1", + "enrollments": [ + { + "personId": "p1", + "benefit": "OtherBenefit" + } + ], + "simpleChecks": { + "ownerOccupant": true, + "livesInPhiladelphiaPa": true, + "taxDelinquent": false, + "disqualifyingTaxAbatement": true + }, + "householdIncome": 100000, + "householdSize": 1, + "yearsOwnerOccupied": 10, + "propertyAssessment": 150000, + "previousYearPropertyAssessment": 100000, + "lowestPropertyAssessmentLastFiveYears": 100000 + } + } +} + +assert { + res.status: eq 200 + res.body.isEligible: eq false + res.body.checks.NoDisqualifyingTaxAbatement: eq false +} diff --git a/library-api/test/bdt/benefits/pa/phl/PhlLoop/Fails NotOnHomestead.bru b/library-api/test/bdt/benefits/pa/phl/PhlLoop/Fails NotOnHomestead.bru new file mode 100644 index 00000000..616e7520 --- /dev/null +++ b/library-api/test/bdt/benefits/pa/phl/PhlLoop/Fails NotOnHomestead.bru @@ -0,0 +1,43 @@ +meta { + name: Fails NotOnHomestead + type: http + seq: 9 +} + +post { + url: {{host}}/benefits/pa/phl/loop + body: json + auth: inherit +} + +body:json { + { + "situation": { + "primaryPersonId": "p1", + "enrollments": [ + { + "personId": "p1", + "benefit": "PhlHomesteadExemption" + } + ], + "simpleChecks": { + "ownerOccupant": true, + "livesInPhiladelphiaPa": true, + "taxDelinquent": false, + "disqualifyingTaxAbatement": false + }, + "householdIncome": 100000, + "householdSize": 1, + "yearsOwnerOccupied": 10, + "propertyAssessment": 150000, + "previousYearPropertyAssessment": 100000, + "lowestPropertyAssessmentLastFiveYears": 100000 + } + } +} + +assert { + res.status: eq 200 + res.body.isEligible: eq false + res.body.checks.NotOnHomestead: eq false +} diff --git a/library-api/test/bdt/benefits/pa/phl/PhlLoop/Fails TaxesCurrentOrPaymentPlan.bru b/library-api/test/bdt/benefits/pa/phl/PhlLoop/Fails TaxesCurrentOrPaymentPlan.bru new file mode 100644 index 00000000..a842a485 --- /dev/null +++ b/library-api/test/bdt/benefits/pa/phl/PhlLoop/Fails TaxesCurrentOrPaymentPlan.bru @@ -0,0 +1,44 @@ +meta { + name: Fails TaxesCurrentOrPaymentPlan + type: http + seq: 7 +} + +post { + url: {{host}}/benefits/pa/phl/loop + body: json + auth: inherit +} + +body:json { + { + "situation": { + "primaryPersonId": "p1", + "enrollments": [ + { + "personId": "p1", + "benefit": "OtherBenefit" + } + ], + "simpleChecks": { + "ownerOccupant": true, + "livesInPhiladelphiaPa": true, + "taxDelinquent": true, + "disqualifyingTaxAbatement": false, + "propertyTaxPaymentPlan": false + }, + "householdIncome": 100000, + "householdSize": 1, + "yearsOwnerOccupied": 10, + "propertyAssessment": 150000, + "previousYearPropertyAssessment": 100000, + "lowestPropertyAssessmentLastFiveYears": 100000 + } + } +} + +assert { + res.status: eq 200 + res.body.isEligible: eq false + res.body.checks.TaxesCurrentOrPaymentPlan: eq false +} diff --git a/library-api/test/bdt/benefits/pa/phl/PhlLoop/Fails TenYearsOwnerOccupant.bru b/library-api/test/bdt/benefits/pa/phl/PhlLoop/Fails TenYearsOwnerOccupant.bru new file mode 100644 index 00000000..f8d02669 --- /dev/null +++ b/library-api/test/bdt/benefits/pa/phl/PhlLoop/Fails TenYearsOwnerOccupant.bru @@ -0,0 +1,43 @@ +meta { + name: Fails TenYearsOwnerOccupant + type: http + seq: 5 +} + +post { + url: {{host}}/benefits/pa/phl/loop + body: json + auth: inherit +} + +body:json { + { + "situation": { + "primaryPersonId": "p1", + "enrollments": [ + { + "personId": "p1", + "benefit": "OtherBenefit" + } + ], + "simpleChecks": { + "ownerOccupant": true, + "livesInPhiladelphiaPa": true, + "taxDelinquent": false, + "disqualifyingTaxAbatement": false + }, + "householdIncome": 100000, + "householdSize": 1, + "yearsOwnerOccupied": 9, + "propertyAssessment": 150000, + "previousYearPropertyAssessment": 100000, + "lowestPropertyAssessmentLastFiveYears": 100000 + } + } +} + +assert { + res.status: eq 200 + res.body.isEligible: eq false + res.body.checks.TenYearsOwnerOccupant: eq false +} diff --git a/library-api/test/bdt/benefits/pa/phl/PhlLoop/Ineligible.bru b/library-api/test/bdt/benefits/pa/phl/PhlLoop/Ineligible.bru new file mode 100644 index 00000000..effa3f72 --- /dev/null +++ b/library-api/test/bdt/benefits/pa/phl/PhlLoop/Ineligible.bru @@ -0,0 +1,43 @@ +meta { + name: Ineligible + type: http + seq: 3 +} + +post { + url: {{host}}/benefits/pa/phl/loop + body: json + auth: inherit +} + +body:json { + { + "situation": { + "primaryPersonId": "p1", + "enrollments": [ + { + "personId": "p1", + "benefit": "OtherBenefit" + } + ], + "simpleChecks": { + "ownerOccupant": false, + "livesInPhiladelphiaPa": true, + "taxDelinquent": false, + "disqualifyingTaxAbatement": false + }, + "householdIncome": 100000, + "householdSize": 1, + "yearsOwnerOccupied": 10, + "propertyAssessment": 150000, + "previousYearPropertyAssessment": 100000, + "lowestPropertyAssessmentLastFiveYears": 100000 + } + } +} + +assert { + res.status: eq 200 + res.body.isEligible: eq false + res.body.checks.OwnerOccupant: eq false +} diff --git a/library-api/test/bdt/benefits/pa/phl/PhlLoop/Smoke Test.bru b/library-api/test/bdt/benefits/pa/phl/PhlLoop/Smoke Test.bru new file mode 100644 index 00000000..078fee22 --- /dev/null +++ b/library-api/test/bdt/benefits/pa/phl/PhlLoop/Smoke Test.bru @@ -0,0 +1,27 @@ +meta { + name: Smoke Test + type: http + seq: 1 +} + +post { + url: {{host}}/benefits/pa/phl/loop + body: json + auth: inherit +} + +body:json { + { + "situation": { + "primaryPersonId": "p1", + "simpleChecks": { + "livesInPhiladelphiaPa": true + } + } + } +} + +assert { + res.status: eq 200 + res.body.isEligible: eq null +} diff --git a/library-api/test/bdt/benefits/pa/phl/PhlLoop/folder.bru b/library-api/test/bdt/benefits/pa/phl/PhlLoop/folder.bru new file mode 100644 index 00000000..b07c8218 --- /dev/null +++ b/library-api/test/bdt/benefits/pa/phl/PhlLoop/folder.bru @@ -0,0 +1,8 @@ +meta { + name: PhlLoop + seq: 5 +} + +auth { + mode: inherit +} diff --git a/library-api/test/bdt/benefits/pa/phl/PhlLowIncomeTaxFreeze/Already enrolled.bru b/library-api/test/bdt/benefits/pa/phl/PhlLowIncomeTaxFreeze/Already enrolled.bru new file mode 100644 index 00000000..f850ab6e --- /dev/null +++ b/library-api/test/bdt/benefits/pa/phl/PhlLowIncomeTaxFreeze/Already enrolled.bru @@ -0,0 +1,37 @@ +meta { + name: Already enrolled + type: http + seq: 4 +} + +post { + url: {{host}}/benefits/pa/phl/low-income-tax-freeze + body: json + auth: inherit +} + +body:json { + { + "situation": { + "primaryPersonId": "p1", + "enrollments": [ + { + "personId": "p1", + "benefit": "PhlLowIncomeTaxFreeze" + } + ], + "simpleChecks": { + "ownerOccupant": true, + "livesInPhiladelphiaPa": true, + "isMarried": false + }, + "householdIncome": 33500 + } + } +} + +assert { + res.status: eq 200 + res.body.isEligible: eq false + res.body.checks.NotAlreadyEnrolled: eq false +} diff --git a/library-api/test/bdt/benefits/pa/phl/PhlLowIncomeTaxFreeze/Eligible.bru b/library-api/test/bdt/benefits/pa/phl/PhlLowIncomeTaxFreeze/Eligible.bru new file mode 100644 index 00000000..c794aea8 --- /dev/null +++ b/library-api/test/bdt/benefits/pa/phl/PhlLowIncomeTaxFreeze/Eligible.bru @@ -0,0 +1,36 @@ +meta { + name: Eligible + type: http + seq: 2 +} + +post { + url: {{host}}/benefits/pa/phl/low-income-tax-freeze + body: json + auth: inherit +} + +body:json { + { + "situation": { + "primaryPersonId": "p1", + "enrollments": [ + { + "personId": "p1", + "benefit": "OtherBenefit" + } + ], + "simpleChecks": { + "ownerOccupant": true, + "livesInPhiladelphiaPa": true, + "isMarried": false + }, + "householdIncome": 33500 + } + } +} + +assert { + res.status: eq 200 + res.body.isEligible: eq true +} diff --git a/library-api/test/bdt/benefits/pa/phl/PhlLowIncomeTaxFreeze/Fails MeetsIncomeRequirement.bru b/library-api/test/bdt/benefits/pa/phl/PhlLowIncomeTaxFreeze/Fails MeetsIncomeRequirement.bru new file mode 100644 index 00000000..7f5f85bc --- /dev/null +++ b/library-api/test/bdt/benefits/pa/phl/PhlLowIncomeTaxFreeze/Fails MeetsIncomeRequirement.bru @@ -0,0 +1,37 @@ +meta { + name: Fails MeetsIncomeRequirement + type: http + seq: 5 +} + +post { + url: {{host}}/benefits/pa/phl/low-income-tax-freeze + body: json + auth: inherit +} + +body:json { + { + "situation": { + "primaryPersonId": "p1", + "enrollments": [ + { + "personId": "p1", + "benefit": "OtherBenefit" + } + ], + "simpleChecks": { + "ownerOccupant": true, + "livesInPhiladelphiaPa": true, + "isMarried": false + }, + "householdIncome": 100000 + } + } +} + +assert { + res.status: eq 200 + res.body.isEligible: eq false + res.body.checks.MeetsIncomeRequirement: eq false +} diff --git a/library-api/test/bdt/benefits/pa/phl/PhlLowIncomeTaxFreeze/Ineligible.bru b/library-api/test/bdt/benefits/pa/phl/PhlLowIncomeTaxFreeze/Ineligible.bru new file mode 100644 index 00000000..053eae02 --- /dev/null +++ b/library-api/test/bdt/benefits/pa/phl/PhlLowIncomeTaxFreeze/Ineligible.bru @@ -0,0 +1,37 @@ +meta { + name: Ineligible + type: http + seq: 3 +} + +post { + url: {{host}}/benefits/pa/phl/low-income-tax-freeze + body: json + auth: inherit +} + +body:json { + { + "situation": { + "primaryPersonId": "p1", + "enrollments": [ + { + "personId": "p1", + "benefit": "OtherBenefit" + } + ], + "simpleChecks": { + "ownerOccupant": false, + "livesInPhiladelphiaPa": true, + "isMarried": false + }, + "householdIncome": 33500 + } + } +} + +assert { + res.status: eq 200 + res.body.isEligible: eq false + res.body.checks.OwnerOccupant: eq false +} diff --git a/library-api/test/bdt/benefits/pa/phl/PhlLowIncomeTaxFreeze/Married at limit.bru b/library-api/test/bdt/benefits/pa/phl/PhlLowIncomeTaxFreeze/Married at limit.bru new file mode 100644 index 00000000..83a73579 --- /dev/null +++ b/library-api/test/bdt/benefits/pa/phl/PhlLowIncomeTaxFreeze/Married at limit.bru @@ -0,0 +1,36 @@ +meta { + name: Married at limit + type: http + seq: 10 +} + +post { + url: {{host}}/benefits/pa/phl/low-income-tax-freeze + body: json + auth: inherit +} + +body:json { + { + "situation": { + "primaryPersonId": "p1", + "enrollments": [ + { + "personId": "p1", + "benefit": "OtherBenefit" + } + ], + "simpleChecks": { + "ownerOccupant": true, + "livesInPhiladelphiaPa": true, + "isMarried": true + }, + "householdIncome": 41500 + } + } +} + +assert { + res.status: eq 200 + res.body.isEligible: eq true +} diff --git a/library-api/test/bdt/benefits/pa/phl/PhlLowIncomeTaxFreeze/Smoke Test.bru b/library-api/test/bdt/benefits/pa/phl/PhlLowIncomeTaxFreeze/Smoke Test.bru new file mode 100644 index 00000000..8f0f7d4b --- /dev/null +++ b/library-api/test/bdt/benefits/pa/phl/PhlLowIncomeTaxFreeze/Smoke Test.bru @@ -0,0 +1,27 @@ +meta { + name: Smoke Test + type: http + seq: 1 +} + +post { + url: {{host}}/benefits/pa/phl/low-income-tax-freeze + body: json + auth: inherit +} + +body:json { + { + "situation": { + "primaryPersonId": "p1", + "simpleChecks": { + "livesInPhiladelphiaPa": true + } + } + } +} + +assert { + res.status: eq 200 + res.body.isEligible: eq null +} diff --git a/library-api/test/bdt/benefits/pa/phl/PhlLowIncomeTaxFreeze/folder.bru b/library-api/test/bdt/benefits/pa/phl/PhlLowIncomeTaxFreeze/folder.bru new file mode 100644 index 00000000..678cc7bc --- /dev/null +++ b/library-api/test/bdt/benefits/pa/phl/PhlLowIncomeTaxFreeze/folder.bru @@ -0,0 +1,8 @@ +meta { + name: PhlLowIncomeTaxFreeze + seq: 4 +} + +auth { + mode: inherit +} diff --git a/library-api/test/bdt/benefits/pa/phl/PhlRetip/Already enrolled.bru b/library-api/test/bdt/benefits/pa/phl/PhlRetip/Already enrolled.bru new file mode 100644 index 00000000..9c8b8312 --- /dev/null +++ b/library-api/test/bdt/benefits/pa/phl/PhlRetip/Already enrolled.bru @@ -0,0 +1,41 @@ +meta { + name: Already enrolled + type: http + seq: 4 +} + +post { + url: {{host}}/benefits/pa/phl/retip + body: json + auth: inherit +} + +body:json { + { + "situation": { + "primaryPersonId": "p1", + "enrollments": [ + { + "personId": "p1", + "benefit": "PhlRetip" + } + ], + "simpleChecks": { + "ownerOccupant": true, + "livesInPhiladelphiaPa": true + }, + "people": [ + { + "id": "p1", + "dateOfBirth": "1940-01-01" + } + ] + } + } +} + +assert { + res.status: eq 200 + res.body.isEligible: eq false + res.body.checks.NotAlreadyEnrolled: eq false +} diff --git a/library-api/test/bdt/benefits/pa/phl/PhlRetip/Eligible by income.bru b/library-api/test/bdt/benefits/pa/phl/PhlRetip/Eligible by income.bru new file mode 100644 index 00000000..9502f47a --- /dev/null +++ b/library-api/test/bdt/benefits/pa/phl/PhlRetip/Eligible by income.bru @@ -0,0 +1,44 @@ +meta { + name: Eligible by income + type: http + seq: 6 +} + +post { + url: {{host}}/benefits/pa/phl/retip + body: json + auth: inherit +} + +body:json { + { + "situation": { + "primaryPersonId": "p1", + "enrollments": [ + { + "personId": "p1", + "benefit": "OtherBenefit" + } + ], + "simpleChecks": { + "ownerOccupant": true, + "livesInPhiladelphiaPa": true + }, + "people": [ + { + "id": "p1", + "dateOfBirth": "2000-01-01" + } + ], + "relationships": [], + "householdSize": 1, + "householdIncome": 30000 + } + } +} + +assert { + res.status: eq 200 + res.body.isEligible: eq true + res.body.checks.MeetsAgeOrIncomeRequirement: eq true +} diff --git a/library-api/test/bdt/benefits/pa/phl/PhlRetip/Eligible by live in spouse age.bru b/library-api/test/bdt/benefits/pa/phl/PhlRetip/Eligible by live in spouse age.bru new file mode 100644 index 00000000..0b21c627 --- /dev/null +++ b/library-api/test/bdt/benefits/pa/phl/PhlRetip/Eligible by live in spouse age.bru @@ -0,0 +1,55 @@ +meta { + name: Eligible by live in spouse age + type: http + seq: 7 +} + +post { + url: {{host}}/benefits/pa/phl/retip + body: json + auth: inherit +} + +body:json { + { + "situation": { + "primaryPersonId": "p1", + "enrollments": [ + { + "personId": "p1", + "benefit": "OtherBenefit" + } + ], + "simpleChecks": { + "spouseLivesInHousehold": true, + "ownerOccupant": true, + "livesInPhiladelphiaPa": true + }, + "people": [ + { + "id": "p1", + "dateOfBirth": "2000-01-01" + }, + { + "id": "p2", + "dateOfBirth": "1940-01-01" + } + ], + "relationships": [ + { + "personId": "p1", + "relatedPersonId": "p2", + "type": "spouse" + } + ], + "householdIncome": 1000000, + "householdSize": 2 + } + } +} + +assert { + res.status: eq 200 + res.body.isEligible: eq true + res.body.checks.MeetsAgeOrIncomeRequirement: eq true +} diff --git a/library-api/test/bdt/benefits/pa/phl/PhlRetip/Eligible.bru b/library-api/test/bdt/benefits/pa/phl/PhlRetip/Eligible.bru new file mode 100644 index 00000000..50264b72 --- /dev/null +++ b/library-api/test/bdt/benefits/pa/phl/PhlRetip/Eligible.bru @@ -0,0 +1,40 @@ +meta { + name: Eligible + type: http + seq: 2 +} + +post { + url: {{host}}/benefits/pa/phl/retip + body: json + auth: inherit +} + +body:json { + { + "situation": { + "primaryPersonId": "p1", + "enrollments": [ + { + "personId": "p1", + "benefit": "OtherBenefit" + } + ], + "simpleChecks": { + "ownerOccupant": true, + "livesInPhiladelphiaPa": true + }, + "people": [ + { + "id": "p1", + "dateOfBirth": "1940-01-01" + } + ] + } + } +} + +assert { + res.status: eq 200 + res.body.isEligible: eq true +} diff --git a/library-api/test/bdt/benefits/pa/phl/PhlRetip/Fails MeetsAgeOrIncomeRequirement.bru b/library-api/test/bdt/benefits/pa/phl/PhlRetip/Fails MeetsAgeOrIncomeRequirement.bru new file mode 100644 index 00000000..6ef89458 --- /dev/null +++ b/library-api/test/bdt/benefits/pa/phl/PhlRetip/Fails MeetsAgeOrIncomeRequirement.bru @@ -0,0 +1,44 @@ +meta { + name: Fails MeetsAgeOrIncomeRequirement + type: http + seq: 5 +} + +post { + url: {{host}}/benefits/pa/phl/retip + body: json + auth: inherit +} + +body:json { + { + "situation": { + "primaryPersonId": "p1", + "enrollments": [ + { + "personId": "p1", + "benefit": "OtherBenefit" + } + ], + "simpleChecks": { + "ownerOccupant": true, + "livesInPhiladelphiaPa": true + }, + "people": [ + { + "id": "p1", + "dateOfBirth": "2000-01-01" + } + ], + "relationships": [], + "householdIncome": 100000, + "householdSize": 1 + } + } +} + +assert { + res.status: eq 200 + res.body.isEligible: eq false + res.body.checks.MeetsAgeOrIncomeRequirement: eq false +} diff --git a/library-api/test/bdt/benefits/pa/phl/PhlRetip/Ineligible.bru b/library-api/test/bdt/benefits/pa/phl/PhlRetip/Ineligible.bru new file mode 100644 index 00000000..66749cd1 --- /dev/null +++ b/library-api/test/bdt/benefits/pa/phl/PhlRetip/Ineligible.bru @@ -0,0 +1,41 @@ +meta { + name: Ineligible + type: http + seq: 3 +} + +post { + url: {{host}}/benefits/pa/phl/retip + body: json + auth: inherit +} + +body:json { + { + "situation": { + "primaryPersonId": "p1", + "enrollments": [ + { + "personId": "p1", + "benefit": "OtherBenefit" + } + ], + "simpleChecks": { + "ownerOccupant": false, + "livesInPhiladelphiaPa": true + }, + "people": [ + { + "id": "p1", + "dateOfBirth": "1940-01-01" + } + ] + } + } +} + +assert { + res.status: eq 200 + res.body.isEligible: eq false + res.body.checks.OwnerOccupant: eq false +} diff --git a/library-api/test/bdt/benefits/pa/phl/PhlRetip/Smoke Test.bru b/library-api/test/bdt/benefits/pa/phl/PhlRetip/Smoke Test.bru new file mode 100644 index 00000000..6dc5e535 --- /dev/null +++ b/library-api/test/bdt/benefits/pa/phl/PhlRetip/Smoke Test.bru @@ -0,0 +1,27 @@ +meta { + name: Smoke Test + type: http + seq: 1 +} + +post { + url: {{host}}/benefits/pa/phl/retip + body: json + auth: inherit +} + +body:json { + { + "situation": { + "primaryPersonId": "p1", + "simpleChecks": { + "livesInPhiladelphiaPa": true + } + } + } +} + +assert { + res.status: eq 200 + res.body.isEligible: eq null +} diff --git a/library-api/test/bdt/benefits/pa/phl/PhlRetip/folder.bru b/library-api/test/bdt/benefits/pa/phl/PhlRetip/folder.bru new file mode 100644 index 00000000..5322a82a --- /dev/null +++ b/library-api/test/bdt/benefits/pa/phl/PhlRetip/folder.bru @@ -0,0 +1,8 @@ +meta { + name: PhlRetip + seq: 6 +} + +auth { + mode: inherit +} diff --git a/library-api/test/bdt/checks/age/ApplicantOrSpouseMinAge/Fail.bru b/library-api/test/bdt/checks/age/ApplicantOrSpouseMinAge/Fail.bru new file mode 100644 index 00000000..8d550e80 --- /dev/null +++ b/library-api/test/bdt/checks/age/ApplicantOrSpouseMinAge/Fail.bru @@ -0,0 +1,34 @@ +meta { + name: Fail + type: http + seq: 2 +} + +post { + url: {{host}}/checks/age/applicant-or-spouse-min-age + body: json + auth: inherit +} + +body:json { + { + "situation": { + "primaryPersonId": "p1", + "people": [ + { + "id": "p1", + "dateOfBirth": "2000-01-01" + } + ], + "relationships": [] + }, + "parameters": { + "minAge": 65 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq false +} diff --git a/library-api/test/bdt/checks/age/ApplicantOrSpouseMinAge/Live in spouse.bru b/library-api/test/bdt/checks/age/ApplicantOrSpouseMinAge/Live in spouse.bru new file mode 100644 index 00000000..b96b4202 --- /dev/null +++ b/library-api/test/bdt/checks/age/ApplicantOrSpouseMinAge/Live in spouse.bru @@ -0,0 +1,47 @@ +meta { + name: Live in spouse + type: http + seq: 4 +} + +post { + url: {{host}}/checks/age/applicant-or-spouse-min-age + body: json + auth: inherit +} + +body:json { + { + "situation": { + "primaryPersonId": "p1", + "people": [ + { + "id": "p1", + "dateOfBirth": "2000-01-01" + }, + { + "id": "p2", + "dateOfBirth": "1940-01-01" + } + ], + "relationships": [ + { + "personId": "p1", + "relatedPersonId": "p2", + "type": "spouse" + } + ], + "simpleChecks": { + "spouseLivesInHousehold": true + } + }, + "parameters": { + "minAge": 65 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq true +} diff --git a/library-api/test/bdt/checks/age/ApplicantOrSpouseMinAge/Null.bru b/library-api/test/bdt/checks/age/ApplicantOrSpouseMinAge/Null.bru new file mode 100644 index 00000000..235dcf4e --- /dev/null +++ b/library-api/test/bdt/checks/age/ApplicantOrSpouseMinAge/Null.bru @@ -0,0 +1,27 @@ +meta { + name: Null + type: http + seq: 3 +} + +post { + url: {{host}}/checks/age/applicant-or-spouse-min-age + body: json + auth: inherit +} + +body:json { + { + "situation": { + "primaryPersonId": "p1" + }, + "parameters": { + "minAge": 65 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq null +} diff --git a/library-api/test/bdt/checks/age/ApplicantOrSpouseMinAge/Pass.bru b/library-api/test/bdt/checks/age/ApplicantOrSpouseMinAge/Pass.bru new file mode 100644 index 00000000..05d6155d --- /dev/null +++ b/library-api/test/bdt/checks/age/ApplicantOrSpouseMinAge/Pass.bru @@ -0,0 +1,33 @@ +meta { + name: Pass + type: http + seq: 1 +} + +post { + url: {{host}}/checks/age/applicant-or-spouse-min-age + body: json + auth: inherit +} + +body:json { + { + "situation": { + "primaryPersonId": "p1", + "people": [ + { + "id": "p1", + "dateOfBirth": "1940-01-01" + } + ] + }, + "parameters": { + "minAge": 65 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq true +} diff --git a/library-api/test/bdt/checks/age/ApplicantOrSpouseMinAge/Spouse lives elsewhere.bru b/library-api/test/bdt/checks/age/ApplicantOrSpouseMinAge/Spouse lives elsewhere.bru new file mode 100644 index 00000000..ae6f53cd --- /dev/null +++ b/library-api/test/bdt/checks/age/ApplicantOrSpouseMinAge/Spouse lives elsewhere.bru @@ -0,0 +1,47 @@ +meta { + name: Spouse lives elsewhere + type: http + seq: 5 +} + +post { + url: {{host}}/checks/age/applicant-or-spouse-min-age + body: json + auth: inherit +} + +body:json { + { + "situation": { + "primaryPersonId": "p1", + "people": [ + { + "id": "p1", + "dateOfBirth": "2000-01-01" + }, + { + "id": "p2", + "dateOfBirth": "1940-01-01" + } + ], + "relationships": [ + { + "personId": "p1", + "relatedPersonId": "p2", + "type": "spouse" + } + ], + "simpleChecks": { + "spouseLivesInHousehold": false + } + }, + "parameters": { + "minAge": 65 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq false +} diff --git a/library-api/test/bdt/checks/age/ApplicantOrSpouseMinAge/Spouse residency unknown.bru b/library-api/test/bdt/checks/age/ApplicantOrSpouseMinAge/Spouse residency unknown.bru new file mode 100644 index 00000000..aa63b828 --- /dev/null +++ b/library-api/test/bdt/checks/age/ApplicantOrSpouseMinAge/Spouse residency unknown.bru @@ -0,0 +1,47 @@ +meta { + name: Spouse residency unknown + type: http + seq: 6 +} + +post { + url: {{host}}/checks/age/applicant-or-spouse-min-age + body: json + auth: inherit +} + +body:json { + { + "situation": { + "primaryPersonId": "p1", + "people": [ + { + "id": "p1", + "dateOfBirth": "2000-01-01" + }, + { + "id": "p2", + "dateOfBirth": "1940-01-01" + } + ], + "relationships": [ + { + "personId": "p1", + "relatedPersonId": "p2", + "type": "spouse" + } + ], + "simpleChecks": { + "spouseLivesInHousehold": null + } + }, + "parameters": { + "minAge": 65 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq null +} diff --git a/library-api/test/bdt/checks/age/ApplicantOrSpouseMinAge/Spouse unknown.bru b/library-api/test/bdt/checks/age/ApplicantOrSpouseMinAge/Spouse unknown.bru new file mode 100644 index 00000000..d9ae632a --- /dev/null +++ b/library-api/test/bdt/checks/age/ApplicantOrSpouseMinAge/Spouse unknown.bru @@ -0,0 +1,33 @@ +meta { + name: Spouse unknown + type: http + seq: 8 +} + +post { + url: {{host}}/checks/age/applicant-or-spouse-min-age + body: json + auth: inherit +} + +body:json { + { + "situation": { + "primaryPersonId": "p1", + "people": [ + { + "id": "p1", + "dateOfBirth": "2000-01-01" + } + ] + }, + "parameters": { + "minAge": 65 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq null +} diff --git a/library-api/test/bdt/checks/age/ApplicantOrSpouseMinAge/Unrelated senior.bru b/library-api/test/bdt/checks/age/ApplicantOrSpouseMinAge/Unrelated senior.bru new file mode 100644 index 00000000..0083820d --- /dev/null +++ b/library-api/test/bdt/checks/age/ApplicantOrSpouseMinAge/Unrelated senior.bru @@ -0,0 +1,38 @@ +meta { + name: Unrelated senior + type: http + seq: 7 +} + +post { + url: {{host}}/checks/age/applicant-or-spouse-min-age + body: json + auth: inherit +} + +body:json { + { + "situation": { + "primaryPersonId": "p1", + "people": [ + { + "id": "p1", + "dateOfBirth": "2000-01-01" + }, + { + "id": "p2", + "dateOfBirth": "1940-01-01" + } + ], + "relationships": [] + }, + "parameters": { + "minAge": 65 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq false +} diff --git a/library-api/test/bdt/checks/age/ApplicantOrSpouseMinAge/folder.bru b/library-api/test/bdt/checks/age/ApplicantOrSpouseMinAge/folder.bru new file mode 100644 index 00000000..2c5c3c25 --- /dev/null +++ b/library-api/test/bdt/checks/age/ApplicantOrSpouseMinAge/folder.bru @@ -0,0 +1,8 @@ +meta { + name: ApplicantOrSpouseMinAge + seq: 1 +} + +auth { + mode: inherit +} diff --git a/library-api/test/bdt/checks/enrollment/PersonEnrolledInBenefit/Enrollments empty.bru b/library-api/test/bdt/checks/enrollment/PersonEnrolledInBenefit/Enrollments empty.bru index 7eff128e..19c55453 100644 --- a/library-api/test/bdt/checks/enrollment/PersonEnrolledInBenefit/Enrollments empty.bru +++ b/library-api/test/bdt/checks/enrollment/PersonEnrolledInBenefit/Enrollments empty.bru @@ -29,7 +29,7 @@ body:json { } assert { - res.body.checkResult: eq null + res.body.checkResult: eq false } settings { diff --git a/library-api/test/bdt/checks/enrollment/PersonNotEnrolledInBenefit/Enrollments empty.bru b/library-api/test/bdt/checks/enrollment/PersonNotEnrolledInBenefit/Enrollments empty.bru index 11c4b173..8ee4e5bd 100644 --- a/library-api/test/bdt/checks/enrollment/PersonNotEnrolledInBenefit/Enrollments empty.bru +++ b/library-api/test/bdt/checks/enrollment/PersonNotEnrolledInBenefit/Enrollments empty.bru @@ -29,7 +29,7 @@ body:json { } assert { - res.body.checkResult: eq null + res.body.checkResult: eq true } settings { diff --git a/library-api/test/bdt/checks/income/LoopIncome/Fail.bru b/library-api/test/bdt/checks/income/LoopIncome/Fail.bru new file mode 100644 index 00000000..68e093e5 --- /dev/null +++ b/library-api/test/bdt/checks/income/LoopIncome/Fail.bru @@ -0,0 +1,25 @@ +meta { + name: Fail + type: http + seq: 2 +} + +post { + url: {{host}}/checks/income/loop-income + body: json + auth: inherit +} + +body:json { + { + "situation": { + "householdIncome": 103051, + "householdSize": 1 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq false +} diff --git a/library-api/test/bdt/checks/income/LoopIncome/Invalid income.bru b/library-api/test/bdt/checks/income/LoopIncome/Invalid income.bru new file mode 100644 index 00000000..141e133c --- /dev/null +++ b/library-api/test/bdt/checks/income/LoopIncome/Invalid income.bru @@ -0,0 +1,25 @@ +meta { + name: Invalid income + type: http + seq: 29 +} + +post { + url: {{host}}/checks/income/loop-income + body: json + auth: inherit +} + +body:json { + { + "situation": { + "householdIncome": -1, + "householdSize": 1 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq null +} diff --git a/library-api/test/bdt/checks/income/LoopIncome/Null.bru b/library-api/test/bdt/checks/income/LoopIncome/Null.bru new file mode 100644 index 00000000..25532e5d --- /dev/null +++ b/library-api/test/bdt/checks/income/LoopIncome/Null.bru @@ -0,0 +1,24 @@ +meta { + name: Null + type: http + seq: 3 +} + +post { + url: {{host}}/checks/income/loop-income + body: json + auth: inherit +} + +body:json { + { + "situation": { + "householdSize": 1 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq null +} diff --git a/library-api/test/bdt/checks/income/LoopIncome/Pass.bru b/library-api/test/bdt/checks/income/LoopIncome/Pass.bru new file mode 100644 index 00000000..5c47d581 --- /dev/null +++ b/library-api/test/bdt/checks/income/LoopIncome/Pass.bru @@ -0,0 +1,25 @@ +meta { + name: Pass + type: http + seq: 1 +} + +post { + url: {{host}}/checks/income/loop-income + body: json + auth: inherit +} + +body:json { + { + "situation": { + "householdIncome": 103049, + "householdSize": 1 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq true +} diff --git a/library-api/test/bdt/checks/income/LoopIncome/Size 1 at cap.bru b/library-api/test/bdt/checks/income/LoopIncome/Size 1 at cap.bru new file mode 100644 index 00000000..98007ae0 --- /dev/null +++ b/library-api/test/bdt/checks/income/LoopIncome/Size 1 at cap.bru @@ -0,0 +1,25 @@ +meta { + name: Size 1 at cap + type: http + seq: 5 +} + +post { + url: {{host}}/checks/income/loop-income + body: json + auth: inherit +} + +body:json { + { + "situation": { + "householdIncome": 103050, + "householdSize": 1 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq false +} diff --git a/library-api/test/bdt/checks/income/LoopIncome/Size 1 below cap.bru b/library-api/test/bdt/checks/income/LoopIncome/Size 1 below cap.bru new file mode 100644 index 00000000..cef5c951 --- /dev/null +++ b/library-api/test/bdt/checks/income/LoopIncome/Size 1 below cap.bru @@ -0,0 +1,25 @@ +meta { + name: Size 1 below cap + type: http + seq: 4 +} + +post { + url: {{host}}/checks/income/loop-income + body: json + auth: inherit +} + +body:json { + { + "situation": { + "householdIncome": 103049.99, + "householdSize": 1 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq true +} diff --git a/library-api/test/bdt/checks/income/LoopIncome/Size 10 at cap.bru b/library-api/test/bdt/checks/income/LoopIncome/Size 10 at cap.bru new file mode 100644 index 00000000..a58db66f --- /dev/null +++ b/library-api/test/bdt/checks/income/LoopIncome/Size 10 at cap.bru @@ -0,0 +1,25 @@ +meta { + name: Size 10 at cap + type: http + seq: 23 +} + +post { + url: {{host}}/checks/income/loop-income + body: json + auth: inherit +} + +body:json { + { + "situation": { + "householdIncome": 217900, + "householdSize": 10 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq false +} diff --git a/library-api/test/bdt/checks/income/LoopIncome/Size 10 below cap.bru b/library-api/test/bdt/checks/income/LoopIncome/Size 10 below cap.bru new file mode 100644 index 00000000..49b0bc5d --- /dev/null +++ b/library-api/test/bdt/checks/income/LoopIncome/Size 10 below cap.bru @@ -0,0 +1,25 @@ +meta { + name: Size 10 below cap + type: http + seq: 22 +} + +post { + url: {{host}}/checks/income/loop-income + body: json + auth: inherit +} + +body:json { + { + "situation": { + "householdIncome": 217899.99, + "householdSize": 10 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq true +} diff --git a/library-api/test/bdt/checks/income/LoopIncome/Size 2 at cap.bru b/library-api/test/bdt/checks/income/LoopIncome/Size 2 at cap.bru new file mode 100644 index 00000000..6506412d --- /dev/null +++ b/library-api/test/bdt/checks/income/LoopIncome/Size 2 at cap.bru @@ -0,0 +1,25 @@ +meta { + name: Size 2 at cap + type: http + seq: 7 +} + +post { + url: {{host}}/checks/income/loop-income + body: json + auth: inherit +} + +body:json { + { + "situation": { + "householdIncome": 117800, + "householdSize": 2 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq false +} diff --git a/library-api/test/bdt/checks/income/LoopIncome/Size 2 below cap.bru b/library-api/test/bdt/checks/income/LoopIncome/Size 2 below cap.bru new file mode 100644 index 00000000..c779d3e2 --- /dev/null +++ b/library-api/test/bdt/checks/income/LoopIncome/Size 2 below cap.bru @@ -0,0 +1,25 @@ +meta { + name: Size 2 below cap + type: http + seq: 6 +} + +post { + url: {{host}}/checks/income/loop-income + body: json + auth: inherit +} + +body:json { + { + "situation": { + "householdIncome": 117799.99, + "householdSize": 2 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq true +} diff --git a/library-api/test/bdt/checks/income/LoopIncome/Size 3 at cap.bru b/library-api/test/bdt/checks/income/LoopIncome/Size 3 at cap.bru new file mode 100644 index 00000000..3c166b4b --- /dev/null +++ b/library-api/test/bdt/checks/income/LoopIncome/Size 3 at cap.bru @@ -0,0 +1,25 @@ +meta { + name: Size 3 at cap + type: http + seq: 9 +} + +post { + url: {{host}}/checks/income/loop-income + body: json + auth: inherit +} + +body:json { + { + "situation": { + "householdIncome": 132500, + "householdSize": 3 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq false +} diff --git a/library-api/test/bdt/checks/income/LoopIncome/Size 3 below cap.bru b/library-api/test/bdt/checks/income/LoopIncome/Size 3 below cap.bru new file mode 100644 index 00000000..c76cca2f --- /dev/null +++ b/library-api/test/bdt/checks/income/LoopIncome/Size 3 below cap.bru @@ -0,0 +1,25 @@ +meta { + name: Size 3 below cap + type: http + seq: 8 +} + +post { + url: {{host}}/checks/income/loop-income + body: json + auth: inherit +} + +body:json { + { + "situation": { + "householdIncome": 132499.99, + "householdSize": 3 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq true +} diff --git a/library-api/test/bdt/checks/income/LoopIncome/Size 4 at cap.bru b/library-api/test/bdt/checks/income/LoopIncome/Size 4 at cap.bru new file mode 100644 index 00000000..f2347d54 --- /dev/null +++ b/library-api/test/bdt/checks/income/LoopIncome/Size 4 at cap.bru @@ -0,0 +1,25 @@ +meta { + name: Size 4 at cap + type: http + seq: 11 +} + +post { + url: {{host}}/checks/income/loop-income + body: json + auth: inherit +} + +body:json { + { + "situation": { + "householdIncome": 147200, + "householdSize": 4 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq false +} diff --git a/library-api/test/bdt/checks/income/LoopIncome/Size 4 below cap.bru b/library-api/test/bdt/checks/income/LoopIncome/Size 4 below cap.bru new file mode 100644 index 00000000..47d6ae82 --- /dev/null +++ b/library-api/test/bdt/checks/income/LoopIncome/Size 4 below cap.bru @@ -0,0 +1,25 @@ +meta { + name: Size 4 below cap + type: http + seq: 10 +} + +post { + url: {{host}}/checks/income/loop-income + body: json + auth: inherit +} + +body:json { + { + "situation": { + "householdIncome": 147199.99, + "householdSize": 4 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq true +} diff --git a/library-api/test/bdt/checks/income/LoopIncome/Size 5 at cap.bru b/library-api/test/bdt/checks/income/LoopIncome/Size 5 at cap.bru new file mode 100644 index 00000000..d3d6dfbb --- /dev/null +++ b/library-api/test/bdt/checks/income/LoopIncome/Size 5 at cap.bru @@ -0,0 +1,25 @@ +meta { + name: Size 5 at cap + type: http + seq: 13 +} + +post { + url: {{host}}/checks/income/loop-income + body: json + auth: inherit +} + +body:json { + { + "situation": { + "householdIncome": 159000, + "householdSize": 5 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq false +} diff --git a/library-api/test/bdt/checks/income/LoopIncome/Size 5 below cap.bru b/library-api/test/bdt/checks/income/LoopIncome/Size 5 below cap.bru new file mode 100644 index 00000000..965110ed --- /dev/null +++ b/library-api/test/bdt/checks/income/LoopIncome/Size 5 below cap.bru @@ -0,0 +1,25 @@ +meta { + name: Size 5 below cap + type: http + seq: 12 +} + +post { + url: {{host}}/checks/income/loop-income + body: json + auth: inherit +} + +body:json { + { + "situation": { + "householdIncome": 158999.99, + "householdSize": 5 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq true +} diff --git a/library-api/test/bdt/checks/income/LoopIncome/Size 6 at cap.bru b/library-api/test/bdt/checks/income/LoopIncome/Size 6 at cap.bru new file mode 100644 index 00000000..9e77d4b4 --- /dev/null +++ b/library-api/test/bdt/checks/income/LoopIncome/Size 6 at cap.bru @@ -0,0 +1,25 @@ +meta { + name: Size 6 at cap + type: http + seq: 15 +} + +post { + url: {{host}}/checks/income/loop-income + body: json + auth: inherit +} + +body:json { + { + "situation": { + "householdIncome": 170800, + "householdSize": 6 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq false +} diff --git a/library-api/test/bdt/checks/income/LoopIncome/Size 6 below cap.bru b/library-api/test/bdt/checks/income/LoopIncome/Size 6 below cap.bru new file mode 100644 index 00000000..7d82841a --- /dev/null +++ b/library-api/test/bdt/checks/income/LoopIncome/Size 6 below cap.bru @@ -0,0 +1,25 @@ +meta { + name: Size 6 below cap + type: http + seq: 14 +} + +post { + url: {{host}}/checks/income/loop-income + body: json + auth: inherit +} + +body:json { + { + "situation": { + "householdIncome": 170799.99, + "householdSize": 6 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq true +} diff --git a/library-api/test/bdt/checks/income/LoopIncome/Size 7 at cap.bru b/library-api/test/bdt/checks/income/LoopIncome/Size 7 at cap.bru new file mode 100644 index 00000000..e1aaba8b --- /dev/null +++ b/library-api/test/bdt/checks/income/LoopIncome/Size 7 at cap.bru @@ -0,0 +1,25 @@ +meta { + name: Size 7 at cap + type: http + seq: 17 +} + +post { + url: {{host}}/checks/income/loop-income + body: json + auth: inherit +} + +body:json { + { + "situation": { + "householdIncome": 182550, + "householdSize": 7 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq false +} diff --git a/library-api/test/bdt/checks/income/LoopIncome/Size 7 below cap.bru b/library-api/test/bdt/checks/income/LoopIncome/Size 7 below cap.bru new file mode 100644 index 00000000..875521f4 --- /dev/null +++ b/library-api/test/bdt/checks/income/LoopIncome/Size 7 below cap.bru @@ -0,0 +1,25 @@ +meta { + name: Size 7 below cap + type: http + seq: 16 +} + +post { + url: {{host}}/checks/income/loop-income + body: json + auth: inherit +} + +body:json { + { + "situation": { + "householdIncome": 182549.99, + "householdSize": 7 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq true +} diff --git a/library-api/test/bdt/checks/income/LoopIncome/Size 8 at cap.bru b/library-api/test/bdt/checks/income/LoopIncome/Size 8 at cap.bru new file mode 100644 index 00000000..240041b9 --- /dev/null +++ b/library-api/test/bdt/checks/income/LoopIncome/Size 8 at cap.bru @@ -0,0 +1,25 @@ +meta { + name: Size 8 at cap + type: http + seq: 19 +} + +post { + url: {{host}}/checks/income/loop-income + body: json + auth: inherit +} + +body:json { + { + "situation": { + "householdIncome": 194350, + "householdSize": 8 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq false +} diff --git a/library-api/test/bdt/checks/income/LoopIncome/Size 8 below cap.bru b/library-api/test/bdt/checks/income/LoopIncome/Size 8 below cap.bru new file mode 100644 index 00000000..ec3afff6 --- /dev/null +++ b/library-api/test/bdt/checks/income/LoopIncome/Size 8 below cap.bru @@ -0,0 +1,25 @@ +meta { + name: Size 8 below cap + type: http + seq: 18 +} + +post { + url: {{host}}/checks/income/loop-income + body: json + auth: inherit +} + +body:json { + { + "situation": { + "householdIncome": 194349.99, + "householdSize": 8 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq true +} diff --git a/library-api/test/bdt/checks/income/LoopIncome/Size 9 at cap.bru b/library-api/test/bdt/checks/income/LoopIncome/Size 9 at cap.bru new file mode 100644 index 00000000..83fff930 --- /dev/null +++ b/library-api/test/bdt/checks/income/LoopIncome/Size 9 at cap.bru @@ -0,0 +1,25 @@ +meta { + name: Size 9 at cap + type: http + seq: 21 +} + +post { + url: {{host}}/checks/income/loop-income + body: json + auth: inherit +} + +body:json { + { + "situation": { + "householdIncome": 206100, + "householdSize": 9 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq false +} diff --git a/library-api/test/bdt/checks/income/LoopIncome/Size 9 below cap.bru b/library-api/test/bdt/checks/income/LoopIncome/Size 9 below cap.bru new file mode 100644 index 00000000..3f7a1757 --- /dev/null +++ b/library-api/test/bdt/checks/income/LoopIncome/Size 9 below cap.bru @@ -0,0 +1,25 @@ +meta { + name: Size 9 below cap + type: http + seq: 20 +} + +post { + url: {{host}}/checks/income/loop-income + body: json + auth: inherit +} + +body:json { + { + "situation": { + "householdIncome": 206099.99, + "householdSize": 9 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq true +} diff --git a/library-api/test/bdt/checks/income/LoopIncome/Unknown or unsupported size -1.bru b/library-api/test/bdt/checks/income/LoopIncome/Unknown or unsupported size -1.bru new file mode 100644 index 00000000..1a74a859 --- /dev/null +++ b/library-api/test/bdt/checks/income/LoopIncome/Unknown or unsupported size -1.bru @@ -0,0 +1,25 @@ +meta { + name: Unknown or unsupported size -1 + type: http + seq: 25 +} + +post { + url: {{host}}/checks/income/loop-income + body: json + auth: inherit +} + +body:json { + { + "situation": { + "householdIncome": 10000, + "householdSize": -1 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq null +} diff --git a/library-api/test/bdt/checks/income/LoopIncome/Unknown or unsupported size 0.bru b/library-api/test/bdt/checks/income/LoopIncome/Unknown or unsupported size 0.bru new file mode 100644 index 00000000..ad21985d --- /dev/null +++ b/library-api/test/bdt/checks/income/LoopIncome/Unknown or unsupported size 0.bru @@ -0,0 +1,25 @@ +meta { + name: Unknown or unsupported size 0 + type: http + seq: 24 +} + +post { + url: {{host}}/checks/income/loop-income + body: json + auth: inherit +} + +body:json { + { + "situation": { + "householdIncome": 10000, + "householdSize": 0 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq null +} diff --git a/library-api/test/bdt/checks/income/LoopIncome/Unknown or unsupported size 1.5.bru b/library-api/test/bdt/checks/income/LoopIncome/Unknown or unsupported size 1.5.bru new file mode 100644 index 00000000..c2e7c8d5 --- /dev/null +++ b/library-api/test/bdt/checks/income/LoopIncome/Unknown or unsupported size 1.5.bru @@ -0,0 +1,25 @@ +meta { + name: Unknown or unsupported size 1.5 + type: http + seq: 26 +} + +post { + url: {{host}}/checks/income/loop-income + body: json + auth: inherit +} + +body:json { + { + "situation": { + "householdIncome": 10000, + "householdSize": 1.5 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq null +} diff --git a/library-api/test/bdt/checks/income/LoopIncome/Unknown or unsupported size 11.bru b/library-api/test/bdt/checks/income/LoopIncome/Unknown or unsupported size 11.bru new file mode 100644 index 00000000..bfe0e2ee --- /dev/null +++ b/library-api/test/bdt/checks/income/LoopIncome/Unknown or unsupported size 11.bru @@ -0,0 +1,25 @@ +meta { + name: Unknown or unsupported size 11 + type: http + seq: 27 +} + +post { + url: {{host}}/checks/income/loop-income + body: json + auth: inherit +} + +body:json { + { + "situation": { + "householdIncome": 10000, + "householdSize": 11 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq null +} diff --git a/library-api/test/bdt/checks/income/LoopIncome/Unknown or unsupported size None.bru b/library-api/test/bdt/checks/income/LoopIncome/Unknown or unsupported size None.bru new file mode 100644 index 00000000..675d33ac --- /dev/null +++ b/library-api/test/bdt/checks/income/LoopIncome/Unknown or unsupported size None.bru @@ -0,0 +1,25 @@ +meta { + name: Unknown or unsupported size None + type: http + seq: 28 +} + +post { + url: {{host}}/checks/income/loop-income + body: json + auth: inherit +} + +body:json { + { + "situation": { + "householdIncome": 10000, + "householdSize": null + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq null +} diff --git a/library-api/test/bdt/checks/income/LoopIncome/folder.bru b/library-api/test/bdt/checks/income/LoopIncome/folder.bru new file mode 100644 index 00000000..e5dd1ee3 --- /dev/null +++ b/library-api/test/bdt/checks/income/LoopIncome/folder.bru @@ -0,0 +1,8 @@ +meta { + name: LoopIncome + seq: 1 +} + +auth { + mode: inherit +} diff --git a/library-api/test/bdt/checks/income/RetipIncome/Fail.bru b/library-api/test/bdt/checks/income/RetipIncome/Fail.bru new file mode 100644 index 00000000..428c1714 --- /dev/null +++ b/library-api/test/bdt/checks/income/RetipIncome/Fail.bru @@ -0,0 +1,25 @@ +meta { + name: Fail + type: http + seq: 2 +} + +post { + url: {{host}}/checks/income/retip-income + body: json + auth: inherit +} + +body:json { + { + "situation": { + "householdIncome": 42951, + "householdSize": 1 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq false +} diff --git a/library-api/test/bdt/checks/income/RetipIncome/Invalid income.bru b/library-api/test/bdt/checks/income/RetipIncome/Invalid income.bru new file mode 100644 index 00000000..64b6b867 --- /dev/null +++ b/library-api/test/bdt/checks/income/RetipIncome/Invalid income.bru @@ -0,0 +1,25 @@ +meta { + name: Invalid income + type: http + seq: 25 +} + +post { + url: {{host}}/checks/income/retip-income + body: json + auth: inherit +} + +body:json { + { + "situation": { + "householdIncome": -1, + "householdSize": 1 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq null +} diff --git a/library-api/test/bdt/checks/income/RetipIncome/Null.bru b/library-api/test/bdt/checks/income/RetipIncome/Null.bru new file mode 100644 index 00000000..79023bfa --- /dev/null +++ b/library-api/test/bdt/checks/income/RetipIncome/Null.bru @@ -0,0 +1,24 @@ +meta { + name: Null + type: http + seq: 3 +} + +post { + url: {{host}}/checks/income/retip-income + body: json + auth: inherit +} + +body:json { + { + "situation": { + "householdSize": 1 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq null +} diff --git a/library-api/test/bdt/checks/income/RetipIncome/Pass.bru b/library-api/test/bdt/checks/income/RetipIncome/Pass.bru new file mode 100644 index 00000000..329af536 --- /dev/null +++ b/library-api/test/bdt/checks/income/RetipIncome/Pass.bru @@ -0,0 +1,25 @@ +meta { + name: Pass + type: http + seq: 1 +} + +post { + url: {{host}}/checks/income/retip-income + body: json + auth: inherit +} + +body:json { + { + "situation": { + "householdIncome": 42949, + "householdSize": 1 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq true +} diff --git a/library-api/test/bdt/checks/income/RetipIncome/Size 1 at cap.bru b/library-api/test/bdt/checks/income/RetipIncome/Size 1 at cap.bru new file mode 100644 index 00000000..bab3ada9 --- /dev/null +++ b/library-api/test/bdt/checks/income/RetipIncome/Size 1 at cap.bru @@ -0,0 +1,25 @@ +meta { + name: Size 1 at cap + type: http + seq: 5 +} + +post { + url: {{host}}/checks/income/retip-income + body: json + auth: inherit +} + +body:json { + { + "situation": { + "householdIncome": 42950, + "householdSize": 1 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq false +} diff --git a/library-api/test/bdt/checks/income/RetipIncome/Size 1 below cap.bru b/library-api/test/bdt/checks/income/RetipIncome/Size 1 below cap.bru new file mode 100644 index 00000000..c891ea0b --- /dev/null +++ b/library-api/test/bdt/checks/income/RetipIncome/Size 1 below cap.bru @@ -0,0 +1,25 @@ +meta { + name: Size 1 below cap + type: http + seq: 4 +} + +post { + url: {{host}}/checks/income/retip-income + body: json + auth: inherit +} + +body:json { + { + "situation": { + "householdIncome": 42949.99, + "householdSize": 1 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq true +} diff --git a/library-api/test/bdt/checks/income/RetipIncome/Size 2 at cap.bru b/library-api/test/bdt/checks/income/RetipIncome/Size 2 at cap.bru new file mode 100644 index 00000000..408616e5 --- /dev/null +++ b/library-api/test/bdt/checks/income/RetipIncome/Size 2 at cap.bru @@ -0,0 +1,25 @@ +meta { + name: Size 2 at cap + type: http + seq: 7 +} + +post { + url: {{host}}/checks/income/retip-income + body: json + auth: inherit +} + +body:json { + { + "situation": { + "householdIncome": 49100, + "householdSize": 2 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq false +} diff --git a/library-api/test/bdt/checks/income/RetipIncome/Size 2 below cap.bru b/library-api/test/bdt/checks/income/RetipIncome/Size 2 below cap.bru new file mode 100644 index 00000000..30b36323 --- /dev/null +++ b/library-api/test/bdt/checks/income/RetipIncome/Size 2 below cap.bru @@ -0,0 +1,25 @@ +meta { + name: Size 2 below cap + type: http + seq: 6 +} + +post { + url: {{host}}/checks/income/retip-income + body: json + auth: inherit +} + +body:json { + { + "situation": { + "householdIncome": 49099.99, + "householdSize": 2 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq true +} diff --git a/library-api/test/bdt/checks/income/RetipIncome/Size 3 at cap.bru b/library-api/test/bdt/checks/income/RetipIncome/Size 3 at cap.bru new file mode 100644 index 00000000..cadb01a8 --- /dev/null +++ b/library-api/test/bdt/checks/income/RetipIncome/Size 3 at cap.bru @@ -0,0 +1,25 @@ +meta { + name: Size 3 at cap + type: http + seq: 9 +} + +post { + url: {{host}}/checks/income/retip-income + body: json + auth: inherit +} + +body:json { + { + "situation": { + "householdIncome": 55250, + "householdSize": 3 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq false +} diff --git a/library-api/test/bdt/checks/income/RetipIncome/Size 3 below cap.bru b/library-api/test/bdt/checks/income/RetipIncome/Size 3 below cap.bru new file mode 100644 index 00000000..28ae5df3 --- /dev/null +++ b/library-api/test/bdt/checks/income/RetipIncome/Size 3 below cap.bru @@ -0,0 +1,25 @@ +meta { + name: Size 3 below cap + type: http + seq: 8 +} + +post { + url: {{host}}/checks/income/retip-income + body: json + auth: inherit +} + +body:json { + { + "situation": { + "householdIncome": 55249.99, + "householdSize": 3 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq true +} diff --git a/library-api/test/bdt/checks/income/RetipIncome/Size 4 at cap.bru b/library-api/test/bdt/checks/income/RetipIncome/Size 4 at cap.bru new file mode 100644 index 00000000..b4959548 --- /dev/null +++ b/library-api/test/bdt/checks/income/RetipIncome/Size 4 at cap.bru @@ -0,0 +1,25 @@ +meta { + name: Size 4 at cap + type: http + seq: 11 +} + +post { + url: {{host}}/checks/income/retip-income + body: json + auth: inherit +} + +body:json { + { + "situation": { + "householdIncome": 61350, + "householdSize": 4 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq false +} diff --git a/library-api/test/bdt/checks/income/RetipIncome/Size 4 below cap.bru b/library-api/test/bdt/checks/income/RetipIncome/Size 4 below cap.bru new file mode 100644 index 00000000..eac3cf35 --- /dev/null +++ b/library-api/test/bdt/checks/income/RetipIncome/Size 4 below cap.bru @@ -0,0 +1,25 @@ +meta { + name: Size 4 below cap + type: http + seq: 10 +} + +post { + url: {{host}}/checks/income/retip-income + body: json + auth: inherit +} + +body:json { + { + "situation": { + "householdIncome": 61349.99, + "householdSize": 4 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq true +} diff --git a/library-api/test/bdt/checks/income/RetipIncome/Size 5 at cap.bru b/library-api/test/bdt/checks/income/RetipIncome/Size 5 at cap.bru new file mode 100644 index 00000000..02e21d18 --- /dev/null +++ b/library-api/test/bdt/checks/income/RetipIncome/Size 5 at cap.bru @@ -0,0 +1,25 @@ +meta { + name: Size 5 at cap + type: http + seq: 13 +} + +post { + url: {{host}}/checks/income/retip-income + body: json + auth: inherit +} + +body:json { + { + "situation": { + "householdIncome": 66300, + "householdSize": 5 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq false +} diff --git a/library-api/test/bdt/checks/income/RetipIncome/Size 5 below cap.bru b/library-api/test/bdt/checks/income/RetipIncome/Size 5 below cap.bru new file mode 100644 index 00000000..d1e8a4e6 --- /dev/null +++ b/library-api/test/bdt/checks/income/RetipIncome/Size 5 below cap.bru @@ -0,0 +1,25 @@ +meta { + name: Size 5 below cap + type: http + seq: 12 +} + +post { + url: {{host}}/checks/income/retip-income + body: json + auth: inherit +} + +body:json { + { + "situation": { + "householdIncome": 66299.99, + "householdSize": 5 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq true +} diff --git a/library-api/test/bdt/checks/income/RetipIncome/Size 6 at cap.bru b/library-api/test/bdt/checks/income/RetipIncome/Size 6 at cap.bru new file mode 100644 index 00000000..c34ff57c --- /dev/null +++ b/library-api/test/bdt/checks/income/RetipIncome/Size 6 at cap.bru @@ -0,0 +1,25 @@ +meta { + name: Size 6 at cap + type: http + seq: 15 +} + +post { + url: {{host}}/checks/income/retip-income + body: json + auth: inherit +} + +body:json { + { + "situation": { + "householdIncome": 71200, + "householdSize": 6 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq false +} diff --git a/library-api/test/bdt/checks/income/RetipIncome/Size 6 below cap.bru b/library-api/test/bdt/checks/income/RetipIncome/Size 6 below cap.bru new file mode 100644 index 00000000..6a2cc491 --- /dev/null +++ b/library-api/test/bdt/checks/income/RetipIncome/Size 6 below cap.bru @@ -0,0 +1,25 @@ +meta { + name: Size 6 below cap + type: http + seq: 14 +} + +post { + url: {{host}}/checks/income/retip-income + body: json + auth: inherit +} + +body:json { + { + "situation": { + "householdIncome": 71199.99, + "householdSize": 6 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq true +} diff --git a/library-api/test/bdt/checks/income/RetipIncome/Size 7 at cap.bru b/library-api/test/bdt/checks/income/RetipIncome/Size 7 at cap.bru new file mode 100644 index 00000000..b1f90eb5 --- /dev/null +++ b/library-api/test/bdt/checks/income/RetipIncome/Size 7 at cap.bru @@ -0,0 +1,25 @@ +meta { + name: Size 7 at cap + type: http + seq: 17 +} + +post { + url: {{host}}/checks/income/retip-income + body: json + auth: inherit +} + +body:json { + { + "situation": { + "householdIncome": 76100, + "householdSize": 7 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq false +} diff --git a/library-api/test/bdt/checks/income/RetipIncome/Size 7 below cap.bru b/library-api/test/bdt/checks/income/RetipIncome/Size 7 below cap.bru new file mode 100644 index 00000000..2a751f30 --- /dev/null +++ b/library-api/test/bdt/checks/income/RetipIncome/Size 7 below cap.bru @@ -0,0 +1,25 @@ +meta { + name: Size 7 below cap + type: http + seq: 16 +} + +post { + url: {{host}}/checks/income/retip-income + body: json + auth: inherit +} + +body:json { + { + "situation": { + "householdIncome": 76099.99, + "householdSize": 7 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq true +} diff --git a/library-api/test/bdt/checks/income/RetipIncome/Size 8 at cap.bru b/library-api/test/bdt/checks/income/RetipIncome/Size 8 at cap.bru new file mode 100644 index 00000000..5ca4cd15 --- /dev/null +++ b/library-api/test/bdt/checks/income/RetipIncome/Size 8 at cap.bru @@ -0,0 +1,25 @@ +meta { + name: Size 8 at cap + type: http + seq: 19 +} + +post { + url: {{host}}/checks/income/retip-income + body: json + auth: inherit +} + +body:json { + { + "situation": { + "householdIncome": 81000, + "householdSize": 8 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq false +} diff --git a/library-api/test/bdt/checks/income/RetipIncome/Size 8 below cap.bru b/library-api/test/bdt/checks/income/RetipIncome/Size 8 below cap.bru new file mode 100644 index 00000000..403ddb5b --- /dev/null +++ b/library-api/test/bdt/checks/income/RetipIncome/Size 8 below cap.bru @@ -0,0 +1,25 @@ +meta { + name: Size 8 below cap + type: http + seq: 18 +} + +post { + url: {{host}}/checks/income/retip-income + body: json + auth: inherit +} + +body:json { + { + "situation": { + "householdIncome": 80999.99, + "householdSize": 8 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq true +} diff --git a/library-api/test/bdt/checks/income/RetipIncome/Unknown or unsupported size -1.bru b/library-api/test/bdt/checks/income/RetipIncome/Unknown or unsupported size -1.bru new file mode 100644 index 00000000..f80f7acd --- /dev/null +++ b/library-api/test/bdt/checks/income/RetipIncome/Unknown or unsupported size -1.bru @@ -0,0 +1,25 @@ +meta { + name: Unknown or unsupported size -1 + type: http + seq: 21 +} + +post { + url: {{host}}/checks/income/retip-income + body: json + auth: inherit +} + +body:json { + { + "situation": { + "householdIncome": 10000, + "householdSize": -1 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq null +} diff --git a/library-api/test/bdt/checks/income/RetipIncome/Unknown or unsupported size 0.bru b/library-api/test/bdt/checks/income/RetipIncome/Unknown or unsupported size 0.bru new file mode 100644 index 00000000..cf93990f --- /dev/null +++ b/library-api/test/bdt/checks/income/RetipIncome/Unknown or unsupported size 0.bru @@ -0,0 +1,25 @@ +meta { + name: Unknown or unsupported size 0 + type: http + seq: 20 +} + +post { + url: {{host}}/checks/income/retip-income + body: json + auth: inherit +} + +body:json { + { + "situation": { + "householdIncome": 10000, + "householdSize": 0 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq null +} diff --git a/library-api/test/bdt/checks/income/RetipIncome/Unknown or unsupported size 1.5.bru b/library-api/test/bdt/checks/income/RetipIncome/Unknown or unsupported size 1.5.bru new file mode 100644 index 00000000..1a6469a9 --- /dev/null +++ b/library-api/test/bdt/checks/income/RetipIncome/Unknown or unsupported size 1.5.bru @@ -0,0 +1,25 @@ +meta { + name: Unknown or unsupported size 1.5 + type: http + seq: 22 +} + +post { + url: {{host}}/checks/income/retip-income + body: json + auth: inherit +} + +body:json { + { + "situation": { + "householdIncome": 10000, + "householdSize": 1.5 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq null +} diff --git a/library-api/test/bdt/checks/income/RetipIncome/Unknown or unsupported size 9.bru b/library-api/test/bdt/checks/income/RetipIncome/Unknown or unsupported size 9.bru new file mode 100644 index 00000000..3be77686 --- /dev/null +++ b/library-api/test/bdt/checks/income/RetipIncome/Unknown or unsupported size 9.bru @@ -0,0 +1,25 @@ +meta { + name: Unknown or unsupported size 9 + type: http + seq: 23 +} + +post { + url: {{host}}/checks/income/retip-income + body: json + auth: inherit +} + +body:json { + { + "situation": { + "householdIncome": 10000, + "householdSize": 9 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq null +} diff --git a/library-api/test/bdt/checks/income/RetipIncome/Unknown or unsupported size None.bru b/library-api/test/bdt/checks/income/RetipIncome/Unknown or unsupported size None.bru new file mode 100644 index 00000000..e1e23fef --- /dev/null +++ b/library-api/test/bdt/checks/income/RetipIncome/Unknown or unsupported size None.bru @@ -0,0 +1,25 @@ +meta { + name: Unknown or unsupported size None + type: http + seq: 24 +} + +post { + url: {{host}}/checks/income/retip-income + body: json + auth: inherit +} + +body:json { + { + "situation": { + "householdIncome": 10000, + "householdSize": null + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq null +} diff --git a/library-api/test/bdt/checks/income/RetipIncome/folder.bru b/library-api/test/bdt/checks/income/RetipIncome/folder.bru new file mode 100644 index 00000000..45d2c166 --- /dev/null +++ b/library-api/test/bdt/checks/income/RetipIncome/folder.bru @@ -0,0 +1,8 @@ +meta { + name: RetipIncome + seq: 1 +} + +auth { + mode: inherit +} diff --git a/library-api/test/bdt/checks/income/TaxFreezeIncome/Fail.bru b/library-api/test/bdt/checks/income/TaxFreezeIncome/Fail.bru new file mode 100644 index 00000000..87bb04cb --- /dev/null +++ b/library-api/test/bdt/checks/income/TaxFreezeIncome/Fail.bru @@ -0,0 +1,27 @@ +meta { + name: Fail + type: http + seq: 2 +} + +post { + url: {{host}}/checks/income/tax-freeze-income + body: json + auth: inherit +} + +body:json { + { + "situation": { + "householdIncome": 33500.01, + "simpleChecks": { + "isMarried": false + } + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq false +} diff --git a/library-api/test/bdt/checks/income/TaxFreezeIncome/Income unknown.bru b/library-api/test/bdt/checks/income/TaxFreezeIncome/Income unknown.bru new file mode 100644 index 00000000..39390ef9 --- /dev/null +++ b/library-api/test/bdt/checks/income/TaxFreezeIncome/Income unknown.bru @@ -0,0 +1,26 @@ +meta { + name: Income unknown + type: http + seq: 6 +} + +post { + url: {{host}}/checks/income/tax-freeze-income + body: json + auth: inherit +} + +body:json { + { + "situation": { + "simpleChecks": { + "isMarried": false + } + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq null +} diff --git a/library-api/test/bdt/checks/income/TaxFreezeIncome/Invalid income.bru b/library-api/test/bdt/checks/income/TaxFreezeIncome/Invalid income.bru new file mode 100644 index 00000000..dd75b818 --- /dev/null +++ b/library-api/test/bdt/checks/income/TaxFreezeIncome/Invalid income.bru @@ -0,0 +1,27 @@ +meta { + name: Invalid income + type: http + seq: 8 +} + +post { + url: {{host}}/checks/income/tax-freeze-income + body: json + auth: inherit +} + +body:json { + { + "situation": { + "householdIncome": -1, + "simpleChecks": { + "isMarried": false + } + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq null +} diff --git a/library-api/test/bdt/checks/income/TaxFreezeIncome/Married at limit.bru b/library-api/test/bdt/checks/income/TaxFreezeIncome/Married at limit.bru new file mode 100644 index 00000000..4da5d857 --- /dev/null +++ b/library-api/test/bdt/checks/income/TaxFreezeIncome/Married at limit.bru @@ -0,0 +1,27 @@ +meta { + name: Married at limit + type: http + seq: 4 +} + +post { + url: {{host}}/checks/income/tax-freeze-income + body: json + auth: inherit +} + +body:json { + { + "situation": { + "householdIncome": 41500, + "simpleChecks": { + "isMarried": true + } + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq true +} diff --git a/library-api/test/bdt/checks/income/TaxFreezeIncome/Married over limit.bru b/library-api/test/bdt/checks/income/TaxFreezeIncome/Married over limit.bru new file mode 100644 index 00000000..602f26ee --- /dev/null +++ b/library-api/test/bdt/checks/income/TaxFreezeIncome/Married over limit.bru @@ -0,0 +1,27 @@ +meta { + name: Married over limit + type: http + seq: 5 +} + +post { + url: {{host}}/checks/income/tax-freeze-income + body: json + auth: inherit +} + +body:json { + { + "situation": { + "householdIncome": 41500.01, + "simpleChecks": { + "isMarried": true + } + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq false +} diff --git a/library-api/test/bdt/checks/income/TaxFreezeIncome/Null.bru b/library-api/test/bdt/checks/income/TaxFreezeIncome/Null.bru new file mode 100644 index 00000000..5a0811ea --- /dev/null +++ b/library-api/test/bdt/checks/income/TaxFreezeIncome/Null.bru @@ -0,0 +1,27 @@ +meta { + name: Null + type: http + seq: 3 +} + +post { + url: {{host}}/checks/income/tax-freeze-income + body: json + auth: inherit +} + +body:json { + { + "situation": { + "householdIncome": 30000, + "simpleChecks": { + "isMarried": null + } + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq null +} diff --git a/library-api/test/bdt/checks/income/TaxFreezeIncome/Pass.bru b/library-api/test/bdt/checks/income/TaxFreezeIncome/Pass.bru new file mode 100644 index 00000000..19e3d3b2 --- /dev/null +++ b/library-api/test/bdt/checks/income/TaxFreezeIncome/Pass.bru @@ -0,0 +1,27 @@ +meta { + name: Pass + type: http + seq: 1 +} + +post { + url: {{host}}/checks/income/tax-freeze-income + body: json + auth: inherit +} + +body:json { + { + "situation": { + "householdIncome": 33500, + "simpleChecks": { + "isMarried": false + } + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq true +} diff --git a/library-api/test/bdt/checks/income/TaxFreezeIncome/Zero income.bru b/library-api/test/bdt/checks/income/TaxFreezeIncome/Zero income.bru new file mode 100644 index 00000000..00d39fdc --- /dev/null +++ b/library-api/test/bdt/checks/income/TaxFreezeIncome/Zero income.bru @@ -0,0 +1,27 @@ +meta { + name: Zero income + type: http + seq: 7 +} + +post { + url: {{host}}/checks/income/tax-freeze-income + body: json + auth: inherit +} + +body:json { + { + "situation": { + "householdIncome": 0, + "simpleChecks": { + "isMarried": false + } + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq true +} diff --git a/library-api/test/bdt/checks/income/TaxFreezeIncome/folder.bru b/library-api/test/bdt/checks/income/TaxFreezeIncome/folder.bru new file mode 100644 index 00000000..96294704 --- /dev/null +++ b/library-api/test/bdt/checks/income/TaxFreezeIncome/folder.bru @@ -0,0 +1,8 @@ +meta { + name: TaxFreezeIncome + seq: 1 +} + +auth { + mode: inherit +} diff --git a/library-api/test/bdt/checks/income/folder.bru b/library-api/test/bdt/checks/income/folder.bru new file mode 100644 index 00000000..385a1779 --- /dev/null +++ b/library-api/test/bdt/checks/income/folder.bru @@ -0,0 +1,8 @@ +meta { + name: Income + seq: 5 +} + +auth { + mode: inherit +} diff --git a/library-api/test/bdt/checks/internal/RetipAgeOrIncomeRequirement/Fail.bru b/library-api/test/bdt/checks/internal/RetipAgeOrIncomeRequirement/Fail.bru new file mode 100644 index 00000000..9ad86e3e --- /dev/null +++ b/library-api/test/bdt/checks/internal/RetipAgeOrIncomeRequirement/Fail.bru @@ -0,0 +1,33 @@ +meta { + name: Fail + type: http + seq: 2 +} + +post { + url: {{host}}/checks/internal/retip-age-or-income-requirement + body: json + auth: inherit +} + +body:json { + { + "situation": { + "primaryPersonId": "p1", + "people": [ + { + "id": "p1", + "dateOfBirth": "2000-01-01" + } + ], + "relationships": [], + "householdSize": 1, + "householdIncome": 100000 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq false +} diff --git a/library-api/test/bdt/checks/internal/RetipAgeOrIncomeRequirement/Late spouse does not qualify.bru b/library-api/test/bdt/checks/internal/RetipAgeOrIncomeRequirement/Late spouse does not qualify.bru new file mode 100644 index 00000000..6dcdded4 --- /dev/null +++ b/library-api/test/bdt/checks/internal/RetipAgeOrIncomeRequirement/Late spouse does not qualify.bru @@ -0,0 +1,36 @@ +meta { + name: Late spouse does not qualify + type: http + seq: 7 +} + +post { + url: {{host}}/checks/internal/retip-age-or-income-requirement + body: json + auth: inherit +} + +body:json { + { + "situation": { + "primaryPersonId": "p1", + "people": [ + { + "id": "p1", + "dateOfBirth": "2000-01-01" + } + ], + "relationships": [], + "simpleChecks": { + "lateSpouseWasAtLeast65": true + }, + "householdSize": 1, + "householdIncome": 100000 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq false +} diff --git a/library-api/test/bdt/checks/internal/RetipAgeOrIncomeRequirement/Live in spouse.bru b/library-api/test/bdt/checks/internal/RetipAgeOrIncomeRequirement/Live in spouse.bru new file mode 100644 index 00000000..705776b5 --- /dev/null +++ b/library-api/test/bdt/checks/internal/RetipAgeOrIncomeRequirement/Live in spouse.bru @@ -0,0 +1,44 @@ +meta { + name: Live in spouse + type: http + seq: 6 +} + +post { + url: {{host}}/checks/internal/retip-age-or-income-requirement + body: json + auth: inherit +} + +body:json { + { + "situation": { + "primaryPersonId": "p1", + "people": [ + { + "id": "p1", + "dateOfBirth": "2000-01-01" + }, + { + "id": "p2", + "dateOfBirth": "1940-01-01" + } + ], + "relationships": [ + { + "personId": "p1", + "relatedPersonId": "p2", + "type": "spouse" + } + ], + "simpleChecks": { + "spouseLivesInHousehold": true + } + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq true +} diff --git a/library-api/test/bdt/checks/internal/RetipAgeOrIncomeRequirement/Low income without age.bru b/library-api/test/bdt/checks/internal/RetipAgeOrIncomeRequirement/Low income without age.bru new file mode 100644 index 00000000..55ab758b --- /dev/null +++ b/library-api/test/bdt/checks/internal/RetipAgeOrIncomeRequirement/Low income without age.bru @@ -0,0 +1,25 @@ +meta { + name: Low income without age + type: http + seq: 4 +} + +post { + url: {{host}}/checks/internal/retip-age-or-income-requirement + body: json + auth: inherit +} + +body:json { + { + "situation": { + "householdSize": 1, + "householdIncome": 30000 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq true +} diff --git a/library-api/test/bdt/checks/internal/RetipAgeOrIncomeRequirement/Null.bru b/library-api/test/bdt/checks/internal/RetipAgeOrIncomeRequirement/Null.bru new file mode 100644 index 00000000..08848f68 --- /dev/null +++ b/library-api/test/bdt/checks/internal/RetipAgeOrIncomeRequirement/Null.bru @@ -0,0 +1,22 @@ +meta { + name: Null + type: http + seq: 3 +} + +post { + url: {{host}}/checks/internal/retip-age-or-income-requirement + body: json + auth: inherit +} + +body:json { + { + "situation": {} + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq null +} diff --git a/library-api/test/bdt/checks/internal/RetipAgeOrIncomeRequirement/Pass.bru b/library-api/test/bdt/checks/internal/RetipAgeOrIncomeRequirement/Pass.bru new file mode 100644 index 00000000..4f035f99 --- /dev/null +++ b/library-api/test/bdt/checks/internal/RetipAgeOrIncomeRequirement/Pass.bru @@ -0,0 +1,30 @@ +meta { + name: Pass + type: http + seq: 1 +} + +post { + url: {{host}}/checks/internal/retip-age-or-income-requirement + body: json + auth: inherit +} + +body:json { + { + "situation": { + "primaryPersonId": "p1", + "people": [ + { + "id": "p1", + "dateOfBirth": "1940-01-01" + } + ] + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq true +} diff --git a/library-api/test/bdt/checks/internal/RetipAgeOrIncomeRequirement/Senior above income limit.bru b/library-api/test/bdt/checks/internal/RetipAgeOrIncomeRequirement/Senior above income limit.bru new file mode 100644 index 00000000..1d4b9f98 --- /dev/null +++ b/library-api/test/bdt/checks/internal/RetipAgeOrIncomeRequirement/Senior above income limit.bru @@ -0,0 +1,32 @@ +meta { + name: Senior above income limit + type: http + seq: 5 +} + +post { + url: {{host}}/checks/internal/retip-age-or-income-requirement + body: json + auth: inherit +} + +body:json { + { + "situation": { + "primaryPersonId": "p1", + "people": [ + { + "id": "p1", + "dateOfBirth": "1940-01-01" + } + ], + "householdSize": 1, + "householdIncome": 1000000 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq true +} diff --git a/library-api/test/bdt/checks/internal/RetipAgeOrIncomeRequirement/folder.bru b/library-api/test/bdt/checks/internal/RetipAgeOrIncomeRequirement/folder.bru new file mode 100644 index 00000000..e71dc5b2 --- /dev/null +++ b/library-api/test/bdt/checks/internal/RetipAgeOrIncomeRequirement/folder.bru @@ -0,0 +1,8 @@ +meta { + name: RetipAgeOrIncomeRequirement + seq: 1 +} + +auth { + mode: inherit +} diff --git a/library-api/test/bdt/checks/residence/MinYearsOwnerOccupant/Fail.bru b/library-api/test/bdt/checks/residence/MinYearsOwnerOccupant/Fail.bru new file mode 100644 index 00000000..78974c71 --- /dev/null +++ b/library-api/test/bdt/checks/residence/MinYearsOwnerOccupant/Fail.bru @@ -0,0 +1,27 @@ +meta { + name: Fail + type: http + seq: 2 +} + +post { + url: {{host}}/checks/residence/min-years-owner-occupant + body: json + auth: inherit +} + +body:json { + { + "situation": { + "yearsOwnerOccupied": 9.99 + }, + "parameters": { + "minYears": 10 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq false +} diff --git a/library-api/test/bdt/checks/residence/MinYearsOwnerOccupant/Invalid years.bru b/library-api/test/bdt/checks/residence/MinYearsOwnerOccupant/Invalid years.bru new file mode 100644 index 00000000..7cac99d2 --- /dev/null +++ b/library-api/test/bdt/checks/residence/MinYearsOwnerOccupant/Invalid years.bru @@ -0,0 +1,27 @@ +meta { + name: Invalid years + type: http + seq: 5 +} + +post { + url: {{host}}/checks/residence/min-years-owner-occupant + body: json + auth: inherit +} + +body:json { + { + "situation": { + "yearsOwnerOccupied": -1 + }, + "parameters": { + "minYears": 10 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq null +} diff --git a/library-api/test/bdt/checks/residence/MinYearsOwnerOccupant/Longer residency.bru b/library-api/test/bdt/checks/residence/MinYearsOwnerOccupant/Longer residency.bru new file mode 100644 index 00000000..27be12bf --- /dev/null +++ b/library-api/test/bdt/checks/residence/MinYearsOwnerOccupant/Longer residency.bru @@ -0,0 +1,27 @@ +meta { + name: Longer residency + type: http + seq: 4 +} + +post { + url: {{host}}/checks/residence/min-years-owner-occupant + body: json + auth: inherit +} + +body:json { + { + "situation": { + "yearsOwnerOccupied": 20 + }, + "parameters": { + "minYears": 10 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq true +} diff --git a/library-api/test/bdt/checks/residence/MinYearsOwnerOccupant/Null.bru b/library-api/test/bdt/checks/residence/MinYearsOwnerOccupant/Null.bru new file mode 100644 index 00000000..7c22e822 --- /dev/null +++ b/library-api/test/bdt/checks/residence/MinYearsOwnerOccupant/Null.bru @@ -0,0 +1,25 @@ +meta { + name: Null + type: http + seq: 3 +} + +post { + url: {{host}}/checks/residence/min-years-owner-occupant + body: json + auth: inherit +} + +body:json { + { + "situation": {}, + "parameters": { + "minYears": 10 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq null +} diff --git a/library-api/test/bdt/checks/residence/MinYearsOwnerOccupant/Pass.bru b/library-api/test/bdt/checks/residence/MinYearsOwnerOccupant/Pass.bru new file mode 100644 index 00000000..907c1157 --- /dev/null +++ b/library-api/test/bdt/checks/residence/MinYearsOwnerOccupant/Pass.bru @@ -0,0 +1,27 @@ +meta { + name: Pass + type: http + seq: 1 +} + +post { + url: {{host}}/checks/residence/min-years-owner-occupant + body: json + auth: inherit +} + +body:json { + { + "situation": { + "yearsOwnerOccupied": 10 + }, + "parameters": { + "minYears": 10 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq true +} diff --git a/library-api/test/bdt/checks/residence/MinYearsOwnerOccupant/folder.bru b/library-api/test/bdt/checks/residence/MinYearsOwnerOccupant/folder.bru new file mode 100644 index 00000000..baaf48a5 --- /dev/null +++ b/library-api/test/bdt/checks/residence/MinYearsOwnerOccupant/folder.bru @@ -0,0 +1,8 @@ +meta { + name: MinYearsOwnerOccupant + seq: 1 +} + +auth { + mode: inherit +} diff --git a/library-api/test/bdt/checks/residence/NoDisqualifyingTaxAbatement/Fail.bru b/library-api/test/bdt/checks/residence/NoDisqualifyingTaxAbatement/Fail.bru new file mode 100644 index 00000000..2c770c54 --- /dev/null +++ b/library-api/test/bdt/checks/residence/NoDisqualifyingTaxAbatement/Fail.bru @@ -0,0 +1,26 @@ +meta { + name: Fail + type: http + seq: 2 +} + +post { + url: {{host}}/checks/residence/no-disqualifying-tax-abatement + body: json + auth: inherit +} + +body:json { + { + "situation": { + "simpleChecks": { + "disqualifyingTaxAbatement": true + } + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq false +} diff --git a/library-api/test/bdt/checks/residence/NoDisqualifyingTaxAbatement/Null.bru b/library-api/test/bdt/checks/residence/NoDisqualifyingTaxAbatement/Null.bru new file mode 100644 index 00000000..2366c87d --- /dev/null +++ b/library-api/test/bdt/checks/residence/NoDisqualifyingTaxAbatement/Null.bru @@ -0,0 +1,26 @@ +meta { + name: Null + type: http + seq: 3 +} + +post { + url: {{host}}/checks/residence/no-disqualifying-tax-abatement + body: json + auth: inherit +} + +body:json { + { + "situation": { + "simpleChecks": { + "disqualifyingTaxAbatement": null + } + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq null +} diff --git a/library-api/test/bdt/checks/residence/NoDisqualifyingTaxAbatement/Pass.bru b/library-api/test/bdt/checks/residence/NoDisqualifyingTaxAbatement/Pass.bru new file mode 100644 index 00000000..2a9680b8 --- /dev/null +++ b/library-api/test/bdt/checks/residence/NoDisqualifyingTaxAbatement/Pass.bru @@ -0,0 +1,26 @@ +meta { + name: Pass + type: http + seq: 1 +} + +post { + url: {{host}}/checks/residence/no-disqualifying-tax-abatement + body: json + auth: inherit +} + +body:json { + { + "situation": { + "simpleChecks": { + "disqualifyingTaxAbatement": false + } + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq true +} diff --git a/library-api/test/bdt/checks/residence/NoDisqualifyingTaxAbatement/folder.bru b/library-api/test/bdt/checks/residence/NoDisqualifyingTaxAbatement/folder.bru new file mode 100644 index 00000000..dacc95ac --- /dev/null +++ b/library-api/test/bdt/checks/residence/NoDisqualifyingTaxAbatement/folder.bru @@ -0,0 +1,8 @@ +meta { + name: NoDisqualifyingTaxAbatement + seq: 1 +} + +auth { + mode: inherit +} diff --git a/library-api/test/bdt/checks/residence/PropertyAssessmentIncrease/Fail.bru b/library-api/test/bdt/checks/residence/PropertyAssessmentIncrease/Fail.bru new file mode 100644 index 00000000..ccb3c4a9 --- /dev/null +++ b/library-api/test/bdt/checks/residence/PropertyAssessmentIncrease/Fail.bru @@ -0,0 +1,30 @@ +meta { + name: Fail + type: http + seq: 2 +} + +post { + url: {{host}}/checks/residence/property-assessment-increase + body: json + auth: inherit +} + +body:json { + { + "situation": { + "propertyAssessment": 149999, + "previousYearPropertyAssessment": 100000, + "lowestPropertyAssessmentLastFiveYears": 100000 + }, + "parameters": { + "oneYearPercent": 50, + "fiveYearPercent": 75 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq false +} diff --git a/library-api/test/bdt/checks/residence/PropertyAssessmentIncrease/Five year only.bru b/library-api/test/bdt/checks/residence/PropertyAssessmentIncrease/Five year only.bru new file mode 100644 index 00000000..826fa6da --- /dev/null +++ b/library-api/test/bdt/checks/residence/PropertyAssessmentIncrease/Five year only.bru @@ -0,0 +1,29 @@ +meta { + name: Five year only + type: http + seq: 5 +} + +post { + url: {{host}}/checks/residence/property-assessment-increase + body: json + auth: inherit +} + +body:json { + { + "situation": { + "propertyAssessment": 175000, + "lowestPropertyAssessmentLastFiveYears": 100000 + }, + "parameters": { + "oneYearPercent": 50, + "fiveYearPercent": 75 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq true +} diff --git a/library-api/test/bdt/checks/residence/PropertyAssessmentIncrease/Five year path.bru b/library-api/test/bdt/checks/residence/PropertyAssessmentIncrease/Five year path.bru new file mode 100644 index 00000000..408f0af4 --- /dev/null +++ b/library-api/test/bdt/checks/residence/PropertyAssessmentIncrease/Five year path.bru @@ -0,0 +1,30 @@ +meta { + name: Five year path + type: http + seq: 4 +} + +post { + url: {{host}}/checks/residence/property-assessment-increase + body: json + auth: inherit +} + +body:json { + { + "situation": { + "propertyAssessment": 175000, + "previousYearPropertyAssessment": 175000, + "lowestPropertyAssessmentLastFiveYears": 100000 + }, + "parameters": { + "oneYearPercent": 50, + "fiveYearPercent": 75 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq true +} diff --git a/library-api/test/bdt/checks/residence/PropertyAssessmentIncrease/Null.bru b/library-api/test/bdt/checks/residence/PropertyAssessmentIncrease/Null.bru new file mode 100644 index 00000000..6c8f317e --- /dev/null +++ b/library-api/test/bdt/checks/residence/PropertyAssessmentIncrease/Null.bru @@ -0,0 +1,26 @@ +meta { + name: Null + type: http + seq: 3 +} + +post { + url: {{host}}/checks/residence/property-assessment-increase + body: json + auth: inherit +} + +body:json { + { + "situation": {}, + "parameters": { + "oneYearPercent": 50, + "fiveYearPercent": 75 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq null +} diff --git a/library-api/test/bdt/checks/residence/PropertyAssessmentIncrease/Pass.bru b/library-api/test/bdt/checks/residence/PropertyAssessmentIncrease/Pass.bru new file mode 100644 index 00000000..6edd9fac --- /dev/null +++ b/library-api/test/bdt/checks/residence/PropertyAssessmentIncrease/Pass.bru @@ -0,0 +1,29 @@ +meta { + name: Pass + type: http + seq: 1 +} + +post { + url: {{host}}/checks/residence/property-assessment-increase + body: json + auth: inherit +} + +body:json { + { + "situation": { + "propertyAssessment": 150000, + "previousYearPropertyAssessment": 100000 + }, + "parameters": { + "oneYearPercent": 50, + "fiveYearPercent": 75 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq true +} diff --git a/library-api/test/bdt/checks/residence/PropertyAssessmentIncrease/Unknown five year history.bru b/library-api/test/bdt/checks/residence/PropertyAssessmentIncrease/Unknown five year history.bru new file mode 100644 index 00000000..e68c5b0d --- /dev/null +++ b/library-api/test/bdt/checks/residence/PropertyAssessmentIncrease/Unknown five year history.bru @@ -0,0 +1,29 @@ +meta { + name: Unknown five year history + type: http + seq: 6 +} + +post { + url: {{host}}/checks/residence/property-assessment-increase + body: json + auth: inherit +} + +body:json { + { + "situation": { + "propertyAssessment": 149999, + "previousYearPropertyAssessment": 100000 + }, + "parameters": { + "oneYearPercent": 50, + "fiveYearPercent": 75 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq null +} diff --git a/library-api/test/bdt/checks/residence/PropertyAssessmentIncrease/Zero baseline.bru b/library-api/test/bdt/checks/residence/PropertyAssessmentIncrease/Zero baseline.bru new file mode 100644 index 00000000..985b0f69 --- /dev/null +++ b/library-api/test/bdt/checks/residence/PropertyAssessmentIncrease/Zero baseline.bru @@ -0,0 +1,30 @@ +meta { + name: Zero baseline + type: http + seq: 7 +} + +post { + url: {{host}}/checks/residence/property-assessment-increase + body: json + auth: inherit +} + +body:json { + { + "situation": { + "propertyAssessment": 150000, + "previousYearPropertyAssessment": 0, + "lowestPropertyAssessmentLastFiveYears": 0 + }, + "parameters": { + "oneYearPercent": 50, + "fiveYearPercent": 75 + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq null +} diff --git a/library-api/test/bdt/checks/residence/PropertyAssessmentIncrease/folder.bru b/library-api/test/bdt/checks/residence/PropertyAssessmentIncrease/folder.bru new file mode 100644 index 00000000..a00c9aa3 --- /dev/null +++ b/library-api/test/bdt/checks/residence/PropertyAssessmentIncrease/folder.bru @@ -0,0 +1,8 @@ +meta { + name: PropertyAssessmentIncrease + seq: 1 +} + +auth { + mode: inherit +} diff --git a/library-api/test/bdt/checks/residence/PropertyTaxesCurrentOrPaymentPlan/Delinquent with unknown plan.bru b/library-api/test/bdt/checks/residence/PropertyTaxesCurrentOrPaymentPlan/Delinquent with unknown plan.bru new file mode 100644 index 00000000..4732278e --- /dev/null +++ b/library-api/test/bdt/checks/residence/PropertyTaxesCurrentOrPaymentPlan/Delinquent with unknown plan.bru @@ -0,0 +1,26 @@ +meta { + name: Delinquent with unknown plan + type: http + seq: 6 +} + +post { + url: {{host}}/checks/residence/property-taxes-current-or-payment-plan + body: json + auth: inherit +} + +body:json { + { + "situation": { + "simpleChecks": { + "taxDelinquent": true + } + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq null +} diff --git a/library-api/test/bdt/checks/residence/PropertyTaxesCurrentOrPaymentPlan/Fail.bru b/library-api/test/bdt/checks/residence/PropertyTaxesCurrentOrPaymentPlan/Fail.bru new file mode 100644 index 00000000..ac29eacb --- /dev/null +++ b/library-api/test/bdt/checks/residence/PropertyTaxesCurrentOrPaymentPlan/Fail.bru @@ -0,0 +1,27 @@ +meta { + name: Fail + type: http + seq: 2 +} + +post { + url: {{host}}/checks/residence/property-taxes-current-or-payment-plan + body: json + auth: inherit +} + +body:json { + { + "situation": { + "simpleChecks": { + "taxDelinquent": true, + "propertyTaxPaymentPlan": false + } + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq false +} diff --git a/library-api/test/bdt/checks/residence/PropertyTaxesCurrentOrPaymentPlan/Null.bru b/library-api/test/bdt/checks/residence/PropertyTaxesCurrentOrPaymentPlan/Null.bru new file mode 100644 index 00000000..efdb27bd --- /dev/null +++ b/library-api/test/bdt/checks/residence/PropertyTaxesCurrentOrPaymentPlan/Null.bru @@ -0,0 +1,27 @@ +meta { + name: Null + type: http + seq: 3 +} + +post { + url: {{host}}/checks/residence/property-taxes-current-or-payment-plan + body: json + auth: inherit +} + +body:json { + { + "situation": { + "simpleChecks": { + "taxDelinquent": null, + "propertyTaxPaymentPlan": null + } + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq null +} diff --git a/library-api/test/bdt/checks/residence/PropertyTaxesCurrentOrPaymentPlan/Pass.bru b/library-api/test/bdt/checks/residence/PropertyTaxesCurrentOrPaymentPlan/Pass.bru new file mode 100644 index 00000000..9beacafe --- /dev/null +++ b/library-api/test/bdt/checks/residence/PropertyTaxesCurrentOrPaymentPlan/Pass.bru @@ -0,0 +1,26 @@ +meta { + name: Pass + type: http + seq: 1 +} + +post { + url: {{host}}/checks/residence/property-taxes-current-or-payment-plan + body: json + auth: inherit +} + +body:json { + { + "situation": { + "simpleChecks": { + "taxDelinquent": false + } + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq true +} diff --git a/library-api/test/bdt/checks/residence/PropertyTaxesCurrentOrPaymentPlan/Payment plan.bru b/library-api/test/bdt/checks/residence/PropertyTaxesCurrentOrPaymentPlan/Payment plan.bru new file mode 100644 index 00000000..fb2d5f57 --- /dev/null +++ b/library-api/test/bdt/checks/residence/PropertyTaxesCurrentOrPaymentPlan/Payment plan.bru @@ -0,0 +1,27 @@ +meta { + name: Payment plan + type: http + seq: 4 +} + +post { + url: {{host}}/checks/residence/property-taxes-current-or-payment-plan + body: json + auth: inherit +} + +body:json { + { + "situation": { + "simpleChecks": { + "taxDelinquent": true, + "propertyTaxPaymentPlan": true + } + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq true +} diff --git a/library-api/test/bdt/checks/residence/PropertyTaxesCurrentOrPaymentPlan/Plan with unknown delinquency.bru b/library-api/test/bdt/checks/residence/PropertyTaxesCurrentOrPaymentPlan/Plan with unknown delinquency.bru new file mode 100644 index 00000000..fca29e45 --- /dev/null +++ b/library-api/test/bdt/checks/residence/PropertyTaxesCurrentOrPaymentPlan/Plan with unknown delinquency.bru @@ -0,0 +1,26 @@ +meta { + name: Plan with unknown delinquency + type: http + seq: 5 +} + +post { + url: {{host}}/checks/residence/property-taxes-current-or-payment-plan + body: json + auth: inherit +} + +body:json { + { + "situation": { + "simpleChecks": { + "propertyTaxPaymentPlan": true + } + } + } +} + +assert { + res.status: eq 200 + res.body.checkResult: eq true +} diff --git a/library-api/test/bdt/checks/residence/PropertyTaxesCurrentOrPaymentPlan/folder.bru b/library-api/test/bdt/checks/residence/PropertyTaxesCurrentOrPaymentPlan/folder.bru new file mode 100644 index 00000000..b70998ec --- /dev/null +++ b/library-api/test/bdt/checks/residence/PropertyTaxesCurrentOrPaymentPlan/folder.bru @@ -0,0 +1,8 @@ +meta { + name: PropertyTaxesCurrentOrPaymentPlan + seq: 1 +} + +auth { + mode: inherit +} From 5494c3072385a71a7c5722b7604b9f940e3cb8b4 Mon Sep 17 00:00:00 2001 From: Preston Cabe Date: Tue, 6 Oct 2026 13:23:27 -0400 Subject: [PATCH 02/13] Show recovery options when a screener fails to load Keep failed loads from throwing during editor rendering. Show a return link and retry action for stale screener URLs, and test both recovery paths. --- .../screenerEditor/ScreenerEditor.test.tsx | 40 +++++++++++++++++++ .../screenerEditor/ScreenerEditor.tsx | 25 ++++++++++-- 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/builder-frontend/src/components/screenerEditor/ScreenerEditor.test.tsx b/builder-frontend/src/components/screenerEditor/ScreenerEditor.test.tsx index 86644ed5..850b513f 100644 --- a/builder-frontend/src/components/screenerEditor/ScreenerEditor.test.tsx +++ b/builder-frontend/src/components/screenerEditor/ScreenerEditor.test.tsx @@ -49,6 +49,10 @@ describe("ScreenerEditor", () => { () => ( +
Screeners list
} + />
@@ -89,4 +93,40 @@ describe("ScreenerEditor", () => { expect(container.textContent).not.toContain("Configuring benefit-1"); expect(button("Configure benefit-1")).toBeDefined(); }); + + it("shows a recovery link when loading fails and can return to the list", async () => { + vi.mocked(fetchScreener).mockRejectedValueOnce(new Error("Fetch failed")); + const container = mount(); + + await vi.waitFor(() => { + expect(currentCrumb(container)).toBe("Screener unavailable"); + expect(container.querySelector('[role="alert"]')?.textContent).toContain( + "Unable to load this screener", + ); + }); + expect(container.textContent).not.toContain("Loading ..."); + const back = [...container.querySelectorAll("a")].find( + (link) => link.textContent === "Back to screeners", + )!; + back.click(); + await vi.waitFor(() => + expect(container.textContent).toContain("Screeners list"), + ); + }); + + it("can retry a failed load", async () => { + vi.mocked(fetchScreener).mockRejectedValueOnce(new Error("Fetch failed")); + const container = mount(); + await vi.waitFor(() => + expect(container.querySelector('[role="alert"]')).not.toBeNull(), + ); + const retry = [...container.querySelectorAll("button")].find( + (button) => button.textContent === "Try again", + )!; + retry.click(); + + await vi.waitFor(() => expect(currentCrumb(container)).toBe("My screener")); + expect(container.querySelector('[role="alert"]')).toBeNull(); + expect(container.textContent).toContain("Configure benefit-1"); + }); }); diff --git a/builder-frontend/src/components/screenerEditor/ScreenerEditor.tsx b/builder-frontend/src/components/screenerEditor/ScreenerEditor.tsx index 4edef7db..373317cb 100644 --- a/builder-frontend/src/components/screenerEditor/ScreenerEditor.tsx +++ b/builder-frontend/src/components/screenerEditor/ScreenerEditor.tsx @@ -1,6 +1,6 @@ import EditorNavigation from "@/components/shared/EditorNavigation"; import { createSignal, createResource, Accessor } from "solid-js"; -import { useParams } from "@solidjs/router"; +import { A, useParams } from "@solidjs/router"; import FormEditorView from "./FormEditorView"; import Loading from "../Loading"; @@ -83,13 +83,32 @@ function ScreenerEditor() { return (
- {screener.loading ? ( + {screener.error ? ( + + ) : screener.loading ? ( ) : ( <> From 341bfda4414942b833a1aef434c62e33b6395aa5 Mon Sep 17 00:00:00 2001 From: Preston Cabe Date: Tue, 6 Oct 2026 13:32:13 -0400 Subject: [PATCH 03/13] Remove superfluous doc --- library-api/PROPERTY_TAX_BENEFITS.md | 64 ---------------------------- 1 file changed, 64 deletions(-) delete mode 100644 library-api/PROPERTY_TAX_BENEFITS.md diff --git a/library-api/PROPERTY_TAX_BENEFITS.md b/library-api/PROPERTY_TAX_BENEFITS.md deleted file mode 100644 index 85308dde..00000000 --- a/library-api/PROPERTY_TAX_BENEFITS.md +++ /dev/null @@ -1,64 +0,0 @@ -# Philadelphia property tax benefits - -All six benefits are exposed at `POST /api/v1/benefits/pa/phl/{slug}`. -Their OpenAPI operations publish `x-bdt-benefit.checks`, including check -operation IDs, aliases, fixed parameters, and primary-person bindings, so the -builder can import their compositions. Computed alternatives are encapsulated -in checks under `checks/internal/`. - -| Benefit | Slug | Composed checks | -| --- | --- | --- | -| Homestead Exemption | `homestead-exemption` | Not already enrolled, owner-occupant, Philadelphia residence, no ten-year abatement | -| Owner-Occupied Payment Agreement | `owner-occupied-payment-agreement` | Not already enrolled, owner-occupant, Philadelphia residence, delinquent taxes | -| Senior Citizen Tax Freeze | `senior-citizen-tax-freeze` | Not already enrolled, owner-occupant, Philadelphia residence, existing applicant/spouse/late-spouse age requirement | -| Low-Income Tax Freeze | `low-income-tax-freeze` | Not already enrolled, owner-occupant, Philadelphia residence, single/married income limit | -| Longtime Owner Occupants Program (LOOP) | `loop` | Not already enrolled, owner-occupant, Philadelphia residence, ten years of ownership and occupancy, assessment increase, taxes current or payment plan, household income limit, no Homestead enrollment, no disqualifying abatement | -| Real Estate Tax Installment Plan (RETIP) | `retip` | Not already enrolled, owner-occupant, Philadelphia residence, applicant/live-in spouse age **or** household income limit | - -## New inputs - -Each model projects only the fields it uses from `BDT.tSituation`. - -| Situation field | Meaning | -| --- | --- | -| `householdIncome` | Annual income in dollars, calculated under the applicable program's income rules; the freeze uses the applicant's gross income plus the spouse's when married, while LOOP/RETIP use household income | -| `householdSize` | Family size for LOOP/RETIP income tables; a positive integer | -| `yearsOwnerOccupied` | Completed duration of ownership and occupancy of the primary residence, in years | -| `propertyAssessment` | Current assessment before exemption reductions | -| `previousYearPropertyAssessment` | Prior year's assessment before exemption reductions | -| `lowestPropertyAssessmentLastFiveYears` | Lowest assessment in the preceding five years, before exemption reductions | -| `simpleChecks.isMarried` | Whether the applicant is married; selects the freeze income cap | -| `simpleChecks.spouseLivesInHousehold` | Whether the current spouse lives with the applicant; required for the RETIP spouse age pathway | -| `simpleChecks.propertyTaxPaymentPlan` | Whether property taxes are covered by an active OOPA or installment plan | -| `simpleChecks.disqualifyingTaxAbatement` | Whether the current owner received an abatement on this property, or a previous owner who received one is a close relative; includes expired abatements | - -RETIP matches the spouse relationship to `primaryPersonId`; an unrelated senior -household member does not qualify the applicant. An empty `relationships` list -means no spouse, while an omitted list means unknown. RETIP checks age as of -today and does not use SCTF's late-spouse pathway. - -Missing information returns `null`. An alternative that is known to pass can -pass even when the other path is unknown. A failed required check makes the -benefit ineligible. Unsupported family sizes, negative income, and invalid -assessment baselines return unknown rather than extrapolating a policy rule. -An empty enrollment list means the applicant is enrolled in none of the -listed benefits; an omitted or null list means unanswered. - -## Policy sources and maintenance - -The three added benefits implement new-applicant eligibility from the city -guidance linked in issues #387, #389, and #537, verified October 5, 2026: - -- [Low-Income Tax Freeze](https://www.phila.gov/services/payments-assistance-taxes/taxes/property-and-real-estate-taxes/get-real-estate-tax-relief/tax-freeze/apply-for-the-low-income-real-estate-tax-freeze/): income at or below $33,500 single or $41,500 married, without an age restriction. -- [LOOP](https://www.phila.gov/services/payments-assistance-taxes/taxes/property-and-real-estate-taxes/get-real-estate-tax-relief/apply-for-the-longtime-owner-occupants-program-loop/): at least ten years of occupancy and a 50% one-year **or** 75% five-year assessment increase. New applicants use the 120% AMI table for family sizes 1–10, with income strictly below the cap. The 150% table for participants enrolled before 2023 is a renewal rule and is not used here. Homestead recipients must remove that exemption to enroll in LOOP. The historical owner/relative abatement rule differs from Homestead's current ten-year abatement check. -- [RETIP](https://www.phila.gov/services/payments-assistance-taxes/payment-plans-and-assistance-programs/income-based-programs-for-residents/set-up-real-estate-tax-installment-plan/): applicant or live-in spouse at least 65, regardless of income, **or** household income strictly below the annual family-size cap for sizes 1–8. The [July 2026 application](https://www.phila.gov/media/20260710071917/Real-Estate-Tax-Installment-Plan-application-form-revised.pdf) also describes income as below the cap. - -Income tables live in `checks/income/loop-income.dmn` and -`checks/income/retip-income.dmn`; the single/married caps live in -`checks/income/tax-freeze-income.dmn`. Update their boundary scenarios under -`test/bdt/checks/income/` when city limits change. Do not extrapolate larger -family sizes without a published rule. - -These screeners evaluate eligibility conditions, not application deadlines, -document submission, bill amounts, or administrative approval. The three -previously implemented benefits retain their existing compositions. From 9ff5833f611103943994f3505e6f1bf1824fb7d5 Mon Sep 17 00:00:00 2001 From: Preston Cabe Date: Tue, 6 Oct 2026 14:00:01 -0400 Subject: [PATCH 04/13] Remove README link to deleted property tax doc Co-Authored-By: Claude Opus 5.5 --- library-api/README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/library-api/README.md b/library-api/README.md index fad5ad98..87ea7af7 100644 --- a/library-api/README.md +++ b/library-api/README.md @@ -61,10 +61,6 @@ File: src/main/resources/checks/age/person-min-age.dmn ### Checks + Benefits Architecture -The library includes all six Philadelphia property-tax benefits. See -[Property tax benefits](PROPERTY_TAX_BENEFITS.md) for their compositions, -input fields, policy sources, and income-table maintenance. - **Checks** = Reusable eligibility logic (e.g., "is person 65+?") - Location: `src/main/resources/checks/{category}/` - Example: `checks/age/person-min-age.dmn` → `/api/v1/checks/age/person-min-age` From 7b5d50e59a01d91cf5b10b058b1f07f08647526a Mon Sep 17 00:00:00 2001 From: Preston Cabe Date: Tue, 6 Oct 2026 14:01:30 -0400 Subject: [PATCH 05/13] Use shared spouse id BKM in ApplicantOrSpouseMinAge Co-Authored-By: Claude Opus 5.5 --- .../resources/checks/age/applicant-or-spouse-min-age.dmn | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/library-api/src/main/resources/checks/age/applicant-or-spouse-min-age.dmn b/library-api/src/main/resources/checks/age/applicant-or-spouse-min-age.dmn index 7a860b19..7d623125 100644 --- a/library-api/src/main/resources/checks/age/applicant-or-spouse-min-age.dmn +++ b/library-api/src/main/resources/checks/age/applicant-or-spouse-min-age.dmn @@ -56,6 +56,9 @@ + + + @@ -69,7 +72,7 @@ - if situation.relationships = null or situation.primaryPersonId = null then null else situation.relationships[personId = situation.primaryPersonId and type = "spouse"][1].relatedPersonId + if situation.relationships = null or situation.primaryPersonId = null then null else BDT.spouse id(situation.relationships, situation.primaryPersonId) From 899b772b522b50e4c3bb5ab135746f997c332949 Mon Sep 17 00:00:00 2001 From: Preston Cabe Date: Tue, 6 Oct 2026 14:01:30 -0400 Subject: [PATCH 06/13] Let Homestead recipients qualify for LOOP The city requires Homestead recipients to remove that exemption when they enroll in LOOP, so current enrollment should not make them ineligible. Co-Authored-By: Claude Opus 5.5 --- .../main/resources/benefits/pa/phl/loop.dmn | 54 +------------------ .../library/api/OpenAPISchemaPatternTest.java | 2 +- ...ad.bru => Eligible while on Homestead.bru} | 5 +- 3 files changed, 4 insertions(+), 57 deletions(-) rename library-api/test/bdt/benefits/pa/phl/PhlLoop/{Fails NotOnHomestead.bru => Eligible while on Homestead.bru} (87%) diff --git a/library-api/src/main/resources/benefits/pa/phl/loop.dmn b/library-api/src/main/resources/benefits/pa/phl/loop.dmn index 5d080884..75e454e9 100644 --- a/library-api/src/main/resources/benefits/pa/phl/loop.dmn +++ b/library-api/src/main/resources/benefits/pa/phl/loop.dmn @@ -1,6 +1,6 @@ - Philadelphia Longtime Owner Occupants Program (LOOP) for new applicants. Homestead recipients must remove that exemption to enroll; historical owner/close-relative abatements are excluded. + Philadelphia Longtime Owner Occupants Program (LOOP) for new applicants. Homestead recipients can qualify but must remove that exemption when they enroll; historical owner/close-relative abatements are excluded. @@ -240,37 +240,6 @@ - - - - - PersonNotEnrolledInBenefit.PersonNotEnrolledInBenefitService - - - - - situation - - - - - - - - - situation.primaryPersonId - - - - - - "PhlHomesteadExemption" - - - - - - @@ -420,27 +389,6 @@ 290 - - 50 - 239 - 290 - - - - - 290 - - - 50 - 120 - 100 - - - 100 - - - 100 - 50 239 diff --git a/library-api/src/test/java/org/codeforphilly/bdt/library/api/OpenAPISchemaPatternTest.java b/library-api/src/test/java/org/codeforphilly/bdt/library/api/OpenAPISchemaPatternTest.java index dacbeff4..a072d46f 100644 --- a/library-api/src/test/java/org/codeforphilly/bdt/library/api/OpenAPISchemaPatternTest.java +++ b/library-api/src/test/java/org/codeforphilly/bdt/library/api/OpenAPISchemaPatternTest.java @@ -146,7 +146,7 @@ public void testAllSixPropertyTaxBenefitsPublishResolvableComposition() { List> loopChecks = openApiSpec.getList( "paths.'/api/v1/benefits/pa/phl/loop'.post.'x-bdt-benefit'.checks"); - assertEquals(9, loopChecks.size()); + assertEquals(8, loopChecks.size()); Map years = loopChecks.stream() .filter(check -> "MinYearsOwnerOccupantService".equals(check.get("operationId"))) .findFirst().orElseThrow(); diff --git a/library-api/test/bdt/benefits/pa/phl/PhlLoop/Fails NotOnHomestead.bru b/library-api/test/bdt/benefits/pa/phl/PhlLoop/Eligible while on Homestead.bru similarity index 87% rename from library-api/test/bdt/benefits/pa/phl/PhlLoop/Fails NotOnHomestead.bru rename to library-api/test/bdt/benefits/pa/phl/PhlLoop/Eligible while on Homestead.bru index 616e7520..ed4be614 100644 --- a/library-api/test/bdt/benefits/pa/phl/PhlLoop/Fails NotOnHomestead.bru +++ b/library-api/test/bdt/benefits/pa/phl/PhlLoop/Eligible while on Homestead.bru @@ -1,5 +1,5 @@ meta { - name: Fails NotOnHomestead + name: Eligible while on Homestead type: http seq: 9 } @@ -38,6 +38,5 @@ body:json { assert { res.status: eq 200 - res.body.isEligible: eq false - res.body.checks.NotOnHomestead: eq false + res.body.isEligible: eq true } From b8603a656bfac2229bfd4642d376a9d9091e2d8c Mon Sep 17 00:00:00 2001 From: Preston Cabe Date: Tue, 6 Oct 2026 14:02:38 -0400 Subject: [PATCH 07/13] Base tax freeze income on applicant and spouse income The Low-Income Tax Freeze limit covers the applicant's and spouse's income, not the household total that LOOP and RETIP use, so it reads a new applicantAndSpouseIncome field. Marriage now comes from relationships via the shared spouse id BKM, matching the age checks, and the unused simpleChecks.isMarried field is removed. Co-Authored-By: Claude Opus 5.5 --- library-api/src/main/resources/BDT.dmn | 6 +++--- .../benefits/pa/phl/low-income-tax-freeze.dmn | 10 ++++----- .../checks/income/tax-freeze-income.dmn | 21 ++++++++++--------- .../Already enrolled.bru | 6 +++--- .../pa/phl/PhlLowIncomeTaxFreeze/Eligible.bru | 6 +++--- .../Fails MeetsIncomeRequirement.bru | 6 +++--- .../phl/PhlLowIncomeTaxFreeze/Ineligible.bru | 6 +++--- .../Married at limit.bru | 12 ++++++++--- .../checks/income/TaxFreezeIncome/Fail.bru | 7 +++---- .../income/TaxFreezeIncome/Income unknown.bru | 5 ++--- .../income/TaxFreezeIncome/Invalid income.bru | 7 +++---- .../TaxFreezeIncome/Married at limit.bru | 13 ++++++++---- .../TaxFreezeIncome/Married over limit.bru | 13 ++++++++---- .../checks/income/TaxFreezeIncome/Null.bru | 6 ++---- .../checks/income/TaxFreezeIncome/Pass.bru | 7 +++---- .../income/TaxFreezeIncome/Zero income.bru | 7 +++---- 16 files changed, 74 insertions(+), 64 deletions(-) diff --git a/library-api/src/main/resources/BDT.dmn b/library-api/src/main/resources/BDT.dmn index 4882416f..0d360038 100644 --- a/library-api/src/main/resources/BDT.dmn +++ b/library-api/src/main/resources/BDT.dmn @@ -25,6 +25,9 @@ number + + number + number @@ -116,9 +119,6 @@ boolean - - boolean - boolean diff --git a/library-api/src/main/resources/benefits/pa/phl/low-income-tax-freeze.dmn b/library-api/src/main/resources/benefits/pa/phl/low-income-tax-freeze.dmn index da2048bc..27fa2b67 100644 --- a/library-api/src/main/resources/benefits/pa/phl/low-income-tax-freeze.dmn +++ b/library-api/src/main/resources/benefits/pa/phl/low-income-tax-freeze.dmn @@ -1,6 +1,6 @@ - Philadelphia Low-Income Real Estate Tax Freeze for owner-occupants meeting single/married gross annual income limits. + Philadelphia Low-Income Real Estate Tax Freeze for owner-occupants whose income plus any spouse's income meets the single/married gross annual limits. @@ -16,9 +16,12 @@ tSimpleChecks - + number + + BDT.tRelationshipList + string @@ -30,9 +33,6 @@ boolean - - boolean - diff --git a/library-api/src/main/resources/checks/income/tax-freeze-income.dmn b/library-api/src/main/resources/checks/income/tax-freeze-income.dmn index 028592f4..6b4adfef 100644 --- a/library-api/src/main/resources/checks/income/tax-freeze-income.dmn +++ b/library-api/src/main/resources/checks/income/tax-freeze-income.dmn @@ -1,20 +1,18 @@ - Philadelphia tax freeze gross annual income: at most $33,500 single or $41,500 married (city guidance verified 2026-10-05). Married income includes both spouses. + Philadelphia tax freeze gross annual income: at most $33,500 single or $41,500 married (city guidance verified 2026-10-05). Counts the applicant's income plus a spouse's; a spouse in relationships means the married limit applies. An empty relationships list means no spouse; omitted relationships mean unknown. - + number - - tSimpleChecks + + string - - - - boolean + + BDT.tRelationshipList @@ -29,9 +27,12 @@ + + + - if situation.householdIncome = null or situation.householdIncome < 0 or situation.simpleChecks.isMarried = null then null -else situation.householdIncome <= (if situation.simpleChecks.isMarried then 41500 else 33500) + if situation.applicantAndSpouseIncome = null or situation.applicantAndSpouseIncome < 0 or situation.relationships = null or situation.primaryPersonId = null then null +else situation.applicantAndSpouseIncome <= (if BDT.spouse id(situation.relationships, situation.primaryPersonId) = null then 33500 else 41500) diff --git a/library-api/test/bdt/benefits/pa/phl/PhlLowIncomeTaxFreeze/Already enrolled.bru b/library-api/test/bdt/benefits/pa/phl/PhlLowIncomeTaxFreeze/Already enrolled.bru index f850ab6e..85169e61 100644 --- a/library-api/test/bdt/benefits/pa/phl/PhlLowIncomeTaxFreeze/Already enrolled.bru +++ b/library-api/test/bdt/benefits/pa/phl/PhlLowIncomeTaxFreeze/Already enrolled.bru @@ -22,10 +22,10 @@ body:json { ], "simpleChecks": { "ownerOccupant": true, - "livesInPhiladelphiaPa": true, - "isMarried": false + "livesInPhiladelphiaPa": true }, - "householdIncome": 33500 + "applicantAndSpouseIncome": 33500, + "relationships": [] } } } diff --git a/library-api/test/bdt/benefits/pa/phl/PhlLowIncomeTaxFreeze/Eligible.bru b/library-api/test/bdt/benefits/pa/phl/PhlLowIncomeTaxFreeze/Eligible.bru index c794aea8..373165e6 100644 --- a/library-api/test/bdt/benefits/pa/phl/PhlLowIncomeTaxFreeze/Eligible.bru +++ b/library-api/test/bdt/benefits/pa/phl/PhlLowIncomeTaxFreeze/Eligible.bru @@ -22,10 +22,10 @@ body:json { ], "simpleChecks": { "ownerOccupant": true, - "livesInPhiladelphiaPa": true, - "isMarried": false + "livesInPhiladelphiaPa": true }, - "householdIncome": 33500 + "applicantAndSpouseIncome": 33500, + "relationships": [] } } } diff --git a/library-api/test/bdt/benefits/pa/phl/PhlLowIncomeTaxFreeze/Fails MeetsIncomeRequirement.bru b/library-api/test/bdt/benefits/pa/phl/PhlLowIncomeTaxFreeze/Fails MeetsIncomeRequirement.bru index 7f5f85bc..f2a13203 100644 --- a/library-api/test/bdt/benefits/pa/phl/PhlLowIncomeTaxFreeze/Fails MeetsIncomeRequirement.bru +++ b/library-api/test/bdt/benefits/pa/phl/PhlLowIncomeTaxFreeze/Fails MeetsIncomeRequirement.bru @@ -22,10 +22,10 @@ body:json { ], "simpleChecks": { "ownerOccupant": true, - "livesInPhiladelphiaPa": true, - "isMarried": false + "livesInPhiladelphiaPa": true }, - "householdIncome": 100000 + "applicantAndSpouseIncome": 100000, + "relationships": [] } } } diff --git a/library-api/test/bdt/benefits/pa/phl/PhlLowIncomeTaxFreeze/Ineligible.bru b/library-api/test/bdt/benefits/pa/phl/PhlLowIncomeTaxFreeze/Ineligible.bru index 053eae02..e72cb9ce 100644 --- a/library-api/test/bdt/benefits/pa/phl/PhlLowIncomeTaxFreeze/Ineligible.bru +++ b/library-api/test/bdt/benefits/pa/phl/PhlLowIncomeTaxFreeze/Ineligible.bru @@ -22,10 +22,10 @@ body:json { ], "simpleChecks": { "ownerOccupant": false, - "livesInPhiladelphiaPa": true, - "isMarried": false + "livesInPhiladelphiaPa": true }, - "householdIncome": 33500 + "applicantAndSpouseIncome": 33500, + "relationships": [] } } } diff --git a/library-api/test/bdt/benefits/pa/phl/PhlLowIncomeTaxFreeze/Married at limit.bru b/library-api/test/bdt/benefits/pa/phl/PhlLowIncomeTaxFreeze/Married at limit.bru index 83a73579..fc528d2d 100644 --- a/library-api/test/bdt/benefits/pa/phl/PhlLowIncomeTaxFreeze/Married at limit.bru +++ b/library-api/test/bdt/benefits/pa/phl/PhlLowIncomeTaxFreeze/Married at limit.bru @@ -22,10 +22,16 @@ body:json { ], "simpleChecks": { "ownerOccupant": true, - "livesInPhiladelphiaPa": true, - "isMarried": true + "livesInPhiladelphiaPa": true }, - "householdIncome": 41500 + "applicantAndSpouseIncome": 41500, + "relationships": [ + { + "type": "spouse", + "personId": "p1", + "relatedPersonId": "p2" + } + ] } } } diff --git a/library-api/test/bdt/checks/income/TaxFreezeIncome/Fail.bru b/library-api/test/bdt/checks/income/TaxFreezeIncome/Fail.bru index 87bb04cb..4d54dafb 100644 --- a/library-api/test/bdt/checks/income/TaxFreezeIncome/Fail.bru +++ b/library-api/test/bdt/checks/income/TaxFreezeIncome/Fail.bru @@ -13,10 +13,9 @@ post { body:json { { "situation": { - "householdIncome": 33500.01, - "simpleChecks": { - "isMarried": false - } + "primaryPersonId": "p1", + "applicantAndSpouseIncome": 33500.01, + "relationships": [] } } } diff --git a/library-api/test/bdt/checks/income/TaxFreezeIncome/Income unknown.bru b/library-api/test/bdt/checks/income/TaxFreezeIncome/Income unknown.bru index 39390ef9..a4855df5 100644 --- a/library-api/test/bdt/checks/income/TaxFreezeIncome/Income unknown.bru +++ b/library-api/test/bdt/checks/income/TaxFreezeIncome/Income unknown.bru @@ -13,9 +13,8 @@ post { body:json { { "situation": { - "simpleChecks": { - "isMarried": false - } + "primaryPersonId": "p1", + "relationships": [] } } } diff --git a/library-api/test/bdt/checks/income/TaxFreezeIncome/Invalid income.bru b/library-api/test/bdt/checks/income/TaxFreezeIncome/Invalid income.bru index dd75b818..5cac6646 100644 --- a/library-api/test/bdt/checks/income/TaxFreezeIncome/Invalid income.bru +++ b/library-api/test/bdt/checks/income/TaxFreezeIncome/Invalid income.bru @@ -13,10 +13,9 @@ post { body:json { { "situation": { - "householdIncome": -1, - "simpleChecks": { - "isMarried": false - } + "primaryPersonId": "p1", + "applicantAndSpouseIncome": -1, + "relationships": [] } } } diff --git a/library-api/test/bdt/checks/income/TaxFreezeIncome/Married at limit.bru b/library-api/test/bdt/checks/income/TaxFreezeIncome/Married at limit.bru index 4da5d857..a093c696 100644 --- a/library-api/test/bdt/checks/income/TaxFreezeIncome/Married at limit.bru +++ b/library-api/test/bdt/checks/income/TaxFreezeIncome/Married at limit.bru @@ -13,10 +13,15 @@ post { body:json { { "situation": { - "householdIncome": 41500, - "simpleChecks": { - "isMarried": true - } + "primaryPersonId": "p1", + "applicantAndSpouseIncome": 41500, + "relationships": [ + { + "type": "spouse", + "personId": "p1", + "relatedPersonId": "p2" + } + ] } } } diff --git a/library-api/test/bdt/checks/income/TaxFreezeIncome/Married over limit.bru b/library-api/test/bdt/checks/income/TaxFreezeIncome/Married over limit.bru index 602f26ee..e7493b64 100644 --- a/library-api/test/bdt/checks/income/TaxFreezeIncome/Married over limit.bru +++ b/library-api/test/bdt/checks/income/TaxFreezeIncome/Married over limit.bru @@ -13,10 +13,15 @@ post { body:json { { "situation": { - "householdIncome": 41500.01, - "simpleChecks": { - "isMarried": true - } + "primaryPersonId": "p1", + "applicantAndSpouseIncome": 41500.01, + "relationships": [ + { + "type": "spouse", + "personId": "p1", + "relatedPersonId": "p2" + } + ] } } } diff --git a/library-api/test/bdt/checks/income/TaxFreezeIncome/Null.bru b/library-api/test/bdt/checks/income/TaxFreezeIncome/Null.bru index 5a0811ea..d28e34bc 100644 --- a/library-api/test/bdt/checks/income/TaxFreezeIncome/Null.bru +++ b/library-api/test/bdt/checks/income/TaxFreezeIncome/Null.bru @@ -13,10 +13,8 @@ post { body:json { { "situation": { - "householdIncome": 30000, - "simpleChecks": { - "isMarried": null - } + "primaryPersonId": "p1", + "applicantAndSpouseIncome": 30000 } } } diff --git a/library-api/test/bdt/checks/income/TaxFreezeIncome/Pass.bru b/library-api/test/bdt/checks/income/TaxFreezeIncome/Pass.bru index 19e3d3b2..6257d074 100644 --- a/library-api/test/bdt/checks/income/TaxFreezeIncome/Pass.bru +++ b/library-api/test/bdt/checks/income/TaxFreezeIncome/Pass.bru @@ -13,10 +13,9 @@ post { body:json { { "situation": { - "householdIncome": 33500, - "simpleChecks": { - "isMarried": false - } + "primaryPersonId": "p1", + "applicantAndSpouseIncome": 33500, + "relationships": [] } } } diff --git a/library-api/test/bdt/checks/income/TaxFreezeIncome/Zero income.bru b/library-api/test/bdt/checks/income/TaxFreezeIncome/Zero income.bru index 00d39fdc..c0c63bcb 100644 --- a/library-api/test/bdt/checks/income/TaxFreezeIncome/Zero income.bru +++ b/library-api/test/bdt/checks/income/TaxFreezeIncome/Zero income.bru @@ -13,10 +13,9 @@ post { body:json { { "situation": { - "householdIncome": 0, - "simpleChecks": { - "isMarried": false - } + "primaryPersonId": "p1", + "applicantAndSpouseIncome": 0, + "relationships": [] } } } From d52cfecfbe08c36586f5ae4387e45670f01acdeb Mon Sep 17 00:00:00 2001 From: Preston Cabe Date: Tue, 6 Oct 2026 14:03:25 -0400 Subject: [PATCH 08/13] Share household income limit logic between LOOP and RETIP Co-Authored-By: Claude Opus 5.5 --- .../main/resources/checks/income/Income.dmn | 40 ++++++++++++++++++- .../resources/checks/income/loop-income.dmn | 7 ++-- .../resources/checks/income/retip-income.dmn | 7 ++-- 3 files changed, 46 insertions(+), 8 deletions(-) diff --git a/library-api/src/main/resources/checks/income/Income.dmn b/library-api/src/main/resources/checks/income/Income.dmn index 8c40f793..a2d61f4c 100644 --- a/library-api/src/main/resources/checks/income/Income.dmn +++ b/library-api/src/main/resources/checks/income/Income.dmn @@ -1,5 +1,41 @@ - + Shared definitions for income eligibility checks. - \ No newline at end of file + + number + + + + + + + + + + if income = null or income < 0 or household size = null or limits = null then null +else if household size < 1 or household size > count(limits) or floor(household size) != household size then null +else income < limits[household size] + + + + + + + + + 917 + + + 50 + 917 + + + + + + + + + + diff --git a/library-api/src/main/resources/checks/income/loop-income.dmn b/library-api/src/main/resources/checks/income/loop-income.dmn index 94bde142..56d516db 100644 --- a/library-api/src/main/resources/checks/income/loop-income.dmn +++ b/library-api/src/main/resources/checks/income/loop-income.dmn @@ -24,10 +24,11 @@ + + + - if situation.householdIncome = null or situation.householdIncome < 0 or situation.householdSize = null then null -else if situation.householdSize < 1 or situation.householdSize > 10 or floor(situation.householdSize) != situation.householdSize then null -else situation.householdIncome < [103050, 117800, 132500, 147200, 159000, 170800, 182550, 194350, 206100, 217900][situation.householdSize] + Income.below household income limit(situation.householdIncome, situation.householdSize, [103050, 117800, 132500, 147200, 159000, 170800, 182550, 194350, 206100, 217900]) diff --git a/library-api/src/main/resources/checks/income/retip-income.dmn b/library-api/src/main/resources/checks/income/retip-income.dmn index 1cd7b201..eecbd899 100644 --- a/library-api/src/main/resources/checks/income/retip-income.dmn +++ b/library-api/src/main/resources/checks/income/retip-income.dmn @@ -24,10 +24,11 @@ + + + - if situation.householdIncome = null or situation.householdIncome < 0 or situation.householdSize = null then null -else if situation.householdSize < 1 or situation.householdSize > 8 or floor(situation.householdSize) != situation.householdSize then null -else situation.householdIncome < [42950, 49100, 55250, 61350, 66300, 71200, 76100, 81000][situation.householdSize] + Income.below household income limit(situation.householdIncome, situation.householdSize, [42950, 49100, 55250, 61350, 66300, 71200, 76100, 81000]) From 5d848d3f84cf5c5a04a060d53b996533d2b46ed8 Mon Sep 17 00:00:00 2001 From: Preston Cabe Date: Tue, 6 Oct 2026 14:04:19 -0400 Subject: [PATCH 09/13] Generate household-size examples that checks can evaluate Example payloads used 42 for every number, so household-size checks returned null and the endpoint smoke test was loosened to accept that. Generate size fields as 1 and restore the non-null checkResult assertion. Co-Authored-By: Claude Opus 5.5 --- .../org/codeforphilly/bdt/library/api/DMNSchemaResolver.java | 3 +++ .../bdt/library/api/DynamicEndpointPatternTest.java | 5 +---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/library-api/src/main/java/org/codeforphilly/bdt/library/api/DMNSchemaResolver.java b/library-api/src/main/java/org/codeforphilly/bdt/library/api/DMNSchemaResolver.java index 9afc58c7..3ea2cf12 100644 --- a/library-api/src/main/java/org/codeforphilly/bdt/library/api/DMNSchemaResolver.java +++ b/library-api/src/main/java/org/codeforphilly/bdt/library/api/DMNSchemaResolver.java @@ -270,6 +270,9 @@ private Object generatePrimitiveExample(String fieldName, String type, String fo if (fieldName.contains("Age") || fieldName.contains("age")) { return 65; } + if (fieldName.contains("Size") || fieldName.contains("size")) { + return 1; // Household sizes index income tables starting at 1 + } return 42; case "boolean": diff --git a/library-api/src/test/java/org/codeforphilly/bdt/library/api/DynamicEndpointPatternTest.java b/library-api/src/test/java/org/codeforphilly/bdt/library/api/DynamicEndpointPatternTest.java index ae41173e..eb16c353 100644 --- a/library-api/src/test/java/org/codeforphilly/bdt/library/api/DynamicEndpointPatternTest.java +++ b/library-api/src/test/java/org/codeforphilly/bdt/library/api/DynamicEndpointPatternTest.java @@ -13,7 +13,6 @@ import java.util.stream.Collectors; import static io.restassured.RestAssured.given; -import static org.hamcrest.Matchers.hasKey; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; import static org.junit.jupiter.api.Assertions.*; @@ -55,9 +54,7 @@ public void testAllCheckEndpointsReturnCheckResult() { .post(path) .then() .statusCode(200) - // Generated examples are structural samples; unknown or unsupported values - // may legitimately return null under the check's three-valued contract. - .body("$", hasKey("checkResult")) + .body("checkResult", notNullValue()) .body("situation", notNullValue()); // 'parameters' is only echoed back for checks that declare a parameters input } From 32faaac9629a188cccec92663316429191adb230 Mon Sep 17 00:00:00 2001 From: Preston Cabe Date: Tue, 6 Oct 2026 14:06:39 -0400 Subject: [PATCH 10/13] Keep the screener editor usable across retries and refetches Show the loading state while retrying a failed load, keep the loaded editor open when a later refetch fails, and stop refetches from replacing unsaved form edits with the server copy. Co-Authored-By: Claude Opus 5.5 --- .../screenerEditor/ScreenerEditor.test.tsx | 83 ++++++++++++++++++- .../screenerEditor/ScreenerEditor.tsx | 72 +++++++++------- 2 files changed, 121 insertions(+), 34 deletions(-) diff --git a/builder-frontend/src/components/screenerEditor/ScreenerEditor.test.tsx b/builder-frontend/src/components/screenerEditor/ScreenerEditor.test.tsx index 850b513f..c00ed210 100644 --- a/builder-frontend/src/components/screenerEditor/ScreenerEditor.test.tsx +++ b/builder-frontend/src/components/screenerEditor/ScreenerEditor.test.tsx @@ -29,9 +29,30 @@ vi.mock("./manageBenefits/configureBenefit/ConfigureBenefit", () => ({
Configuring {props.benefitId()}
), })); -vi.mock("./FormEditorView", () => ({ default: () =>
Form editor
})); +vi.mock("./FormEditorView", () => ({ + default: (props: { + formSchema: () => unknown; + setFormSchema: (schema: unknown) => void; + }) => ( +
+
{JSON.stringify(props.formSchema())}
+ +
+ ), +})); vi.mock("./preview/Preview", () => ({ default: () =>
Preview
})); -vi.mock("./Publish", () => ({ default: () =>
Publish
})); +vi.mock("./Publish", () => ({ + default: (props: { refetchScreener: () => void }) => ( + + ), +})); describe("ScreenerEditor", () => { let dispose: (() => void) | undefined; @@ -65,6 +86,11 @@ describe("ScreenerEditor", () => { const currentCrumb = (container: HTMLElement) => container.querySelector(".breadcrumb-current")?.textContent; + const button = (container: HTMLElement, text: string) => + [...container.querySelectorAll("button")].find( + (b) => b.textContent?.trim() === text, + ) as HTMLButtonElement; + it("shows the loading label only while the screener loads", async () => { let resolveScreener!: (screener: unknown) => void; vi.mocked(fetchScreener).mockReturnValueOnce( @@ -129,4 +155,57 @@ describe("ScreenerEditor", () => { expect(container.querySelector('[role="alert"]')).toBeNull(); expect(container.textContent).toContain("Configure benefit-1"); }); + + it("shows loading while retrying a failed load", async () => { + vi.mocked(fetchScreener).mockRejectedValueOnce(new Error("Fetch failed")); + const container = mount(); + await vi.waitFor(() => + expect(container.querySelector('[role="alert"]')).not.toBeNull(), + ); + + let resolveScreener!: (screener: unknown) => void; + vi.mocked(fetchScreener).mockReturnValueOnce( + new Promise((resolve) => (resolveScreener = resolve)), + ); + button(container, "Try again").click(); + + await vi.waitFor(() => { + expect(container.textContent).toContain("Loading ..."); + expect(container.querySelector('[role="alert"]')).toBeNull(); + }); + resolveScreener({ screenerName: "My screener", formSchema: {} }); + await vi.waitFor(() => expect(currentCrumb(container)).toBe("My screener")); + }); + + it("keeps the editor and unsaved form edits across refetches", async () => { + const container = mount(); + await vi.waitFor(() => + expect(button(container, "Form Editor")).toBeDefined(), + ); + button(container, "Form Editor").click(); + button(container, "Edit form").click(); + const formSchema = () => + container.querySelector('[data-testid="form-schema"]')?.textContent; + expect(formSchema()).toBe('{"edited":true}'); + + button(container, "Publish").click(); + vi.mocked(fetchScreener).mockRejectedValueOnce(new Error("Fetch failed")); + button(container, "Refetch screener").click(); + await vi.waitFor(() => + expect(button(container, "Refetch screener")).toBeDefined(), + ); + expect(container.querySelector('[role="alert"]')).toBeNull(); + expect(currentCrumb(container)).toBe("My screener"); + + vi.mocked(fetchScreener).mockResolvedValueOnce({ + screenerName: "Renamed screener", + formSchema: { fromServer: true }, + }); + button(container, "Refetch screener").click(); + await vi.waitFor(() => + expect(currentCrumb(container)).toBe("Renamed screener"), + ); + button(container, "Form Editor").click(); + expect(formSchema()).toBe('{"edited":true}'); + }); }); diff --git a/builder-frontend/src/components/screenerEditor/ScreenerEditor.tsx b/builder-frontend/src/components/screenerEditor/ScreenerEditor.tsx index 373317cb..b618e228 100644 --- a/builder-frontend/src/components/screenerEditor/ScreenerEditor.tsx +++ b/builder-frontend/src/components/screenerEditor/ScreenerEditor.tsx @@ -26,9 +26,18 @@ function ScreenerEditor() { const [formSchema, setFormSchema] = createSignal(); const [forceUpdate, setForceUpdate] = createSignal(0); + // The last screener loaded for this route. Keeping it lets the editor stay + // open when a later refetch (such as after publishing) fails. + const [loaded, setLoaded] = createSignal<{ id: string; screener: any }>(); + const loadedScreener = () => + loaded()?.id === params.screenerId ? loaded()?.screener : undefined; + const fetchAndCacheScreener = async (keys) => { const screenerData = await fetchScreener(keys[0]); - setFormSchema(screenerData.formSchema); + // Only take the form from the server on first load so that refetches + // don't discard unsaved form edits. + if (loaded()?.id !== keys[0]) setFormSchema(screenerData.formSchema); + setLoaded({ id: keys[0], screener: screenerData }); return screenerData; }; @@ -42,12 +51,9 @@ function ScreenerEditor() { fetchAndCacheScreener, ); - // `latest` keeps the loaded screener during refetches; reading it after a - // failed load would throw, so check the error first. - const screenerLabel = () => { - if (screener.error) return "Screener unavailable"; - return screener.latest?.screenerName ?? "Loading screener…"; - }; + const screenerLabel = () => + loadedScreener()?.screenerName ?? + (screener.loading ? "Loading screener…" : "Screener unavailable"); const navbarDefs: Accessor = () => { return { @@ -83,36 +89,19 @@ function ScreenerEditor() { return (
- {screener.error ? ( - - ) : screener.loading ? ( + {screener.loading ? ( - ) : ( + ) : loadedScreener() ? ( <> - BDT - {screener().screenerName} + BDT - {loadedScreener().screenerName} {activeTab() == "formEditor" && ( )} {activeTab() == "preview" && ( - + )} {activeTab() == "publish" && ( setForceUpdate((prev) => prev + 1)} /> )} + ) : ( + )}
); From 7544cf3bb7ceef6e4861362eccb0acb37d5beaab Mon Sep 17 00:00:00 2001 From: Preston Cabe Date: Tue, 6 Oct 2026 14:10:38 -0400 Subject: [PATCH 11/13] Describe None of these as not enrolled in the example screener The library now treats an empty enrollment list as not enrolled in any program, so the seed form and README no longer warn that None of these leaves enrollment unknown. Co-Authored-By: Claude Opus 5.5 --- .../main/resources/seed-data/example-screener/README.md | 8 ++++---- .../storage/form/working/eCI03lKZqnNtPRgGbag1.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/builder-api/src/main/resources/seed-data/example-screener/README.md b/builder-api/src/main/resources/seed-data/example-screener/README.md index c2a6d6d9..15a4d6ce 100644 --- a/builder-api/src/main/resources/seed-data/example-screener/README.md +++ b/builder-api/src/main/resources/seed-data/example-screener/README.md @@ -31,10 +31,10 @@ Try these scenarios in Preview: - Spouse age: applicant under 65, spouse Yes with birth date over 65 years ago. - Existing enrollment: select one program to fail only its enrollment check. -The current library returns unknown for an empty enrollment list, even when -None of these is explicitly selected. The form explains this behavior rather -than changing library rules. Selecting an existing program makes that program -fail and allows the other enrollment checks to pass. +Selecting None of these sends an empty enrollment list, which the library +treats as not enrolled in any program, so each enrollment check passes. Leaving +the question blank keeps enrollment unknown. Selecting an existing program +makes that program fail and allows the other enrollment checks to pass. Run `mvn test -Dtest=ExampleScreenerImportServiceTest,ExampleScreenerSeedTest` in builder-api to check account import, resource integrity, and custom DMN diff --git a/builder-api/src/main/resources/seed-data/example-screener/storage/form/working/eCI03lKZqnNtPRgGbag1.json b/builder-api/src/main/resources/seed-data/example-screener/storage/form/working/eCI03lKZqnNtPRgGbag1.json index e1ba5e75..52aafad3 100644 --- a/builder-api/src/main/resources/seed-data/example-screener/storage/form/working/eCI03lKZqnNtPRgGbag1.json +++ b/builder-api/src/main/resources/seed-data/example-screener/storage/form/working/eCI03lKZqnNtPRgGbag1.json @@ -1 +1 @@ -{"components":[{"text":"# Philadelphia benefits example\n\n- Explore three library tax benefits and fictional Philly Cash.\n- Use the Preview page to try answers and see the effects in the Results section.\n- Go to the Publish page to get a unique URL for this screener and see what it will look like to end users.","id":"Field_example_0","type":"text","layout":{"row":"Row_example_0"},"_path":["components",0],"_parent":"Form_philadelphia_example"},{"label":"Do you live in Philadelphia, Pennsylvania?","values":[{"label":"Value","value":"value"}],"id":"Field_example_1","type":"yes_no","layout":{"row":"Row_example_1"},"key":"simpleChecks.livesInPhiladelphiaPa","description":"Shared by all four benefits: one answer can feed several checks.","_path":["components",1],"_parent":"Form_philadelphia_example"},{"label":"Would you like extra cash?","values":[{"label":"Value","value":"value"}],"id":"Field_example_2","type":"yes_no","layout":{"row":"Row_example_2"},"key":"custom.wantsExtraCash","description":"Only used by fictional Philly Cash. A No answer fails that benefit; leaving this blank means unknown. Custom check questions use the custom. prefix; the DMN input is named wantsExtraCash.","_path":["components",2],"_parent":"Form_philadelphia_example"},{"label":"What is your household’s total yearly income?","id":"Field_example_14","type":"number","layout":{"row":"Row_example_14"},"key":"custom.householdIncome","appearance":{"prefixAdorner":"$"},"decimalDigits":0,"validate":{"min":0},"description":"Only used by fictional Philly Cash, whose limit is $40,000. A custom check compares this answer with an incomeLimit parameter set on the benefit, so another benefit could reuse the check with a different limit.","_path":["components",3],"_parent":"Form_philadelphia_example"},{"text":"## Your home\n\nThese questions feed the three library tax benefits. Watch which questions become unnecessary as you answer in Preview.","id":"Field_example_3","type":"text","layout":{"row":"Row_example_3"},"_path":["components",4],"_parent":"Form_philadelphia_example"},{"label":"Do you own and live in your home?","values":[{"label":"Value","value":"value"}],"id":"Field_example_4","type":"yes_no","layout":{"row":"Row_example_4"},"key":"simpleChecks.ownerOccupant","description":"Shared by Homestead, OOPA, and SCTF.","_path":["components",5],"_parent":"Form_philadelphia_example"},{"label":"Do you owe delinquent property taxes?","values":[{"label":"Value","value":"value"}],"id":"Field_example_5","type":"yes_no","layout":{"row":"Row_example_5"},"key":"simpleChecks.taxDelinquent","description":"Used by OOPA.","_path":["components",6],"_parent":"Form_philadelphia_example"},{"label":"Does your property have a ten-year tax abatement?","values":[{"label":"Value","value":"value"}],"id":"Field_example_6","type":"yes_no","layout":{"row":"Row_example_6"},"key":"simpleChecks.tenYearTaxAbatement","description":"Homestead uses a negative check: No passes, Yes fails.","_path":["components",7],"_parent":"Form_philadelphia_example"},{"label":"Which of these programs are you already enrolled in?","values":[{"label":"Homestead Exemption","value":"PhlHomesteadExemption"},{"label":"Owner-Occupied Payment Agreement (OOPA)","value":"PhlOwnerOccupiedPaymentAgreement"},{"label":"Senior Citizen Tax Freeze (SCTF)","value":"PhlSeniorCitizenTaxFreeze"}],"id":"Field_example_7","type":"checklist_none","layout":{"row":"Row_example_7"},"key":"people.client.enrollments","description":"Choose None of these explicitly if you are not enrolled. The current library treats an empty enrollment list as unknown, so the tax benefits may still show Need more information. Select a program to see its enrollment check fail and the other programs’ checks pass.","_path":["components",8],"_parent":"Form_philadelphia_example"},{"text":"## Age and family\n\nSCTF can use your age, your spouse’s age, or whether a late spouse was at least 65. The library compares birth dates with the current date.","id":"Field_example_8","type":"text","layout":{"row":"Row_example_8"},"_path":["components",9],"_parent":"Form_philadelphia_example"},{"subtype":"date","id":"Field_example_9","type":"datetime","layout":{"row":"Row_example_9"},"dateLabel":"Date of birth","label":"Your date of birth","key":"people.client.dateOfBirth","description":"Try a date more than 65 years ago, then try a younger age.","_path":["components",10],"_parent":"Form_philadelphia_example"},{"label":"Do you currently have a spouse?","values":[{"label":"Value","value":"value"}],"id":"Field_example_10","type":"yes_no","layout":{"row":"Row_example_10"},"key":"people.spouse.exists","description":"No explicitly records no spouse. Yes links the spouse’s birth date to you for the library age check.","_path":["components",11],"_parent":"Form_philadelphia_example"},{"subtype":"date","id":"Field_example_11","type":"datetime","layout":{"row":"Row_example_11"},"dateLabel":"Spouse date of birth","conditional":{"hide":"= people.spouse.exists != true"},"label":"Your spouse’s date of birth","key":"people.spouse.dateOfBirth","description":"Used only when you have a spouse.","_path":["components",12],"_parent":"Form_philadelphia_example"},{"label":"Was your late spouse at least 65?","values":[{"label":"Value","value":"value"}],"id":"Field_example_12","type":"yes_no","layout":{"row":"Row_example_12"},"key":"simpleChecks.lateSpouseWasAtLeast65","description":"Answer No if this does not apply. Leaving it blank can keep the age result unknown when neither birth date establishes eligibility.","_path":["components",13],"_parent":"Form_philadelphia_example"},{"text":"## Try a few scenarios\n\n**Explore all four:** Philadelphia resident, wants cash, yearly income of $30,000, owns and occupies home, owes delinquent taxes, no abatement, None of these enrollments, birth date over 65 years ago.\n\n**Only Philly Cash passes:** Philadelphia resident, wants cash, yearly income of $30,000, does not own and occupy a home.\n\n**Over the income limit:** change the yearly income to $50,000 to see Philly Cash fail only its income check.\n\n**Unknown cash result:** Philadelphia resident with the extra-cash or income question unanswered.\n\nPhilly Cash passes in the first scenario. With None of these, the current library enrollment check returns unknown for the tax benefits.\n\nChange an enrollment to see a parameterized check fail for just that program. Then inspect the expanded results and compare the shared residence checks.","id":"Field_example_13","type":"text","layout":{"row":"Row_example_13"},"_path":["components",14],"_parent":"Form_philadelphia_example"}],"id":"Form_philadelphia_example","type":"default","schemaVersion":18,"exporter":{"name":"form-js","version":"1.15.2"},"_path":[]} \ No newline at end of file +{"components":[{"text":"# Philadelphia benefits example\n\n- Explore three library tax benefits and fictional Philly Cash.\n- Use the Preview page to try answers and see the effects in the Results section.\n- Go to the Publish page to get a unique URL for this screener and see what it will look like to end users.","id":"Field_example_0","type":"text","layout":{"row":"Row_example_0"},"_path":["components",0],"_parent":"Form_philadelphia_example"},{"label":"Do you live in Philadelphia, Pennsylvania?","values":[{"label":"Value","value":"value"}],"id":"Field_example_1","type":"yes_no","layout":{"row":"Row_example_1"},"key":"simpleChecks.livesInPhiladelphiaPa","description":"Shared by all four benefits: one answer can feed several checks.","_path":["components",1],"_parent":"Form_philadelphia_example"},{"label":"Would you like extra cash?","values":[{"label":"Value","value":"value"}],"id":"Field_example_2","type":"yes_no","layout":{"row":"Row_example_2"},"key":"custom.wantsExtraCash","description":"Only used by fictional Philly Cash. A No answer fails that benefit; leaving this blank means unknown. Custom check questions use the custom. prefix; the DMN input is named wantsExtraCash.","_path":["components",2],"_parent":"Form_philadelphia_example"},{"label":"What is your household’s total yearly income?","id":"Field_example_14","type":"number","layout":{"row":"Row_example_14"},"key":"custom.householdIncome","appearance":{"prefixAdorner":"$"},"decimalDigits":0,"validate":{"min":0},"description":"Only used by fictional Philly Cash, whose limit is $40,000. A custom check compares this answer with an incomeLimit parameter set on the benefit, so another benefit could reuse the check with a different limit.","_path":["components",3],"_parent":"Form_philadelphia_example"},{"text":"## Your home\n\nThese questions feed the three library tax benefits. Watch which questions become unnecessary as you answer in Preview.","id":"Field_example_3","type":"text","layout":{"row":"Row_example_3"},"_path":["components",4],"_parent":"Form_philadelphia_example"},{"label":"Do you own and live in your home?","values":[{"label":"Value","value":"value"}],"id":"Field_example_4","type":"yes_no","layout":{"row":"Row_example_4"},"key":"simpleChecks.ownerOccupant","description":"Shared by Homestead, OOPA, and SCTF.","_path":["components",5],"_parent":"Form_philadelphia_example"},{"label":"Do you owe delinquent property taxes?","values":[{"label":"Value","value":"value"}],"id":"Field_example_5","type":"yes_no","layout":{"row":"Row_example_5"},"key":"simpleChecks.taxDelinquent","description":"Used by OOPA.","_path":["components",6],"_parent":"Form_philadelphia_example"},{"label":"Does your property have a ten-year tax abatement?","values":[{"label":"Value","value":"value"}],"id":"Field_example_6","type":"yes_no","layout":{"row":"Row_example_6"},"key":"simpleChecks.tenYearTaxAbatement","description":"Homestead uses a negative check: No passes, Yes fails.","_path":["components",7],"_parent":"Form_philadelphia_example"},{"label":"Which of these programs are you already enrolled in?","values":[{"label":"Homestead Exemption","value":"PhlHomesteadExemption"},{"label":"Owner-Occupied Payment Agreement (OOPA)","value":"PhlOwnerOccupiedPaymentAgreement"},{"label":"Senior Citizen Tax Freeze (SCTF)","value":"PhlSeniorCitizenTaxFreeze"}],"id":"Field_example_7","type":"checklist_none","layout":{"row":"Row_example_7"},"key":"people.client.enrollments","description":"Choose None of these explicitly if you are not enrolled; leaving this blank means enrollment is unknown. Select a program to see its enrollment check fail and the other programs’ checks pass.","_path":["components",8],"_parent":"Form_philadelphia_example"},{"text":"## Age and family\n\nSCTF can use your age, your spouse’s age, or whether a late spouse was at least 65. The library compares birth dates with the current date.","id":"Field_example_8","type":"text","layout":{"row":"Row_example_8"},"_path":["components",9],"_parent":"Form_philadelphia_example"},{"subtype":"date","id":"Field_example_9","type":"datetime","layout":{"row":"Row_example_9"},"dateLabel":"Date of birth","label":"Your date of birth","key":"people.client.dateOfBirth","description":"Try a date more than 65 years ago, then try a younger age.","_path":["components",10],"_parent":"Form_philadelphia_example"},{"label":"Do you currently have a spouse?","values":[{"label":"Value","value":"value"}],"id":"Field_example_10","type":"yes_no","layout":{"row":"Row_example_10"},"key":"people.spouse.exists","description":"No explicitly records no spouse. Yes links the spouse’s birth date to you for the library age check.","_path":["components",11],"_parent":"Form_philadelphia_example"},{"subtype":"date","id":"Field_example_11","type":"datetime","layout":{"row":"Row_example_11"},"dateLabel":"Spouse date of birth","conditional":{"hide":"= people.spouse.exists != true"},"label":"Your spouse’s date of birth","key":"people.spouse.dateOfBirth","description":"Used only when you have a spouse.","_path":["components",12],"_parent":"Form_philadelphia_example"},{"label":"Was your late spouse at least 65?","values":[{"label":"Value","value":"value"}],"id":"Field_example_12","type":"yes_no","layout":{"row":"Row_example_12"},"key":"simpleChecks.lateSpouseWasAtLeast65","description":"Answer No if this does not apply. Leaving it blank can keep the age result unknown when neither birth date establishes eligibility.","_path":["components",13],"_parent":"Form_philadelphia_example"},{"text":"## Try a few scenarios\n\n**Explore all four:** Philadelphia resident, wants cash, yearly income of $30,000, owns and occupies home, owes delinquent taxes, no abatement, None of these enrollments, birth date over 65 years ago.\n\n**Only Philly Cash passes:** Philadelphia resident, wants cash, yearly income of $30,000, does not own and occupy a home.\n\n**Over the income limit:** change the yearly income to $50,000 to see Philly Cash fail only its income check.\n\n**Unknown cash result:** Philadelphia resident with the extra-cash or income question unanswered.\n\nPhilly Cash passes in the first scenario. With None of these, each tax benefit’s enrollment check passes.\n\nChange an enrollment to see a parameterized check fail for just that program. Then inspect the expanded results and compare the shared residence checks.","id":"Field_example_13","type":"text","layout":{"row":"Row_example_13"},"_path":["components",14],"_parent":"Form_philadelphia_example"}],"id":"Form_philadelphia_example","type":"default","schemaVersion":18,"exporter":{"name":"form-js","version":"1.15.2"},"_path":[]} \ No newline at end of file From 4d749147ccb6703988438c584d59a570aeee8071 Mon Sep 17 00:00:00 2001 From: Preston Cabe Date: Tue, 6 Oct 2026 14:46:01 -0400 Subject: [PATCH 12/13] Let nav links leave the screener editor Navigating away clears screenerId while the editor is still mounted, so the title read of the loaded screener threw and blocked navigation, and the resource refetched with no ID. Skip fetching without an ID and read the title defensively. Co-Authored-By: Claude Opus 5.5 --- .../screenerEditor/ScreenerEditor.test.tsx | 20 +++++++++++++++++++ .../screenerEditor/ScreenerEditor.tsx | 7 ++++--- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/builder-frontend/src/components/screenerEditor/ScreenerEditor.test.tsx b/builder-frontend/src/components/screenerEditor/ScreenerEditor.test.tsx index c00ed210..fd5c199a 100644 --- a/builder-frontend/src/components/screenerEditor/ScreenerEditor.test.tsx +++ b/builder-frontend/src/components/screenerEditor/ScreenerEditor.test.tsx @@ -120,6 +120,26 @@ describe("ScreenerEditor", () => { expect(button("Configure benefit-1")).toBeDefined(); }); + it("can leave the editor from the breadcrumb", async () => { + const errors: unknown[] = []; + const onError = (event: ErrorEvent) => errors.push(event.error); + window.addEventListener("error", onError); + const container = mount(); + await vi.waitFor(() => expect(currentCrumb(container)).toBe("My screener")); + + const screenersLink = [...container.querySelectorAll("a")].find( + (link) => link.textContent === "Screeners", + )!; + screenersLink.click(); + + await vi.waitFor(() => + expect(container.textContent).toContain("Screeners list"), + ); + window.removeEventListener("error", onError); + expect(errors).toEqual([]); + expect(fetchScreener).not.toHaveBeenCalledWith(undefined); + }); + it("shows a recovery link when loading fails and can return to the list", async () => { vi.mocked(fetchScreener).mockRejectedValueOnce(new Error("Fetch failed")); const container = mount(); diff --git a/builder-frontend/src/components/screenerEditor/ScreenerEditor.tsx b/builder-frontend/src/components/screenerEditor/ScreenerEditor.tsx index b618e228..13705bcf 100644 --- a/builder-frontend/src/components/screenerEditor/ScreenerEditor.tsx +++ b/builder-frontend/src/components/screenerEditor/ScreenerEditor.tsx @@ -46,8 +46,9 @@ function ScreenerEditor() { // However resources only refetch when key has changed. // In order to force refetch even thought he screenerId hasn't change, // including a dummy signal 'forceUpdate' that can be unique for - // each call to the refetch - () => [params.screenerId, forceUpdate()], + // each call to the refetch. Navigating away clears screenerId before the + // editor unmounts, so skip fetching without one. + () => params.screenerId && [params.screenerId, forceUpdate()], fetchAndCacheScreener, ); @@ -101,7 +102,7 @@ function ScreenerEditor() { ) : loadedScreener() ? ( <> - BDT - {loadedScreener().screenerName} + BDT - {loadedScreener()?.screenerName} {activeTab() == "formEditor" && ( Date: Tue, 6 Oct 2026 15:07:15 -0400 Subject: [PATCH 13/13] Complete diagrams for imported eligibility helpers Add the imported spouse and income helper shapes and connect their knowledge requirements to the output decisions. Clean DMN validation now reports no diagram warnings. --- .../resources/checks/age/applicant-or-spouse-min-age.dmn | 8 ++++++++ .../src/main/resources/checks/income/loop-income.dmn | 8 ++++++++ .../src/main/resources/checks/income/retip-income.dmn | 8 ++++++++ .../main/resources/checks/income/tax-freeze-income.dmn | 8 ++++++++ 4 files changed, 32 insertions(+) diff --git a/library-api/src/main/resources/checks/age/applicant-or-spouse-min-age.dmn b/library-api/src/main/resources/checks/age/applicant-or-spouse-min-age.dmn index 7d623125..e0f27c58 100644 --- a/library-api/src/main/resources/checks/age/applicant-or-spouse-min-age.dmn +++ b/library-api/src/main/resources/checks/age/applicant-or-spouse-min-age.dmn @@ -149,6 +149,10 @@ else situation.simpleChecks.spouseLivesInHousehold and PersonMinAge.PersonMinAge + + + + @@ -161,6 +165,10 @@ else situation.simpleChecks.spouseLivesInHousehold and PersonMinAge.PersonMinAge + + + +
\ No newline at end of file diff --git a/library-api/src/main/resources/checks/income/loop-income.dmn b/library-api/src/main/resources/checks/income/loop-income.dmn index 56d516db..42245de6 100644 --- a/library-api/src/main/resources/checks/income/loop-income.dmn +++ b/library-api/src/main/resources/checks/income/loop-income.dmn @@ -60,10 +60,18 @@ + + + + + + + +
\ No newline at end of file diff --git a/library-api/src/main/resources/checks/income/retip-income.dmn b/library-api/src/main/resources/checks/income/retip-income.dmn index eecbd899..7e29a777 100644 --- a/library-api/src/main/resources/checks/income/retip-income.dmn +++ b/library-api/src/main/resources/checks/income/retip-income.dmn @@ -60,10 +60,18 @@ + + + + + + + + \ No newline at end of file diff --git a/library-api/src/main/resources/checks/income/tax-freeze-income.dmn b/library-api/src/main/resources/checks/income/tax-freeze-income.dmn index 6b4adfef..7724bc5d 100644 --- a/library-api/src/main/resources/checks/income/tax-freeze-income.dmn +++ b/library-api/src/main/resources/checks/income/tax-freeze-income.dmn @@ -64,10 +64,18 @@ else situation.applicantAndSpouseIncome <= (if BDT.spouse id(situation.relati + + + + + + + + \ No newline at end of file