From 90de1fc976bee9754dc20305dd0062852f3e1c43 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Wed, 10 Jun 2026 08:48:15 +0000 Subject: [PATCH] fix(elmhurst-mapper): map "Bottled gas" main fuel to bottled LPG, not mains gas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An LPG-boiler dwelling on the Summary → from_elmhurst_site_notes path mapped to main_fuel_type=26 (mains gas), making it indistinguishable from a mains-gas boiler downstream — wrong Table 12/32 cost / CO2 / PE (bottled LPG is ~10.30 p/kWh vs mains gas 3.48), and it defeats any "non-gas → gas only with a mains-gas connection" gate (an LPG dwelling looks already-gas). Root cause: the recommendation worksheets lodge the boiler carrier as §15.0 "Water Heating Fuel Type: Bottled gas" (§14.0 carries only SAP code 115, a Table 4b gas-family row, + "Main gas: Yes" in §14.2 — a mains-gas CONNECTION, not the heating fuel). "Bottled gas" was absent from `_ELMHURST_MAIN_FUEL_TO_SAP10`, so the §15.0 fuel resolved to None and `_elmhurst_gas_boiler_main_fuel` fell through priority-1 to the mains-gas meter flag → 26. Map "Bottled gas" → 3 (bottled LPG MAIN heating): code 3 routes via `API_FUEL_TO_TABLE_32`/`API_FUEL_TO_TABLE_12` → Table-code 3 (10.30 / 9.46 p/kWh). NOT the legacy "LPG bottled": 5 entry — API code 5 = anthracite, and `canonical_fuel_code` resolves the same-valued Table-32 code 5 to anthracite (3.64 p/kWh), so a 5 here mis-prices the dwelling as cheap solid fuel (verified: a 5 mapping moved SAP the WRONG way, 42.33 → 45.11; code 3 moves it to -6.40 vs the worksheet's -6.6499). Also add 3 to `_GAS_LPG_MAIN_FUEL_CODES` so the §15.0-lodged bottled-LPG water fuel is adopted as the boiler's space-heating carrier (priority 1) instead of the meter flag. Effect: main_fuel_type=3 (bottled LPG) and water_heating_fuel=3 (was None). Mains-gas certs still → 26 (full regression suite green bar the 3 pre-existing unrelated fails); the MissingMainFuelType tripwire still fires for genuinely-undeterminable carriers. Spec: SAP 10.2 Table 12 / RdSAP 10 Table 32 (PDF p.95) — bottled LPG main heating fuel code 3. Co-Authored-By: Claude Opus 4.8 --- .../fixtures/Summary_001431_lpg_boiler.pdf | Bin 0 -> 78774 bytes .../tests/test_summary_pdf_mapper_chain.py | 22 ++++++++++++++++++ datatypes/epc/domain/mapper.py | 18 +++++++++++++- 3 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 backend/documents_parser/tests/fixtures/Summary_001431_lpg_boiler.pdf diff --git a/backend/documents_parser/tests/fixtures/Summary_001431_lpg_boiler.pdf b/backend/documents_parser/tests/fixtures/Summary_001431_lpg_boiler.pdf new file mode 100644 index 0000000000000000000000000000000000000000..2b7a72872eff91ad1e1c95760ba210dddb1b713e GIT binary patch literal 78774 zcmeF)1ymeQqA2_b5+p!^B?M3K;2zxFWr7Xv4ue~O;O;I79%O(7cXto&7Th)HH~d%b z-QDlo@9nlz!{6End!D)RCgS{cBCAV0kOPeK0(LN8?PU5^ zuy`2gMNKUrhIaI#7P=5aVMBdu14DXgLn~ug#H?&g9K5{t5IaL%OXQD!ds;~L%3{%X zSkolBgvr+xY zmx*z+g7Jn7u=={9%EXSV$0MdQ5x=K=u*F2$FiTegD&|ZMjpH`_5(k%vg!p zx{egB2|qL-L2!dkq1GMB8YN!8hsTL;^+^!-;Kwt?AS`fD{oXfDeAWIYa8wQCG^I~0 z`<~Y8=YD&<5b-C59P}9jMXlOdp8!lL`t^XOMjJyHk5x<1#InuO`pgWX%Wozx|Bbd4 z=Yp zM|~Yk8m<(%9>-$(vw2O|;6X2_^-ULLm2gDuWjn>y75wB|H0!!@$DXW(FABVAN7*p zKCJ&QTtzLkOa3yYY-;So3DVw0W;5^D)b85!! z8;MW+ZPfIV(@Pslo+*_PDig?dmDjj6jk}zaU~c}p6Vv5s{f9{#(1CjjB)16JBf3MK ziofgjsug=(>|WEM?Y{V|xS0N%wHn)(+^i4>vb2{LpU|^2d3*UI(1&odC-310ilOYI zr~UN5sqiGBa5!qq*ktZkKQuPb54YJDzt#IZJ9f!w%%cw@XSqN;c|kD0z=rL>m&rwS zxDDDm4jx_H!E;>krf_O091Yk@bR~V0LeprKS^pNJsZllGB`Er1(Q81 z@bgUzuDPEF-;9Tzw-taDy-ug$ih-J%)_WMJ6#M{ABidI z?U9hXZ*LC%A~k*G4k>$9J2twl&QjU%I%JuMVfyr(kkhFLEaJk!vU2ai*Qjh@T~t+=4g<8AB~0KxsMy3q`xNiJelZMvOCBgKYw8isIMts6~t^* zr1B&llqh_|b>w+(U)fXorF`lhZbaceU7OS^yuu{-qZBt((JwxQMVjXX3MwBbl>)P# zGJR`Zw3^6!PlWUQr-eZ!F8wR3Ui36;Gfx)oZ`_~Djb}T5ILFOtOs5!MdqGH2x5srN z5i>Em*&ahiSFsOOv|{d7vc8O1{4hd~uh$!B{*6uJ+U4Z*w1Z=8fHEJ^xdha#I4+$6 zYGw}`xH?d+aGGK7LO_Pf#%X?^`!1dWDyq>M8!^=0C{80Vv0V+3lCN)zQZdB(c)We$ z8-+u)uE+K0j8jdyq9fg-xUis`2$H6ihlhx`J3nEJeJ@Ao4wkr zwyaosL5a?_oObW8r?3(?(W3jUo)ucMv(7J7Wzoo!c}~;xr0$ceT7o|8Qalx#galnV z(rQ?iFDrk;1U^LmCX#74_ABnCda|PJj{bdBEQ4aMupNJu2Lvp@H>r(|chHYeo2zh@ z8FrVgG^l7~)Jn5-xn6j6VB}O0pq@54g@4-d!=812m5G%~%r{ z1b)-g>B=P5uf>9N2ac)r7L-gXzgRTg6)0 zU4;!teYC^0Z@@*)saw1uK_GtJJ|QK~Q|2|%Uj(=Fo^UKty+jp#ucrzaa*`8}pt;&~ zJ5A3}NPSg}Ft?5*iPA}|X}kxxYfgIusO8Vx$a4{lHa>`U-B*>6JO^8Ltx_RPU;f~; zeEY&qf?j?a9bSmHQ8{}}qUKRL67=rCF$PSNFSJ#`xbv)bAzYTN2XU;Wl229n`|qQ% zD+FW~M=j9NhRP_HNkX)`xb`@!Kq=hb*Ac%`a4gtftE}tifwtXD{dPvW;hR7b{2vaS zPx=abD~T9AbgC81h+g|Ln7)2>Je|$9=O*Z7baK5rq!-PjVn>Ssjb1QIL?>B?l;>;a z&#_Tf-}6&F9;;2Kt#7qBz0Nq5iU3`pUUQgu#yijH9aiyja*yIf8<6$8qqs%A9)ustHR{CGMjIRqVc~9KU|>dk zqsAvhqj+HO0r89d=b}S1#`0dA!4ieH)6$~soY-_^&}g@vlB1Q0u6S0WR2f^+7cq6m zy-~jLnOnXyStL4m%MH^yXs{_#J!}@g?$J?{|N^geVKDHc#CY|L%W9 zHpZH9M*9mZV<_qQ&0JT)&x+owblwa$BR`NgT1ew-gVsV6nTuN#qGXNX#d1DWC{YsZ z?k(4(&Nlso#9AXPZDvz5CmL2v*iX+VA&{+tBh!296BV@U)KfQ&3-X0WAHiQ1br`*| zV|7g4rco7++Fu>Cml+f8S}rSEvN8$Lwr=#c#grlOG}#5y@BJu!sk;+{1U+1qE*w61 zvO5Xwys$o^G@Y#{$)RbmP5?D`s_q8rF41u6E*tqNleE2pyN*5noxR;Sx%{r&Rz#zX)0-jVjj<99)uMD8gSgwP-p_hX22o7j z)Vp*V{?~&COizx2=RKb%$#H!*{;|*drnKNyooE!Td2)juRP)SdKA9HmcfL<>C7>|o^WS{%NvGNht^TxPC)?j(D^(b;r5=^*m+NYcMV z_N|ASeaJw+q<^$>m*kgi{BbnxBxmf1E~*o4=;ApeDV%h`%!OI1Xc_Bf1y9AqTf zx@h2ce;4v$#cdI^rZ45Rr*C|Q-?K-S+EBS?(YN!j*Vp>q*EceCE?33y66xm6L~VhA z%h@Vk6^O9==*eM3t!M*A2a8R;V)OoS0IE}f%te;aLZ!uZ_p7z1zjBWb6;<5ws3`gh z*B#Ej=BI~y+P~J8hZkjbT)Zg zPjbRXY-{)@aJ7deA|G)H(n~h#?m2gSwOoV0&iOh!>-wvL(n~o)M!qL{u9DRneSbf9 zPg#bO>v=JF$o|I8)-^$sVP`9+rv!JA9sIj4^Tj9GyAhGj%B$LCx)eUbh*?dNb_0QH z?W=kI42O}pFd0?e#L>JC>?n%yTZZ)wI#cGpE5= zmFbKPZ~wPVvSgV%?j;<4ZwtNZ)P_9GZ+=)Lv`qLmhV{qF;>0Bo)NAEDMSu_GJ-2HR z$4yR_K}0pSEF!?cGS6J-L_y)Q){C% zIl`v~U7H-=0v2#Qd=@b1OGK(}pxjH)qAUj%$%~|O0dS+8zTY~wUyQgNI4E4}{oE10 zZKgx*$wxAi_ea6?5rSa~OhlYcNCOEc8O8H;9{N{}`P^DKn3q4Q1q<>H&%EzFwY<6@ z^hjRgS!ABhjl?;45w%EnXJzwD? zy9g9zE`DuOu(#v#wGFt>`HG`Tigdt?#OGQ5^Xs;N=!kY7VMP|->Jdk<&kd?+3x5x8 zBsz3B>^Me=Y?2pRM0R#<0_GLX5Bwm|f?~zJC9pwJe{BMBP^iYia=N)!n0)3v$97Db zgX#IA0`c>UIIQqrYs+%=c&TYw!)}oqQ3I5$sdRBWRhL@`_#cGju~@|-{Ic@{;a0}e zl}v`d?87eP5)uet6SH$V%K5yK&JE`m;1Aml1h;gCS(&D}A%dr0&g#6^B2~5HzNTc~ z7d5?2FRdr7@rI-N7Hd7D7nkpCv}wLg@^*yeU51xajxu-l!o)oPNX5kv^|!bU7EuwDQW_eSRkq97uSh1kBCBvbuftKGi z`)B=}ZCW!KG}5kh5RGX>ESilnl5|`V6ZJ*Vz3RCTWqCSiWcG`%Ra!R{{;W+clokc& z#j(-FlX`e-c|@)yRwkzGN1s@WrIWluaXB^a1Nq+Ls${>hz0Ze7(*@NTtc-zM}O;hXH^ zzzqg%Lb3PIO*_VGj~>hUZh_lWe#NhXtJp<+l{xQj44se#TtrS-uZtPudy4q!1>*F^ zkNE4qmUW&}dl1xsfPaVTqsOL99fnhq+(|5`myruFnE`0O6-isL$U`O zEVyh(6wNH;!ArV6{d4wRnx{h}zo&S7;O?gc{NZAIUd%vH{5AMJwELDmrD=8*+dL!L zd0hV^Zcd~q6V!d;wMn%HX+av82b#29aD(BpBmW*V*i(OZO%M{ZS=?(P@|%C?4w|n2I+dH z;;NnctGud}cF_Qmp&!9fdOb8x2A0ZUv2{p~{dh9k@6kHgOZ+SG-IrFH72h8H2sBKn zIBAj~Ma+on&9TZlr;PjFTeq3(Lw62?a6-XFl$W!)(E=MSsmI^uG)i;{CL<3)sjKiS zDvB|jZ_?}airol=Eu@ARykQT3phxX2+vu3k=*Bg#T^F|N`M85qzLrehkyEE1=;{DC8x%DgAtYQNIT6b>P*fBTFv>@G$_s(Wp|(+ zr=;GyzSAh?iSL%m#B*5v`!fP6ohl+fd+$svI<(!jx*U#Vg6AVRDr?EzTCLv;myke& zE`;R#i-^43S8^;?Bs2d_jVlp%==gxl5d#^t)v+EKaV>5toJ1k@(2?4d@o~~fh8WsJ zZ+qcuYwt~Iweh)QWC@B>W(n?&Q4;d32DhjP#WEBE@k&SVDi3APPc0oEo2B>fbmhTh zWOAQ2=~kp$l{!qZmCwU>+6uFO+@W(liGOhwHbJISBw#xV!j4YgCX-emqBn*MRZ|}Y zz5JYB&P&g25i&h2W}Sz=+(zTV*LSa(F<7|P*ii6phVvG7KUj{NBKqlVo0s5A|8*m_ z;Vk!*H^4fqK%V}$uxHR5#a(?Xcg_iNGv%-pn#;1Lp<7D^NP-?5y5b*hfvL`Fs10jc z(Btc);Awd#l-18=_wdNV;w#h>a75~3C6qn%S};&=DIy@jIc6Ycs+>ac2Z8?d4?*)Q z8eY0=)QO-i!iv^MvG)5sM@SCI;`}d)w>Etc;Awiq))#`h6fh|d;~$faAYl+g zBA@>F>Ey`o+cM2LMoR5Ev;jq-iMm?gjYW+isu-H%v|bbFR=v|;wwbFSy%Ri#rzsk!^wH*vA#CnqTqS5JGBH`aL-Ly zkoEfEf=}#81RV}GOO;?17Nf_Zrs{ZRV{4_KgAh)Mr)x`#07C!cM4eJOKgLVL0or+F zOs>4)wdr~A-&Jq|=9cdti=)*&LWVVQ|G63X&yC$I|6cPntfl&2Yo2EOC(YCBEUXNF z(>#p`c{3R|o^ClED^u=P&l%)2*}x^6Y7h}KrMSys6+K7R7f-9^*KmQXTI!^LRfk+4 zSjJxJ-GKS-V>7}_I4*(G!0f5I(u7w?j~}Bx8^sA>xw!EaLc8m~XSTn0ySPP&3VZ^O zj72G*$?yG-3$4CENjVxJh0I)9D9Gq|9JkYIJgO)(t6W+ykkQdp6NH~W6)LkdlbENd zqmXi}{eVS?F_&J?!}^Z;AgH$Go+aJOhZU>5zP5BM?rZ4=BNG!759!J&&fMl@t@u)G zGhfoUIa}j!#n{=`(txtkQU(qVjt@+{Of2c`%!8X_eLGcBd!v$T87p$X_8@zdyDHMM zvdC~g%gbMKY!%ps*pn}8qduk2iuLU>hV5|-64q3YVP}_8Fn;(zjfET#5U|lH{G10K zSA&n55Mp$$wsAha!d$YN^7Zj#2lh7gs0{9Pbe{m!}SumHT?u2py^gjWV7q zTJh1Aov^G;oQ@95g5q1#TTV}ZOd*x%nmC&{2)3J?JTt#FpUy;Vp?PQXK1YhDSYu?5oYAUfEJa6ykx4~f@sP!d`rDC)e68&KaZGi0n5=XV`SOj||6?`E$hLg$C#n78bNHEJ(||#&kExviW5e27%PWs#+>+dCv_bhR75q4?fo=yDbDrR z1?gWaO614QMXq)6DY6S2_Pu@DMO^iP)`Y?OsT$~6!|LyC@*KV~s+mcq_=?-AN1(>i zQk#uI5B7xNuO}xbQc_YEb(sy}>+9?BzkaE+)isuc4{a>gI-ry-U*+3CY&Eeyx&?x- zu`b`rQBRfmJQkZw)*hqIV4%G&c)C+Cy>6M-hN|d?5a3+v78)M9a8O>YSyGXsBB*(D zOJ@SrcQir>HCwZZ+%_}U&`{5;(5V&`70uStCE;r=-OXTj>o>Ssh@uW~Z%aW>O1Sp{ z-;z${s8t^zU$i?eJaL9sR8#x-_18t+dv3DltY3dpOO7&7bTC!)4#Ly}hU_ivNX!DYTmShJD{jVDast`ar16cP8el@53Zt zN^@(~TMKh@$q$kJLnar{ido5VPCnD1nsXSboMreumHq6BSQiWrapq4*PRMN3DS8GT zF$XizT-Utasi|2H+N*&N2K6fRmGHQ2LlpH!9dVFr8pA;sFZ0Lee!jza*3N7Ds-!gg zq+E2Q5w4N^Czm+9V~Zm^Ya{LXC;AuJq|s6XP*&t~vSXRb4ID}^6(Xu%V+r9~S`RLi zy{-NDbKg-U-WLURg(#AqRx7eik=81A`PyyfhOHHhX}u&(JA2w1vG=V7SDdDbL1Q65 zdb@Ng5R5oxZBVeFvHD`pey=cEG6lgTFZ3G2`uv!&WJ2iJJqxw3BIFNP+cw7Po?BAi zPnK*;1R=ss81Mwu4DS5-o>RdaI11{>)L5+7gfHw!F0aPT{owsDG0XSkLP4eX-F(dU zPAe>Pu8yh`>mdm?6;p{RaoBN8ia4Obp}vU5m)I|GJXdGVG>z!+Vx`rN>$kVJdvy#n z!-5;JTo`{{g=vy2-4*Tc?}whEx>MIopI4suhe|JJ%G+i8PI}XzBO?TPQBY7wNl9V^ zAJ4c%b3ZGzvrnA-3jI2m&)GdTGfMs2(cJ=d1f(nAol_$+H!w9471g+~Aj_-G!I7U$ zzKAk2JJXEM1eS0o4HzqBc&~T1C{MHClyxgS9!V4$?a<*RB-0ZUEPk#cmmGiRHl%zu zL5(MjV3VwZ7vzA1DQ#;%udomr48 z-~_RAwD~zcIx;p978;nC(ZqHB*{!{e&+RJo>A+x|te6o2ttERc&4SMDWpNRY=KQXyqxv>pezRXQakr6yc%a`9*LfAH3hKkcDn>0(IT7G zZ2u{0_r%!zQs2SUs#r;GEp-C2MOr>xwbsqj+7jPOC7PP`N{AZmeK4Hy7ux zSUqYCDf!I2adURA>dB4aR$7*6YtQ;|yCcrwg9?kkm|B|pC8A9f6xv&!KdbY%%q$G3 zLpajul{a?(N?w}UjY>$0{$4}Cituu4ZOM6kbUnEIc%}90ZvXqrpJR1IAJxF*99gNL z3wJ$z<2DbOZ>{ZJWro|_tgN=q z-kFv`LeDS`!A=^Cr(Mf~!{W;*1|?6?K7Gx69MJaIYf0FENm@o00Y1H}Mkj5J*x69m z3=K`Q#(lcGxm2peUr9*`cMx&8uzZxMr-k;Ux4#`rj1CU|i+A_4Ps2+X`cK>NIQ^Sz zF3!&d1;xXazkkzhf3<~5mgCjb{N}N4=8p2RiqhM>CYNLdB9qRmDEqUMUWp1Ls9DB^ zsmm|+_@Unv;6>%)X2eTkRTXSC|NcP(H5&;9p*4dxKmGLne-$LH%pQRd)jb;4ytX|yhTTI$FKG|U}347&IhDwk|P8&9H zxen@%t`2X8$dAsd&ve%?i!i{`6`Lk-*JFB!Vx2S`NW`V871 zYUtwUeAww=7;I}^FDWB2J~iBgRs!|L#{(&xrq!Yd7Kc8G=n zf51hl-ptZsmDK^Ubg{zV7O|jg+gY0HE{t7T){~#_*!v`k-4eZF^{{8DS~BMOOU#!? z3^X*<=_{e*3A$no~nW zcE5KmQ=t6R*%xWN_Z7<-D0mXu87vrZrsigh-|dx~LU@`BZ=PUfe+C5}_&gcXNLHg< z!8X-=?|XD)TZ$6?#v%UvF0e>t*Vs^$7hRxcpRz_(1%~Lh1 z+*SJRuScQd0#_SwP*@RgQQbiQGM%7YAm^c#R`x}~+2oaQU2s(?qO*$pBY)y|BwT)D z&E6MPdD9-a;1}IP1I%m;$j{ND@CynGM#e_wfBi(2}^1L&d?t8H9%$ zCP;Ilg;Mv@EX`rAV0O#a*zg7#>F=y9UCz6ZVIga!ZV{I3W3D=7bx*{rzG4(tWzzNH zd-Q>fjQp2@ah6QW3tu+sQuAWpQ?uem3*~OtmWYd~q|K`AFzhH#<731uP0CMhVT;OHE!`K)VU7gr8F>r3* zidkGST`lYD@-?XUq4pOJco&oOmeJ*!Z8y5o-)Q?vEGj)YH?MeSeK}}S48pN^z)H@L zvSn)e+r>q=*N~~Dq<(HL_}FeZB~)uKOe-wx`95n?mEogMac6s*pG{oM8hz+<_2pXjTIn#uQ!Nk6=d?bYR1#Da5nf7K9nq=LsfjmdWve+I z$c|hA#o@Jf@L^9{k#;2|WLvb}byDzuG0iU!N^A9(+R)b064_}Ul-`#M1J>Ys?w%Br`g`$_E*|xhkoh}4n{=~N(Kr0jFV2Os4EJ-A8(1N!DTCvDC+me zV4jFT(qErFzhqxN*k|V~{1Uw%H=7jf~Hr&`*WTCmo?mj7Lu&9cA*N}ZLcKxa< ztx*b@>(lYEoxieJd~gAE9&NecK;@QpL^{_^{JO1QHfgSoR&P_KEg2=Z&E~-Y=p}aa zad1x?U^g$k}5}%t6yb)iuLwVHmNrynU&b^h9Ys>Iq|~uSGwwyuWNyqZGVL=;+tKtmM{Dyj{^QZc?P2zkn5rNKc4`>sSh|gOgK$ z|L2!GST`QGo3exAIb`KA4I(Q&@ky^=w=dd%{~m-CBzqoCCp!ox1aovu%#iy=eaR+v zlnH}YMSRb~+sG1+Lj5Gt_mB__mwN5&7K&jVN@-Kgp_+iK>UHcILw*6uh&&`?*JO_=Dq) zs;zF+mvm2OlSJuXODHYv0*S0BO7^8+OG>OD5G)GWP&s84)vc{faAUsCA$-DfR1RZ3 zli0L49?Dm#+KJlNW!x<~nifUzwFrcMkYB%lyGYrGsGrZopXri(5k>G%3Jnbo4)*r0 z^Y}GhWM=uiIPmZpZw5M!jEwAaG&CGs>=e9|cFiV|R~VnH+n1{4DSwVl6hIesX1wH= ze%s$YPVs=$VWj%|XScTxBo=9^%DO2h6=?M}5u$v!%+0Y-ekKKP>yV>kFV$ex*}l26 zNu}8ihpTU-7d-jSEj3oQ@3emAw`pGN8G;8kNWJ)kj8?t7e7fq z3O|bV^q zXE;1Ms+Vm&HG6WQ#$sD$J6Qr287qlY51{is*?1BqcGVrW!@fis*DooEXpL zR<*Sf0TVK3t{dJKZ7h_3prb9*DO!SK6$)ijdCZOrdTsv>o!e#$DN-#d3HG>1^=?NF zDLXeiJ)s=EO{mSNGxzygRxRn#5ZRQvnmyjWI_InWz)~A~Oy=KP7qCZIjuLmFl(lRF z*kcS5^bryL!DTgvU(#}<=|p9RNVfThbJF2zj5jNW1){|_l1BEz0|`b&8H9GXny$y= zBv-g{=1q8@l3jYp4&$!MWJb+~un0Sl!E= z(mq@{vs<>v7qta~D90$fmZs@e`&JNKCtQ#p5_LyM&uiTT14&efTkKpX0n;OBwmyw; zGK9idQ$|AIvUg#Ej^<>4a5?j_EEV62317syB5 zba`~Dc^F&zd%Ty`cNB?@LAqdDTPs7GF;AXft6mjqI~^Z3-A`flnuT&MZl;==2B-p# zGULJ$?8^EM$tr{|CrtsO9D8b^>le9kC$fAWGt8U)ZoQ%dS zw-^c+Jb&Y0$@A*doX;|;>OQ3b2>Uzhm#Lw$-xOUw3toN4DUaeZv+2y9k;~(fp3ghBf?cYcYASB=mf)npf^P zXCXG4sh-;{Dq3q7-gK#XoJMU#*p_kKpOn&cAM!hhf<-%sibRVp4~}_^SrsR~HzBo8 zAEn;R%z}lD1&{cEH#*=YnAMIWx0Bpg@?uX~PO6GUV{(Z2u^_g@BCoI(`ES*C=+v3+ zu+}nU6lBGi7$qe|oyPs=n`;TooKdN5t}YRcnP>4AFfe|6!D@zq^OB0)MoXK8V z^HVp_ISWxP$BEDF<=W&2Hve41((9=ZnGSZ2i;F_~5Y)cIv;^1HRdp7iDFM3t)GoU+3 z`3qZKMJ=Zw*OrmVPFPa-?Ch)sr-h+)Vy)F24U(k@a@!H0^F$0qhxQO<>!jIFbi<_JJf~cTV{~yPjTU!V` z7R5!j1F#101;X@0bK!gr$DQ~PW-W%yM39h>P$z=_n}rUcklKPJcEO7dEGxY%5;Fd_ zfH}YUXUNpsulIP{X)Z*Z!N5l1&JB2XM0qaeT_S!x!(<-R8)OIKtQmBt%i$S z_^r36#n#FZQ{y?eh4_J8rlEV|SjCh>cW=MXF2yy@0yD{73qHn8BZHe5hPo~zBU#~R zBBS@TwA0hWqY`Y6OOd~548tmz^Z?qrZ z$js83M|)90B<&E3Qe5BBu`*_HdS-OA(=z6UBbJ-IlY#?ZHe!^BEc^eh1M(lbB>nFZ zNs}@@|6}LvgE;!HutiM&r1KWAMSv~(Kgx{&TLjo5z!m|v2(U$fEdp#2V2c1-1lS_L z76G;hutk6^0&EdrivU{$*do9d{oiPd*#5od>3`W4G5?e1X}}f%wg|9AfGq-S5nzh| zTLjo5z!m|v2(U$fEdp#2V2c1-1lS_L76G;hutk6^0&EdrivU{$*do9d0k#ORMSv~( zA8(5uhB5lDuthBYr1KWAMSv{=Y!NVS5io9%Juq$&Fm4erZV@nU5io8MFm4erZV@nU z(ck^T|HE5g+#+DyB4FGiVB8{L+#+DyB4FGiVBDhr*5ej&{CmyQ|FSJ&{U^=SfGq-S z5nzh|TLjo5z!m|v2(U$fEdp#2V2c1-1lS_L76G;hutk6^0&EdrivU{$*do9d0k#OR zMSv{=Y!P6K09*7w-WGBGd!4t;Ec8Ow7S?u3HoE$T^umUYruv48VuJL7rVx91LpvdB zOB-t|Ln{a|2feJWr6Iip+dt{N1#A&uivU{$*do9d0k-HXV2c1-1lS_L76G;hutk6^ z0&Ee|LGOKW2^ey@8iiZ5qs|@cwguQCz!m|v2(U$fEdp#2V2l1++agAWf3JP|U&cl3 z|D=5yz(oKq0&o$4ivU~%;35DQ0k{ajMF1`Wa1nru09*v%A^;ZwxCp>S04@S>5rB&T zTm;}E02cwc2*5=EE&^~7fQ$ae<03}Jf3NrUU)Dt&|D^X8&_#eQ0(23eivV2&=psND zfdO3v=psND0lEm#MSv~>bP=G7Jj;K6-4+lX(e5Lx$l_Z);t2KubP=G709^#=B0v`b zy6C^RE@Jxk+Nb|zUBvlM+NS|s1n43_7Xi8m&_#eQ0(23eivV2&=psND0lEm#MSv~> zbP=G709^#=B0v`bx(LukfGz@b5ul3zT?FVNKoHqYGn2m#jm)9O*XQ*q5e0O_ycXx|>b9=LW zwmP=m*1A;KyqMd!UbTEUadCZi2HQvso6fIJ+LueB+U=}Mlt5`hDyjUsIg+324N52D zex1zU-Co|2pI#kR&!*|;iHk;Yibs7=N#O;h@+l_qh(~e4_6Tb=+c;UcyT7};r@Fei z{63MclPxY1!LE|Rr=2BalrL&hAZA=3rkgFKl*B6)!xi0bzjeBDckeZ~GwS?ZODuvz zDTznxiy$nd6;#r$T-vrw%H@ZKZJ`fk@?KKFMefP#V8(wy;Hs zgmaagd#$2Jt?J-f>-Nd7@CHxWI4-XWgRPS_*YA3=@m$ieADl}KgX`_Bp;E5Z3IVO) z+PSRjo8LD#*TX9V3JD}U#<{XXYaKdSV$!i(T4^#*xx;XuP{Of4>>gHB7Fp?Ji-<*W@&&WW#fZzt zio!m4(Fk^-FgDRh4xMaawG`>D`HqLLk$m#kq;9&FUXIwubOBg?J_Imx1u*jlvnnL= z=;eya#q*a8l>EN_{SfvoESs~Nvz9rgA5+D(vqZJCMYOVnb@Rm3(?#?$Gzxn1VF9mU z0UrY6Z6EJI`=K5sZtBS@Dv5Gt*+$9D$rI}nut+ZNkqxa3{#RP-Pw6-vy7q!qewkKw0 z`eUP@ptZ9GEeks*F)ceg3o#=L3j;AD0|T2DFYjN@Teg2pfnLc$58`5DNUv5)h6FUd-!^F(WO3cd2_Q%A*!AZ=<#!k%0$w1VbwvLs9;AIe zU-tR`oBuUDERKhG{+M9tF)}c+{*l;2g6vF8e@qX{Y%qb$4$IiXx`&-v85sTu_mB_= zECYXpg{}W%zdwij>wd6(9+o*-SpFLKLwtY9#KWA04R%1VxE>DhubGFX@t5U4(){xW z!t{^_*z}NxhxtSLe+l=O`5*B-Y=6k>!+x;khde&a|2%2^Q6B!I>=45aoq<^KKgt37 zKbC|42>o{@?O!fr9RC>k|5qVn{JTO%|JU;LzgWK5IDzu@UthjpWf^vYvN8NIe>lM& zeu4de@83gd{+l!8FJ}ZSm4{OPu#KIW{U1yALs9-uCHwEjDwWqYHl&x=wR;$5^x+RV z73pOR4NP?(u9Oe?W#we0V`7Iz&%we-2fG?^Xu&G}RuEWddt#=C0E+aAhW6GDcKU|) z+}wYh7!Nbpb@ky&sz@)&NX+<9++i7@|0t(tW~dK=ZI^(3Fn?^8U?hI{Y+zd-UVpv_ z{}C^&7WEL3sRhIkc4fEFg&6+r^8zCWE5lzO7(V)?sma*Sa=dKV0WBpK;Ei39iVqr^ z-LM{&>>bS{59k`BmBLA+%71>Uc(i%3YkI%VCLH|PLc*dh`rA8HaFZ3w)t5JSY>Kg$ z+R>t^&m^PYhmW#I^%%Ig%JldV+?tCoQY7br?CLH0!f*?b8-U7@k;QOP?9)bIPq^sCTNtu7~55u_Lc?l-nA zq=T3?^b({kRPL#su9~z7GsQg)oZqbqcIM*wLhtdk!Y}Y78wlixLi|1TTRhm!cRW7? z>OT+f(OUAz-9*ZX)qO^GVB_R!YGd$tlO*H)kCbQ)tx1##g4;@Fd@rX};SJIxkXOLm%L`TiU=M*p)$9-d`s;>70Av{3=2|sg3exj4{ zgxe*{A51%u?2aGUrL<+yf6Y=m83n&Zm!4BY%@g-6P6zR`i&3yQgX2#G6O>Tj zqCQs*}98b|Hde);QU_7}qYN1w%=U*EP7gi}A#@hx=h}VVL%==PX+IKYLli5#meCtQ;hk;#8y;5Sk0q(;I9099s%UCN-W7ge(JF8}EoTIvSoMA^g|S zjHq32Z;idi5??tJOl>5po3|)?>R~ttk6z@G`214&;~?^nIL} zTzyl+JHr8&LNPu87ecw!|MO!G3M%S|VfA>it*leLc2uFDzN;lk?d=+d0<#9bQY1qn zVF*&L_U%Uv;|a0O5{7(2HjZ*<9J;DLzMZ`K2{}I!^a;8s3m#3qsl?5x(DSN^0C6`O z-)vsPSnC87@ua4$?rCcJ@O;pcEqdd)%QS&*YzEJHYN_-vW_-_{6xeq&&mlcGrK6>^ z@s@?7@FcrXdFySzzy{mUheW*fZ-qW+9P*j8iDX=xJLPO8KjHnE!xyUxdEQcflYEw7 z+nxzO_0vkX_>E}!GuMS!n|$<+%U@)-xW=c%82cVb)RG*a7g>rfOJ4DW2$Sm3k+$oT ztsB#M18?yyo19Pc_z*L4mRzxY%F_bfsZgJeO+bk=hsZPN5e!37UQ4_V0g*hvPA;cB zNl@G@gzDSr7q);z~_qFS>xk#b8!2Tmq&2Yy?eU-ai7BzdMmR=@XDV; z;j6muo^JFG`&KG31MF~3LBPeHM3|19Ttl4WhP>a|w>kxmsYd$0U8Ezu-H3_Xoq488p# z;T5~&<;oJYX~2jRb*Cx8<$2zz*zw00uPG5V0eaASj+e|Mk^8H+HEQFJoXbkU&3vdj z_1ofg7Bvb#lKTQfYsLjR6$@S{-YxC zw-to{5GVhng24GV6@(mh4X0VQm!6Z*=DzdY^cbdUjQZ1oNfwRCGf_vk^QdUzkT=0L z61~CK5m)q1{Az2>Oz`rYE`dTPDb@_ki>Ul-LlVM4zcEKYVG2m3_7Y2se(s+n;K}{mcF~%K!52j>v;SBP2Wd~?ZO-;Y(@A6956`OUd7#O_ifH65)KJkms zJKUqzwrKrvr*?*n-t|@+mx1nQ2*W=MJnHfKxg34R~KiL5LE4dc792 ziUkt{@O4kkPYBK2JKi|-`L1O{0>|w#@3~0Fl0k=93dm&>5y=B zCzpAEcZf@~7SLW>rq>;c#(uv=KE|(1A@!G8EFeFD3^v4)3t+CN!UN#y&v^qS_bsw zB4bGBw3`4hco|$+~5p#t=0v_Et z5<=YU$cbrST7yx%k4*jelU{gZ`Kgf2R7VUMYLwS&s6aG*RFQ}I3h9-ZDdwgJv~q30 z#{}!!td9lhCU{ug=n|i>-(a0BAlTD38bJXw1#H1AV(e7iOH;_G5DSE7I3|A}IyQ=< z@5+dLo%Xam+-wKLQimM%_F~?7Ksyx~rX5X!)FJe}35bzA;oP|;O&{~wb_a;|84LoB zFUd2bcYvMv9rGfBmxzFt_GNL9ri_QG%3Crbl^n7u=|P@8!5H!xo&gKR>a*);fY{rH zoA;1r2jEsW|BgA2bEV*o7pPHRhIuBBGm@o%lX4}8bF__qAc#6rr{`(RK^_@#%UiOU zsN6TFp(NU?LXLig4n4LlvW2g;^qG4E`A(VlAMw4y?r|PG8kBMKt)+9>UVH!~o5jIN z<_KK9en2KeZ&T98pf##*OLdP3{aV1*pF(EIOHDqo#35*C1d(g_f=M^uvyaDsSRye6 zH$_j&ST1j`IEaR|vtH+5fAwd`(fdx}J!-v&{rC#2La@^Z8lSh^@sst+43^nj^*8#z z&|407(7UA~60UTle{0!IY-iQdim~F-!naR&i3@KZ2^U3QM>;k~Kv8JBt>PM-BkPur zl8=LsZM#KN%S{wsyxlFm z`w5I5-sx9}1Ii)7GDkiPzd2LY3{tl}mjKXOdYhMuAkr=8sq~TZA2`yoqA&(mxe-VV#Bch)gad` zBUgB#qWJUL20O-^f$`A}QIUK>IW@gT7~R~Z-r-V1Ys_bib-hzRS@^r1f02ZoQzieKB;*f*;Xh(2B!Pc$Ct^ql@P9ds zzp@m6N<9Bh5+Z>>{Dq|`HPE;>qfC3oQf%|-A_ogoS$tmTfKx(lB+;$}w`&hs__`P~)dR&pPE; z&*mdKFg+x2SgkecT4ef0r++850f*`K_XZi=tpCo#tx!?}Vy_5HZED)<*LW^)z#`ARSwGp$3`AEG%-8YPzVaVtm#?l~Yypp~a_iN;So#>N1Pl>?FUf zdX~k4yiNO6W2fM4bgyejLd3R5FOIit-)BhFGdM+|oG&TRxzOTU65rdDs_}z)GWs`C z6jrN<2l_2FUMb?y`w-Evv;^*74Gyw%g0&&`j-$2}w}8J$qA-?}Zu`9{dBh&7S%!Dntv(&zfsk@@5V#K4#Ri#!>sfXzzkbZ=XAag@RoqLzSr0fC+T6QAtfO55 zeS2*pdQ?b!F%cd9-fNtz4{5SSu-eJo=4nC`e58imvcEVwdKXX(&lk_*OvVY+RV!XY zRzzl*72%`~v87kT{ME$~1nkWZ0*OW!-U@1lIwh27#A!dSu%+DRW~mLUFZ;G0H+yYH z#pGFZf~GEl4{sP@ko5p29PFy*#PXD6vUDnAKx3ph1OEA_qIYd9ONF#~%DdcL zb}Wo9`&Vk_BtqTZfoa%Pn;J#N>I#)cV1AF0 z*n(G;8Qbm`nZ{VMY(_2JBwKuLiIY-cYazVRD&kh&qLf*Pt>Si=9y@sfFf8fy=g^&| zdwO(EgSTx8PZCDPMtbJh!rv-A0`yvu<8VwL?ma-~(Wm$}bUM$yHoRjZbE=9Uqq1#h zcyuZLweCZ&NrhaEpI&V$-Ql46?#=X)u-3qFDWfml9w~x*@c^2^?gJWV-p5Zn{2yVG zcbu}n8XiYvs;$Li?(ST(w@{7*C(jl#nd6#HXGPVLPdiP5hm&XP892Vd9D*vDF8($+ z5xLfaEEQVDk@qKhU3>rIi+h5!gf8FkxPwIUAfDHihqhg?Ra?9|qXcb0(Qkd(EFp=R zn40b>&kRbM!dR|Qh1#Ioy^<48Uwvc5brcIU_%P~?QrzuumZ0USEm{65rt(yg)LQdM z5EoC8P)bW0T};^vv)|--%`Xt0#vYv(8|!#X+TRU9=l8RBMGegLqMz=Js~KYtzgu@z zj#M;1SiWc>GyjSE?H$wH0QY?!XrmN zw=k*Qd`I>NO!VcKK?Rc~h75d>Ff03~GUdqa8M5+`p}k2hHGSbNc~&;cyM;P8rtIq# ze~5YzZl4m_BtF>t7wB?MNBnQlxd!G{TycVdiMgF$s(;MK-=ZIJEK*@Cz%KiIs=-DT?pYj4Sf0R=-`Mz?b z9~F_}slBYzy6d|@U|>hviJRDb^Wmp@PQDWyc`?H@7_r}A(XM~VQK>H|SawkWfOOO+ z;E(|%G3w-dsVx6wB{8eaRXq&%#!s&$~#=2KN^%@2XZs7;-XbOstUiv)igM$PcK_^BL$&(@19s$ZihChV^dIj)dS z?8^d-*F`x_uDKdR;lnMFEf0J-XgkFRwLem;k^gSpjK~+Kv$0gvLe-cXKQ{~*h0#Gb z$w&=k%|_ZkU1&)g1mz09*`f&Lhjqo^ysSYFH`c!{4rHD&vNQfqD47xaI^7Wz2OSjp zIk`W&%(=wO%dvv^W*b}-6Dx!|BaJN&1vjS)%=48xHXCHM`&Th>PVxM2V&acv=|9T2 z;J`orc6}BU2q5?$>=E)TIKcl@i_X-ff1wxssi*$?P(i>Ue+iX>{Yj@G$=ZR1N<>Jw*G`!9E$|YCHjcL$L;}j3dDqZ{U`?8JNVG+z-QM6lP+OR|SY-kbw?AI_^CAN_SUHJ&G>78ygT4vNTh3s#-^F{thcW zk&ZWw+?>WwdtcI;L(jPz=#&QJ*ayayd@k;&A_Du%~ zk?-y1^ZRyfm3B^0VvYt9M3qzg!Xpn)bxRr=IYx4zN_r(1CK zy1|p(ul9A(k2t6Swc#xRQqw+?K-UkYKHRIU`=LVE!@y;ARpE7;iq{wAklvjFPf${Q zBXOD?UC}OAZ^z1AgUP8?7L@61jU~-#cAUu3vIXB-4DN+q;3T*4m7HjOWx^E_`fcLM z?Nc%dZN}0*QtNMdoz)zZtc9L!1=JrLn>HfrU%W`Z^Blad<4Wwj%{WJTlAx4@GMw3p zLm^)so>qq}kI?ul3*DO}YLH>fpe$rI?ipha?66DjJQZAQv-vyJ<1tp&6lbL%2nx{u zH6RoMk$?cLfq&Z0koflobpF!@IXiyN*}xKKIQ^>)g7}>;|HTHDfSif{=WGZG@w24k zmww_#VjKlZF57kd8Lpl}4_Z}TC3uc-Rui77*rg3{(ZpC9{6wm!9cLHMC_kyip8Mp9IY@E($W-S s_w2l^e&1JOI%xD+LGE0QbyFIsgCw literal 0 HcmV?d00001 diff --git a/backend/documents_parser/tests/test_summary_pdf_mapper_chain.py b/backend/documents_parser/tests/test_summary_pdf_mapper_chain.py index c05f1437..b376f876 100644 --- a/backend/documents_parser/tests/test_summary_pdf_mapper_chain.py +++ b/backend/documents_parser/tests/test_summary_pdf_mapper_chain.py @@ -84,6 +84,7 @@ _SUMMARY_000890_PDF = _FIXTURES / "Summary_000890.pdf" # cert 7800 (two electri _SUMMARY_000565_PDF = _FIXTURES / "Summary_000565.pdf" # cert 000565 (5-bp Elmhurst-only) _SUMMARY_001431_CASE20_PDF = _FIXTURES / "Summary_001431_case20.pdf" # sim case 20 (storage heaters + RR type-2 + wrapped "Double between 2002 and 2021" glazing) _SUMMARY_001431_TOPFLOOR_PDF = _FIXTURES / "Summary_001431_topfloor_flat.pdf" # gas-boiler-upgrade recommendation "after" — top-floor flat, PS sloping roof; exercises the Date-Built age-band + flat-position layout regressions +_SUMMARY_001431_LPG_PDF = _FIXTURES / "Summary_001431_lpg_boiler.pdf" # lpg-boiler recommendation "before" — §14 SAP code 115, §15 "Bottled gas"; exercises the bottled-LPG main-fuel mapping # GOV.UK EPB API JSON for cert 001479 — the API-path counterpart of the # Summary_001479.pdf fixture. Together they drive the API ≡ Summary @@ -180,6 +181,27 @@ def test_summary_001431_topfloor_extracts_main_property_age_band() -> None: assert survey.construction_age_band == "C 1930-1949" +def test_summary_001431_lpg_boiler_maps_main_fuel_to_bottled_lpg() -> None: + # Arrange — the lpg-boiler recommendation "before" Summary lodges + # §14.0 SAP code 115 (a Table 4b gas-family boiler row), §15.0 + # "Water Heating Fuel Type: Bottled gas", and §14.2 "Main gas: Yes". + # The boiler burns bottled LPG, not mains gas; the mapper must + # resolve the carrier from the "Bottled gas" label, NOT default to + # mains gas via the (contradictory) meter flag. Table-route code 3 = + # bottled LPG main heating (Table 32/12 10.30/9.46 p/kWh) — NOT code + # 5, which collides with anthracite (`canonical_fuel_code`). + pages = _summary_pdf_to_textract_style_pages(_SUMMARY_001431_LPG_PDF) + site_notes = ElmhurstSiteNotesExtractor(pages).extract() + + # Act + epc = EpcPropertyDataMapper.from_elmhurst_site_notes(site_notes) + + # Assert + main = epc.sap_heating.main_heating_details[0] + assert main.main_fuel_type == 3 + assert epc.sap_heating.water_heating_fuel == 3 + + def test_summary_001431_topfloor_flat_classified_as_top_floor() -> None: # Arrange — the recommendation "after" Summary lodges §6.0 "Position # of flat in block of flats: Top Floor": floor "A Another dwelling diff --git a/datatypes/epc/domain/mapper.py b/datatypes/epc/domain/mapper.py index 01512ddf..32af358d 100644 --- a/datatypes/epc/domain/mapper.py +++ b/datatypes/epc/domain/mapper.py @@ -4537,6 +4537,17 @@ _ELMHURST_MAIN_FUEL_TO_SAP10: Dict[str, int] = { # existing oddity as "Oil" → 8; both labels are unused by any live # fixture). Live form on Elmhurst worksheets is "Bulk LPG". "Bulk LPG": 27, + # Elmhurst Summary §14.0 / §15.0 lodging form for BOTTLED LPG + # (cylinders) — the recommendation worksheets lodge "Bottled gas" as + # the §15.0 "Water Heating Fuel Type" for an SAP-code-115 boiler. + # 3 = API/epc-codes `main_fuel` code for bottled LPG main heating, + # which routes via `API_FUEL_TO_TABLE_32`/`API_FUEL_TO_TABLE_12` → + # Table-code 3 (bottled LPG main heating, 10.30 / 9.46 p/kWh). NOT + # the legacy "LPG bottled": 5 above — API code 5 = anthracite, and + # `canonical_fuel_code` resolves the same-valued Table-32 code 5 to + # anthracite (3.64 p/kWh), so a 5 here would mis-price the dwelling + # as cheap solid fuel (the cohort-2100 -61-SAP collision class). + "Bottled gas": 3, # Elmhurst Summary §15.0 "Water Heating Fuel Type" labels for the # bio-liquid fuels added to the EES dict above. Values are Table 32 # codes verbatim (no API enum collision). Spec: SAP 10.2 Table 12 @@ -4873,7 +4884,12 @@ _GAS_BOILER_SAP_MAIN_HEATING_CODES: Final[frozenset[int]] = ( # of these so it can't mis-assign electricity from a separate immersion # (where §15.0 lodges the immersion's fuel, not the boiler's) — that # case still strict-raises `MissingMainFuelType` to force a mapper fix. -_GAS_LPG_MAIN_FUEL_CODES: Final[frozenset[int]] = frozenset({1, 5, 6, 7, 26, 27}) +# 3 = bottled LPG main heating ("Bottled gas" label); the other LPG +# carriers are the legacy API LPG codes (5/6/7) + the live "Bulk LPG" +# (27). All count as a gas/LPG carrier so `_elmhurst_gas_boiler_main_fuel` +# adopts a §15.0-lodged bottled-LPG water fuel for the boiler's space- +# heating carrier instead of falling through to the mains-gas meter flag. +_GAS_LPG_MAIN_FUEL_CODES: Final[frozenset[int]] = frozenset({1, 3, 5, 6, 7, 26, 27}) # SAP10 main-fuel code for mains gas (`_ELMHURST_MAIN_FUEL_TO_SAP10` # "Mains gas"). Used when a Table 4b gas boiler's carrier can't be read