diff --git a/data/ira_incentives.json b/data/ira_incentives.json index 088e095d..a22de675 100644 --- a/data/ira_incentives.json +++ b/data/ira_incentives.json @@ -7,6 +7,7 @@ ], "program": "residentialCleanEnergyCredit", "authority_type": "federal", + "v0_item": "battery_storage_installation", "items": [ "battery_storage_installation" ], @@ -38,6 +39,7 @@ ], "program": "residentialCleanEnergyCredit", "authority_type": "federal", + "v0_item": "geothermal_heating_installation", "items": [ "geothermal_heating_installation" ], @@ -69,6 +71,7 @@ ], "program": "energyEfficientHomeImprovementCredit", "authority_type": "federal", + "v0_item": "electric_panel", "items": [ "electric_panel" ], @@ -99,6 +102,7 @@ ], "program": "homeElectrificationAndApplianceRebates", "authority_type": "federal", + "v0_item": "electric_panel", "items": [ "electric_panel" ], @@ -129,6 +133,7 @@ ], "program": "homeElectrificationAndApplianceRebates", "authority_type": "federal", + "v0_item": "electric_panel", "items": [ "electric_panel" ], @@ -159,6 +164,7 @@ ], "program": "homeElectrificationAndApplianceRebates", "authority_type": "federal", + "v0_item": "electric_stove", "items": [ "electric_stove" ], @@ -190,6 +196,7 @@ ], "program": "homeElectrificationAndApplianceRebates", "authority_type": "federal", + "v0_item": "electric_stove", "items": [ "electric_stove" ], @@ -221,6 +228,7 @@ ], "program": "homeElectrificationAndApplianceRebates", "authority_type": "federal", + "v0_item": "electric_wiring", "items": [ "electric_wiring" ], @@ -251,6 +259,7 @@ ], "program": "homeElectrificationAndApplianceRebates", "authority_type": "federal", + "v0_item": "electric_wiring", "items": [ "electric_wiring" ], @@ -281,6 +290,7 @@ ], "program": "alternativeFuelVehicleRefuelingPropertyCredit", "authority_type": "federal", + "v0_item": "electric_vehicle_charger", "items": [ "electric_vehicle_charger" ], @@ -312,6 +322,7 @@ ], "program": "cleanVehicleCredit", "authority_type": "federal", + "v0_item": "new_electric_vehicle", "items": [ "new_electric_vehicle" ], @@ -345,6 +356,7 @@ ], "program": "cleanVehicleCredit", "authority_type": "federal", + "v0_item": "new_electric_vehicle", "items": [ "new_electric_vehicle" ], @@ -378,6 +390,7 @@ ], "program": "cleanVehicleCredit", "authority_type": "federal", + "v0_item": "new_electric_vehicle", "items": [ "new_electric_vehicle" ], @@ -411,6 +424,7 @@ ], "program": "cleanVehicleCredit", "authority_type": "federal", + "v0_item": "new_electric_vehicle", "items": [ "new_electric_vehicle" ], @@ -444,6 +458,7 @@ ], "program": "creditForPreviouslyOwnedCleanVehicles", "authority_type": "federal", + "v0_item": "used_electric_vehicle", "items": [ "used_electric_vehicle" ], @@ -477,6 +492,7 @@ ], "program": "creditForPreviouslyOwnedCleanVehicles", "authority_type": "federal", + "v0_item": "used_electric_vehicle", "items": [ "used_electric_vehicle" ], @@ -510,6 +526,7 @@ ], "program": "creditForPreviouslyOwnedCleanVehicles", "authority_type": "federal", + "v0_item": "used_electric_vehicle", "items": [ "used_electric_vehicle" ], @@ -543,6 +560,7 @@ ], "program": "creditForPreviouslyOwnedCleanVehicles", "authority_type": "federal", + "v0_item": "used_electric_vehicle", "items": [ "used_electric_vehicle" ], @@ -576,8 +594,11 @@ ], "program": "energyEfficientHomeImprovementCredit", "authority_type": "federal", + "v0_item": "heat_pump_air_conditioner_heater", "items": [ - "heat_pump_air_conditioner_heater" + "air_to_water_heat_pump", + "ducted_heat_pump", + "ductless_heat_pump" ], "amount": { "type": "dollar_amount", @@ -606,6 +627,7 @@ ], "program": "energyEfficientHomeImprovementCredit", "authority_type": "federal", + "v0_item": "heat_pump_water_heater", "items": [ "heat_pump_water_heater" ], @@ -636,6 +658,7 @@ ], "program": "homeElectrificationAndApplianceRebates", "authority_type": "federal", + "v0_item": "heat_pump_water_heater", "items": [ "heat_pump_water_heater" ], @@ -666,8 +689,11 @@ ], "program": "homeElectrificationAndApplianceRebates", "authority_type": "federal", + "v0_item": "heat_pump_air_conditioner_heater", "items": [ - "heat_pump_air_conditioner_heater" + "air_to_water_heat_pump", + "ducted_heat_pump", + "ductless_heat_pump" ], "amount": { "type": "dollar_amount", @@ -697,6 +723,7 @@ ], "program": "homeElectrificationAndApplianceRebates", "authority_type": "federal", + "v0_item": "heat_pump_clothes_dryer", "items": [ "heat_pump_clothes_dryer" ], @@ -728,6 +755,7 @@ ], "program": "homeElectrificationAndApplianceRebates", "authority_type": "federal", + "v0_item": "heat_pump_water_heater", "items": [ "heat_pump_water_heater" ], @@ -758,8 +786,11 @@ ], "program": "homeElectrificationAndApplianceRebates", "authority_type": "federal", + "v0_item": "heat_pump_air_conditioner_heater", "items": [ - "heat_pump_air_conditioner_heater" + "air_to_water_heat_pump", + "ducted_heat_pump", + "ductless_heat_pump" ], "amount": { "type": "dollar_amount", @@ -789,6 +820,7 @@ ], "program": "homeElectrificationAndApplianceRebates", "authority_type": "federal", + "v0_item": "heat_pump_clothes_dryer", "items": [ "heat_pump_clothes_dryer" ], @@ -820,6 +852,7 @@ ], "program": "residentialCleanEnergyCredit", "authority_type": "federal", + "v0_item": "rooftop_solar_installation", "items": [ "rooftop_solar_installation" ], @@ -850,8 +883,11 @@ ], "program": "homeElectrificationAndApplianceRebates", "authority_type": "federal", + "v0_item": "weatherization", "items": [ - "weatherization" + "air_sealing", + "other_insulation", + "other_weatherization" ], "amount": { "type": "dollar_amount", @@ -880,8 +916,12 @@ ], "program": "energyEfficientHomeImprovementCredit", "authority_type": "federal", + "v0_item": "weatherization", "items": [ - "weatherization" + "air_sealing", + "door_replacement", + "window_replacement", + "other_insulation" ], "amount": { "type": "dollar_amount", @@ -894,7 +934,7 @@ "start_date": "2023", "end_date": "2032", "short_description": { - "en": "30% tax credit (up to $1,200) for weatherization projects: insulation, air sealing, doors, windows, and energy audits. Yearly reset.", + "en": "30% tax credit (up to $1,200) for weatherization projects: insulation, air sealing, doors, windows. Yearly reset.", "es": "Crédito fiscal del 30% (hasta $1,200) por los proyectos de impermeabilización. Reinicio anual." }, "more_info_url": { @@ -902,6 +942,37 @@ "es": "https://homes.rewiringamerica.org/es/federal-incentives/25c-weatherization-tax-credits" } }, + { + "id": "IRA-19", + "type": "tax_credit", + "payment_methods": [ + "tax_credit" + ], + "program": "energyEfficientHomeImprovementCredit", + "authority_type": "federal", + "v0_item": null, + "items": [ + "energy_audit" + ], + "amount": { + "type": "dollar_amount", + "number": 150 + }, + "owner_status": [ + "homeowner" + ], + "ami_qualification": null, + "start_date": "2023", + "end_date": "2032", + "short_description": { + "en": "Tax credit of up to $150 for an energy audit. Yearly reset.", + "es": "Crédito fiscal de hasta $150) por . Reinicio anual." + }, + "more_info_url": { + "en": "https://homes.rewiringamerica.org/federal-incentives/25c-weatherization-tax-credits", + "es": "https://homes.rewiringamerica.org/es/federal-incentives/25c-weatherization-tax-credits" + } + }, { "id": "IRA-17", "type": "pos_rebate", @@ -910,8 +981,11 @@ ], "program": "homeElectrificationAndApplianceRebates", "authority_type": "federal", + "v0_item": "weatherization", "items": [ - "weatherization" + "air_sealing", + "other_insulation", + "other_weatherization" ], "amount": { "type": "dollar_amount", @@ -940,6 +1014,7 @@ ], "program": "homeEfficiencyRebates", "authority_type": "federal", + "v0_item": "efficiency_rebates", "items": [ "efficiency_rebates" ], @@ -970,6 +1045,7 @@ ], "program": "homeEfficiencyRebates", "authority_type": "federal", + "v0_item": "efficiency_rebates", "items": [ "efficiency_rebates" ], diff --git a/locales/en.json b/locales/en.json index 78aad9bc..305ea197 100644 --- a/locales/en.json +++ b/locales/en.json @@ -17,28 +17,19 @@ }, "items": { "battery_storage_installation": "Battery Storage Installation", - "ebike": "E-Bike", "efficiency_rebates": "Efficiency Rebates", - "electric_outdoor_equipment": "Electric Outdoor Equipment", "electric_panel": "Electric Panel", "electric_stove": "Electric/Induction Stove", - "electric_thermal_storage_and_slab": "Electric Thermal Storage and Slab", "electric_vehicle_charger": "Electric Vehicle Charger", "electric_wiring": "Electric Wiring", - "evaporative_cooler": "Evaporative Cooler", "geothermal_heating_installation": "Geothermal Heating Installation", "heat_pump_air_conditioner_heater": "Heat Pump Air Conditioner/Heater", "heat_pump_clothes_dryer": "Heat Pump Clothes Dryer", "heat_pump_water_heater": "Heat Pump Water Heater", "new_electric_vehicle": "New Electric Vehicle", - "non_heat_pump_clothes_dryer": "Electric Resistance Clothes Dryer", - "non_heat_pump_water_heater": "Electric Resistance Water Heater", - "other": "Other", "rooftop_solar_installation": "Rooftop Solar Installation", - "smart_thermostat": "Smart Thermostat", "used_electric_vehicle": "Used Electric Vehicle", - "weatherization": "Weatherization", - "whole_house_fan": "Whole House Fan" + "weatherization": "Weatherization" }, "errors": { "cannot_locate_address": "We can't locate that address.", diff --git a/locales/es.json b/locales/es.json index 4098474c..d860b32d 100644 --- a/locales/es.json +++ b/locales/es.json @@ -17,28 +17,19 @@ }, "items": { "battery_storage_installation": "Instalación de baterías", - "ebike": "Bicicleta Eléctrica", "efficiency_rebates": "Bonificaciones por eficiencia", - "electric_outdoor_equipment": "Equipo eléctrico para exteriores", "electric_panel": "Cuadro eléctrico", "electric_stove": "Estufa eléctrica/de inducción", - "electric_thermal_storage_and_slab": "Almacenamiento de energía térmica y el bloque", "electric_vehicle_charger": "Cargador de vehículos eléctricos", "electric_wiring": "Cableado eléctrico", - "evaporative_cooler": "Enfriador evaporativo", "geothermal_heating_installation": "Instalación de calefacción geotérmica", "heat_pump_air_conditioner_heater": "Aire acondicionado/calentador con bomba de calor", "heat_pump_clothes_dryer": "Secadora de ropa con bomba de calor", "heat_pump_water_heater": "Calentador de agua con bomba de calor", "new_electric_vehicle": "Nuevo vehículo eléctrico", - "non_heat_pump_clothes_dryer": "Secadora de ropa eléctrica", - "non_heat_pump_water_heater": "Calentador de agua eléctrico", - "other": "Otro", "rooftop_solar_installation": "Instalación solar en el techo", - "smart_thermostat": "Termostato inteligente", "used_electric_vehicle": "Vehículo eléctrico usado", - "weatherization": "Climatización", - "whole_house_fan": "Ventilador para toda la casa" + "weatherization": "Climatización" }, "errors": { "cannot_locate_address": "No se puede encontrar la ubicación de esa dirección.", diff --git a/src/data/ira_incentives.ts b/src/data/ira_incentives.ts index fffd9401..8f7d8f65 100644 --- a/src/data/ira_incentives.ts +++ b/src/data/ira_incentives.ts @@ -6,7 +6,7 @@ import { PROGRAMS } from './programs'; import { FilingStatus } from './tax_brackets'; import { AMOUNT_SCHEMA, Amount } from './types/amount'; import { PaymentMethod } from './types/incentive-types'; -import { IRAItem, IRA_ITEMS } from './types/items'; +import { ALL_ITEMS, IRAItem, IRA_ITEMS, Item } from './types/items'; import { LocalizableString } from './types/localizable-string'; import { OwnerStatus } from './types/owner-status'; @@ -24,7 +24,8 @@ export interface IRAIncentive { authority_type: AuthorityType.Federal; end_date: string; filing_status?: FilingStatus; - items: IRAItem[]; + v0_item: IRAItem | null; + items: Item[]; owner_status: OwnerStatus[]; program: string; start_date: string; @@ -64,10 +65,13 @@ export const SCHEMA: JSONSchemaType = { }, program: { type: 'string', enum: Object.keys(PROGRAMS) }, authority_type: { type: 'string', const: AuthorityType.Federal }, + v0_item: nullable({ + type: 'string', + enum: IRA_ITEMS, + }), items: { type: 'array', - maxItems: 1, - items: { type: 'string', enum: IRA_ITEMS }, + items: { type: 'string', enum: ALL_ITEMS }, }, amount: AMOUNT_SCHEMA, owner_status: { @@ -107,6 +111,7 @@ export const SCHEMA: JSONSchemaType = { 'amount', 'authority_type', 'end_date', + 'v0_item', 'items', 'owner_status', 'program', diff --git a/src/data/locale.ts b/src/data/locale.ts index ac851015..af65e458 100644 --- a/src/data/locale.ts +++ b/src/data/locale.ts @@ -4,20 +4,20 @@ import { ALL_ERROR_MESSAGES, ERROR_MESSAGES_SCHEMA, } from './types/error-message'; -import { IRA_ITEMS, ITEMS_SCHEMA } from './types/items'; +import { IRA_ITEMS, IRA_ITEMS_SCHEMA } from './types/items'; export const SCHEMA = { type: 'object', properties: { items: { type: 'object', - properties: ITEMS_SCHEMA, + properties: IRA_ITEMS_SCHEMA, required: IRA_ITEMS, additionalProperties: false, }, urls: { type: 'object', - properties: ITEMS_SCHEMA, + properties: IRA_ITEMS_SCHEMA, required: IRA_ITEMS, additionalProperties: false, }, diff --git a/src/data/types/items.ts b/src/data/types/items.ts index fd245676..d3e531c8 100644 --- a/src/data/types/items.ts +++ b/src/data/types/items.ts @@ -28,7 +28,6 @@ export const ITEMS_SCHEMA = { evaporative_cooler: { type: 'string' }, floor_insulation: { type: 'string' }, geothermal_heating_installation: { type: 'string' }, - heat_pump_air_conditioner_heater: { type: 'string' }, // TODO remove heat_pump_clothes_dryer: { type: 'string' }, heat_pump_water_heater: { type: 'string' }, new_electric_vehicle: { type: 'string' }, @@ -44,31 +43,38 @@ export const ITEMS_SCHEMA = { used_electric_vehicle: { type: 'string' }, used_plugin_hybrid_vehicle: { type: 'string' }, wall_insulation: { type: 'string' }, - weatherization: { type: 'string' }, // TODO remove whole_house_fan: { type: 'string' }, window_replacement: { type: 'string' }, } as const; export type Item = keyof typeof ITEMS_SCHEMA; -/** Only these items appear in the IRA incentives (and thus in API v0). */ -export const IRA_ITEMS = [ - 'battery_storage_installation', - 'efficiency_rebates', - 'electric_panel', - 'electric_stove', - 'electric_vehicle_charger', - 'electric_wiring', - 'geothermal_heating_installation', - 'heat_pump_air_conditioner_heater', - 'heat_pump_clothes_dryer', - 'heat_pump_water_heater', - 'new_electric_vehicle', - 'rooftop_solar_installation', - 'used_electric_vehicle', - 'weatherization', -] as const satisfies readonly Item[]; +/** + * These items are for the legacy v0 API, and only used with IRA incentives. + * + * Note that this contains two items that don't appear above: weatherization and + * heat_pump_air_conditioner_heater. In v1 (and state and utility incentives), + * these are broken up into more granular categories. + */ +export const IRA_ITEMS_SCHEMA = { + battery_storage_installation: { type: 'string' }, + efficiency_rebates: { type: 'string' }, + electric_panel: { type: 'string' }, + electric_stove: { type: 'string' }, + electric_vehicle_charger: { type: 'string' }, + electric_wiring: { type: 'string' }, + geothermal_heating_installation: { type: 'string' }, + heat_pump_air_conditioner_heater: { type: 'string' }, + heat_pump_clothes_dryer: { type: 'string' }, + heat_pump_water_heater: { type: 'string' }, + new_electric_vehicle: { type: 'string' }, + rooftop_solar_installation: { type: 'string' }, + used_electric_vehicle: { type: 'string' }, + weatherization: { type: 'string' }, +} as const; -export type IRAItem = (typeof IRA_ITEMS)[number]; +export type IRAItem = keyof typeof IRA_ITEMS_SCHEMA; export const ALL_ITEMS = Object.keys(ITEMS_SCHEMA) as unknown as Item[]; + +export const IRA_ITEMS = Object.keys(IRA_ITEMS_SCHEMA) as unknown as IRAItem[]; diff --git a/src/routes/v0.ts b/src/routes/v0.ts index 494528b4..203d65c9 100644 --- a/src/routes/v0.ts +++ b/src/routes/v0.ts @@ -26,48 +26,50 @@ import { IRA_INCENTIVES.forEach(incentive => Object.freeze(incentive)); function translateIncentives(incentives: IRAIncentive[]): WebsiteIncentive[] { - return incentives.map(incentive => ({ - ...incentive, - start_date: parseInt(incentive.start_date), - end_date: parseInt(incentive.end_date), - item_es: t('items', incentive.items[0], 'es'), - item: t('items', incentive.items[0], 'en'), - program_es: tr(PROGRAMS[incentive.program as keyof Programs].name, 'es'), - program: tr(PROGRAMS[incentive.program as keyof Programs].name, 'en'), - // strip domain from v0 links: - more_info_url_es: t('urls', incentive.items[0], 'es').replace( - 'https://www.rewiringamerica.org', - '', - ), - more_info_url: t('urls', incentive.items[0], 'en').replace( - 'https://www.rewiringamerica.org', - '', - ), - // TODO: remove when PEP is migrated to v1 API - more_info_url_internal: tr(incentive.more_info_url, 'en'), - short_description: tr(incentive.short_description, 'en'), - - type: - incentive.type === PaymentMethod.PerformanceRebate - ? PaymentMethod.PosRebate - : incentive.type, - - // Reconstruct item_type - item_type: - incentive.items[0] === 'rooftop_solar_installation' - ? 'solar_tax_credit' - : incentive.items[0] === 'electric_vehicle_charger' - ? 'ev_charger_credit' - : incentive.type, - - // Transform amounts from v1 to v0 format - amount: incentive.amount.number, - amount_type: incentive.amount.type as 'dollar_amount' | 'percent', - representative_amount: incentive.amount.representative ?? 0, - - // agi_max_limit is not nullable - agi_max_limit: incentive.agi_max_limit ?? 0, - })); + return incentives + .filter(incentive => incentive.v0_item !== null) + .map(incentive => ({ + ...incentive, + start_date: parseInt(incentive.start_date), + end_date: parseInt(incentive.end_date), + item_es: t('items', incentive.v0_item!, 'es'), + item: t('items', incentive.v0_item!, 'en'), + program_es: tr(PROGRAMS[incentive.program as keyof Programs].name, 'es'), + program: tr(PROGRAMS[incentive.program as keyof Programs].name, 'en'), + // strip domain from v0 links: + more_info_url_es: t('urls', incentive.v0_item!, 'es').replace( + 'https://www.rewiringamerica.org', + '', + ), + more_info_url: t('urls', incentive.v0_item!, 'en').replace( + 'https://www.rewiringamerica.org', + '', + ), + // TODO: remove when PEP is migrated to v1 API + more_info_url_internal: tr(incentive.more_info_url, 'en'), + short_description: tr(incentive.short_description, 'en'), + + type: + incentive.type === PaymentMethod.PerformanceRebate + ? PaymentMethod.PosRebate + : incentive.type, + + // Reconstruct item_type + item_type: + incentive.items[0] === 'rooftop_solar_installation' + ? 'solar_tax_credit' + : incentive.items[0] === 'electric_vehicle_charger' + ? 'ev_charger_credit' + : incentive.type, + + // Transform amounts from v1 to v0 format + amount: incentive.amount.number, + amount_type: incentive.amount.type as 'dollar_amount' | 'percent', + representative_amount: incentive.amount.representative ?? 0, + + // agi_max_limit is not nullable + agi_max_limit: incentive.agi_max_limit ?? 0, + })); } const CalculatorSchema = { diff --git a/test/fixtures/v0-80212-homeowner-80000-joint-4.json b/test/fixtures/v0-80212-homeowner-80000-joint-4.json index f55474b4..0be0fbe3 100644 --- a/test/fixtures/v0-80212-homeowner-80000-joint-4.json +++ b/test/fixtures/v0-80212-homeowner-80000-joint-4.json @@ -381,7 +381,7 @@ "ami_qualification": null, "agi_max_limit": 0, "eligible": true, - "short_description": "30% tax credit (up to $1,200) for weatherization projects: insulation, air sealing, doors, windows, and energy audits. Yearly reset.", + "short_description": "30% tax credit (up to $1,200) for weatherization projects: insulation, air sealing, doors, windows. Yearly reset.", "more_info_url_internal": "https://homes.rewiringamerica.org/federal-incentives/25c-weatherization-tax-credits" }, { diff --git a/test/fixtures/v1-15289-homeowner-85000-joint-4.json b/test/fixtures/v1-15289-homeowner-85000-joint-4.json index 9ceb9bfd..9061c8b9 100644 --- a/test/fixtures/v1-15289-homeowner-85000-joint-4.json +++ b/test/fixtures/v1-15289-homeowner-85000-joint-4.json @@ -289,7 +289,9 @@ "program_url": "https://www.irs.gov/credits-deductions/energy-efficient-home-improvement-credit", "more_info_url": "https://homes.rewiringamerica.org/federal-incentives/25c-heat-pump-tax-credits", "items": [ - "heat_pump_air_conditioner_heater" + "air_to_water_heat_pump", + "ducted_heat_pump", + "ductless_heat_pump" ], "amount": { "type": "dollar_amount", @@ -337,7 +339,10 @@ "program_url": "https://www.irs.gov/credits-deductions/energy-efficient-home-improvement-credit", "more_info_url": "https://homes.rewiringamerica.org/federal-incentives/25c-weatherization-tax-credits", "items": [ - "weatherization" + "air_sealing", + "door_replacement", + "window_replacement", + "other_insulation" ], "amount": { "type": "dollar_amount", @@ -350,7 +355,7 @@ "end_date": "2032", "ami_qualification": null, "eligible": true, - "short_description": "30% tax credit (up to $1,200) for weatherization projects: insulation, air sealing, doors, windows, and energy audits. Yearly reset." + "short_description": "30% tax credit (up to $1,200) for weatherization projects: insulation, air sealing, doors, windows. Yearly reset." }, { "payment_methods": [ @@ -400,6 +405,30 @@ "ami_qualification": null, "eligible": true, "short_description": "30% tax credit (up to $600) for electrical panel upgrade when done with another 25C or 25D upgrade. Yearly reset." + }, + { + "payment_methods": [ + "tax_credit" + ], + "authority_type": "federal", + "program": "Federal Energy Efficient Home Improvement Credit (25C)", + "program_url": "https://www.irs.gov/credits-deductions/energy-efficient-home-improvement-credit", + "more_info_url": "https://homes.rewiringamerica.org/federal-incentives/25c-weatherization-tax-credits", + "items": [ + "energy_audit" + ], + "amount": { + "type": "dollar_amount", + "number": 150 + }, + "owner_status": [ + "homeowner" + ], + "start_date": "2023", + "end_date": "2032", + "ami_qualification": null, + "eligible": true, + "short_description": "Tax credit of up to $150 for an energy audit. Yearly reset." } ] } diff --git a/test/fixtures/v1-84106-homeowner-80000-joint-4.json b/test/fixtures/v1-84106-homeowner-80000-joint-4.json index 441edd82..ddebfea7 100644 --- a/test/fixtures/v1-84106-homeowner-80000-joint-4.json +++ b/test/fixtures/v1-84106-homeowner-80000-joint-4.json @@ -151,7 +151,9 @@ "program_url": "https://www.irs.gov/credits-deductions/energy-efficient-home-improvement-credit", "more_info_url": "https://homes.rewiringamerica.org/federal-incentives/25c-heat-pump-tax-credits", "items": [ - "heat_pump_air_conditioner_heater" + "air_to_water_heat_pump", + "ducted_heat_pump", + "ductless_heat_pump" ], "amount": { "type": "dollar_amount", @@ -199,7 +201,10 @@ "program_url": "https://www.irs.gov/credits-deductions/energy-efficient-home-improvement-credit", "more_info_url": "https://homes.rewiringamerica.org/federal-incentives/25c-weatherization-tax-credits", "items": [ - "weatherization" + "air_sealing", + "door_replacement", + "window_replacement", + "other_insulation" ], "amount": { "type": "dollar_amount", @@ -212,7 +217,7 @@ "end_date": "2032", "ami_qualification": null, "eligible": true, - "short_description": "30% tax credit (up to $1,200) for weatherization projects: insulation, air sealing, doors, windows, and energy audits. Yearly reset." + "short_description": "30% tax credit (up to $1,200) for weatherization projects: insulation, air sealing, doors, windows. Yearly reset." }, { "payment_methods": [ @@ -262,6 +267,30 @@ "ami_qualification": null, "eligible": true, "short_description": "30% tax credit (up to $600) for electrical panel upgrade when done with another 25C or 25D upgrade. Yearly reset." + }, + { + "payment_methods": [ + "tax_credit" + ], + "authority_type": "federal", + "program": "Federal Energy Efficient Home Improvement Credit (25C)", + "program_url": "https://www.irs.gov/credits-deductions/energy-efficient-home-improvement-credit", + "more_info_url": "https://homes.rewiringamerica.org/federal-incentives/25c-weatherization-tax-credits", + "items": [ + "energy_audit" + ], + "amount": { + "type": "dollar_amount", + "number": 150 + }, + "owner_status": [ + "homeowner" + ], + "start_date": "2023", + "end_date": "2032", + "ami_qualification": null, + "eligible": true, + "short_description": "Tax credit of up to $150 for an energy audit. Yearly reset." } ] } diff --git a/test/lib/incentives-calculation.test.ts b/test/lib/incentives-calculation.test.ts index d85d5dee..ea1ed9eb 100644 --- a/test/lib/incentives-calculation.test.ts +++ b/test/lib/incentives-calculation.test.ts @@ -40,6 +40,13 @@ const LOCATION_AND_AMIS = { ], } as const; +const IRA_HVAC_ITEMS = + 'air_to_water_heat_pump,ducted_heat_pump,ductless_heat_pump'; +const IRA_25C_WEATHERIZATION_ITEMS = + 'air_sealing,door_replacement,window_replacement,other_insulation'; +const IRA_HEAR_WEATHERIZATION_ITEMS = + 'air_sealing,other_insulation,other_weatherization'; + beforeEach(async t => { t.context.db = await open({ filename: './incentives-api.db', @@ -167,7 +174,7 @@ test('correctly evaluates scenario "Single w/ $120k Household income in Brooklyn ); t.equal(pos_rebate_incentives.length, 7); - t.equal(tax_credit_incentives.length, 10); + t.equal(tax_credit_incentives.length, 11); t.equal(performance_rebate_incentives.length, 1); // count the incentives by key used to de-dupe in UI: @@ -188,7 +195,7 @@ test('correctly evaluates scenario "Single w/ $120k Household income in Brooklyn true, ); - const posRebates = _.keyBy(pos_rebate_incentives, i => i.items[0]); + const posRebates = _.keyBy(pos_rebate_incentives, i => i.items.join(',')); t.equal(posRebates['electric_panel'].eligible, true); t.equal(posRebates['electric_panel'].amount.number, 4000); t.equal(posRebates['electric_panel'].start_date, '2025'); @@ -201,21 +208,21 @@ test('correctly evaluates scenario "Single w/ $120k Household income in Brooklyn t.equal(posRebates['heat_pump_water_heater'].eligible, true); t.equal(posRebates['heat_pump_water_heater'].amount.number, 1750); t.equal(posRebates['heat_pump_water_heater'].start_date, '2025'); - t.equal(posRebates['heat_pump_air_conditioner_heater'].eligible, true); - t.equal(posRebates['heat_pump_air_conditioner_heater'].amount.number, 8000); - t.equal(posRebates['heat_pump_air_conditioner_heater'].start_date, '2025'); + t.equal(posRebates[IRA_HVAC_ITEMS].eligible, true); + t.equal(posRebates[IRA_HVAC_ITEMS].amount.number, 8000); + t.equal(posRebates[IRA_HVAC_ITEMS].start_date, '2025'); t.equal(posRebates['heat_pump_clothes_dryer'].eligible, true); t.equal(posRebates['heat_pump_clothes_dryer'].amount.number, 840); t.equal(posRebates['heat_pump_clothes_dryer'].start_date, '2025'); - t.equal(posRebates['weatherization'].eligible, true); - t.equal(posRebates['weatherization'].amount.number, 1600); - t.equal(posRebates['weatherization'].start_date, '2025'); + t.equal(posRebates[IRA_HEAR_WEATHERIZATION_ITEMS].eligible, true); + t.equal(posRebates[IRA_HEAR_WEATHERIZATION_ITEMS].amount.number, 1600); + t.equal(posRebates[IRA_HEAR_WEATHERIZATION_ITEMS].start_date, '2025'); t.equal(performance_rebate_incentives[0].eligible, true); t.equal(performance_rebate_incentives[0].amount.number, 4000); t.equal(performance_rebate_incentives[0].start_date, '2025'); - const taxCredits = _.keyBy(tax_credit_incentives, i => i.items[0]); + const taxCredits = _.keyBy(tax_credit_incentives, i => i.items.join(',')); t.equal(taxCredits['battery_storage_installation'].eligible, true); t.equal(taxCredits['battery_storage_installation'].amount.number, 0.3); // will be displayed as 30% t.equal(taxCredits['battery_storage_installation'].start_date, '2023'); @@ -231,9 +238,9 @@ test('correctly evaluates scenario "Single w/ $120k Household income in Brooklyn t.equal(taxCredits['new_electric_vehicle'].eligible, true); t.equal(taxCredits['new_electric_vehicle'].amount.number, 7500); t.equal(taxCredits['new_electric_vehicle'].start_date, '2023'); - t.equal(taxCredits['heat_pump_air_conditioner_heater'].eligible, true); - t.equal(taxCredits['heat_pump_air_conditioner_heater'].amount.number, 2000); - t.equal(taxCredits['heat_pump_air_conditioner_heater'].start_date, '2023'); + t.equal(taxCredits[IRA_HVAC_ITEMS].eligible, true); + t.equal(taxCredits[IRA_HVAC_ITEMS].amount.number, 2000); + t.equal(taxCredits[IRA_HVAC_ITEMS].start_date, '2023'); t.equal(taxCredits['heat_pump_water_heater'].eligible, true); t.equal(taxCredits['heat_pump_water_heater'].amount.number, 2000); t.equal(taxCredits['heat_pump_water_heater'].start_date, '2023'); @@ -241,9 +248,9 @@ test('correctly evaluates scenario "Single w/ $120k Household income in Brooklyn t.equal(taxCredits['rooftop_solar_installation'].amount.number, 0.3); t.equal(taxCredits['rooftop_solar_installation'].amount.representative, 4770); t.equal(taxCredits['rooftop_solar_installation'].start_date, '2022'); - t.equal(taxCredits['weatherization'].eligible, true); - t.equal(taxCredits['weatherization'].amount.number, 1200); - t.equal(taxCredits['weatherization'].start_date, '2023'); + t.equal(taxCredits[IRA_25C_WEATHERIZATION_ITEMS].eligible, true); + t.equal(taxCredits[IRA_25C_WEATHERIZATION_ITEMS].amount.number, 1200); + t.equal(taxCredits[IRA_25C_WEATHERIZATION_ITEMS].start_date, '2023'); // everything except used EV should be eligible for tax credit (agi limit is 75k) t.equal(taxCredits['used_electric_vehicle'].eligible, false); t.equal(taxCredits['used_electric_vehicle'].amount.number, 4000); @@ -263,7 +270,7 @@ test('correctly evaluates scenario "Married filing jointly w/ 2 kids and $250k H t.equal(data.is_over_150_ami, false); t.equal(data.savings.pos_rebate, 14000); - t.equal(data.savings.tax_credit, 29872); + t.equal(data.savings.tax_credit, 30022); t.equal(data.savings.performance_rebate, 4000); const pos_rebate_incentives = data.incentives.filter( @@ -277,7 +284,7 @@ test('correctly evaluates scenario "Married filing jointly w/ 2 kids and $250k H ); t.equal(pos_rebate_incentives.length, 7); - t.equal(tax_credit_incentives.length, 10); + t.equal(tax_credit_incentives.length, 11); t.equal(performance_rebate_incentives.length, 1); // count the incentives by key used to de-dupe in UI: @@ -298,7 +305,7 @@ test('correctly evaluates scenario "Married filing jointly w/ 2 kids and $250k H true, ); - const posRebates = _.keyBy(pos_rebate_incentives, i => i.items[0]); + const posRebates = _.keyBy(pos_rebate_incentives, i => i.items.join(',')); t.equal(posRebates['electric_panel'].eligible, true); t.equal(posRebates['electric_panel'].amount.number, 4000); t.equal(posRebates['electric_panel'].start_date, '2025'); @@ -311,21 +318,21 @@ test('correctly evaluates scenario "Married filing jointly w/ 2 kids and $250k H t.equal(posRebates['heat_pump_water_heater'].eligible, true); t.equal(posRebates['heat_pump_water_heater'].amount.number, 1750); t.equal(posRebates['heat_pump_water_heater'].start_date, '2025'); - t.equal(posRebates['heat_pump_air_conditioner_heater'].eligible, true); - t.equal(posRebates['heat_pump_air_conditioner_heater'].amount.number, 8000); - t.equal(posRebates['heat_pump_air_conditioner_heater'].start_date, '2025'); + t.equal(posRebates[IRA_HVAC_ITEMS].eligible, true); + t.equal(posRebates[IRA_HVAC_ITEMS].amount.number, 8000); + t.equal(posRebates[IRA_HVAC_ITEMS].start_date, '2025'); t.equal(posRebates['heat_pump_clothes_dryer'].eligible, true); t.equal(posRebates['heat_pump_clothes_dryer'].amount.number, 840); t.equal(posRebates['heat_pump_clothes_dryer'].start_date, '2025'); - t.equal(posRebates['weatherization'].eligible, true); - t.equal(posRebates['weatherization'].amount.number, 1600); - t.equal(posRebates['weatherization'].start_date, '2025'); + t.equal(posRebates[IRA_HEAR_WEATHERIZATION_ITEMS].eligible, true); + t.equal(posRebates[IRA_HEAR_WEATHERIZATION_ITEMS].amount.number, 1600); + t.equal(posRebates[IRA_HEAR_WEATHERIZATION_ITEMS].start_date, '2025'); t.equal(performance_rebate_incentives[0].eligible, true); t.equal(performance_rebate_incentives[0].amount.number, 4000); t.equal(performance_rebate_incentives[0].start_date, '2025'); - const taxCredits = _.keyBy(tax_credit_incentives, i => i.items[0]); + const taxCredits = _.keyBy(tax_credit_incentives, i => i.items.join(',')); t.equal(taxCredits['battery_storage_installation'].eligible, true); t.equal(taxCredits['battery_storage_installation'].amount.number, 0.3); // will be displayed as 30% t.equal(taxCredits['battery_storage_installation'].start_date, '2023'); @@ -341,9 +348,9 @@ test('correctly evaluates scenario "Married filing jointly w/ 2 kids and $250k H t.equal(taxCredits['new_electric_vehicle'].eligible, true); t.equal(taxCredits['new_electric_vehicle'].amount.number, 7500); t.equal(taxCredits['new_electric_vehicle'].start_date, '2023'); - t.equal(taxCredits['heat_pump_air_conditioner_heater'].eligible, true); - t.equal(taxCredits['heat_pump_air_conditioner_heater'].amount.number, 2000); - t.equal(taxCredits['heat_pump_air_conditioner_heater'].start_date, '2023'); + t.equal(taxCredits[IRA_HVAC_ITEMS].eligible, true); + t.equal(taxCredits[IRA_HVAC_ITEMS].amount.number, 2000); + t.equal(taxCredits[IRA_HVAC_ITEMS].start_date, '2023'); t.equal(taxCredits['heat_pump_water_heater'].eligible, true); t.equal(taxCredits['heat_pump_water_heater'].amount.number, 2000); t.equal(taxCredits['heat_pump_water_heater'].start_date, '2023'); @@ -351,9 +358,9 @@ test('correctly evaluates scenario "Married filing jointly w/ 2 kids and $250k H t.equal(taxCredits['rooftop_solar_installation'].amount.number, 0.3); t.equal(taxCredits['rooftop_solar_installation'].amount.representative, 4572); t.equal(taxCredits['rooftop_solar_installation'].start_date, '2022'); - t.equal(taxCredits['weatherization'].eligible, true); - t.equal(taxCredits['weatherization'].amount.number, 1200); - t.equal(taxCredits['weatherization'].start_date, '2023'); + t.equal(taxCredits[IRA_25C_WEATHERIZATION_ITEMS].eligible, true); + t.equal(taxCredits[IRA_25C_WEATHERIZATION_ITEMS].amount.number, 1200); + t.equal(taxCredits[IRA_25C_WEATHERIZATION_ITEMS].start_date, '2023'); // everything except used EV should be eligible for tax credit (agi limit is 75k) t.equal(taxCredits['used_electric_vehicle'].eligible, false); t.equal(taxCredits['used_electric_vehicle'].amount.number, 4000); @@ -373,7 +380,7 @@ test('correctly evaluates scenario "Hoh w/ 6 kids and $500k Household income in t.equal(data.is_over_150_ami, true); t.equal(data.savings.pos_rebate, 0); - t.equal(data.savings.tax_credit, 22228.9); + t.equal(data.savings.tax_credit, 22378.9); t.equal(data.savings.performance_rebate, 4000); const pos_rebate_incentives = data.incentives.filter( @@ -387,7 +394,7 @@ test('correctly evaluates scenario "Hoh w/ 6 kids and $500k Household income in ); t.equal(pos_rebate_incentives.length, 7); - t.equal(tax_credit_incentives.length, 10); + t.equal(tax_credit_incentives.length, 11); t.equal(performance_rebate_incentives.length, 1); // count the incentives by key used to de-dupe in UI: @@ -408,7 +415,7 @@ test('correctly evaluates scenario "Hoh w/ 6 kids and $500k Household income in true, ); - const posRebates = _.keyBy(pos_rebate_incentives, i => i.items[0]); + const posRebates = _.keyBy(pos_rebate_incentives, i => i.items.join(',')); t.equal(posRebates['electric_panel'].eligible, false); t.equal(posRebates['electric_panel'].amount.number, 4000); t.equal(posRebates['electric_panel'].start_date, '2025'); @@ -421,22 +428,22 @@ test('correctly evaluates scenario "Hoh w/ 6 kids and $500k Household income in t.equal(posRebates['heat_pump_water_heater'].eligible, false); t.equal(posRebates['heat_pump_water_heater'].amount.number, 1750); t.equal(posRebates['heat_pump_water_heater'].start_date, '2025'); - t.equal(posRebates['heat_pump_air_conditioner_heater'].eligible, false); - t.equal(posRebates['heat_pump_air_conditioner_heater'].amount.number, 8000); - t.equal(posRebates['heat_pump_air_conditioner_heater'].start_date, '2025'); + t.equal(posRebates[IRA_HVAC_ITEMS].eligible, false); + t.equal(posRebates[IRA_HVAC_ITEMS].amount.number, 8000); + t.equal(posRebates[IRA_HVAC_ITEMS].start_date, '2025'); t.equal(posRebates['heat_pump_clothes_dryer'].eligible, false); t.equal(posRebates['heat_pump_clothes_dryer'].amount.number, 840); t.equal(posRebates['heat_pump_clothes_dryer'].start_date, '2025'); - t.equal(posRebates['weatherization'].eligible, false); - t.equal(posRebates['weatherization'].amount.number, 1600); - t.equal(posRebates['weatherization'].start_date, '2025'); + t.equal(posRebates[IRA_HEAR_WEATHERIZATION_ITEMS].eligible, false); + t.equal(posRebates[IRA_HEAR_WEATHERIZATION_ITEMS].amount.number, 1600); + t.equal(posRebates[IRA_HEAR_WEATHERIZATION_ITEMS].start_date, '2025'); // only items.efficiency_rebates are eligible here: t.equal(performance_rebate_incentives[0].eligible, true); t.equal(performance_rebate_incentives[0].amount.number, 4000); t.equal(performance_rebate_incentives[0].start_date, '2025'); - const taxCredits = _.keyBy(tax_credit_incentives, i => i.items[0]); + const taxCredits = _.keyBy(tax_credit_incentives, i => i.items.join(',')); t.equal(taxCredits['battery_storage_installation'].eligible, true); t.equal(taxCredits['battery_storage_installation'].amount.number, 0.3); // will be displayed as 30% t.equal(taxCredits['battery_storage_installation'].start_date, '2023'); @@ -449,9 +456,9 @@ test('correctly evaluates scenario "Hoh w/ 6 kids and $500k Household income in t.equal(taxCredits['electric_vehicle_charger'].eligible, false); t.equal(taxCredits['electric_vehicle_charger'].amount.number, 1000); t.equal(taxCredits['electric_vehicle_charger'].start_date, '2023'); - t.equal(taxCredits['heat_pump_air_conditioner_heater'].eligible, true); - t.equal(taxCredits['heat_pump_air_conditioner_heater'].amount.number, 2000); - t.equal(taxCredits['heat_pump_air_conditioner_heater'].start_date, '2023'); + t.equal(taxCredits[IRA_HVAC_ITEMS].eligible, true); + t.equal(taxCredits[IRA_HVAC_ITEMS].amount.number, 2000); + t.equal(taxCredits[IRA_HVAC_ITEMS].start_date, '2023'); t.equal(taxCredits['heat_pump_water_heater'].eligible, true); t.equal(taxCredits['heat_pump_water_heater'].amount.number, 2000); t.equal(taxCredits['heat_pump_water_heater'].start_date, '2023'); @@ -462,9 +469,9 @@ test('correctly evaluates scenario "Hoh w/ 6 kids and $500k Household income in 4428.9, ); t.equal(taxCredits['rooftop_solar_installation'].start_date, '2022'); - t.equal(taxCredits['weatherization'].eligible, true); - t.equal(taxCredits['weatherization'].amount.number, 1200); - t.equal(taxCredits['weatherization'].start_date, '2023'); + t.equal(taxCredits[IRA_25C_WEATHERIZATION_ITEMS].eligible, true); + t.equal(taxCredits[IRA_25C_WEATHERIZATION_ITEMS].amount.number, 1200); + t.equal(taxCredits[IRA_25C_WEATHERIZATION_ITEMS].start_date, '2023'); // new and used EVs should not be eligible: t.equal(taxCredits['new_electric_vehicle'].eligible, false); t.equal(taxCredits['new_electric_vehicle'].amount.number, 7500); @@ -699,10 +706,10 @@ test('correctly excludes IRA rebates', async t => { true, ); - t.equal(data.incentives.length, 10); + t.equal(data.incentives.length, 11); t.equal( data.incentives.filter(i => i.payment_methods[0] === 'tax_credit').length, - 10, + 11, ); t.equal(data.savings.pos_rebate, 0); t.equal(data.savings.performance_rebate, 0);