diff --git a/cypress/e2e/state-calculator.cy.ts b/cypress/e2e/state-calculator.cy.ts index 3dfe558..0f04530 100644 --- a/cypress/e2e/state-calculator.cy.ts +++ b/cypress/e2e/state-calculator.cy.ts @@ -55,7 +55,7 @@ describe('rewiring-america-state-calculator', () => { cy.get('rewiring-america-state-calculator') .shadow() - .contains('$8,000 off a heat pump'); + .contains('Discount off a heat pump'); cy.get('rewiring-america-state-calculator') .shadow() diff --git a/src/api/calculator-types-v1.ts b/src/api/calculator-types-v1.ts index d911606..5daecad 100644 --- a/src/api/calculator-types-v1.ts +++ b/src/api/calculator-types-v1.ts @@ -108,4 +108,6 @@ export interface APIResponse { }; location: APILocation; incentives: Incentive[]; + is_under_80_ami: boolean; + is_under_150_ami: boolean; } diff --git a/src/i18n/strings/es.ts b/src/i18n/strings/es.ts index 21cac1b..6ba6d15 100644 --- a/src/i18n/strings/es.ts +++ b/src/i18n/strings/es.ts @@ -23,6 +23,8 @@ export const templates = { s1af1b11106c11c94: `Vermont`, s1b5c0e397c13f596: `Kansas`, s1be88e157547cb24: `Reembolso por rendimiento`, + s1cc63fbd986ae5d9: `Descuento en impermeabilización`, + s1e73057deee4510e: `Sin embargo, los reembolsos se aplicarán de forma diferente en cada estado, de manera que no podemos garantizar los montos finales, la elegibilidad ni los plazos.`, s1f0dc611dbdf4115: `Ingresa sus ingresos brutos (ingresos antes de impuestos). Incluye salarios y sueldos, además de otras formas de ingresos, como pensiones, intereses, dividendos e ingresos por alquiler. Si está casado y presenta una declaración conjunta, incluye los ingresos de su cónyuge.`, s24ea3f43230aae41: `usted@example.com`, s255857544a9d5ec0: `Reiniciar`, @@ -32,6 +34,7 @@ export const templates = { s2800ea3a8c83c188: `Nueva York`, s280c7b72a9d4d573: `Carolina del Sur`, s282049f502eac1de: `Proyecto`, + s2b1b3dc2f8da1ce4: `Descuento en una bomba de calor`, s2bb426aeeaeede1a: `Texas`, s2c430b6a8a07370e: str`${0} del costo de ${1}`, s2de33deca4490a2f: `Kentucky`, @@ -48,6 +51,7 @@ export const templates = { s3ab697ad32f1c425: `Oregón`, s3e5e50bffd99c077: `Montana`, s3ec21e39dec13351: `Propietario`, + s3fba93629af19a87: str`Los lineamientos federales permiten un descuento de hasta \$${0}.`, s40f2997eb54b317b: `Míchigan`, s4371bac1b748cdc4: `kilovatio`, s4455d8f660ceaa3b: `10,000 Btuh`, @@ -68,6 +72,7 @@ export const templates = { s5c876c284d9dca41: `1 persona`, s5e25aaf62bd99625: `vatio`, s60d7e5b7ccbd7162: `Reembolso`, + s635e3d7c474426ce: `Descuento en una secadora con bomba de calor`, s64fbe9b97e397b12: `Estamos dedicados a proteger su privacidad.`, s675cdfc08b387d5a: `Ohio`, s67ef2eb5002eaaa5: `Podría ser que no hay incentivos en su área o que usted no califica financieramente para recibir ningún incentivo.`, @@ -94,11 +99,15 @@ export const templates = { s8b29a87eb1bdd138: `Otros incentivos disponibles para usted`, s8e35f6b4e6e0adb9: `Pensilvania`, s8e7e52ad112342ab: `un cargador de vehículos eléctricos`, + s8ee1a34f2ada948d: str`Los lineamientos federals permiten un reembolso de hasta \$${0}, basado en el ahorro de energía simulado o en el ahorro de energía medido logrado por el reacondicionamiento.`, + s8f668dd0fa1b8c4e: `Reembolso por eficiencia`, s8fd029fdcc452602: `Ingresa su código postal para seleccionar una empresa de servicios eléctricos.`, + s8fd8a524edc33867: `Esperado en 2025`, s912b944fa287f7d0: `Nuevo Hampshire`, s9afee25dcf31efc1: `un vehículo eléctrico nuevo`, s9b0d347a81e8f0a3: `Oklahoma`, s9ca62f0b7e639157: `Maine`, + s9de4186c3f39ea44: `Descuento en un calentador de agua con bomba de calor`, s9eba0933010df96e: `Más información`, s9fbb21f25571fff1: `Le informaremos sobre incentivos, reembolsos, y más de Rewiring America.`, s9fc7054353f62fda: `Casado que presenta una declaración por separado`, @@ -110,19 +119,23 @@ export const templates = { sabd5662f3f0a76be: `Descuento por adelantado`, sad181d4343ef967f: `Wyoming`, sae246b9f0aee2901: `Virginia Occidental`, + sae2ffd247e500180: `Descuento en cableado`, sae79f47e1f8205fc: str`Ese código postal no está en ${0}.`, saee4c6f0080bac8d: `Utah`, saf510e2c8ddfc21a: `una renovación de eficiencia energética`, safb8e695c2fc2dc3: `Declaración de impuestos`, sb04b1070af7f7b76: `Washington, DC`, sb1b20a59970ba607: `Incluye a cualquier persona con la que viva y que reclame como dependiente en sus impuestos, y a su cónyuge o pareja si presenta impuestos conjuntos.`, + sb1ef6ac20f1ddfff: `Descuento en un tablero eléctrico`, sb21ad862204e6186: `Ver nuestros`, + sb3615709fedc9d4d: `Reembolsos para la Electrificación del Hogar y Electrodomésticos (HEAR)`, sb5b955a693af9c2e: `Virginia`, sb661e8297dd681e2: `Incentivo`, sb694f3d582a0dbb9: `Selecciona "Cabeza de familia" si tiene un hijo o pariente que vive con usted y paga más de la mitad de los gastos de su hogar. Selecciona "Conjunta" si declara sus impuestos como pareja casada.`, sb6975fd8aa3fa26f: `Climatización`, sb932cf0734934406: `Su código postal ayuda a determinar la cantidad de descuentos y créditos fiscales para los que califica.`, sb9e8a9bfc8bcf398: `Rhode Island`, + sbafed52303428041: `https://homes.rewiringamerica.org/es/federal-incentives/home-efficiency-rebates`, sbc39ee880f922370: `Ninguno seleccionado`, sbc6f031a2851e3fd: str`\$${0}/${1} de descuento en ${2}, hasta \$${3}`, sbd6f35e4bb46fc68: `una estufa eléctrica/inducción`, @@ -131,13 +144,16 @@ export const templates = { sc026bbb64721085f: `Casado que presenta una declaración conjunta`, sc373af4c1a974b57: `Cocina`, sc5b20cb72269bc4f: `Los propietarios y inquilinos califican para diferentes incentivos.`, + sc9266b1b6ae1aad4: `Esperado en 2024-2025`, sc997cfdf24ba9b58: `Aún no tenemos datos sobre las empresas de servicios eléctricos en su área.`, sc9e494c8346b7cb5: `Otra`, + sca61f9664c0f6099: `https://homes.rewiringamerica.org/es/federal-incentives/home-electrification-appliance-rebates`, scb043c067bac571c: `Misisipi`, scc21fdd8a2feaeef: `Georgia`, scc3ef5dd3649b934: `Nuevo México`, sd01d1a3738143465: str`\$${0}/${1} de descuento en ${2}`, sd02402d1aaffcb1d: `Secadora`, + sd0a66efc08693b44: `Reembolsos por Eficiencia Doméstica (HER)`, sd0b6082239185272: `Seleccionar todo`, sd26fd2eb6e7f18cb: `Minnesota`, sd4647caeb94889ec: `Términos`, @@ -160,6 +176,7 @@ export const templates = { sf3affd5c9f2915e0: `Connecticut`, sf459faf797f733a9: `Impermeabilización`, sf844ac31d5fba2c9: `impermeabilización`, + sf8b5deb9ea9f5054: `Descuento en una estufa eléctrica`, sfa7338035e1ef173: `Alquilar o poseer`, sfc7214f623fe475d: `Selecciona la empresa a la que paga su factura de electricidad.`, sfe16afc784bb9d76: `Tejado solar`, diff --git a/src/ira-rebates.ts b/src/ira-rebates.ts new file mode 100644 index 0000000..3a1805a --- /dev/null +++ b/src/ira-rebates.ts @@ -0,0 +1,107 @@ +import { APIResponse, IncentiveType } from './api/calculator-types-v1'; +import { str } from './i18n/str'; +import { MsgFn } from './i18n/use-translated'; +import { Project } from './projects'; + +export type IRARebate = { + paymentMethod: IncentiveType; + project: Project; + headline: string; + program: string; + description: string; + url: string; + timeline: string | null; +}; + +const hearRebates: { + project: Project; + getHeadline: (msg: MsgFn) => string; + maxAmount: number; +}[] = [ + { + project: 'wiring', + getHeadline: msg => msg('Discount off an electric panel'), + maxAmount: 4000, + }, + { + project: 'cooking', + getHeadline: msg => msg('Discount off an electric stove'), + maxAmount: 840, + }, + { + project: 'wiring', + getHeadline: msg => msg('Discount off electric wiring'), + maxAmount: 2500, + }, + { + project: 'heat_pump_water_heater', + getHeadline: msg => msg('Discount off a heat pump water heater'), + maxAmount: 1750, + }, + { + project: 'hvac', + getHeadline: msg => msg('Discount off a heat pump'), + maxAmount: 8000, + }, + { + project: 'heat_pump_clothes_dryer', + getHeadline: msg => msg('Discount off a heat pump clothes dryer'), + maxAmount: 840, + }, + { + project: 'weatherization_and_efficiency', + getHeadline: msg => msg('Discount off weatherization'), + maxAmount: 1600, + }, +]; + +export function getRebatesFor(response: APIResponse, msg: MsgFn): IRARebate[] { + const disclaimerText = msg( + 'However, rebates will be implemented differently in each state, so we cannot guarantee final amounts, eligibility, or timeline.', + ); + const maxHerRebate = response.is_under_80_ami ? 8000 : 4000; + + const result: IRARebate[] = []; + + if (response.is_under_150_ami) { + hearRebates.forEach(rebate => + result.push({ + paymentMethod: 'pos_rebate' as IncentiveType, + project: rebate.project, + headline: rebate.getHeadline(msg), + program: msg( + 'Federal Home Electrification and Appliance Rebates (HEAR)', + ), + description: + msg( + str`The federal guidelines allow for a discount of up to $${rebate.maxAmount.toLocaleString()}.`, + ) + + ' ' + + disclaimerText, + url: msg( + 'https://homes.rewiringamerica.org/federal-incentives/home-electrification-appliance-rebates', + ), + timeline: msg('Expected in 2024-2025'), + }), + ); + } + + result.push({ + paymentMethod: 'performance_rebate', + project: 'weatherization_and_efficiency', + headline: msg('Rebate for efficiency retrofits'), + program: msg('Federal Home Efficiency Rebates (HER)'), + description: + msg( + str`The federal guidelines allow for a rebate of up to $${maxHerRebate.toLocaleString()}, based on the modeled energy savings or measured energy savings achieved by the retrofit.`, + ) + + ' ' + + disclaimerText, + url: msg( + 'https://homes.rewiringamerica.org/federal-incentives/home-efficiency-rebates', + ), + timeline: msg('Expected in 2025'), + }); + + return result; +} diff --git a/src/state-incentive-details.tsx b/src/state-incentive-details.tsx index 1731cc0..cf98990 100644 --- a/src/state-incentive-details.tsx +++ b/src/state-incentive-details.tsx @@ -5,6 +5,7 @@ import { APIResponse, AmountUnit, Incentive, + IncentiveType, ItemType, } from './api/calculator-types-v1'; import { getYear, isInFuture } from './api/dates'; @@ -16,6 +17,7 @@ import { str } from './i18n/str'; import { MsgFn, useTranslated } from './i18n/use-translated'; import { IconTabBar } from './icon-tab-bar'; import { ExclamationPoint, UpRightArrow } from './icons'; +import { IRARebate, getRebatesFor } from './ira-rebates'; import { PartnerLogos } from './partner-logos'; import { PROJECTS, Project, shortLabel } from './projects'; import { Separator } from './separator'; @@ -134,16 +136,16 @@ const itemName = (itemType: ItemType, msg: MsgFn) => }) : null; -const formatIncentiveType = (incentive: Incentive, msg: MsgFn) => - incentive.payment_methods[0] === 'tax_credit' +const formatIncentiveType = (payment_methods: IncentiveType[], msg: MsgFn) => + payment_methods[0] === 'tax_credit' ? msg('Tax credit') - : incentive.payment_methods[0] === 'pos_rebate' + : payment_methods[0] === 'pos_rebate' ? msg('Upfront discount') - : incentive.payment_methods[0] === 'rebate' + : payment_methods[0] === 'rebate' ? msg('Rebate') - : incentive.payment_methods[0] === 'account_credit' + : payment_methods[0] === 'account_credit' ? msg('Account credit') - : incentive.payment_methods[0] === 'performance_rebate' + : payment_methods[0] === 'performance_rebate' ? msg('Performance rebate') : msg('Incentive'); @@ -152,6 +154,11 @@ const getStartYearIfInFuture = (incentive: Incentive) => ? getYear(incentive.start_date) : null; +const isIRARebate = (incentive: Incentive) => + incentive.authority_type === 'federal' && + (incentive.payment_methods.includes('pos_rebate') || + incentive.payment_methods.includes('performance_rebate')); + const Chip: FC> = ({ isWarning, children, @@ -171,7 +178,7 @@ const Chip: FC> = ({ 'uppercase', isWarning && 'bg-yellow-200 text-[#806c23] py-[0.1875rem] pl-[0.1875rem] pr-2.5', - !isWarning && 'bg-purple-100 text-gray-700 px-2.5 py-1', + !isWarning && 'bg-purple-100 text-grey-700 px-2.5 py-1', )} > {isWarning ? : null} @@ -208,56 +215,37 @@ const LinkButton: FC> = ({ ); -const IncentiveCard: FC<{ incentive: Incentive }> = ({ incentive }) => { - const { msg } = useTranslated(); - const [buttonUrl, buttonContent] = incentive.more_info_url - ? [incentive.more_info_url, msg('Learn more')] - : [ - incentive.program_url, - <> - {msg('Visit site')} - - , - ]; - const futureStartYear = getStartYearIfInFuture(incentive); - - // The API cannot precisely tell, from zip code alone, whether the user is in - // a specific city or county; it takes a permissive approach and returns - // incentives for localities the user *might* be in. So this indicates that - // the user should check for themselves. - // - // This is a blunt-instrument approach; in many cases there's actually no - // ambiguity as to which city or county a zip code is in, but the API - // currently doesn't take that into account. - const locationEligibilityText = ['city', 'county', 'other'].includes( - incentive.authority_type, - ) - ? msg('Eligibility depends on residence location.') - : null; - return ( - -
- {formatIncentiveType(incentive, msg)} -
- {formatTitle(incentive, msg)} -
-
- {incentive.program} -
- -
- {incentive.short_description} {locationEligibilityText} -
- {futureStartYear && ( - - {msg(str`Expected in ${futureStartYear}`)} - - )} - {buttonContent} +const IncentiveCard: FC<{ + typeChip: string; + headline: string; + subHeadline: string; + body: string; + warningChip: string | null; + buttonUrl: string; + buttonContent: string | React.ReactElement; +}> = ({ + typeChip, + headline, + subHeadline, + body, + warningChip, + buttonUrl, + buttonContent, +}) => ( + +
+ {typeChip} +
{headline}
+
+ {subHeadline}
- - ); -}; + {warningChip && {warningChip}} + +
{body}
+ {buttonContent} +
+
+); function scrollToForm(event: React.MouseEvent) { const calculator = ( @@ -347,23 +335,84 @@ const renderNoResults = (emailSubmitter: ((email: string) => void) | null) => { ); }; -const renderCardCollection = (incentives: Incentive[]) => ( -
- {incentives - // Sort incentives that haven't started yet at the end - .sort( - (a, b) => - (getStartYearIfInFuture(a) ?? 0) - (getStartYearIfInFuture(b) ?? 0), - ) - .map((incentive, index) => ( - - ))} -
-); +const renderCardCollection = ( + incentives: Incentive[], + iraRebates: IRARebate[], +) => { + const { msg } = useTranslated(); + return ( +
+ {incentives + // Sort incentives that haven't started yet at the end + .sort( + (a, b) => + (getStartYearIfInFuture(a) ?? 0) - (getStartYearIfInFuture(b) ?? 0), + ) + .map((incentive, index) => { + const futureStartYear = getStartYearIfInFuture(incentive); + + // The API cannot precisely tell, from zip code alone, whether the + // user is in a specific city or county; it takes a permissive + // approach and returns incentives for localities the user *might* be + // in. So this indicates that the user should check for themselves. + // + // This is a blunt-instrument approach; in many cases there's actually + // no ambiguity as to which city or county a zip code is in, but the + // API currently doesn't take that into account. + const locationEligibilityText = ['city', 'county', 'other'].includes( + incentive.authority_type, + ) + ? msg('Eligibility depends on residence location.') + : ''; + + const [buttonUrl, buttonContent] = incentive.more_info_url + ? [incentive.more_info_url, msg('Learn more')] + : [ + incentive.program_url, + <> + {msg('Visit site')} + + , + ]; + return ( + + ); + }) + .concat( + iraRebates.map((rebate, index) => ( + + )), + )} +
+ ); +}; type IncentiveGridProps = { heading: string; incentives: Incentive[]; + iraRebates: IRARebate[]; tabs: Project[]; selectedTab: Project; onTabSelected: (newSelection: Project) => void; @@ -372,7 +421,15 @@ type IncentiveGridProps = { const IncentiveGrid = forwardRef( ( - { heading, incentives, tabs, selectedTab, onTabSelected, emailSubmitter }, + { + heading, + incentives, + iraRebates, + tabs, + selectedTab, + onTabSelected, + emailSubmitter, + }, ref, ) => { return tabs.length > 0 ? ( @@ -385,8 +442,8 @@ const IncentiveGrid = forwardRef( selectedTab={selectedTab} onTabSelected={onTabSelected} /> - {incentives.length > 0 - ? renderCardCollection(incentives) + {incentives.length > 0 || iraRebates.length > 0 + ? renderCardCollection(incentives, iraRebates) : renderNoResults(emailSubmitter)}
) : null; @@ -419,7 +476,11 @@ export const StateIncentives: FC = ({ emailSubmitter, }) => { const { msg } = useTranslated(); - const allEligible = response.incentives.filter(i => i.eligible); + + // We're filtering out IRA rebates in favor of state-specific handling. + const allEligible = response.incentives + .filter(i => i.eligible) + .filter(i => !isIRARebate(i)); const incentivesByProject = Object.fromEntries( Object.entries(PROJECTS).map(([project, projectInfo]) => [ @@ -451,12 +512,19 @@ export const StateIncentives: FC = ({ const selectedIncentives = incentivesByProject[projectTab] ?? []; const selectedOtherIncentives = incentivesByProject[otherTab] ?? []; + const iraRebates = getRebatesFor(response, msg); + const selectedIraRebates = iraRebates.filter(r => r.project === projectTab); + const selectedOtherIraRebates = iraRebates.filter( + r => r.project === otherTab, + ); + return ( <> = ({ ref={secondResultsRef} heading={msg('Other incentives available to you')} incentives={selectedOtherIncentives} + iraRebates={selectedOtherIraRebates} tabs={otherProjects} selectedTab={otherTab} onTabSelected={setOtherTab} diff --git a/translations/es.xlf b/translations/es.xlf index d43b80f..09bac9c 100644 --- a/translations/es.xlf +++ b/translations/es.xlf @@ -591,6 +591,74 @@ Continue to see other incentives. Continúe para ver otros incentivos. + + + Discount off an electric panel + Descuento en un tablero eléctrico + + + Discount off an electric stove + Descuento en una estufa eléctrica + + + Discount off electric wiring + Descuento en cableado + + + Discount off a heat pump water heater + Descuento en un calentador de agua con bomba de calor + + + Discount off a heat pump + Descuento en una bomba de calor + + + Discount off a heat pump clothes dryer + Descuento en una secadora con bomba de calor + + + Discount off weatherization + Descuento en impermeabilización + + + However, rebates will be implemented differently in each state, so we cannot guarantee final amounts, eligibility, or timeline. + Sin embargo, los reembolsos se aplicarán de forma diferente en cada estado, de manera que no podemos garantizar los montos finales, la elegibilidad ni los plazos. + + + Federal Home Electrification and Appliance Rebates (HEAR) + Reembolsos para la Electrificación del Hogar y Electrodomésticos (HEAR) + + + https://homes.rewiringamerica.org/federal-incentives/home-electrification-appliance-rebates + https://homes.rewiringamerica.org/es/federal-incentives/home-electrification-appliance-rebates + + + Expected in 2024-2025 + Esperado en 2024-2025 + + + The federal guidelines allow for a discount of up to $. + Los lineamientos federales permiten un descuento de hasta $. + + + Rebate for efficiency retrofits + Reembolso por eficiencia + + + Federal Home Efficiency Rebates (HER) + Reembolsos por Eficiencia Doméstica (HER) + + + https://homes.rewiringamerica.org/federal-incentives/home-efficiency-rebates + https://homes.rewiringamerica.org/es/federal-incentives/home-efficiency-rebates + + + Expected in 2025 + Esperado en 2025 + + + The federal guidelines allow for a rebate of up to $, based on the modeled energy savings or measured energy savings achieved by the retrofit. + Los lineamientos federals permiten un reembolso de hasta $, basado en el ahorro de energía simulado o en el ahorro de energía medido logrado por el reacondicionamiento.