ó
    |›²jµ_ ã                   ó  • S r SSKrSSKrSSKrSSKrSSKrSSKJr  SSKrSSK	J
r
JrJrJr  SSKJr  SSKJrJrJrJrJr  SSS	.rS
 rS rS rS r\R6                  R9                  \R6                  R;                  \5      5      r\R6                  RA                  \SS5      r!\RD                  r"SOS jr#S r$S r%S r&S r'S r(S r)Sr*S r+S r,S r-Sr.SPS jr/SQS jr0S r1Sr2SRS  jr3S! r4SSS" jr5S# r6\"4S$ jr7S% r8S& r9S' r:S(S)S*S+S,S-S.S/S0S1S2S3S4.r;STS5 jr<S6 r=S7 r>SRS8 jr?S9 r@SRS: jrASRS; jrBS< rCS= rDS> rE\FS?:X  aÐ  S@ rGSA\R�                  ;   a  \R’                  " SB\5        gSC\R�                  ;   a  SD rJ\R’                  " SE\J5        gSF\R�                  ;   a  \R’                  " SG\C5        gSH\R�                  ;   a  \R’                  " SISJ 5        gSK\R�                  ;   a  \R’                  " SLSM 5        g\R’                  " SN\95        gg)Ua  
BidBrain daily purchases capture.

Motorway and Carwow both close their daily sale at 3:30pm and the day's buys
populate in the purchases screens about 15 minutes later. This run, scheduled
at 3:50pm, reads what was actually bought on Motorway, Carwow and Auction4Cars
(added 2026-08-27, its own auctions run over several days rather than closing
at a fixed daily time, so this simply catches whatever has completed since
the last read), stores it, and emails the NEW buys to dealer_config.
ACCOUNTS_EMAIL with the achievable retail price from that day's BidBrain run,
so accounts can add the cars to the other systems.

The email is sent through the Mail app on this Mac (signed in as whatever
account Mail is set up with), no passwords are stored. No buys, no email. The
very first run baselines the purchase history without emailing it.

  python3 purchases_run.py                       read, store, email new buys
  python3 purchases_run.py --dry-run             read and store, print the email instead
  python3 purchases_run.py --baseline            seed history without emailing it
  python3 purchases_run.py --baseline --motorway seed just Motorway (--carwow/--auction4cars for the others)
  python3 purchases_run.py --check-purchase REG  re read one purchase from its own platform
  python3 purchases_run.py --push-dealerkit REG  push one purchase to DealerKit
                            [--items ...]
  python3 purchases_run.py --check-dealerkit REG check one purchase's real DealerKit record

Reads screens only. Never bids, never acts on the platforms.
é    N)Úsync_playwright)ÚdbÚrunsÚbrowserÚdealerkit_write)Úpounds)ÚmotorwayÚcarwowÚcazanaÚauction4carsÚdealerauctionr   r   )ÚAuction4CarsúDealer Auctionc                 ór  • / nU  H®  nUR                  S5      (       d(  UR                  S5      (       d  UR                  S5      c  MC  UR                  S5      b  MW  [        [        US   5      S-  XS9nUR                  SUR                  S5      5      nUb  Xe:X  a  M›  UR                  XFU45        M°     U$ )	ui  Which live cars get a fresh Retail est. from this dealership's own
rule, as (row, old, new) triples (Steven 2026-09-07: "why is dealer os
still adding Â£500 on to retail estimate pricing and rounding up always
with a 5 on the end"; the estimate was only ever computed on the day a
car was first read, so every car bought before the rule changed kept
the old formula's number). Reworked from the stored cazana_115 (the
plain Cazana retail times 1.15). Left alone: a cancelled sale, a car
with no Cazana figure, a car with a manual override (the person's own
figure always wins), and a car whose figure already matches.Úcancelled_atÚshellÚ
cazana_115Úretail_estimate_overrideçffffffò?©ÚmarginÚendingÚretail_estimate_rawÚretail_estimate)ÚgetÚdealer_retail_priceÚfloatÚappend)Úrowsr   r   ÚoutÚrÚnewÚolds          Ú./Users/stevendouglas/BidBrain/purchases_run.pyÚreprice_planr%   4   sª   € ð €CÛˆØ�5‰5�× Ñ  A§E¡E¨'§N¡N°a·e±e¸LÓ6IÑ6QÙØ�5‰5Ð+Ó,Ñ8ÙÜ!¤%¨¨,©Ó"8¸4Ñ"?ÈÑ^ˆØ�e‰eÐ)¨1¯5©5Ð1BÓ+CÓDˆØ‰;˜#›*ÙØ�
‰
�A˜C�=Ö!ñ ð €Jó    c                  óP  • [         R                  " 5         [        5       u  p[        [         R                  " 5       X5      nU(       d&  [
        R                  " SSSSS5        [        S5        gU HU  u  p4n[         R                  " US   U5        [         R                  " US   SS	S
XESS9  [        SUS    SUS SUS 35        MW     SU S SU 3n[
        R                  " SS[        U5       SU S3[        U5      [        U5      5        [        [        U5       SU S35        g)aŽ  Re price every live car's Retail est. with this dealership's own
margin and ending (the Run modal's "Re price retail estimates"). The
change is written on each car's timeline; a car on DealerKit and still
Due in has the new figure sent to DealerKit by the next stage check
(daily_run.retail_sync_plan, within five minutes), a finished car's
DealerKit price is never overwritten by a formula figure.Úretail_repriceTz/Every retail estimate already follows the rule.é   zNothing to re price.NÚidÚfieldz4Retail est. re priced with the dealership's own ruler   ÚBidBrain)r+   ÚwasÚnow_valÚwhoú  Úregu   : Â£ú,.0fu    -> Â£u	   margin Â£z	, ending z retail estimate(s) re priced (z).z re priced ()r   Úinit_dbÚ_retail_formula_settingsr%   Úlist_purchasesr   ÚfinishÚprintÚset_purchase_retailÚlog_purchase_eventÚlen)r   r   Úplanr!   r#   r"   Úwords          r$   Úreprice_retailr=   L   s!  € ô ‡J‚J„LÜ-Ó/�N€FÜœ×)Ò)Ó+¨VÓ<€DÞÜ�ŠÐ$ dÐ,]Ð_`ÐbcÔdÜÐ$Ô%ØÛ‰ˆ�Ü
×Ò˜q ™w¨Ô,Ü
×Ò˜a ™g wÐ0fØ$5¸3ÐQ[ò	]ä��1�U‘8�*˜D  T 
¨&°°T°
Ð;Ö<ñ	 ð
 �v˜d�m 9¨V¨HÐ5€DÜ‡K‚KÐ  $¬3¨t«9¨+Ð5TÐUYÐTZÐZ\Ð(]Ô_bÐcgÓ_hÔjmÐnrÓjsÔtÜ	ŒS�‹YˆK�| D 6¨Ð
,Õ-r&   c                  óš   •  [         R                  " 5       n U R                  SS5      U R                  SS5      4$ ! [         a    0 n  N2f = f)a>  This dealership's own two knobs for dealer_retail_price: the flat
margin and the price ending, read once per sync (Steven 2026-09-01,
see dealer_retail_price's own docstring for why the formula itself
stays pure). Falls back to the shared factory defaults on any read
failure, never blocks a sync over a settings read.Úretail_estimate_marginéô  Úretail_estimate_endingé_   )r   Úget_settingsÚ	Exceptionr   )Úsaveds    r$   r4   r4   d   sS   € ðÜ—’Ó!ˆð �I‰IÐ.°Ó4Ø�I‰IÐ.°Ó3ð5ð 5øô ó ØŠðús   ‚; »A
Á	A
c                 ó²  • U (       a  [        U 5      O1 Skn [        R                  " 5       nUR	                  S5      =(       d    0 n[        UR	                  S5      =(       d    / 5      n[        5       nU HK  n[
        R	                  U5      nU(       a  UR	                  U5      SL d  Xt;   a  M:  UR                  U5        MM     U$ ! [         a    Us $ f = f)a‹  The real set of platforms this sync will actually read: requested
(or all four when nothing was asked for, the normal full sync, same
"smaller set narrows, never widens" shape as daily_run._select_platforms)
narrowed to whatever is switched ON in Settings. A platform hidden as a
connection, or turned off on its own Settings tab, must never be read
for purchases either, only for the live auction list did that rule
apply before this fix (found live, 2026-09-01: Auction4Cars and Dealer
Auction both hidden and off, yet still read and failed every sync,
painting the nav light red app-wide for something the dealership had
deliberately switched off).>   ÚCarwowÚMotorwayr   r   Úplatform_enabledÚhidden_connectionsF)Úsetr   rC   rD   r   Ú_PLATFORM_SETTINGS_KEYÚadd)Ú	requestedÚbaserE   ÚenabledÚhiddenr    ÚnameÚkeys           r$   Ú_settings_enabled_platformsrT   r   sµ   € ö 'Œ3ˆyŒ>Ò,d€DðÜ—’Ó!ˆð �i‰iÐ*Ó+×1¨r€GÜ�—‘Ð/Ó0×6°BÓ7€FÜ
‹%€CÛˆÜ$×(Ñ(¨Ó.ˆÞ�G—K‘K Ó$¨Ò-°³ÙØ�‰�Žñ	 ð
 €Jøô ó ØŠðús   ˜C ÃCÃCÚdatazlast_run.jsonc                 ó  ^• U c  gU S-  U-   mUS:X  a"  [        TS-
  S-  5      n[        SU-  S-   5      $ TS-  S-  nS VVs/ s H  nS  H  nXE-   U-   U-   PM     M     nnn[        [        UU4S jS	95      $ s  snnf )
ur  The price a dealership asks once a car is its own stock to resell:
Cazana retail times 1.15 (BidBrain's own uplift, see pricing.py) plus
a flat margin, then rounded to this dealership's own price ending.

The two endings are genuinely DIFFERENT shapes, not the same rule with
a different digit, because they came from two different real sources:

ending=95 (Mark's own, the default) rounds to the nearest of just FOUR
specific bands, X295/X495/X795/X995, cycling every Â£1000. Confirmed
against his own real pricing across 16 already bought cars
(2026-08-21): landed on his exact figure for 12 of 16, clearly the
best fit against a flat 20% uplift alone (6 of 16) or the plain uplift
with no added margin (0 of 16). His own words, "roughly Â£500 above the
115% Cazana retail". These four bands are HIS real, empirical pricing,
not a generic rule, so they are never touched by the other ending.

ending=99 (Steven's own) rounds to the nearest hundred, ending 99, no
gaps: X099/X199/X299/.../X999. A first version of this mechanically
reused Mark's own four band SPACING with a 99 suffix (X299/X499/X799/
X999), which put a real car's own Â£10,166 target 133 away from its
nearest allowed price when the genuinely nearest Â£10,199 was only 33
away and simply never offered as an option (Steven caught this live,
2026-09-01: "why does it compute to 10299 instead of 10199?"). Asked
directly rather than guessed a second time; "every Â£100" was his own
confirmed choice.

Deliberately PURE, margin and ending never read from settings here:
two dealerships share this repo and genuinely disagree on both, so
every real caller reads pricing.DEFAULT_SETTINGS' own
retail_estimate_margin/retail_estimate_ending and passes them in, the
same "settings threaded through, function stays testable alone" shape
as daily_run._select_platforms. None in, None out, never guessed when
there is no Cazana figure to work from.Nr   éc   éd   éè  )iüÿÿr   rY   )éÈ   é�  i¼  i„  c                 ó    >• [        U T-
  5      $ ©N)Úabs)ÚcÚtargets    €r$   Ú<lambda>Ú%dealer_retail_price.<locals>.<lambda>¿   s   ø€ ¬s°1°v±:¬r&   )rS   )ÚroundÚmin)	Úcazana_retailr   r   ÚkÚ	thousandsÚdeltaÚhundredsÚ
candidatesr`   s	           @r$   r   r   ‘   s¯   ø€ ðD ÑØØ˜TÑ! FÑ*€FØ�ƒ|ô �6˜B‘; #Ñ%Ó&ˆÜ�S˜1‘W˜r‘\Ó"Ð"Ø˜4‘ 4Ñ'€Iá/ô8Ú/�eÜ"6�hð Ñ# hÑ.°Ô7á"6ñ 8Ù/ð ñ 8ô ”�ZÔ%>Ñ?Ó@Ð@ùó8s   ÁA;c                  óî  • SSK n 0 nU R                  [        R                  R	                  [
        SS5      SS9n UR                  S5       HQ  u  p4SR	                  [        U=(       d    S5      R                  5       R                  5       5      nU(       d  MM  XAU'   MS     UR                  5         0 n [        R                  " [        [        S	S
95      nUR                  S/ 5      UR                  S/ 5      -    HÎ  nUR                  S0 5      UR                  S5      p©U	R                  S5      nU	R                  S5      U	R                  S5      4 Hr  nSR	                  [        U=(       d    S5      R                  5       R                  5       5      nU(       d  MK  U(       a  X¶U'   U
(       d  M_  U
R                  S5      X'   Mt     MÐ     X4$ ! UR                  5         f = f! [          a     X4$ f = f)aE  Map normalised reg to the most recent governing retail value BidBrain
computed in the last few days, from the stored assessments (every run saves
them), plus the latest run cache which also knows each car's selling plate.
Also returns a second map, reg to photo_url, borrowed from that same run
cache (Mark 2026-08-21, "add an image to the line" on the purchases
report) since the daily run already fetched it, no separate read needed.
A car bought a while back, whose cache has long since rotated away, simply
has no photo here, same as it has no retail estimate from this source.r   NrU   úbidbrain.dbé   ©Útimeoutz¾SELECT reg, governing_value FROM assessments
                   WHERE governing_value IS NOT NULL
                     AND run_at >= datetime('now', '-4 days')
                   ORDER BY idÚ úutf-8©ÚencodingÚ	shortlistÚheldÚcarÚpricingÚ	photo_urlr1   Úselling_vrmÚgoverning_value)Úsqlite3ÚconnectÚosÚpathÚjoinÚHEREÚexecuteÚstrÚupperÚsplitÚcloseÚjsonÚloadÚopenÚCACHEr   ÚOSError)r{   r    Úconnr1   Úgvrf   ÚphotosÚdÚar_   ÚpÚphotorS   s                r$   Ú_retail_mapr’   Â   sœ  € ó Ø
€CØ�?‰?œ2Ÿ7™7Ÿ<™<¬¨f°mÓDÈaˆ?ÐP€Dð
Ø—|‘|ð"ö#‰GˆCð
 —‘œ˜CŸI 2›×,Ñ,Ó.×4Ñ4Ó6Ó7ˆAßˆqØ�A“ñ#ð 	�
‰
ŒØ€FðÜ�IŠI”dœ5¨7Ñ3Ó4ˆØ—‘�{ BÓ'¨!¯%©%°¸Ó*;Ô;ˆAØ—5‘5˜ Ó# Q§U¡U¨9Ó%5ˆqØ—E‘E˜+Ó&ˆEØŸ™˜e› a§e¡e¨MÓ&:Ó;�Ø—G‘GœC §	 r›N×0Ñ0Ó2×8Ñ8Ó:Ó;�ÞÙÞØ %˜1‘Iß�1ØŸU™UÐ#4Ó5�C“Fó <ñ <ð ˆ;Ðøð# 	�
‰
�ûô ó ØØˆ;Ððús+   »AG ÂG Â4C:G% Æ2G% ÇG"Ç%
G4Ç3G4c            	      ób  ^• SSK n 0 mU4S jn [        R                  " [        [        SS95      nUR                  S/ 5      UR                  S/ 5      -   UR                  S/ 5      -    H”  nUR                  S	0 5      UR                  S
5      =(       d    0 pTUR                  S5      UR                  S5      4 H@  nU" XdR                  S5      UR                  S5      =(       d    UR                  S5      S9  MB     M–      U R                  [        R                  R                  [        SS5      SS9n UR                  S5       H  u  p‰n
U" X‰U
S9  M     UR                  5          [         R"                  " 5        HH  nU" UR                  S5      UR                  S5      UR                  S5      UR                  S5      S9  MJ     U4S jn [        R                  " [        [        SS95      nUR                  S/ 5      UR                  S/ 5      -   UR                  S/ 5      -    HU  nUR                  S	0 5      nUR                  S5      UR                  S5      4 H  nU" XdR                  S5      5        M     MW      [         R$                  " S5      R'                  5        H  u  p�U" X�5        M     T$ ! [        [        4 a     GNÎf = f! UR                  5         f = f! [         a     GN�f = f! [         a     GN<f = f! [        [        4 a     N˜f = f! [         a     T$ f = f)a=  What BidBrain already knows about a car by its plate, before anyone
asks Cazana again (Steven 2026-09-09: "its totally stupid trying to run
another cazana check when we already have the data"): the mileage and
the Cazana retail from the last run's list (every car it valued, by
reg and by the plate it sells with), the Cazana retail from the
valuation cache (the last 30 days, newest first), and the purchases
table itself (a car already valued on file). {reg: {"mileage",
"cazana", "cazana_115", "retail_estimate"}}, any of which may be
missing. Every auction house alike.r   Nc                 ó0  >• SR                  [        U =(       d    S5      R                  5       R                  5       5      nU(       d  g TR	                  U0 5      nUR                  5        H(  u  pEUS;  d  M  UR                  U5      S;   d  M$  XSU'   M*     g )Nrp   )Nrp   r   )r   r‚   rƒ   r„   Ú
setdefaultÚitemsr   )rS   Úfactsrf   ÚrowÚfÚvÚknowns         €r$   ÚputÚ_known_facts.<locals>.putø   ss   ø€ Ø�G‰G”C˜Ÿ	˜r“N×(Ñ(Ó*×0Ñ0Ó2Ó3ˆÞØØ×Ñ˜q "Ó%ˆØ—K‘K–M‰DˆAØ˜Õ%¨#¯'©'°!«*¸Õ*EØ�A“ò "r&   rq   rr   rt   ru   Úrejectedrv   rw   r1   ry   Úmileagere   )rŸ   r   rU   rl   rm   rn   zæSELECT reg, mileage, value FROM valuation_cache
                       WHERE source='cazana' AND value IS NOT NULL
                         AND valued_at >= datetime('now', '-30 days')
                       ORDER BY valued_at ASCr   r   )rŸ   r   r   c                 ó  >• SR                  [        U =(       d    S5      R                  5       R                  5       5      nU(       a9  Ub5  TR	                  U0 5      R                  S5      c  [        U5      TU   S'   g g g g )Nrp   Úfinance)r   r‚   rƒ   r„   r•   r   Úbool)rS   rš   rf   r›   s      €r$   Úput_financeÚ!_known_facts.<locals>.put_finance  sk   ø€ Ø�G‰G”C˜Ÿ	˜r“N×(Ñ(Ó*×0Ñ0Ó2Ó3ˆÞ�‘ 5×#3Ñ#3°A°rÓ#:×#>Ñ#>¸yÓ#IÑ#QÜ"& q£'ˆE�!‰H�YÒð $R�ˆ1r&   r¡   )r{   r†   r‡   rˆ   r‰   r   rŠ   Ú
ValueErrorr|   r}   r~   r   r€   r�   r…   rD   r   r5   Úrecent_detail_factsr–   )r{   rœ   rŽ   r�   r_   r�   rS   r‹   r1   rŸ   Úvaluer!   r£   rš   r›   s                 @r$   Ú_known_factsr¨   ì   sÍ  ø€ ó Ø€EõðÜ�IŠI”dœ5¨7Ñ3Ó4ˆØ—‘�{ BÓ'¨!¯%©%°¸Ó*;Ñ;¸a¿e¹eÀJÐPRÓ>SÔSˆAØ—5‘5˜ Ó# Q§U¡U¨9Ó%5×%;¸ˆqØŸ™˜e› a§e¡e¨MÓ&:Ó;�Ù�C§¡ yÓ!1¸1¿5¹5ÀÓ;Q×;kÐUV×UZÑUZÐ[jÓUkÔmó <ñ TðØ�‰œrŸw™wŸ|™|¬D°&¸-ÓHÐRSˆÐTˆð	Ø'+§|¡|ð1ö(2Ñ#�˜eñ
 �C°Ô7ñ(2ð �J‰JŒLðÜ×"Ò"Ö$ˆAÙ�—‘�e“ a§e¡e¨IÓ&6À1Ç5Á5ÈÓCVÐhi×hmÑhmÐnó  iAô  Bñ %õ*ðÜ�IŠI”dœ5¨7Ñ3Ó4ˆØ—‘�{ BÓ'¨!¯%©%°¸Ó*;Ñ;¸a¿e¹eÀJÐPRÓ>SÔSˆAØ—‘�e˜RÓ ˆAØŸ™˜e› a§e¡e¨MÓ&:Ó;�Ù˜C§¡ yÓ!1Ö2ó <ñ TðÜ×,Ò,¨YÓ7×=Ñ=Ö?‰FˆCÙ˜Öñ @ð €LøôQ ”ZÐ ó Úðûð �J‰J�LûÜó Úðûô
 ó Úðûô ”ZÐ ó Ùðûô
 ó ØØ€Lðúst   �C2J< Ä3K( Ä6"K ÅK( Å)A!K9 ÇB3L
 Ê5L  Ê<KËKËK%Ë%K( Ë(
K6Ë5K6Ë9
LÌLÌ
LÌLÌ 
L.Ì-L.c                 óî  • / n[         R                  " U5      n[         R                  " US5      nU  H¿  nUR                  S5      (       a  M  UR                  S5      =(       d    SnUR                  S5      =(       d    SS:”  a$  U(       a  Xc:w  a  UR	                  US   U45        Mw  UR                  S5      =(       d    SS:”  d  M—  U(       d  M   U(       a  M©  UR	                  US   U45        MÁ     U$ )	a  Which purchases get their On finance? answer from their own purchase
page (2026-09-16): a car whose page shows negative equity is marked
NEGATIVE EQUITY unless it already is; a car with a settlement figure
and no answer yet is marked as on finance. A NO answer a person typed
is never overwritten; only the platform's own figures move it up.
(purchase id, answer) rows. Pure, tested.Tr   Ú
on_financerp   Únegative_equityr   r*   Úfinance_settlement)r   Únegative_equity_answerÚfinance_answerr   r   )Úon_fileÚoptionsr    ÚnegÚyesr!   Úcurs          r$   Ú_finance_page_updatesr´   1  sÅ   € ð €CÜ
×
#Ò
# GÓ
,€CÜ
×
Ò
˜G TÓ
*€CÛˆØ�5‰5�× Ñ ÙØ�e‰e�LÓ!×' RˆØ�E‰EÐ#Ó$×)¨¨QÓ.¶3¸3»:Ø�J‰J˜˜$™ �~Ö&Ø�e‰eÐ(Ó)×.¨Q°!Õ3¿¸ÇCÀCØ�J‰J˜˜$™ �~Ö&ñ ð €Jr&   c           
      ó$  • / nU  GH  nUR                  S5      (       d  UR                  S5      (       a  M2  UR                  SR                  [        UR                  S5      =(       d    S5      R                  5       R	                  5       5      5      =(       d    0 nUR                  S5      c  M¬  [
        R                  " U[        US   5      5      nU(       d  MØ  UR                  US   XdR                  S5      =(       d    S45        GM	     U$ )	zëWhich purchases get their On finance? answer from the platform: a
car with no answer yet whose listing said whether finance is
outstanding. (purchase id, answer, platform) rows. A person's own
answer is never overwritten. Pure, tested.rª   r   rp   r1   r¡   r*   Úplatformzthe platform)	r   r   r‚   rƒ   r„   r   r®   r¢   r   )r¯   r›   r°   r    r!   r™   Úanswers          r$   Ú_finance_updatesr¸   F  sÏ   € ð
 €CÜˆØ�5‰5�×Ñ !§%¡%¨×"7Ñ"7ÙØ�I‰I�b—g‘gœc !§%¡%¨£,×"4°"Ó5×;Ñ;Ó=×CÑCÓEÓFÓG×MÈ2ˆØ�5‰5�ÓÑ#ÙÜ×"Ò" 7¬D°°9±Ó,>Ó?ˆßˆ6Ø�J‰J˜˜$™ ¯©¨zÓ):×)L¸nÐM×Nñ ð €Jr&   c           
      óÐ  • S=pEU  GHZ  nUR                  SR                  [        UR                  S5      =(       d    S5      R                  5       R	                  5       5      5      =(       d    0 nUR                  S5      (       d#  UR                  S5      (       a  US   US'   US-  nUR                  S5      b  M·  UR                  S5      b<  US   US'   UR                  S5      c  UR                  S5      b  US   US'   US-  nGM  UR                  S5      (       d  GM  [        [        US   5      S	-  S
5      US'   [        [        US   5      X#S9US'   US-  nGM]     XE4$ )zýFill each purchase's mileage, Cazana figure and Retail est. from
what is already known for its plate, so the live Cazana lookup below
only ever runs for a car nobody has valued. Returns (mileages filled,
valuations filled). Pure over its inputs, tested.r   rp   r1   rŸ   r)   r   r   r   r   é   r   )r   r   r‚   rƒ   r„   rc   r   r   )r   r›   r   r   ÚmilesÚvalsr!   r™   s           r$   Ú_reuse_knownr½   X  sG  € ð
 Ð€EÜˆØ�I‰I�b—g‘gœc !§%¡%¨£,×"4°"Ó5×;Ñ;Ó=×CÑCÓEÓFÓG×MÈ2ˆØ�u‰u�Y×Ñ A§E¡E¨)×$4Ñ$4Ø˜Y™<ˆAˆi‰LØ�Q‰JˆEØ�5‰5�ÓÓ&Ø�u‰u�\Ó"Ñ.Ø"# L¡/��,‘Ø—5‘5Ð*Ó+Ñ3¸¿¹Ð>OÓ8PÑ8\Ø+,Ð->Ñ+?�AÐ'Ñ(Ø˜‘	“Ø—‘�x—”Ü"'¬¨a°©kÓ(:¸TÑ(AÀ1Ó"E��,‘Ü':¼5ÀÀ8ÁÓ;MÐV\Ñ'l�Ð#Ñ$Ø˜‘	“ñ ð ˆ;Ðr&   c                 óš   ^• U R                  S5      =(       d    SR                  5       R                  5       m[        U4S jS 5       5      $ )z†A purchase whose sale was cancelled or voided on the platform. Carwow
marks these voided; Motorway shows a cancel or withdrawn status.Ústatusrp   c              3   ó,   >#   • U  H	  oT;   v •  M     g 7fr]   © )Ú.0ÚtÚss     €r$   Ú	<genexpr>Ú _is_cancelled.<locals>.<genexpr>t  s   øé € Ð?Ò>˜!�AŽvÒ>ùs   ƒ)ÚvoidÚcancelÚ	withdrawn)r   ÚstripÚlowerÚany)r!   rÄ   s    @r$   Ú_is_cancelledrÍ   p  s;   ø€ ð 
�‰ˆx‹×	˜B×%Ñ%Ó'×-Ñ-Ó/€AÜÔ?Ñ>Ó?Ó?Ð?r&   ))Úpayment)Úwinning_bid)r�   )Ú
photo_urls)zservice history)Úservice_history_photosÚdocument_paths)ÚV5)Ú	v5_photosÚv5_local_paths)zcollection date)Úcollection_datec                 óJ   • U (       d  g[        U 5      R                  5       S;  $ )zsA stored value that really carries something: not empty, and not an
empty JSON list or object saved as text ("[]").F)z[]z{}Únull)r‚   rÊ   )rš   s    r$   Ú	_has_factrÙ   „  s    € ö ØÜˆq‹6�<‰<‹>Ð!5Ñ5Ð5r&   c                 ó&  • U R                  S5      =(       d    / n[        U[        5      (       a   [        R                  " U5      n[        U[        5      (       a  U(       d  g[        S U 5       5      (       + $ ! [
        [        4 a    / n NHf = f)zzWhether a car's stored gallery is our own saved files rather than
the platform's own web links (2026-09-10). Pure, tested.rÐ   Fc              3   ór   #   • U  H-  n[        U5      R                  5       R                  S 5      v •  M/     g7f))zhttp://zhttps://N)r‚   rË   Ú
startswith)rÂ   Úus     r$   rÅ   Ú#_photos_are_ours.<locals>.<genexpr>—  s)   é € ÐTÊtÈ!”3�q“6—<‘<“>×,Ñ,Ð-D×EÐEÊtùs   ‚57)	r   Ú
isinstancer‚   r†   ÚloadsÚ	TypeErrorr¥   ÚlistrÌ   )ÚexistingÚurlss     r$   Ú_photos_are_oursrå   Œ  sz   € ð �<‰<˜Ó%×+¨€DÜ�$œ×Ñð	Ü—:’:˜dÓ#ˆDô �dœD×!Ñ!®ØÜÑTÉtÓTÓTÔTÐTøô	 œ:Ð&ó 	ØŠDð	ús   ±A; Á;BÂBc                 óŒ  ^ • [          VVs/ s H#  u  p[        U 4S jU 5       5      (       a  M!  UPM%     nnnT R                  S5      =(       d    SnSU;  a'  US:X  a!  [        T 5      (       d  UR	                  S5        US;   a9  T R                  S5      b  T R                  S	5      (       a  UR	                  S
5        U$ s  snnf )aÜ  Which of PAGE_FACTS the row on file still lacks, plus, for a Carwow
car, photos still hosted by Carwow rather than saved as files of ours
(2026-09-10), plus, on Motorway and Carwow, the real buyer's fee while
the one on file is still an estimate or not there at all (2026-09-14,
Steven: seven of nine Carwow cars showed no fee; Carwow only puts the
breakdown on the page once the car is collected). Nothing here is
chased for ever any more: _page_read_needed caps every chase.c              3   óX   >#   • U  H  n[        TR                  U5      5      v •  M!     g 7fr]   )rÙ   r   )rÂ   r™   rã   s     €r$   rÅ   Ú!_missing_facts.<locals>.<genexpr>¢  s%   øé € Ð9eÒ^dÐYZ¼)ÀHÇLÁLÐQRÃO×:TÐ:TÒ^dùs   ƒ'*r¶   rp   r�   rG   zphotos of ours©rG   rH   Úmotorway_feeÚpayment_estimatedÚfee)Ú
PAGE_FACTSrÌ   r   rå   r   )rã   ÚwhatÚfieldsr    Úplats   `    r$   Ú_missing_factsrñ   š  s£   ø€ õ %/Ô
f¢J‘L�D´cÔ9eÑ^dÓ9e×6e�4¡J€CÑ
fØ�<‰<˜
Ó#×) r€DØ�sÓ˜t xÓ/Ô8HÈ×8RÑ8RØ�
‰
Ð#Ô$ØÐ%Ó%¨8¯<©<¸Ó+GÑ+OÐS[×S_ÑS_Ð`s×StÑStØ�
‰
�5ÔØ€Jùó gs
   ‹ C ¯C é<   c                 ó  • U (       d  gU R                  S5      (       a  g[        U R                  S5      =(       d    S5      SS nU(       a-  XA[        R                  " [        S9-
  R                  5       :  a  g[        U 5      nS	U;   d  S
U;   d  SU;   a  gU(       d  gXA[        R                  " US9-
  R                  5       :¼  a  g[        U5      =(       a'    XA[        R                  " US9-
  R                  5       :¬  $ )a3  Whether a purchase's own page is worth reading today. A car not on
file yet: yes. A car bought within the last `days` days: yes, whatever
it has, its documents and collection date are still arriving. A car
bought within `chase_days` days that still lacks anything the page
can give (payment breakdown, photos, service history, V5, collection
date): yes. Older than that, only the payment breakdown and photos are
worth chasing, a seller who never uploaded a V5 is not going to now.
Anything else is complete and left alone. Before this (Steven
2026-09-08) every purchase's page was read every day, 30 odd pages for
a sync that usually had one or two new cars. Steven the same day:
"should this not also be looking for v5 and service history or any
other thing that we try and capture that isnt there yet?" Pure, tested.Tr   FÚbought_daterp   Né
   ©ÚdaysrÎ   r�   rì   )r   r‚   ÚdatetimeÚ	timedeltaÚCHASE_CAP_DAYSÚ	isoformatrñ   r¢   )rã   Útodayr÷   Ú
chase_daysÚboughtÚmissings         r$   Ú_page_read_neededr   ²  sÕ   € ö ØØ‡|�|�N×#Ñ#ØÜ�—‘˜mÓ,×2°Ó3°C°RÐ8€FÞ�&¤H×$6Ò$6¼NÑ$KÑK×VÑVÓXÓXØÜ˜XÓ&€GØ�GÓ˜x¨7Ó2°e¸wÓ6FØÞØØœ(×,Ò,°$Ñ7Ñ7×BÑBÓDÓDØÜ�‹=×`˜V´×0BÒ0BÈ
Ñ0SÑ(S×'^Ñ'^Ó'`Ñ`Ð`r&   c                 ó  • U R                  S5      =(       d    SnUS;  d  U R                  S5      b  0 $ U=(       d    [        R                  nU R                  S5      b  U S   nOvUS:X  a  U R                  S5      nO^Ub  UO#[        R                  " 5       R                  S5      n[        R                  " U R                  S5      XPR                  S	5      5      nUc  0 $ U" X45      nUc  0 $ XF[        US
-  S5      SS.$ )aä  The buyer's fee to hold for a Carwow or Motorway car whose page has
not shown its breakdown yet (2026-09-14, Steven: "a car should never be
added from carwow or motorway from now on without the fees"): the
platform's own published band for the price, with its VAT, marked
estimated so the real breakdown replaces it the moment the page shows
one. Carwow's list price is the car alone; Motorway's is the car alone
while the purchase is still going through and the all in figure once
the checks are complete, so its bid goes through
db.provisional_motorway_bid, which knows the difference from the row's
own status. {} when the row already has a fee, has no price, or no
band fits. Pure: fee_for is db.fee_for, mw_bands Motorway's band
table.r¶   rp   ré   rê   rÏ   rG   ÚpricerH   r¿   gš™™™™™É?rº   T)rÏ   rê   Ú	vat_totalÚ	estimated)r   r   Úfee_forÚget_fee_bandsÚprovisional_motorway_bidrc   )r˜   r  Úmw_bandsrð   r  Úbandsrì   s          r$   Úestimated_feer
  Ð  sí   € ð �7‰7�:Ó×$ "€DØÐ)Ó)¨S¯W©W°^Ó-DÑ-PØˆ	Ø×#œŸ™€GØ
‡w�wˆ}ÓÑ)Ø�MÑ"‰Ø	�Ó	Ø—‘˜Ó ‰à$Ñ0‘´b×6FÒ6FÓ6H×6LÑ6LÈZÓ6XˆÜ×+Ò+¨C¯G©G°GÓ,<¸eÇWÁWÈXÓEVÓWˆØ�}Øˆ	Ù
�$Ó
€CØ
�{Øˆ	Ø ÄEÈ#ÐPSÉ)ÐUVÓDWÐfjÑkÐkr&   c                 ó^  • U R                  S5      =(       d    SS:w  d,  U R                  S5      (       a  U R                  S5      (       a  g[        U R                  S5      =(       d    S5      SS	 n[        U5      =(       a+    X![        R                  " [
        S
9-
  R                  5       :  $ )z�Whether a Carwow car's fee estimate is now final by age (2026-09-14):
still estimated, not cancelled, bought more than CHASE_CAP_DAYS ago.
Pure.r¶   rp   rG   rë   r   Frô   Nrõ   rö   )r   r‚   r¢   rø   rù   rú   rû   )r˜   rü   rþ   s      r$   Úestimate_standsr  ð  sˆ   € ð 	�‰�
Ó×!˜r hÓ.°c·g±gÐ>Q×6RÑ6RÐVY×V]ÑV]Ð^l×VmÑVmØÜ�—‘˜Ó'×-¨2Ó.¨s°Ð3€FÜ�‹<×b˜F¬h×.@Ò.@ÄnÑ.UÑ&U×%`Ñ%`Ó%bÑbÐbr&   ud   Carwow never showed the fee within {days} days, the Â£{fee:,.0f} estimate stands. Check the invoice.c                 ó²  • U =(       d    [         R                  R                  5       n Sn[        R                  " 5        H”  n[        X 5      (       d  M  [        R                  [        UR                  S5      =(       d    SS9n[        S [        R                  " US   5       5       5      (       a  Mu  [        R                  " US   SUSS9  US	-  nM–     U$ )
zZOne timeline line, once, on every Carwow car whose fee estimate has
aged out of the chase.r   rê   )r÷   rì   c              3   ó\   #   • U  H"  nS UR                  S5      =(       d    S;   v •  M$     g7f)zestimate standsÚtextrp   N©r   )rÂ   Úes     r$   rÅ   Ú,note_estimates_that_stand.<locals>.<genexpr>  s%   é € ÐaÒC`¸aÐ  Q§U¡U¨6£]×%8°bÖ9ÒC`ùs   ‚*,r*   Úsyncr,   ©r/   r)   )rø   Údaterü   r   r5   r  ÚESTIMATE_STANDS_NOTEÚformatrú   r   rÌ   Úpurchase_eventsr9   )rü   Únr˜   r  s       r$   Únote_estimates_that_standr  ý  s³   € ð ×*”X—]‘]×(Ñ(Ó*€EØ	€AÜ× Ò Ö"ˆÜ˜s×*Ñ*ÙÜ#×*Ñ*´ÀCÇGÁGÈNÓD[×D`Ð_`Ð*ÐaˆÜÑaÄ2×CUÒCUÐVYÐZ^ÑV_ÔC`Óa×aÑaÙÜ
×Ò˜c $™i¨°¸:ÒFØ	ˆQ‰Šñ #ð €Hr&   c                 óò   • U  Vs/ s HM  n[        UR                  US   [        UR                  S5      =(       d    S5      45      U5      (       d  MK  UPMO     nnU[        U 5      [        U5      -
  4$ s  snf )z6(rows to read, how many complete ones are left alone).r¶   Úvehicle_refrp   )r   r   r‚   r:   )r   r¯   rü   r!   Úkeeps        r$   Ú_rows_worth_readingr    si   € áÓw’t�!Ô0°·±¸aÀ
¹mÌSÐQR×QVÑQVÐWdÓQe×QkÐikÓMlÐ=mÓ1nÐpu×v�A‘t€DÐwØ”�T“œS ›YÑ&Ð&Ð&ùò xs   …A
A4ÁA4c                 óŒ  ^• SSK Jn  [        U5      n/ n/ n[        R                  " 5        Vs0 s H+  oUS   [        UR                  S5      =(       d    S5      4U_M-     nn[        R                  R                  5       n[        5        nS[        R                  4S[        R                  4S[        R                   4S	["        R$                  44 HM  u  pšX‘;  a  M  U" S
SU	 S35         U
" U5      n['        U	 S[)        U5       S35        UR+                  U5        MO     SU;   Gaˆ  U" S
S5         [        R0                  " X€S9n['        S[)        U5       S35        UR+                  U5        U V^s/ s H,  mTS   S:X  d  M  [3        U4S jS 5       5      (       a  M*  TPM.     nn[5        XæU5      u  pïU(       a  ['        SU S35        U(       GaÛ  U" S
S[)        U5       S[)        U5      S:w  a  SOS U(       a  SU S 3OS S!3S[)        U5      5         [6        R8                  " US"S#S$9nUR:                  (       a  UR:                  S   OUR=                  5       nSn[?        U5       GH  u  nm [        R@                  " UTS   5      nU(       a  TRC                  U5        US-  n[D        RF                  RI                  [J        S%S&TS'   5      n[        RL                  " U5      nU(       a(  [        RN                  " UU/U5      nU(       a  US   TS('   [        RP                  " U5      nU(       a8  [        RN                  " UUU5      nU(       a  [R        RT                  " U5      TS)'   [        RV                  " U5      nU(       a8  [        RN                  " UUU5      nU(       a  [R        RT                  " U5      TS*'   [        RX                  " UTS   5      nU(       a8  [        RN                  " UUU5      nU(       a  [R        RT                  " U5      TS+'   [        RZ                  " UTS   5      nU(       a  UTS,'   U" S
S-US-    S.[)        U5       STR                  S'S5       S/3US-   [)        U5      5        GM     UR]                  5         ['        S0U S.[)        U5       S135        SU;   Gax  U Vs/ s H>  oUS   S:X  d  M  UR                  S35      S4:w  d  M$  UR                  S5      (       d  M<  UPM@     nn[5        UXg5      u  nnU(       a  ['        S5U S35        U(       Ga   U" S
S[)        U5       S6[)        U5      S:w  a  SOS U(       a  SU S 3OS S!3S[)        U5      5         [6        R8                  " US7S#S$9nUR:                  (       a  UR:                  S   OUR=                  5       nSn[?        U5       GH8  u  nm [        R@                  " UTS   5      nU(       a  TRC                  U5        US-  nTR                  S85      SL TS9'   [D        RF                  RI                  [J        S%S&TS'   5      n[        RX                  " UTS   5      nU(       a:  [        RN                  " UUU5      n[R        RT                  " U=(       d    U5      TS+'   [D        R^                  " US#S:9  [        R`                  " UTS   US;9nU V s/ s H:  n [D        RF                  Rc                  U 5      Re                  S<5      (       d  M8  U PM<     n!n U V s/ s H:  n [D        RF                  Rc                  U 5      Re                  S=5      (       d  M8  U PM<     n"n U!(       a  [R        RT                  " U!5      TS>'   U"(       a  [R        RT                  " U"5      TS?'   [        RZ                  " UTS   5      nU(       a  UTS,'   U" S
S@US-    S.[)        U5       STR                  S'S5       S/3US-   [)        U5      5        GM;     UR]                  5         ['        SAU S.[)        U5       S135        S	U;   Ga½  U Vs/ s H'  oUS   S	:X  d  M  UR                  SC5      (       d  M%  UPM)     n#n[5        U#Xg5      u  n#n$U$(       a  ['        SDU$ S35        U#(       Ga\  U" S
SE[)        U#5       SF3S[)        U#5      5         [6        R8                  " USGS#S$9nUR:                  (       a  UR:                  S   OUR=                  5       nSn[?        U#5       H¸  u  nm["        Rf                  Ri                  TSC   5      n%U%(       aL   ["        RX                  " UU%Rk                  S5      5      nU(       a  [R        RT                  " U5      TS+'   US-  nU" S
SHUS-    S.[)        U#5       STR                  S'S5       S/3US-   [)        U#5      5        Mº     UR]                  5         ['        SIU S.[)        U#5       S135        SSS5        X44$ s  snf ! [,         a-  n['        U	 SU 35        UR/                  U	5         SnAG	Me  SnAff = f! [,         a*  n['        SU 35        UR/                  S5         SnAGNýSnAff = fs  snf ! [,         a     GNCf = f! [,         a  n['        S2U 35         SnAGNøSnAff = fs  snf s  sn f s  sn f ! [,         a     GN
f = f! [,         a  n['        SBU 35         SnAGN¿SnAff = fs  snf ! [,         a     GN�f = f! [,         a  n['        SJU 35         SnAGN7SnAff = f! , (       d  f       X44$ = f)Ka 	  Read the purchases screens, plus Motorway's dedicated Cancelled list.
Each read fails independently and loudly, one being down never hides another.
which platform(s) are read (a set of "Motorway"/"Carwow"/"Auction4Cars"/
"Dealer Auction"), None or all four means the normal full read, same
"smaller set is a partial run" shape as daily_run.run()'s own platforms
parameter.

Auction4Cars reads its own real "My Purchase History" screen (Mark
2026-08-27: "i now want to add a4c purchase info in to BB"), the account's
completed buys over its own 13 month window, found live to be the real,
wider source, "Won" only ever covers the last 3 months and reads 0 on
this account (nothing currently mid process). No payment breakdown,
documents, V5, collection date or DealerKit push support is built for
this platform yet, only the base capture (reg, name, price, mileage,
bought date), matching how Carwow's own purchases started before those
were added in later, separate sessions; a follow up, not this one.

Dealer Auction reads its own real Invoice history screen, Purchases
tab (Mark, same day: "ok lets do DA now"), found live under Dashboard
> Invoice history: a genuine itemised transaction ledger, no detail
page visit needed at all, registration, the full real fee breakdown
(vehicle price, buyer fee, VAT) and a real advert id are all already
on the one list, the richest of the three platforms' own purchase
records built so far. Newest first, a real page or two covers the
daily top up (dealerauction.read_purchase_invoices' own max_pages
default). It gained its own real photo gallery (Mark 2026-08-27,
"still no images in the DA cars") straight after, read off the won
advert's own page, the Invoice history list itself carries none.

Real progress is written throughout this whole function (Steven
2026-08-27, live: the purchases page's own run bar was showing a
stale message left over from an unrelated earlier daily run, since
this function, unlike push_dealerkit/check_dealerkit/refresh_purchase
in this same file, never wrote to run_progress.json at all, so the
bar had nothing real of its own to show for the several minutes this
can genuinely take, one page visit per purchase for its own payment
breakdown, photos, documents and gallery). Local import, matching
the convention already used everywhere else in this file.r   ©Ú	_progressr¶   r  rp   rH   rG   r   r   ÚreadingzReading ú purchases...ú: z purchases on the screenz purchases read FAILED: Nz.Reading Motorway's cancelled purchases list...)Ú	max_pageszMotorway cancelled list: z
 rows readz Motorway cancelled read FAILED: zMotorway cancelledc              3   óz   >#   • U  H0  nUTR                  S 5      =(       d    SR                  5       ;   v •  M2     g7f)r¿   rp   N)r   rË   )rÂ   rÃ   r!   s     €r$   rÅ   Úread_all.<locals>.<genexpr>d  s4   øé € ð "LÚ+J að #$¨¯©¨h«×(=¸2×'DÑ'DÓ'FÖ"FÚ+Jùs   ƒ8;)rÈ   rÇ   rÉ   z
Motorway: z3 purchases already complete, their pages left alonezReading payment details for z Motorway purchaser)   rÄ   ú (z already complete)ú...r	   T©ÚheadlessrU   Úpurchase_docsr1   rx   rÑ   rÔ   rÐ   rÖ   z.Reading payment details for Motorway purchase ú of ú)...zMotorway payment breakdown: z readz(Motorway payment breakdown read FAILED: r¿   ÚvoidedzCarwow: z Carwow purchaser
   rê   r  ©Úexist_ok©Úout_dirÚservice_record_Úv5c_logbook_rÒ   rÕ   z,Reading payment details for Carwow purchase zCarwow payment breakdown: z&Carwow payment breakdown read FAILED: Úlisting_urlzDealer Auction: zReading photos for z Dealer Auction purchases...r   z+Reading photos for Dealer Auction purchase zDealer Auction gallery: z$Dealer Auction gallery read FAILED: )6Ú	daily_runr!  rT   r   r5   r‚   r   rø   r  rü   r   r	   Úread_purchasesr
   Úread_wonr   Úread_purchase_historyr   Úread_purchase_invoicesr7   r:   ÚextendrD   r   Úread_cancelledrÌ   r  r   Úopen_reader_contextÚpagesÚnew_pageÚ	enumerateÚread_purchase_breakdownÚupdater}   r~   r   r€   Úread_purchase_photoÚdownload_purchase_mediaÚread_service_history_photosr†   ÚdumpsÚread_v5_photosÚread_purchase_galleryÚread_collection_dater…   ÚmakedirsÚread_purchase_documentsÚbasenamerÜ   ÚADVERT_URL_REÚsearchÚgroup)&Úcancelled_pagesÚ	platformsr!  r   Úfailuresr!   r¯   rü   r�   rR   ÚfnÚgotr  ÚcxÚmw_rowsÚmw_doneÚctxÚdpageÚgot_nÚiÚbdÚ
mw_doc_dirr‘   ÚlocalÚhistory_photosrÔ   ÚgalleryÚ	collectedÚcw_rowsÚcw_doneÚdoc_dirÚdocsrŽ   Úservice_docsÚv5_docsÚda_rowsÚda_doneÚms&        `                                r$   Úread_allrl    s˜
  ø€ õN $Ü+¨IÓ6€IØ€DØ€HäLN×L]ÒL]ÔL_Ó`ÒL_Àq�*‘œs 1§5¡5¨Ó#7×#=¸2Ó>Ð?ÀÒBÑL_€GÐ`Ü�M‰M×ÑÓ!€EÜ	Ô	˜aØ$¤h×&=Ñ&=Ð>ÀÌ6Ï?É?Ð@[Ø)¬<×+MÑ+MÐNØ+¬]×-QÑ-QÐRóT‰HˆDð Ó$ÙÙ�i 8¨D¨6°Ð!?Ô@ð&Ù˜“e�Ü˜˜˜b¤ S£ 
Ð*BÐCÔDØ—‘˜CÖ ñTð  ˜Ô"Ù�iÐ!QÔRð6Ü×,Ò,¨QÑJ�ÜÐ1´#°b³'°¸*ÐEÔFØ—‘˜B”ñ #'ô M¢$˜Q¨!¨J©-¸:Ñ*E“qÜ!ô "LÙ+Jó"L÷ L÷ ¡$ˆGð Mô  3°7ÀUÓKÑˆGÞÜ˜
 7 )Ð+^Ð_Ô`ßÙ˜)Ð'CÄCÈÃLÀ>ÐQcÔknÐovÓkwÐ{|Ók|Ñdgð  CEð  dFÞAH˜r ' Ð*<Ñ=ÈbÐQÐQTð&VàœS ›\ô+ðWJÜ!×5Ò5°a¸ÈdÑS�CØ,/¯I¯I˜CŸI™I ašL¸3¿<¹<»>�EØ�EÜ )¨'× 2™˜˜1ðK!Ü!)×!AÒ!AÀ%ÈÈ=ÑIYÓ!Z˜BÞ!Ø !§¡¨¤Ø %¨¡
 ô$ *,¯©¯©´d¸FÀOÐUVÐW\ÑU]Ó)^˜Jô %-×$@Ò$@ÀÓ$G˜EÞ$Ü(0×(HÒ(HÈÐQVÐPWÐYcÓ(d Þ#(Ø5:¸1±X A k¡Nô .6×-QÒ-QÐRWÓ-X˜NÞ-Ü(0×(HÒ(HÈÐP^Ð`jÓ(k Þ#(ÜBFÇ*Â*ÈUÓBS AÐ&>Ñ$?ô )1×(?Ò(?ÀÓ(F˜IÞ(Ü(0×(HÒ(HÈÐPYÐ[eÓ(f Þ#(Ü59·Z²ZÀÓ5F A k¡Nô '/×&DÒ&DÀUÈAÈmÑL\Ó&]˜GÞ&Ü(0×(HÒ(HÈÐPWÐYcÓ(d Þ#(Ü6:·j²jÀÓ6G A l¡Oô )1×(EÒ(EÀeÈQÈ}ÑM]Ó(^˜IÞ(Ø7@ Ð"3Ñ 4ñ " )Ð/]Ø%&¨¡U G¨4´°G³¨~¸RÀÇÁÀeÈRÓ@PÐ?QÐQUð.Wà"# a¡%¬¨W«÷7ñ[ !3ð` —I‘I”KÜÐ8¸¸¸tÄCÈÃLÀ>ÐQVÐWÔXð �yÔ Ù"&ó P¢$˜Q¨J©-¸8Ñ*C“qØŸ%™% ›/¨XÑ5ó Ø:;¿%¹%À×:N÷ ¡$ˆGð Pä2°7¸GÓKÑˆG�WÞÜ˜  	Ð)\Ð]Ô^ßÙ˜)Ð'CÄCÈÃLÀ>ÐQaÔilÐmtÓiuÐyzÓizÑbeð  ACð  bDÞAH˜r ' Ð*<Ñ=ÈbÐQÐQTð&VàœS ›\ô+ð?HÜ!×5Ò5°a¸ÈDÑQ�CØ,/¯I¯I˜CŸI™I ašL¸3¿<¹<»>�EØ�EÜ )¨'× 2™˜˜1ð3!Ü!'×!?Ò!?ÀÀqÈÑGWÓ!X˜BÞ!Ø !§¡¨¤Ø %¨¡
 à-.¯U©U°>Ó-BÀdÐ-J˜A˜k™Nô ')§g¡g§l¡l´4¸ÀÐRSÐTYÑRZÓ&[˜GÜ&,×&BÒ&BÀ5È!ÈMÑJZÓ&[˜GÞ&Ü(.×(FÒ(FÀuÈgÐW^Ó(_ Ü26·*²*¸U×=MÀgÓ2N  ,¡ô ŸKšK¨¸$Ò?Ü#)×#AÒ#AÀ%ÈÈ=ÑIYÐcjÑ#k˜Dñ 8<Ó+q²t°!¼r¿w¹w×?OÑ?OÐPQÓ?R×?]Ñ?]Ð^o×?p¯A±t˜LÐ+qÙ26Ó&i²$¨Q¼"¿'¹'×:JÑ:JÈ1Ó:M×:XÑ:XÐYg×:h§q±$˜GÐ&iÞ+Ü6:·j²jÀÓ6N Ð"2Ñ 3Þ&Ü6:·j²jÀÓ6I Ð"2Ñ 3ô )/×(CÒ(CÀEÈ1È]ÑK[Ó(\˜IÞ(Ø7@ Ð"3Ñ 4ñ " )Ð/[Ø%&¨¡U G¨4´°G³¨~¸RÀÇÁÀeÈRÓ@PÐ?QÐQUð.Wà"# a¡%¬¨W«÷7ñk !3ðp —I‘I”KÜÐ6°u°g¸TÄ#ÀgÃ,ÀÈuÐUÔVð ˜yÔ(Ù"&Óe¢$˜Q¨J©-Ð;KÑ*K“qÐPQ×PUÑPUÐVc×Pd—q¡$ˆGÐeÜ2°7¸GÓKÑˆG�WÞÜÐ(¨¨	Ð1dÐeÔfßÙ˜)Ð':¼3¸w»<¸.ÐHdÐ%eØœS ›\ô+ðFÜ!×5Ò5°a¸ÐSWÑX�CØ,/¯I¯I˜CŸI™I ašL¸3¿<¹<»>�EØ�EÜ )¨'Ö 2™˜˜1Ü)×7Ñ7×>Ñ>¸qÀÑ?OÓP˜Þð%Ü*7×*MÒ*MÈeÐUV×U\ÑU\Ð]^ÓU_Ó*` Þ#*Ü6:·j²jÀÓ6I A l¡OØ$)¨Q¡J Eñ " )Ð/ZØ%&¨¡U G¨4´°G³¨~¸RÀÇÁÀeÈRÓ@PÐ?QÐQUð.Wà"# a¡%¬¨W«ö7ñ !3ð —I‘I”KÜÐ4°U°G¸4ÄÀGÃ¸~ÈUÐSÔT÷q 
ðv ˆ>Ðùò{ aøô ó &Ü˜˜Ð6°q°cÐ:Ô;Ø—‘ ×%Ó%ûð&ûô ó 6ÜÐ8¸¸Ð<Ô=Ø—‘Ð 4×5Ò5ûð6üòMøôr  )ó !Ú ð!ûô !ó JÜÐDÀQÀCÐH×IÒIûðJüòPùòb ,rùÚ&iøô  )ó !Ú ð!ûô !ó HÜÐBÀ1À#ÐF×GÒGûðHüò føô& $-ó %Ú $ð%ûô !ó FÜÐ@ÀÀÐD×EÒEûðFú÷s 
Ô	ðv ˆ>ÐúsÏ  ®2g?Â
A l3Ã+3hÄl3Ä3=h>Å0l3Å5i5Æi5Æ"i5Æ(A2l3ÈAjÉ6Gi:ÑA-jÒ1l3Ò<j1Ój1Ó#j1Ó;j1ÔA3l3Õ5Ak×Dk Û7j6Üj6Ük Ü7j;Ýj;ÝA(k ßA-kà0l3à;k7ák7á#k7á)Al3â:Blä=Ak<æA,lè
h;è!h6è/l3è6h;è;l3è>
i2éi-é'l3é-i2é2l3é:
jêjêjêjê
j.êj)ê#l3ê)j.ê.l3ê6
k ë 
kë
këkëkë
k4ëk/ë)l3ë/k4ë4l3ë<
l
ìlì	l
ì
lì
l0ìl+ì%l3ì+l0ì0l3ì3
mc                 óÎ  • SS/nS GH4  nU  Vs/ s H  o3S   U:X  d  M  UPM     nnU(       d  M)  UR                  U5        U Hã  nUR                  S5      =(       d    Sn[        UR                  S5      =(       d    UR                  S5      5      nUR                  S5      (       a  S	OSnUR                  S
5      nU(       a  S[        U5       S3OSn	UR                  SU SUR                  SS5       35        UR                  SU U SU	 35        UR                  S5        Må     UR                  S5        GM7     UR                  S5        SR                  U5      $ s  snf )z†The email body for accounts: what was bought, what was paid, and the
retail each car can go up at. Plain text, house style, no dashes.z.Cars bought today, confirmed on the platforms.rp   ©rH   rG   r   r   r¶   r1   zno plateÚtotal_pricer  z including fees and VATr   zAchievable retail Ú.z)Not valued by BidBrain, price it by hand.r0   rR   z  Paid z. zCSent automatically by BidBrain after the day's purchases were read.Ú
)r   r   r   r   )
r"   Úlinesrð   r!   Úcarsr1   ÚpaidÚ	paid_noteÚretailÚretail_notes
             r$   Úcomposerx  ?  sD  € ð >¸rÐB€EÜHˆÙÓ8š3�a J¡-°4Ñ"7—™3ˆÐ8ÞÙØ�‰�TÔÛˆAØ—%‘%˜“,×, *ˆCÜ˜!Ÿ%™% Ó.×@°!·%±%¸³.ÓAˆDØ56·U±U¸=×5IÑ5IÑ1ÈrˆIØ—U‘UÐ,Ó-ˆFÞEKÐ/´°v³Ð/?¸qÑAØ Kð à�L‰L˜2˜c˜U " Q§U¡U¨6°2Ó%6Ð$7Ð8Ô9Ø�L‰L˜7 4 &¨¨°2°k°]ÐCÔDØ�L‰L˜Öñ ð 	�‰�R×ñ Ið  
‡L�LÐVÔWØ�9‰9�UÓÐùò! 9s
   �E"ŸE"c                 ó>   • Sn[         R                  " SSX0X/SSSS9  g)z�Send through the Mail app using the account already signed in on this
Mac. The message text is passed as arguments, never built into the script.a˜  
    on run argv
        set theSubject to item 1 of argv
        set theBody to item 2 of argv
        set theTo to item 3 of argv
        tell application "Mail"
            set m to make new outgoing message with properties {subject:theSubject, content:theBody, visible:false}
            tell m to make new to recipient with properties {address:theTo}
            send m
        end tell
    end run
    Ú	osascriptz-eTrò   )ÚcheckÚcapture_outputro   N)Ú
subprocessÚrun)ÚsubjectÚbodyÚtoÚscripts       r$   Ú	send_mailrƒ  W  s*   € ð€Fô ‡N‚N�K  v¸ÐAØ¨d¸Bó@r&   c                 ó°   • SSK Jn  U(       a)  SR                  U5       S3nUR                  " SSU5        gU  SU S3nUR                  " SS	S
U S35        g)zõThe sync's terminal write, both halves, through the shared
bidbrain.runs.finish. A platform that could not be read is a real
failure even though everything else stored fine. Split out of main()
so the outcome logic is testable without a browser.r   ©r   ú, z could not be readÚ	purchasesFz purchases checked, ú newTz	Updated. rp  N)Úbidbrainr   r   r6   )Úrows_nÚnew_nrS  r   Úsummarys        r$   Ú_finish_syncr�  j  s]   € õ
 ÞØ—Y‘Y˜xÓ(Ð)Ð);Ð<ˆØ�Š�K ¨Õ0à�HÐ0°°°tÐ<ˆØ�Š�K ¨°7°)¸1Ð'=Õ>r&   c                  óÐ  • SSK Jn   SSKJn  UR	                  S5        [
        R                  " 5         S[        R                  ;   nS[        R                  ;   n[        5       nS[        R                  ;   a  UR                  S5        S	[        R                  ;   a  UR                  S
5        S[        R                  ;   a  UR                  S5        S[        R                  ;   a  UR                  S5        S[        R                  ;   n[        U(       a  SOSU=(       d    S S9u  pgU Vs/ s H  n[        U5      (       d  M  UPM     n	nU Vs/ s H  n[        U5      (       a  M  UPM     nnU	(       Gaž  [
        R                  " U	5      n
[        [        U	5       S[        U
5       S35        [
        R                   " 5       n[        5       nU	 GH!  nSR#                  [%        UR'                  S5      =(       d    S5      R)                  5       R+                  5       5      nU HÉ  nUS   U;   d  US   (       d  US   US   :w  a  M$  UR'                  S5      =(       a    [%        US   5      [%        US   5      :H  nU=(       a    US   U:H  nU(       d!  UR'                  S5      (       a  M‰  U(       d  M’  [
        R,                  " US   SSUS    3SSS9  UR                  US   5          GM     GM$     U(       a  [        [        U5       S35        [/        5       u  nnU H}  nSR#                  [%        UR'                  S5      =(       d    S5      R)                  5       R+                  5       5      nUR'                  S 5      (       a  Mi  UR'                  U5      US '   M     [1        5       u  nn[3        U[5        5       UU5      u  nnU(       d  U(       a  [        S!U S"U S#35        U Vs/ s H;  oˆS   S$;   d  M  UR'                  S%5      b  M!  UR'                  S&5      (       d  M9  UPM=     nnU(       Ga1  [        S'[        U5       S(35        U " S)S'[        U5       S*3S[        U5      5        [7        5        n[9        U5       HÖ  u  nn UR'                  S+5      =(       d    SR+                  S S5      nU(       a  US   OSn[        U5      S:”  a  US   OUn[:        R<                  " XøS   US&   UU4S,9u  nnUb   [?        US--  S.5      US%'   [A        UUUS/9US0'   U " S)S3US-    S4[        U5       S5UR'                  SS5       S63US-   [        U5      5        MØ     S S S 5        U H  nURE                  [G        U5      5        M     [
        RH                  " XcS79n![        [        U5       S8[        U!5       S935         [K        5       n"U"(       a  [        S1U" S:[L         S;35         [5        5       n#[
        RN                  " 5       =(       d    0 R'                  S=5      =(       d    / n$[Q        [
        R                   " 5       U#U$5       H5  u  n%n&n'[
        RR                  " U%S=U&U' S>3S?9  [        S@U% SAU& SBU' 35        M7     [U        [
        R                   " 5       U$5       H/  u  n%n&[
        RR                  " U%S=U&SCS?9  [        S@U% SAU& SD35        M1     [W        [        U5      [        U!5      U5        U(       a  [        SF5        g [
        RX                  " 5       n(U((       d1  [        SG5        U(       a  [        SHSIR#                  U5       SJ35        g SS K-n)SKU)R\                  R_                  5       Ra                  SL5       3n*[c        U(5      n+U(       a3  [        SM5        [        SN[d        5        [        SOU*5        [        U+5        g [g        U*U+5        [
        Rh                  " U( Vs/ s H  oˆS   PM	     sn5        [        SP[        U(5       SQ[d         SR35        U(       a  [        SHSIR#                  U5       SS35        g g s  snf s  snf s  snf ! [B         a  n [        S1US    S2U  35         S n A GNYS n A ff = f! , (       d  f       GN'= f! [B         a  n [        S<U  35         S n A GNÆS n A ff = f! [B         a  n [        SEU  35         S n A GN S n A ff = fs  snf )TNr   r   r…  r‡  z	--dry-runz
--baselinez
--motorwayrH   z--carwowrG   z--auction4carsr   z--dealerauctionr   z--backfill-cancelledé   r)   )rQ  rR  z cancelled/voided sales seen, z1 newly suppressed so the car is never shown againrp   r1   r*   r   r¶   r  z$Customer cancelled before collectionzCancelled on r,   )r/   ÚsourcezL of those were on the purchases page and are now flagged cancelled there toorx   z#Reused what BidBrain already knew: z mileage(s), z valuation(s)rn  r   rŸ   zLooking up Cazana retail for z, purchases with no recent BidBrain valuationÚvaluingr#  rR   )Úexpectr   rº   r   r   r0   z: Cazana lookup failed, zLooking up Cazana retail, r-  r(  r.  )Úbaselinez purchases stored, rˆ  z( Carwow fee estimate(s) now stand, past z dayszFee estimate notes skipped: rª   z listingr  z  purchase z: On finance? set to z, read from zthe purchase pagez2, from the settlement figures on the purchase pagez#Finance from the platform skipped: z!Baseline seeded, nothing emailed.zNo new buys, no email today.zATTENTION: r†  z9 could not be read, a buy there would not have been seen.zCars bought z%d %B %Yz 
--- DRY RUN, email not sent ---zTo:zSubject:zEmailed z	 buys to rp  z could not be read.)5r7  r!  r‰  r   Úbeginr   r3   ÚsysÚargvrK   rM   rl  rÍ   Úrecord_walked_awayr7   r:   r5   r   r‚   r   rƒ   r„   Úcancel_purchaser’   r4   r½   r¨   r   rA  r   Úlookuprc   r   rD   rC  r
  Úrecord_purchasesr  rú   Úget_purchase_dropdownsr¸   Úset_purchase_fieldr´   r�  Úunemailed_purchasesrø   r  rü   Ústrftimerx  ÚACCOUNTS_EMAILrƒ  Úmark_purchases_emailed),r!  Ú_runsÚdryr“  rR  Úbackfillr   rS  r!   Ú	cancelledÚnewlyr¯   Úflaggedr_   r1   r�   Úsame_refÚsame_regÚ_r�   rf   r   r   Úreused_mÚreused_vÚneed_cazanar\  ÚpartsÚmakeÚmodelÚvalÚ_urlr  r"   ÚagedÚ	fin_knownÚfin_optsÚpidr·   rð   Úpendingrø   r  r€  s,                                               r$   Úmainr·  x  sÐ  € Ý#Ý&Ø	‡K�K�ÔÜ‡J‚J„LØ
œŸ™Ñ
!€CØœsŸx™xÑ'€Hô “€IØ”s—x‘xÓØ�‰�jÔ!Ø”S—X‘XÓØ�‰�hÔØœ3Ÿ8™8Ó#Ø�‰�nÔ%ØœCŸH™HÓ$Ø�‰Ð&Ô'ð &¬¯©Ñ1€HÜ¶H©bÀ!Ø)2×):°dñ<�N€Dñ !Ó5šD�q¤M°!×$4—™D€IÐ5ÙÓ4’t�!¤=°×#3�A‘t€DÐ4ßÜ×%Ò% iÓ0ˆÜ”�Y“Ð Ð >¼sÀ5»z¸lð K0ð 0ô 	1ô ×#Ò#Ó%ˆÜ“%ˆÜˆAØ—'‘'œ#˜aŸe™e E›l×0¨bÓ1×7Ñ7Ó9×?Ñ?ÓAÓBˆCÛ�Ø�T‘7˜gÓ%¨¨>×):¸aÀ
¹mÈqÐQ[É}Ó>\ÙØŸ5™5 Ó/×b´C¸¸-Ñ8HÓ4IÌSÐQRÐS`ÑQaÓMbÑ4b�Ø×2 1 U¡8¨s¡?�Þ A§E¡E¨-×$8Ó$8¿X¸XÜ×&Ò&Ø˜$™Ð!GØ'¨¨*© Ð7¸ZØ)ò+ð —K‘K  $¡Ô(Ûô ñ ö Ü”S˜“\�Nð #0ð 0ô 1ô “�I€A€vÛˆØ�G‰G”C˜Ÿ™˜e›×*¨Ó+×1Ñ1Ó3×9Ñ9Ó;Ó<ˆð* �u‰u�[×!Ó!Ø#ŸZ™Z¨›]ˆAˆk‹Nñ/ ôp .Ó/�N€FˆFÜ% d¬L«N¸FÀFÓKÑ€HˆhÞ–8ÜÐ3°H°:¸]È8È*ÐTaÐbÔcÙ"ó Hšd˜¨
¡mÐ7oÑ&o“1Ø—e‘e˜LÓ)ó Ø67·e±e¸I×6F÷ ™d€Kð HçÜÐ-¬c°+Ó.>Ð-?ð @<ð =ô 	>á�)Ð<¼SÀÓ=MÐ<NÈmÐ\Ø”S˜Ó%ô	'äÔ !Ü! +Ö.‘��1ð	FØŸU™U 6›]×0¨b×7Ñ7¸¸aÓ@�EÞ',˜5 š8°"�DÜ(+¨E«
°Q«˜E !šH¸D�EÜ &§¢¨a°5±¸1¸Y¹<ÐQUÐW\ÐP]Ñ ^‘I�C˜Ø‘Ü*/°°d±
¸AÓ*>˜˜,™Ü/BÀ3ÈvÐ^dÑ/e˜Ð+Ñ,ñ ˜)Ð'AÀ!ÀaÁ%ÀÈÌSÐQ\ÓM]ÐL^ð _ØŸe™e E¨2Ó.Ð/¨tð&5Ø67¸!±e¼SÀÓ=MöOñ /÷ ó& ˆØ	�‰”˜qÓ!Ö"ñ ä
×
Ò
˜dÑ
6€CÜ	ŒS�‹YˆKÐ*¬3¨s«8¨*°DÐ
9Ô:ð2Ü(Ó*ˆÞÜ�B�t�fÐDÄ^ÐDTÐTYÐZÔ[ð
9Ü “Nˆ	Ü×-Ò-Ó/×5°2×:Ñ:¸<ÓH×NÈBˆÜ!1´"×2CÒ2CÓ2EÀyÐRZÖ![ÑˆC�˜Ü×!Ò! # |°VÀDÀ6ÈÐARÒSÜ�K ˜uÐ$9¸&¸ÀÈdÈVÐTÖUñ "\ô 1´×1BÒ1BÓ1DÀhÖO‰KˆC�Ü×!Ò! # |°VÐATÒUÜ�K ˜uÐ$9¸&¸ÐAsÐtÖuñ Pô
 ”�T“œC ›H hÔ/æÜÐ1Ô2Øä×$Ò$Ó&€GÞÜÐ,Ô-ÞÜ�K §	¡	¨(Ó 3Ð4ð 5:ð :ô ;àãØ˜XŸ]™]×0Ñ0Ó2×;Ñ;¸JÓGÐHÐI€GÜ�7Ó€DÞ
ÜÐ1Ô2Üˆe”^Ô$Üˆj˜'Ô"ÜˆdŒØÜˆg�tÔÜ×Ò±Ó8²¨1 œw±Ñ8Ô9Ü	ˆH”S˜“\�N )¬NÐ+;¸1Ð
=Ô>ÞÜ�˜DŸI™I hÓ/Ð0Ð0CÐDÕEð ùòY 6ùÚ4ùò~Høô" !ó FÜ˜B˜q ™x˜jÐ(@ÀÀÐD×EÒEûðFú÷ Öûô6 ó 2ÜÐ,¨Q¨CÐ0×1Ò1ûð2ûô ó 9ÜÐ3°A°3Ð7×8Ò8ûð9üò4 9sŸ   Ä,a
Åa
ÅaÅ(aÏ0aÐ aÐaÐ,aÑ?bÒBaÔ$Ab×'b ×0C+b= ß/c#á
bá#a=á7bá=bâbâ
bâ
b:â!b5â5b:â=
c ãcãc c           
      óŽ  • SSK nSSKnS Hv  n[        5        n[        R                  " USSS9nUR
                  (       a  UR
                  S   OUR                  5       n U " U5      UR                  5         sSSS5        s  $    g! [        R                   a=  nUS:X  a"  [        R                  " SS[        U5      5        e [        S5         SnAOSnAff = f UR                  5         O! UR                  5         f = f SSS5        O! , (       d  f       O= fUR                  " UR                  S	/S
S
SS9nUR                   S:w  d  GMC  UR"                  UR$                  -   R'                  5       R)                  5       SS =(       d    S/n	[        R                  " SSU	S   5        [        R                  " SU	S    35      e)a—  Run work(page) against DealerKit; if the saved login has expired, log
back in once with the stored password (auto_login_dealerkit.py, the
same thing the daily passes do) and try again, so a job started from
Dealer OS never dies on a stale session while nobody is at the Mac
(2026-09-03, found live: a check asked for from Dealer OS failed on
exactly that). A second failure is the real answer and is raised.r   N)r)   rº   Ú	dealerkitFr*  rº   zQDealerKit's saved login has expired, logging in again with the stored password...zauto_login_dealerkit.pyTé,  )r|  r  ro   éÿÿÿÿzno reason givenzADealerKit's saved login has expired and logging in again failed: )r}  r•  r   r   r>  r?  r@  r…   r   ÚDealerkitSessionExpiredr   Úrecord_site_healthr‚   r7   r~  Ú
executableÚ
returncodeÚstdoutÚstderrrÊ   Ú
splitlines)
Úworkr}  r•  Úattemptr�   rY  Úpager  r!   Úmsgs
             r$   Ú_dealerkit_with_reloginrÇ  G  sw  € ó ÛÛˆÜÔ !Ü×-Ò-¨a°ÀuÑMˆCØ#&§9§9�3—9‘9˜Q’<°#·,±,³.ˆDðÙ˜D“zð —	‘	”÷ Óò øô #×:Ñ:ó kØ˜a“<Ü×)Ò)¨+°u¼cÀ!»fÔEØÜÐi×jÑjûð	kúð kà—	‘	•ø�—	‘	•ú�÷ ×Öúð �NŠN˜CŸN™NÐ,EÐFÐW[ÐbfÐpsÑtˆØ�<‰<˜1ÖØ—8‘8˜aŸh™hÑ&×-Ñ-Ó/×:Ñ:Ó<¸R¸SÐA×XÐFWÐEXˆCÜ×!Ò! +¨u°c¸!±fÔ=Ü!×9Ò9ØSÐTWÐXYÑTZÐS[Ð\ó^ð ^sH   ˜ADÁ BÁ'DÂCÂ3CÃC,ÃCÃC,ÃDÃ,C>Ã>DÄ
D	zPurchase pricezBuyer's feeÚMileageÚPhotosÚ	DocumentszRetail pricezDue in datezPrice and fee after the chipÚ	TransportzCheck inÚRemovezPrice and fee)r  rì   rŸ   r�   Ú	documentsrv  Údue_in_dateÚchipÚ	transportÚcheckinÚremoveÚfiguresc           	      ó¢  • [         R                  " U S   SS[        U    SUS    3US9  UR                  S5      c  [         R                  " U S   S	5        gUR                  S
5      (       d  gUS:X  a  [         R
                  " U S   U/S9  gUS;   aG  [         R
                  " U S   US:X  a  / SQOSS/U R                  S5      U R                  S5      S.S9  gUS:X  aM  UR                  S5      (       a  [         R                  " U S   US   US9  [         R                  " U S   S5        gUS	:X  a  [         R                  " U S   S	5        gg)aŒ  Write what one DealerKit follow up (chip, transport, checkin,
remove) actually did to the car's timeline and stamp the purchases
row (2026-09-05). Shared by the one car job and the automatic run so
the two can never disagree. A follow up that found no DealerKit record
at all records the car as not on DealerKit (dk_removed_at), so the
automatic run stops asking and a later Send can start clean.r*   r¹  úDealerKit: Ú Úoutcomer  Ú
vehicle_idNrÒ  ÚdonerÐ  r1   ©r–   )rÏ  rÓ  rÏ  )rÏ  r  rì   r  rì   Ú	car_priceÚeffective_fee©r  rì   ©r–   ÚsentrÑ  Údelivered_on)r   r9   ÚDK_ITEM_LABELSr   Ústamp_dkÚmark_dealerkit_pushedÚset_checked_in_at)r˜   ÚactionÚresultr/   s       r$   Úrecord_followup_outcomerç  l  sB  € ô ×Ò˜#˜d™) [Ø'¬°vÑ(>Ð'?¸qÀÈ	ÑARÐ@SÐTÐZ]ò_à‡z�z�,ÓÑ'Ü
�Š�C˜‘I˜xÔ(ØØ�:‰:�f×ÑØØ�ÓÜ
× Ò   U¡°F°8Ó<Ø	Ð&Ó	&ô 	× Ò   U¡ÈvÐY_ÓO_Ó3KÐfmÐotÐeuØ03·±¸Ó0DÈSÏWÉWÐUdÓMeÑ&fó	hà	�9Ó	ð �:‰:�n×%Ñ%Ü× Ò   T¡¨F°>Ñ,BÈÒLÜ
�Š�C˜‘I˜yÕ)Ø	�8Ó	Ü
�Š�C˜‘I˜xÕ(ð 
r&   c           
      óh  • U=(       d    0 R                  S5      (       a  UR                  S5      (       d  g [        R                  " XS   US   5      nUb  [        R                  " US   U5        gg! [
         a3  n[        SUS    S[        R                  " US5       35         SnAgSnAff = f)a0  After a follow up that left a figure alone on DealerKit (a price a
person typed there, or one held under another contact), read the
record again and store it (2026-09-16), so Dealer OS shows what
DealerKit holds, with the Update the price and fee button, and the
automatic run stops asking. Never raises.ÚleftrØ  Nr1   r0   z6: DealerKit could not be re read after the follow up, rZ   )	r   r   Úcheck_dealerkit_recordr   Úset_dealerkit_checkrD   r7   r   Ú
first_line)rÅ  r˜   ræ  r{  r  s        r$   Úrefresh_check_if_leftrí  �  s«   € ð �L�b×Ñ˜f×%Ñ%¨V¯Z©Z¸×-EÑ-EØðpÜ×6Ò6°tÀ¹ZÈÐP\ÑI]Ó^ˆØÑÜ×"Ò" 3 u¡:¨uÕ5ð øäó pÜ��3�u‘:�,ÐTÔUY×UdÒUdÐefÐhkÓUlÐTmÐn×oÑoûðpús   ¸:A4 Á4
B1Á>)B,Â,B1c           	      ó@  ^ ^• SSK Jn  SR                  S T 5       5      n[        SU ST S    S35        U" S	SU ST S    S
35        UU 4S jn [	        U5      nT H/  n[        T XuU   5        [        S[        U    SXW   S    35        M1     [        R                  " SSSU ST S    S35        U$ ! [
        R                   a&  n[        R                  " T S   SSU SU 3SS9  e SnAf[         a&  n[        R                  " T S   SSU SU 3SS9  e SnAff = f)z±One car, one or more of the four follow ups, in one DealerKit
session (python3 purchases_run.py --push-dealerkit REG --items chip).
Nothing here ever creates a DealerKit record.r   r   r†  c              3   ó4   #   • U  H  n[         U   v •  M     g 7fr]   )rá  )rÂ   r�   s     r$   rÅ   Ú+push_dealerkit_followups.<locals>.<genexpr>¤  s   é € Ð:²'¨Q”~ aÖ(²'ùs   ‚zSending z for r1   z to DealerKit...údealerkit-loginú to DealerKitc                 óp   >• 0 nT H,  n[         R                  " U TU5      X'   [        U TX   5        M.     U$ r]   )r   Úpush_followuprí  )rÅ  r    r�   Úactionsr˜   s      €€r$   rÃ  Ú&push_dealerkit_followups.<locals>.work¨  s=   ø€ ØˆÛˆAÜ$×2Ò2°4¸¸aÓ@ˆC‰FÜ! $¨¨S©VÖ4ñ ð ˆ
r&   r*   r¹  ú
DealerKit z	 failed: r,   r  Nr0   r$  r×  Údealerkit_push_rowTzSent ú to DealerKit.)r7  r!  r   r7   rÇ  r   r¼  r   r9   ÚRuntimeErrorrç  rá  r   r6   )r˜   rõ  r!  ÚlabelsrÃ  Úresultsr  r�   s   ``      r$   Úpush_dealerkit_followupsrý  Ÿ  sI  ù€ õ $Ø�Y‰YÑ:±'Ó:Ó:€FÜ	ˆH�V�H˜E # e¡* Ð-=Ð
>Ô?ÙÐ 8¨F¨8°5¸¸U¹¸ÀMÐ!RÔSöðÜ)¨$Ó/ˆó ˆÜ  Q°©
Ô3Ü�”> !Ñ$Ð% R¨©
°9Ñ(=Ð'>Ð?Ö@ñ ô 	‡K‚KÐ$ d¨e°F°8¸5ÀÀUÁÀÈNÐ,[Ô\Ø€Nøô ×2Ñ2ó Ü
×Ò˜c $™i¨¸
À6À(È)ÐTUÐSVÐ6WÐ]gÒhØûÜó Ü
×Ò˜c $™i¨¸
À6À(È)ÐTUÐSVÐ6WÐ]gÒhØûðús$   ÁB5 Â5DÃ	!C*Ã*DÃ7!DÄDc                 ó  • U c   [         R                  " 5       n U =(       d    0 R                  S5      n Ub  US:X  a  SO
[	        U5      n[        S[        SU5      5      $ ! [         a    0 n  NUf = f! [
        [        4 a    Sn N<f = f)z How many photos a whole send puts on DealerKit: the dealer's own
setting (dealerkit_photos, Buying rules), 4 when nothing is saved,
always 1 to 20 (2026-09-15).Údealerkit_photosrp   é   r)   r�  )	r   rC   rD   r   Úintrá   r¥   Úmaxrd   )rE   Úrawr  s      r$   Údealerkit_photo_countr  ¾  s�   € ð �}ð	Ü—O’OÓ%ˆEð �;�B×
Ñ
Ð.Ó
/€CðØ‘  r£	‰A¬s°3«xˆô ˆq”#�b˜!“*ÓÐøô ó 	ØŠEð	ûô
 ”zÐ"ó ØŠðús"   …A! µA3 Á!A0Á/A0Á3BÂBc                 óp   • [        U R                  S5      5      =(       a    U R                  S5      (       + $ )a’  A chip recorded before a whole send is carried by the price the
send writes (car_price is the price after every chip), so the send
stamps the chip as pushed too (2026-09-15, found live on LP68RHK: the
DealerKit tab read "1 change to send" and the 16:15 run would have set
the same price again). A chip after the send clears the stamp as it
always did (serve._chip_purchase), so a later chip still goes.ÚchipsÚdk_chip_pushed_at)r¢   r   )r˜   s    r$   Úchip_included_in_sendr  Ï  s*   € ô �—‘˜Ó Ó!×F¨#¯'©'Ð2EÓ*FÔ&FÐFr&   c                 óÄ  • U(       d  / $ UR                  S5      (       aã  UR                  S5      (       a  / $ UR                  S5      =(       d    SR                  SS5      R                  5       nU=(       d    S H�  nUR                  S5      UR                  S5      :w  d  M)  UR                  S5      =(       d    SR                  SS5      R                  5       U:X  d  Mg  UR                  S5      (       a  M  / s  $    S/$ / nUR                  S	5      (       a'  UR                  S
5      (       d  UR                  S5        UR                  S5      UR                  S5      4U R                  S5      U R                  S5      4:w  a  UR                  S5        UR                  S5      b5  UR                  S5      U R                  S5      :w  a  UR                  S5        U$ )a1  What changed on a row while its whole send to DealerKit ran, and
so is due on DealerKit the moment the send is in (2026-09-15, Steven:
"never wait till the 4.15 run"). Collected in Dealer OS sends the car
whole; a check in pressed a minute later used to be refused as busy and
fell to the 16:15 run. `before` is the row the send read, `fresh` the
row read again after it. A cancelled sale is a removal and nothing
else; otherwise a check in not yet mirrored, a price or fee that moved
(a chip at the door), a transport fee set meanwhile. The Dealer OS
door's own refusals cannot be asked here (the send is not stamped
until it returns, so the row does not yet read as on DealerKit); the
one that matters is kept: a removal is never done while another live
purchase shares the registration (DealerKit is found by reg).r   Údk_removed_atr1   rp   rÖ  rÁ   r*   rÒ  Úchecked_in_atÚdk_checked_in_atrÑ  rÛ  rÜ  rÓ  Útransport_feerÐ  )r   Úreplacerƒ   r   )ÚbeforeÚfreshr   r1   Úotherr    s         r$   Úafter_send_followupsr  Ù  s~  € ö Øˆ	Ø‡y�y�× Ñ Ø�9‰9�_×%Ñ%ØˆIØ�y‰y˜Ó×% 2×.Ñ.¨s°BÓ7×=Ñ=Ó?ˆØ—Z˜R’ZˆEØ�y‰y˜‹ %§)¡)¨D£/Õ1°u·y±yÀÓ7G×7MÈ2×6VÑ6VÐWZÐ\^Ó6_×6eÑ6eÓ6gÐknÕ6nØ!ŸI™I n×5Ó5Ø’	ñ  ð ˆzÐØ
€CØ‡y�y�×!Ñ!¨%¯)©)Ð4F×*GÑ*GØ�
‰
�9ÔØ�	‰	�+Ó §	¡	¨/Ó :Ð;ÀÇ
Á
È;Ó@WÐY_×YcÑYcÐdsÓYtÐ?uÓuØ�
‰
�9ÔØ‡y�y�Ó!Ñ-°%·)±)¸OÓ2LÐPV×PZÑPZÐ[jÓPkÓ2kØ�
‰
�;ÔØ€Jr&   c           
      óT
  ^ ^^^• SSK Jn  [        R                  " 5       n[	        U 4S jU 5       S5      mT(       d  [        T  S35      e[        T=(       d    S5      [        [        R                  5      -  nU(       aH  [        T=(       d    S5      [        [        R                  5      -
  (       d  [        T[        U5      5      $ [        STS    S	TS
    STS    S35        U" SSTS    S35        UU4S jn [        U5      n[*        mUR-                  S5      =(       d    0 n
U
(       a6  SSR#                  U4S j[        U
R%                  5       5       5       5      -   nOS n[        R                  " TS   SUSS9  US!   (       aä  [        R.                  " TS   US!   TR-                  S"5      TR-                  S#5      S$.S%9  S&US!   ;   až  [1        T5      (       aŽ  [        R.                  " TS   S'/TR-                  S"5      TR-                  S#5      S$.S%9  [        R                  " TS   SS(TR-                  S"5      =(       d    SS) S*TR-                  S+5      =(       d    SS) S,3SS9  UR-                  S-5      =(       d    TnUR-                  S.5      =(       d    0 R%                  5        HÊ  u  pÞ[*        R-                  XÝ5      nUR-                  S/5      (       a9  [        R                  " TS   SS0U S1US/    3SS9  [        S2U S3US/    35        Mi  UR-                  S45      c/  [        R                  " TS   SS0U S53SS9  [        S2U S635        Mª  [3        XÍU5        [        S2U S7US8    35        MÌ     UR-                  S95      b  [        R4                  " TS   US9   5        [&        R6                  " S:S;S<TS    S=35        US>   (       a  [        US    S?35        O[        US    S@35        [        R8                   H"  nUU
;   d  M  [        S2TU    SAU
U    35        M$     [        SBUS4    35        TSC   SD;  a  [        SE5        gg! [        R                   a#  n[        R                  " TS   SSU 3SS9  e SnAf[         a�  n[        R                   =(       d    0 nSR#                  S [        UR%                  5       5       5       5      n	[        R                  " TS   SS[&        R(                  " US5       3U	(       a  SU	 S3OS-   SS9  e SnAff = f)Fa  Manual, one car at a time trigger: python3 purchases_run.py
--push-dealerkit REG [--items price,photos,documents,retail,due_in_date].
Opens a real HEADED DealerKit window (so it
can be watched and reviewed, per Mark's own "test first" plan,
2026-08-21) and pushes that purchase in as a Due In record with its
photos and service history documents. items is an optional subset of
price/photos/documents/retail/due_in_date (2026-08-26, Mark: "choose
which data to push" from the row's own kebab menu; retail and
due_in_date added the same day, "the Retail Est is the retail price
that should be mapped to DK" and "lets now wire up the due in date
in BB to DK"), None pushes everything, the original behaviour.

A car with no mileage is refused outright by dealerkit_write.add_due_in
(Steven 2026-08-27, "NEVER ALLOW Dealerkit to manually guess the
mileage"): the old --allow-missing-mileage escape hatch is gone, the
fix is always to enter the real mileage on the purchases page first.
A DealerkitMissingMileage failure is written to run_progress.json the
same way a session expired one is, so it surfaces as a real alert
rather than a row that just looks unchanged.

Marks db.dealerkit_pushed_at (and the matching per item columns) on
success, a real bug fixed the same day this gained the items option:
this function never marked it at all before, only the bulk Run modal
pass (daily_run.dealerkit_purchases_pass) did, so a row pushed one at
a time from the purchases page kept showing "not pushed" forever
despite a genuinely successful push.r   r   c              3   ót   >#   • U  H-  oS    R                  5       TR                  5       :X  d  M)  Uv •  M/     g7f©r1   N©rƒ   ©rÂ   r!   r1   s     €r$   rÅ   Ú!push_dealerkit.<locals>.<genexpr>  ó)   øé € ÐCš4�a U¡8§>¡>Ó#3°s·y±y³{Ñ#B—‘š4ùó   ƒ(8¯	8Nú is not in the purchases table.rÁ   zPushing r1   r(  rR   r†  r¿   z) to DealerKit...rñ  rò  c                 ó  >• [         R                  " U TT[        5       S9n [         R                  " U TS   UR	                  S5      5      US'   [        R                  " 5       n[        U4S jU 5       S 5      n[        TX25      nX1S'   0 US'   U H!  n [         R                  " XU5      US   U'   M#     U$ ! [
         a    S US'    Nzf = f! [
         a*  nS[        R                  " US	5      0US   U'    S nAMk  S nAff = f)
N)r–   Ú
max_photosr1   rØ  r{  c              3   óB   >#   • U  H  oS    TS    :X  d  M  Uv •  M     g7f)r*   NrÁ   )rÂ   r!   r˜   s     €r$   rÅ   Ú/push_dealerkit.<locals>.work.<locals>.<genexpr>:  s    øé € ÐB¢˜A¨t©W¸¸D¹	Ñ-A—a‘a¢ùs   ƒ–	r  Ú	followupsÚerrorr[   )r   Úpush_purchaser  rê  r   rD   r   r5   Únextr  rô  r   rì  )	rÅ  ræ  Úrows_nowr  Úduer�   r  r–   r˜   s	          €€r$   rÃ  Úpush_dealerkit.<locals>.work)  s
  ø€ Ü ×.Ò.¨t°SÀÔRgÓRiÑjˆð
	#Ü-×DÒDÀTÈ3ÈuÉ:ÐW]×WaÑWaÐbnÓWoÓpˆF�7‰Oô ×$Ò$Ó&ˆÜÔB¡ÓBÀDÓIˆÜ" 3¨Ó8ˆØˆw‰Ø ˆˆ{ÑÛˆAðLÜ)8×)FÒ)FÀtÐTUÓ)V��{Ñ# AÓ&ñ ð
 ˆøô% ó 	#Ø"ˆF�7‹Oð	#ûô  ó LØ*1´4·?²?À1ÀcÓ3JÐ)K��{Ñ# A×&ûðLús)   ¢-B8 ÂCÂ8C
Ã	C
Ã
DÃC<Ã<Dr*   r¹  zDealerKit push failed: r,   r  z; c              3   óZ   #   • U  H!  u  p[         R                  X5       S U 3v •  M#     g7f©rÖ  N)rá  r   )rÂ   rf   rš   s      r$   rÅ   r  P  s+   é € Ð^ÒI]ÁÀœn×0Ñ0°Ó6Ð7°q¸¸Õ<ÒI]ùs   ‚)+zDealerKit send failed: r[   z (before that: Ú)rp   ÚoutcomesrÕ  c              3   óT   >#   • U  H  u  pTR                  X5       S U 3v •  M     g7fr(  r  )rÂ   rf   rš   Ú_LABELs      €r$   rÅ   r  [  s*   øé € Ð(hÒOgÁtÀq¨F¯J©J°qÓ,<Ð+=¸Q¸q¸cÕ)BÒOgùs   ƒ%(zDealerKit: nothing to doÚ
items_donerÛ  rÜ  rÝ  rÞ  r  rÏ  u   DealerKit: the price sent, Â£r2   u   , already takes the Â£Ú
chip_totalz	 chip offr  r   r!  r÷  z after the send failed: r0   z (after the send): failed, rØ  zC after the send: the record was not found yet, the next run does itz' (after the send): record not found yetz (after the send): r×  r{  rø  TzPushed rù  Úalready_existedz& was already in DealerKit's stocklist.z- created as a new Due In record in DealerKit.r$  úVehicle id: r¶   ©rH   rG   z9V5 and service history not sourced for this platform yet.)r7  r!  r   r5   r#  r¥   rK   r   ÚFOLLOWUP_ACTIONSrý  Úsortedr7   rÇ  r¼  r9   rD   ÚLAST_OUTCOMESr   r–   r   rì  rá  r   rã  r  rç  rë  r6   Ú
SEND_ITEMS)r1   r–   r!  r   ÚfollowrÃ  ræ  r  rÙ  Úso_farr*  Únoter  r�   ÚresÚlabelÚ_kr,  r˜   s   ``               @@r$   Úpush_dealerkitr<  û  s  û€ õ6 $Ü×ÒÓ€DÜ
ÔC™4ÓCÀTÓ
J€CÞÜ˜C˜5Ð ?Ð@ÓAÐAô �—˜"Ó¤¤O×$DÑ$DÓ EÑE€FÞ”s˜5Ÿ; BÓ'¬#¬o×.NÑ.NÓ*O×OÜ'¨¬V°F«^Ó<Ð<ä	ˆH�S˜‘Z�L  3 v¡; -¨r°#°h±-°Ð@QÐ
RÔSÙÐ 8¨C°©J¨<°}Ð!EÔFöð8Ü(¨Ó.ˆô$ €FØ�z‰z˜*Ó%×+¨€HÞØ˜tŸy™yÔ(hÌvÐV^×VdÑVdÓVfÔOgÓ(hÓhÑh‰à)ˆÜ×Ò˜#˜d™) [°$¸JÒGð ˆl×Ü
× Ò   U¡°6¸,Ñ3GØ03·±¸Ó0DÈSÏWÉWÐUdÓMeÑ&fò	hà�f˜\Ñ*Ó*Ô/DÀS×/IÑ/IÜ×$Ò$ S¨¡Z¸°xØ47·G±G¸KÓ4HÐQT×QXÑQXÐYhÓQiÑ*jòlô ×!Ò! # d¡)¨[Ø$AÀ#Ç'Á'È+ÓBV×B[ÐZ[Ð\`ÐAaÐawÐx{×xÑxð  AMó  yN÷  ySð  RSð  TXð  xYð  Ybð  #cØ&0ò2ð �J‰J�wÓ×& 3€EØ—:‘:˜kÓ*×0¨b×7Ñ7Ö9‰ˆÜ×"Ñ" 1Ó(ˆØ�7‰7�7×ÑÜ×!Ò! # d¡)¨[¸JÀuÀgÐMeÐfiÐjqÑfrÐesÐ:tð  {Eò  FÜ�B�u�gÐ8¸¸W¹¸ÐGÖHØ�W‰W�\Ó"Ñ*Ü×!Ò! # d¡)¨[¸JÀuÀgð  NQð  ;Rð  Xbò  cÜ�B�u�gÐDÐEÖFä# E¨cÔ2Ü�B�u�gÐ0°°Y±Ð0@ÐAÖBñ :ð ‡z�z�'ÓÑ&Ü
×Ò˜s 5™z¨6°'©?Ô;Ü‡K‚KÐ$ d¨g°c¸%±j°\ÀÐ,PÔQàÐ× Ü�˜‘�ÐEÐFÕGä�˜‘�ÐLÐMÔNô ×(Ô(ˆØ��>Ü�B�v˜b‘z�l " X¨b¡\ NÐ3Ö4ñ )ô 
ˆL˜ Ñ-Ð.Ð
/Ô0ð ˆ:�Ð4Ó4ÜÐIÕJð 5øôU ×2Ñ2ó Ü
×Ò˜c $™i¨Ð8OÐPQÈsÐ6SÐYcÒdØûÜó 	ô
 ×,Ñ,×2°ˆØ—‘Ñ^ÌÐPT×PZÑPZÓP\ÔI]Ó^Ó^ˆÜ
×Ò˜c $™i¨Ø 7¼¿ºÈÈ3Ó8OÐ7PÐQÖtzÐWfÐgmÐfnÐnoÑUpð  ACñ  Dð  JTò	Uàûð	ús%   Ã>Q ÑT'Ñ,R
Ò
T'ÒBT"Ô"T'c                  óÂ  ^^^• SSK Jn   [        R                  " 5       n[	        S U 5       S5      =(       d    [	        S U 5       S5      mT(       d  [
        R                  " S5      e[        R                  R                  [        R                  R                  [        R                  R                  [        5      5      SSS	5      m[        R                  " [        R                  R                  T5      S
S9  [        TSSS9 nUR                  S5        SSS5        U4S jmT" S[         R                   R#                  5       R%                  SS9 STS    35        U " SSTS    35         ['        UU4S j5        [
        R,                  " SS
S5        g! , (       d  f       N‡= f! [(         a*  nT" S[
        R*                  " US5       35         SnANYSnAff = f)a~  Look at DealerKit's costs screen for one car and write every step
down, saving nothing (2026-09-16, Steven: "learn the steps and screens
properly to make this work every time"). The car is the first live one
on DealerKit whose price never landed there (db.dk_figures_missing),
else the first live car on DealerKit. Written to
data/logs/dealerkit_survey.log for the diagnostics pull.r   r   c              3   ó`   #   • U  H$  n[         R                  " U5      (       d  M   Uv •  M&     g 7fr]   )r   Údk_figures_missing©rÂ   r!   s     r$   rÅ   Ú#survey_dealerkit.<locals>.<genexpr>ž  s   é € Ð<š4�a¤2×#8Ò#8¸×#;—‘š4ùs   ‚.¥	.Nc              3   ó�   #   • U  H<  n[         R                  " U5      (       d  M   UR                  S 5      (       a  M8  Uv •  M>     g7f)r   N)r   Úon_dealerkitr   r@  s     r$   rÅ   rA  ž  sI   é € ð  MRÒX\ÐSTÔ`b×`oÒ`oÐpq×`rËQÐ{|÷  |Añ  |Að  BP÷  |QÏQÉQÒX\ùs   ‚A¥A½	AzNo car on DealerKit to survey.rU   Úlogszdealerkit_survey.logTr0  Úwrq   rr   rp   c                 ó°   >• [        U 5         [        TSSS9 nUR                  U S-   5        S S S 5        g ! , (       d  f       g = f! [         a     g f = f)Nr�   rq   rr   rq  )r7   rˆ   ÚwriterD   )Úliner™   Úlog_paths     €r$   ÚsayÚsurvey_dealerkit.<locals>.say«  sJ   ø€ ÜˆdŒð	Ü�h ¨gÒ6¸!Ø—‘˜˜t™Ô$÷ 7×6Ö6ûäó 	Ùð	ús+   ŽA ™7®A ·
AÁA ÁA Á
AÁAzDealerKit costs screen survey Úseconds)Útimespecz, car r1   rñ  z(Looking at DealerKit's costs screen for c                 ód   >• [         R                  " U TS   [         R                  " T5      T5      $ )Nr1   )r   Úsurvey_expense_formÚ_default_supplier)rÅ  r˜   rJ  s    €€r$   ra   Ú"survey_dealerkit.<locals>.<lambda>µ  sC   ø€ ¬_×-PÒ-PÐQUÐWZÐ[`ÑWaÔcr÷  dEò  dEð  FIó  dJð  LOô  .Pr&   zSurvey stopped: rº  Údealerkit_surveyz0Survey written to data/logs/dealerkit_survey.log)r7  r!  r   r5   r#  r   Ú
RunSkippedr}   r~   r   ÚdirnameÚabspathÚ__file__rK  rˆ   rG  rø   Únowrû   rÇ  rD   rì  r6   )r!  r   r™   r  rI  r˜   rJ  s       @@@r$   Úsurvey_dealerkitrX  •  s�  ú€ õ $Ü×ÒÓ€DÜ
Ñ<™4Ó<¸dÓ
C÷  YÄtñ  MRÑX\ó  MRð  TXó  HY€CÞÜ�oŠoÐ>Ó?Ð?Ü�w‰w�|‰|œBŸG™GŸO™O¬B¯G©G¯O©O¼HÓ,EÓFÈÐPVÐXnÓo€HÜ‡K‚K”—‘—‘ Ó)°DÒ9ô 
ˆh˜ gÒ	.°!Ø	�‰�Œ÷ 
/õñ Ð
(¬×):Ñ):×)>Ñ)>Ó)@×)JÑ)JÐT]Ð)JÐ)^Ð(_Ð_eÐfiÐjoÑfpÐeqÐrÔsÙÐÐ#KÈCÐPUÉJÈ<Ð!XÔYð:Üõ  !Pô  	Qô 	‡K‚KÐ" DÐ*\Õ]÷ 
/Õ	.ûô ó :ÙÐœtŸš¨q°#Ó6Ð7Ð8×9Ñ9ûð:ús$   ÄFÅ0F* Æ
F'Æ*
GÆ4 GÇGc                 ó”  ^ ^• SSK Jn  [        R                  " 5       n[	        U 4S jU 5       S5      mT(       d  [        T  S35      e[        STS    STS	    S
35        U" SSTS    S35        U4S jn [        U5      n[        R                  " TS   SU(       a;  SUR                  S5       SUR                  SS5       SUR                  SS5       S3OSSS9  [        R                  " SS5        [        R                  " TS   U5        U(       d0  [        R                  " SSTS    S35        [        TS    S35        g[        R                  " SSSTS    S35        [        S US    35        [        S!US    35        [        S"US    35        UR                  S#5      nUc%  [        R                   " UR                  S$5      5      n[        S%Ub  S&US' -   OS( 35        [        S)US*   b  S&[#        US*   5      -   OS( 35        g! [        R                   a#  n[        R                  " TS   SSU 3SS9  e SnAff = f)+uí  Manual, one car at a time trigger: python3 purchases_run.py
--check-dealerkit REG (2026-08-26, Mark: "add a check DK record for
whats been added or present on the record"). Opens a real HEADED
DealerKit window (same reasoning as push_dealerkit, watched and
reviewed), reads what is genuinely on that car's own DealerKit record
right now (dealerkit_write.check_dealerkit_record, pure API reads, no
clicking, fast once logged in) and stores it via
db.set_dealerkit_check, independent of whatever BidBrain's own push
flags say was attempted.

Writes to the same run_progress.json every other on demand pass uses
(2026-08-26, a real bug found the hard way: a session expired failure
used to only ever reach the log file, so a click on the row's own
"Check DK record" that silently failed looked to Mark like it had
done nothing at all, "doesnt seem to check whats already present").
The purchases page's own JS now reads this same file's phase to
surface a real error instead of quietly reloading nothing new.

Also records db.record_site_health("dealerkit", ...) either way
(2026-08-26, found live: Mark could see a real red "not logged in"
flag on the settings page for Auction4Cars/Dealer Auction/DealerWay
but never for DealerKit, "DK didnt" â€” nothing anywhere had ever
called this for dealerkit at all). A genuine session expired error
here is actually the most direct, real time proof DealerKit is not
logged in that this project has, a real live API call catching it
in the act, not just a login page timing out.r   r   c              3   ót   >#   • U  H-  oS    R                  5       TR                  5       :X  d  M)  Uv •  M/     g7fr  r  r  s     €r$   rÅ   Ú"check_dealerkit.<locals>.<genexpr>Ø  r  r  Nr  z	Checking r1   r(  rR   z) on DealerKit...rñ  z on DealerKitc                 óª   >•  U R                  [        R                  S   S    S3SSS9  [        R
                  " U TS   5      $ ! [         a     N&f = f)Nr¹  Úbase_urlz	/vehiclesÚdomcontentloadedi0u  )Ú
wait_untilro   r1   )Úgotor   ÚSITESrD   r   rê  )rÅ  r˜   s    €r$   Ú_checkÚcheck_dealerkit.<locals>._checkÞ  sd   ø€ ð	Ø�I‰IœŸ™ {Ñ3°JÑ?Ð@À	ÐJØ!3¸Uð ñ Dô ×5Ò5°d¸CÀ¹JÓGÐGøô ó 	Ùð	ús   ƒ(A Á
AÁAr*   r¹  zDealerKit check failed: r,   r  zDealerKit checked: vehicle rØ  r†  Úimagesz	 photos, rÍ  z
 documentsz-DealerKit checked: no record for this car yetTÚdealerkit_check_rowz$ has no DealerKit record at all yet.zChecked z on DealerKit.r0  zImages on record: zDocuments on record: Úpurchase_cost_valueÚpurchase_costzPurchase price on record: ô   Â£Úgznot setzRetail price on record: Úretail_price)r7  r!  r   r5   r#  r¥   r7   rÇ  r   r¼  r9   r   r½  rë  r   r6   Ú_expense_item_amountr‚   )r1   r!  r   rb  ræ  r  Ú_costr˜   s   `      @r$   Úcheck_dealerkitrm  »  s”  ù€ õ6 $Ü×ÒÓ€DÜ
ÔC™4ÓCÀTÓ
J€CÞÜ˜C˜5Ð ?Ð@ÓAÐAä	ˆI�c˜%‘j�\  C¨¡K =Ð0AÐ
BÔCÙÐ 9¨S°©Z¨L¸Ð!FÔGõHðÜ(¨Ó0ˆô ×Ò˜#˜d™) [æ$ð 9¸¿¹ÀLÓ9QÐ8RÐRTÐU[×U_ÑU_Ð`hÐjkÓUlÐTmÐmvÐw}÷  xBñ  xBð  CNð  PQó  xRð  wSð  S]ñ  ^Ø*YÐ`jòlô ×Ò˜+ tÔ,Ü×Ò˜3˜u™: vÔ.æÜ�ŠÐ)¨4°C¸±J°<Ð?cÐ1dÔeÜ��U‘�Ð@ÐAÔBØÜ‡K‚KÐ% t¨x¸¸E¹
°|À>Ð-RÔSÜ	ˆL˜ Ñ-Ð.Ð
/Ô0Ü	Ð˜v hÑ/Ð0Ð
1Ô2Ü	Ð! &¨Ñ"5Ð!6Ð
7Ô8Ø�J‰JÐ,Ó-€EØ�}Ü×4Ò4°V·Z±ZÀÓ5PÓQˆÜ	Ð&¸eÑ>O t°°q°	Ò':ÐU^Ð&_Ð
`ÔaÜ	Ð$Ø39¸.Ñ3IÑ3UˆT”C˜˜~Ñ.Ó/Ò/Ð[dÐeðgõ høô3 ×2Ñ2ó ô
 	×Ò˜c $™i¨Ð8PÐQRÐPSÐ6TÐZdÒeØûðús   Á8H ÈIÈ$IÉIc           
      óÒ  ^ • SSK Jn  [        U 4S j[        R                  " 5        5       S5      nU(       d  [        T  S35      eUS   nUS;  a  [        T  SU S	35      eUUS
   US   UR                  S5      UR                  S5      UR                  S5      UR                  S5      UR                  S5      S.n[        SUS    SUS    SU S35        U" SSUS    SU 35        [        5        nUS:X  a  SOSn[        R                  " XVSS9nUR                  (       a  UR                  S   OUR                  5       n US:X  GaÞ  [        R                  " X„S
   5      n	U	(       a  UR                  U	5        UR                  [!        U5      5        ["        R$                  R'                  [(        SSUS   5      n
[        R*                  " U5      nU(       a'  [        R,                  " X‹/U
5      nU(       a  US   US'   [        R.                  " U5      nU(       a7  [        R,                  " X�U
5      nU(       a  [0        R2                  " U5      US '   [        R4                  " U5      nU(       a7  [        R,                  " XŽU
5      nU(       a  [0        R2                  " U5      US!'   [        R6                  " X„S
   5      nU(       a7  [        R,                  " X�U
5      nU(       a  [0        R2                  " U5      US"'   [        R8                  " X„S
   5      nU(       a  UUS#'   GO[:        R                  " X„S
   5      n	U	(       a  UR                  U	5        UR                  S$5      SL US%'   UR                  [!        U5      5        ["        R$                  R'                  [(        SSUS   5      n[:        R6                  " X„S
   5      nU(       a9  [:        R,                  " X�U5      n[0        R2                  " U=(       d    U5      US"'   ["        R<                  " USS&9  [:        R>                  " X„S
   US'9nU Vs/ s H:  n["        R$                  RA                  U5      RC                  S(5      (       d  M8  UPM<     nnU Vs/ s H:  n["        R$                  RA                  U5      RC                  S)5      (       d  M8  UPM<     nnU(       a  [0        R2                  " U5      US*'   U(       a  [0        R2                  " U5      US+'   [:        R8                  " X„S
   5      nU(       a  UUS#'   URG                  5         SSS5        [        RJ                  " U/5        [L        RN                  " S.SS/US    SU S035        [        US    S1U S035        gs  snf s  snf ! [D         a-  nURG                  5         [I        S,US    SU S-U 35      UeSnAff = f! , (       d  f       N = f)2aÍ  Manual, one car at a time re read of a single already known purchase
straight off its own platform, Motorway or Carwow, whichever it was
bought on (python3 purchases_run.py --check-purchase REG, 2026-08-26,
Mark: replacing the per row kebab's push options with "a check
motorway or carwow button"). The single car version of read_all()'s
own per platform detail pass, same reads, same functions, just for
one row rather than every purchase in one sweep, useful when the
platform had not finished processing yet at first capture (Motorway
"Reviewing documents", a Carwow document bundle not yet available)
and the daily 15:45 sync has not come back round to it. db.
record_purchases' own fill once, never overwrite rules (see its own
docstring) mean this only ever adds what is now newly available,
it can never clobber a figure already confirmed.

Writes to run_progress.json the same way check_dealerkit does, so a
failure surfaces on the page rather than only in the log.r   r   c              3   ó¦   >#   • U  HF  n[         R                  " UR                  S 5      5      [         R                  " T5      :X  d  MB  Uv •  MH     g7fr  )r   Ú	_norm_regr   r  s     €r$   rÅ   Ú#refresh_purchase.<locals>.<genexpr>  s=   øé € ð JÒ 3˜1ÜŸš a§e¡e¨E£lÓ3´r·|²|ÀCÓ7HÑH÷ ‘QÒ 3ùs   ƒAAÁ	ANr  r¶   r1  z was bought on z), which has no re read for purchases yet.r  r1   rR   r¿   r  ro  rô   )r¶   r  r1   rR   r¿   r  ro  rô   zRe reading r(  z) on r)  r"  z on rH   r	   r
   Tr*  rU   r,  rx   rÑ   rÔ   rÐ   rÖ   rê   r  r0  r2  r4  r5  rÒ   rÕ   zCould not re read r$  Úpurchase_checkzRe read rp  z updated from )(r7  r!  r#  r   r5   r¥   r   r7   r   r   r>  r?  r@  r	   rB  rC  r
  r}   r~   r   r€   rD  rE  rF  r†   rG  rH  rI  rJ  r
   rK  rL  rM  rÜ   rD   r…   rú  rš  r   r6   )r1   r!  rã   r¶   r˜   r�   ÚsiterY  rZ  r]  r^  r‘   r_  r`  rÔ   ra  rb  re  rf  rŽ   rg  rh  r  s   `                      r$   Úrefresh_purchasert    sù  ø€ õ" $Üô J¤× 1Ò 1Ô 3ó JØKOóQ€HæÜ˜C˜5Ð ?Ð@ÓAÐAØ˜
Ñ#€HØÐ-Ó-Ü˜C˜5 °¨zÐ9bÐcÓdÐdð Ø Ñ.Ø˜‰Ø—‘˜VÓ$Ø—,‘,˜xÓ(Ø—‘˜gÓ&Ø—|‘| MÓ2Ø—|‘| MÓ2ñ	€Cô 
ˆK˜˜E™
�| 2 c¨&¡k ]°%¸°zÀÐ
EÔFÙˆi˜; s¨5¡z l°$°x°jÐAÔBÜ	Ô	˜aØ%¨Ó3‰z¸ˆÜ×)Ò)¨!¸DÑAˆØ #§	§	�—	‘	˜!’¨s¯|©|«~ˆð:	\Ø˜:Ô%Ü×5Ò5°eÀÑ=OÓP�ÞØ—J‘J˜r”NØ—
‘
œ=¨Ó-Ô.ô
  ŸW™WŸ\™\¬$°¸ÈÈUÉÓT�
Ü ×4Ò4°UÓ;�ÞÜ$×<Ò<¸UÀGÈZÓX�EÞØ+0°©8˜˜KÑ(Ü!)×!EÒ!EÀeÓ!L�Þ!Ü$×<Ò<¸UÐT^Ó_�EÞÜ8<¿
º
À5Ó8I˜Ð4Ñ5Ü$×3Ò3°EÓ:�	ÞÜ$×<Ò<¸UÈzÓZ�EÞÜ+/¯:ª:°eÓ+<˜˜KÑ(Ü"×8Ò8¸ÀMÑ@RÓS�ÞÜ$×<Ò<¸UÈZÓX�EÞÜ,0¯JªJ°uÓ,=˜˜LÑ)Ü$×9Ò9¸%À]ÑASÓT�	ÞØ-6�CÐ)Ñ*ùä×3Ò3°E¸}Ñ;MÓN�ÞØ—J‘J˜r”NØ#&§7¡7¨>Ó#:¸dÐ#B��KÑ Ø—
‘
œ=¨Ó-Ô.ÜŸ'™'Ÿ,™,¤t¨V°_ÀcÈ%ÁjÓQ�Ü ×6Ò6°uÀ-Ñ>PÓQ�ÞÜ"×:Ò:¸5È7ÓS�EÜ(,¯
ª
°5×3C¸GÓ(D�C˜Ñ%Ü—’˜G¨dÒ3Ü×5Ò5°eÀÑ=OÐY`Ña�Ù+/Óeª4 a´2·7±7×3CÑ3CÀAÓ3F×3QÑ3QÐRc×3d§©4�ÐeÙ&*Ó]¢d ¬b¯g©g×.>Ñ.>¸qÓ.A×.LÑ.LÈ^×.\Ÿ1¡d�Ð]ÞÜ,0¯JªJ°|Ó,D�CÐ(Ñ)ÞÜ,0¯JªJ°wÓ,?�CÐ(Ñ)Ü"×7Ò7¸À=Ñ?QÓR�	ÞØ-6�CÐ)Ñ*ð 	�	‰	Œ÷ 
ôB ×Ò˜˜ÔÜ‡K‚KÐ  $¨(°3°u±:°,¸dÀ8À*ÈAÐ(NÔOÜ	ˆS�‰ZˆL˜ x j°Ð
2Õ3ùò!  fùÚ]øô ó 	\Ø�I‰IŒKÜÐ!3°C¸±J°<¸tÀHÀ:ÈRÐPQÈsÐSÓTÐZ[Ð[ûð	\ú÷y 
Õ	úsj   Ã5AWÅG#VÌ)WÌ+DVÐ?7VÑ:VÒ VÒ7VÓVÓA'VÔ.WÖ
VÖ
WÖ((W×W×W×
W&Ú__main__c                 óÂ   • [         R                  R                  U 5      nUS-   [        [         R                  5      :¼  a  [	        U5      e[         R                  US-      $ )Nr)   )r•  r–  Úindexr:   r¥   )ÚflagÚusager\  s      r$   Ú
_arg_afterrz  x  sE   € Ü�H‰H�N‰N˜4Ó ˆØˆq‰5”CœŸ™“MÓ!Ü˜UÓ#Ð#Ü�x‰x˜˜A™‰Ðr&   z--reprice-retailr(   ú--push-dealerkitc            
      ó  • [        SS5      n S nS[        R                  ;   aÑ  [        SS5      nUR                  S5       Vs1 s H)  o3R	                  5       (       d  M  UR	                  5       iM+     nn[        [        R                  5      [        [        R                  5      -  nX-
  nU(       aB  [        SSR                  [        U5      5       SSR                  [        U5      5       S	35      e[        XS
9  g s  snf )Nr{  zhUsage: python3 purchases_run.py --push-dealerkit REG [--items price,photos,documents,retail,due_in_date]z--itemszSUsage: --items price,photos,documents,retail,due_in_date (a comma separated subset)Ú,zUnknown --items value(s): r†  z. Use one or more of rp  rÚ  )rz  r•  r–  r„   rÊ   rK   r   r5  r2  r¥   r   r3  r<  )Úreg_argÚ	items_argr  rš   ÚallowedÚbads         r$   Ú_pushr‚  €  sä   € Ü Ð!3ð  6`ó  aˆGØˆIØœCŸH™HÓ$Ü  ð  -Bó  C�Ø03·	±	¸#´ÓL²¨1Ç'Á'Ç)›Y˜QŸW™WžY±�	ÐLÜœo×8Ñ8Ó9¼CÄ×@`Ñ@`Ó<aÑa�ØÑ)�ÞÜ$Ð'AÀ$Ç)Á)ÌFÐSVËKÓBXÐAYð Z;Ø;?¿9¹9ÄVÈGÃ_Ó;UÐ:VÐVWð&Yó Zð Zä˜7Ó4ùò Ms   ÁC>ÁC>rø  z--survey-dealerkitrR  ú--check-dealerkitre  c                  ó,   • [        [        SS5      5      $ )Nrƒ  z5Usage: python3 purchases_run.py --check-dealerkit REG)rm  rz  rÁ   r&   r$   ra   ra   �  s   € ´/ÜÐ*Ð,cÓdô3fr&   ú--check-purchaserr  c                  ó,   • [        [        SS5      5      $ )Nr…  z4Usage: python3 purchases_run.py --check-purchase REG)rt  rz  rÁ   r&   r$   ra   ra   “  s   € Ô-=ÜÐ)Ð+aÓbô.dr&   r‡  )r@   rB   )é   é   )NNr]   )r)   N)r,   )KÚ__doc__rø   r†   r}   r}  r•  Úplaywright.sync_apir   Údealer_configr‰  r   r   r   r   Úbidbrain.pricingr   Úbidbrain.readersr	   r
   r   r   r   rL   r%   r=   r4   rT   r~   rT  rU  rV  r€   r   r‰   rŸ  r   r’   r¨   r´   r¸   r½   rÍ   rí   rÙ   rå   rñ   rú   r   r
  r  r  r  r  rl  rx  rƒ  r�  r·  rÇ  rá  rç  rí  rý  r  r  r  r<  rX  rm  rt  Ú__name__rz  r–  Úguardr‚  rÁ   r&   r$   Ú<module>r�     sV  ðñó8 Û Û 	Û Û 
å /ã ç 7Ó 7Ý #ß RÕ Rð +9ÈOÑ\Ð òò0.ò05òð4 
‡w�w‡��r—w‘w—‘ xÓ0Ó1€Ø
�‰�‰�T˜6 ?Ó3€Ø×-Ñ-€ô.Aòb'òTBòJò*ò$ò0@ð€
ò6òUòð* €ôaô<lò@cð ~Ð ôò 'ôiòX	ð0 !/ô @ò&?òLFò^^ð< ,°MÈiØ$°;Ø*¸=Ø8À{Ð_iÐu}Ø,ñ	.€ô )òFpò ô>ò"GôôDWKòt#^òLHhòVi4ðX ˆzÓòð
 ˜SŸX™XÓ%Ø�
Š
Ð# ^Õ4Ø	˜sŸx™xÓ	'ò	5ð 	�
Š
Ð'¨Õ/Ø	 §¡Ó	)Ø�
Š
Ð%Ð'7Õ8Ø	 §¡Ó	(Ø�
Š
Ð(ñ +fõ 	gà	˜sŸx™xÓ	'Ø�
Š
Ð#ñ &dõ 	eð 	�
Š
�; Õ%ðI r&   