ó
    W6¯j6÷ ã                   ó˜  • 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rSSKJr  SSK	r	SSK
Jr  \	R                  rSœS jrSœS jrS�S jrSžS	 jrSœS
 jrSŸS jr " S S\5      rS rS rS rS rS rS S jrS¡S jrS rS rS rS¢S jr S£S jr!S r"S£S jr#S£S jr$S r%S r&S r'S  r(S! r)S" r*S# r+S$ r,S¤S% jr-S& r.S'r/0 q0\Rb                  Rd                  4S( jr3S) r4S¥S* jr5S¦S+ jr6S,S-S.S/S0.r7S11r8S2r9S3 r:S4 r;S§S5 jr<S6 r=S S7 jr>S8 r?S9 r@S: rAS; rBS< rCS¨S= jrDS>S?S@SA.rESBSCS@SA.rFSD rGS¨SE jrHSF rISG rJSH rKS¨SI jrLSJ rMSK rNSL rOS©SN jrPSMSO.SP jrQSQ rRS©SR jrSSS rTSœST jrUSU rV SªSV jrWSW rXS£SX jrYSY rZSZ r[ " S[ S\\5      r\S] r] " S^ S_\5      r^S` r_Sa r`Sb raS«Sc jrbSd rcSe rdSf reSg rfS¬Sh jrgSiSjSkSl.rhSmSnSoSpSqSr.riS¨Ss jrjSt rkSu rlSvrmSwrnSqroSx rpSy rqSzrrS{rsS|rtS} ruS~ rvS­S jrwS€rxS�ryS‚rzSƒ r{S„ r|S… r}S† r~S‡ rSˆ r€S‰ r�SŠ r‚S‹ rƒSŒ r„S®S� jr…S®SŽ jr†S� r‡S� rˆS¯S‘ jr‰S’ rŠS“ r‹S” rŒS¨S• jr�S– rŽS—r�\�" S˜ \� 5       5      r‘S°S™ jr’S±Sš jr“S› r”g)²a½  
Pushes a purchased car into DealerKit (Right Drive's DMS) as a real "Due In"
stock record, with its photos and service history documents attached.

Built against the real live app (rightdrive.app.dealerkit.uk), not guessed
markup: see the 2026-08-21 CLAUDE.md entry for the reconnaissance this was
built from. DealerKit is a Vue single page app whose form fields carry no
name/id/placeholder attributes worth relying on (custom ui-textbox/ui-select
components, several rendered as hidden duplicate instances at once), so every
click here finds the one currently VISIBLE match rather than trusting a fixed
selector index, the same defence used throughout this module.

Scope, deliberately narrow for this first build (2026-08-21, Mark: "lets do a
test to confirm everything works first then automatic"):
  - Creates the due in record from the registration alone (DealerKit's own
    DVLA lookup fills in make/model/spec).
  - Types BidBrain's own purchase price into the Purchase Details step's
    Purchase Price field before Save. DealerKit has its own live Motorway
    integration that can fill this in (and a full itemised Buyers Premium /
    Delivery Charge / Indemnity Fee breakdown, plus real documents) entirely
    on its own, proven live twice, but ONLY ever seen firing on a genuinely
    human driven session (Mark himself, mouse and keyboard, twice) and NEVER
    on an automated one (three separate attempts: two headless script runs
    and one slow, deliberate, click by click session driven by Claude
    through the Browser pane, the last one checked again 15+ minutes later
    and still blank). See the 2026-08-21 evening CLAUDE.md entries for the
    full pattern. So for an automated push, typing the figure in ourselves
    is the only way it does not sit blank forever, confirmed with Mark
    before re adding this (the same mechanism was built, proven to work,
    then removed earlier the same day on the mistaken theory that DealerKit's
    own integration made it redundant for every kind of session, not just a
    human one). Only the single combined Purchase Price is set (BidBrain's
    own already computed total), not an itemised Buyers Premium / Delivery
    Charge / Indemnity Fee breakdown, that needs a separate Add Expense flow
    not yet investigated. Nothing else on the Stocklisting step is touched
    (Bought From, invoice ref, VAT qualifier, finance settlement, funding,
    sales location, Retail Price): Retail Price is Mark's own market call,
    typed in by hand, and "Bought From" needs matching or creating a
    DealerKit contact, not attempted here.
  - Uploads up to 4 real exterior photos, and any service history document
    and V5C document photos already captured, via DealerKit's own Edit >
    Images / Edit > Documents upload widgets (a plain multi file input
    each, drag and drop or a file picker, unrelated to the two despite the
    shared UI chrome). Service history and V5 photos go up together as one
    Documents upload (2026-08-23, Mark: "add the v5 to the automation").
    Still never the seller's driving licence, which stays uncaptured
    everywhere in this project (see motorway.read_v5_photos), and still
    never captured at all for a listing that has not actually been bought,
    the V5 reader only ever runs against a genuine purchase's own summary
    page in purchases_run.py.

Never guesses: if a step's expected element is not found, this raises loudly
rather than clicking something else and leaving a half filled record.
é    N)Úurlparseé   )Úplatesc                 óÔ  • [         R                   " 5       U-   n[         R                   " 5       U:  a¥   U R                  5       n[        U5       HT  nU R	                  U5      R                  5       nU(       d  M+  US   S:”  d  M6  US   S:”  d  MA  U R	                  U5      U4s  $    [         R                  " S5        [         R                   " 5       U:  a  M¥  g! [         a    Sn N£f = f)a  The first match of a locator that is actually rendered on screen right
now. DealerKit renders several hidden duplicate instances of the same
modal/component at once, so `.first` or a fixed index is not reliable,
only the real bounding box tells you which one a human would see.r   ÚwidthÚheightç      Ð?©NN)ÚtimeÚcountÚ	ExceptionÚrangeÚnthÚbounding_boxÚsleep)ÚlocatorÚtimeoutÚdeadliner   ÚiÚbs         Ú9/Users/stevendouglas/BidBrain/bidbrain/dealerkit_write.pyÚ_visibler   U   s½   € ô
 �yŠy‹{˜WÑ$€HÜ
�)Š)‹+˜Ó
 ð	Ø—M‘M“OˆEô �u–ˆAØ—‘˜A“×+Ñ+Ó-ˆAßˆq�Q�w‘Z !•^¨¨(©°a­Ø—{‘{ 1“~ qÐ(Ò(ñ ô 	�
Š
�4Ôô �)Š)‹+˜Õ
 ð øô ó 	ØŠEð	ús   ³C ÃC'Ã&C'c                 ó–   • [        X5      u  p4U(       d  gU R                  R                  US   US   S-  -   US   US   S-  -   5        g)NFÚxr   é   Úyr   T)r   ÚmouseÚclick)Úpager   r   Úelr   s        r   Ú_click_visibler!   h   sM   € Ü�WÓ&�E€BÞØØ‡J�J×Ñ�Q�s‘V˜a ™j¨1™nÑ,¨a°©f°q¸±{ÀQ±Ñ.FÔGØó    c                 ó–   • [        U[        5      (       a  U R                  U5      O
U=(       d    U n[        XR	                  XS9U5      $ )a«  within, if given, is a locator (or selector string) to scope the text
search to, for example the currently open modal. Needed because
_visible() only checks that an element has a real bounding box, not
that it is actually inside the viewport: a same-worded heading lower
down the page (found live, 2026-08-23: a "Documents" section further
down a vehicle's own page, unrelated to the Edit Vehicle modal's own
"Documents" tile) can still have width/height > 0 while sitting off
screen, and DOM order alone decided which one won, sometimes the wrong
one. Scoping to the open modal removes the ambiguity outright rather
than trying to out-guess which "visible" match a human would pick.©Úexact)Ú
isinstanceÚstrr   r!   Úget_by_text)r   Útextr%   r   ÚwithinÚroots         r   Ú_click_visible_textr,   p   s?   € ô $.¨f´c×#:Ñ#:ˆ4�<‰<˜ÔÀÇÈ4€DÜ˜$× 0Ñ 0°Ð 0Ð CÀWÓMÐMr"   c                 ó$  • [         R                   " 5       U-   n[         R                   " 5       U:  a_  Ub  U R                  X5      OU R                  U5      nU(       a  U$ [         R                  " U5        [         R                   " 5       U:  a  M_  g)aˆ  Like page.evaluate, but retries until it returns a truthy value or
timeout runs out. A fixed page.wait_for_timeout before a ONE SHOT
evaluate (the pattern used throughout this module before this helper
existed) was found live 2026-08-23 to be genuinely racy for
set_expense_tax_date specifically: two back to back runs against the
exact same vehicle, same code, same 1500ms wait, landed in two
different real states (one correctly inside "Edit Expense #NNN", one
still sitting on the outer "Choose an area to edit" menu), meaning
DealerKit's own SPA transition time genuinely varies run to run, not
something a single fixed wait can cover reliably.N)r   Úevaluater   )r   Újs_fnÚargr   Úpollr   Úresults          r   Ú_evaluate_untilr3      si   € ô �yŠy‹{˜WÑ$€HÜ
�)Š)‹+˜Ó
 Ø.1©o�—‘˜uÔ*À4Ç=Á=ÐQVÓCWˆÞØˆMÜ�
Š
�4Ôô	 �)Š)‹+˜Õ
 ð
 r"   c                 óº  • [         R                  " SSUR                  5       5      n[        R                  " 5       U-   n[        R                  " 5       U:  a„  U R	                  SU5      nU(       a:  US   nU R
                  R                  US   US   S-  -   US   US	   S-  -   5        g
[        R                  " S5        [        R                  " 5       U:  a  M„  g)aÐ  Click the smallest visible element whose text contains needle
(case/space insensitive). Used wherever an exact text match is not
reliable, for example a search result card whose wrapping element has
no useful class or attribute of its own, or the EDIT button whose
visible label sits next to a material icon ligature in the same text
node ("mode_edit\nEDIT"), so no element's own text is ever exactly
"EDIT" and get_by_text(..., exact=True) never matches anything.z\s+Ú aÛ  (target) => {
                const els = document.querySelectorAll('a, div, li, button, span');
                const out = [];
                for (const el of els) {
                    if (el.children.length > 20) continue;
                    const t = (el.innerText || '').toUpperCase().replace(/\s+/g, '');
                    if (!t.includes(target)) continue;
                    const r = el.getBoundingClientRect();
                    if (r.width > 0 && r.height > 0) {
                        out.push({x: r.x, y: r.y, w: r.width, h: r.height, area: r.width * r.height});
                    }
                }
                out.sort((a, b) => a.area - b.area);
                return out.slice(0, 3);
            }r   r   Úwr   r   ÚhTç333333Ó?F)ÚreÚsubÚupperr   r.   r   r   r   )r   Úneedler   Útargetr   ÚmatchesÚms          r   Ú_click_containingr@   “   s¹   € ô �VŠV�F˜B §¡£Ó/€FÜ�yŠy‹{˜WÑ$€HÜ
�)Š)‹+˜Ó
 Ø—-‘-ðð ó!
ˆö$ Ø˜‘
ˆAØ�J‰J×Ñ˜Q˜s™V a¨¡f¨q¡jÑ0°!°C±&¸1¸S¹6ÀA¹:Ñ2EÔFØÜ�
Š
�3Œô/ �)Š)‹+˜Õ
 ð0 r"   c                 óž  • Uc  g[        U[        5      (       a   UR                  5       (       a  [        U5      OUnSn[        R                  " 5       U-   n[        R                  " 5       U:  aß  U R                  SU[        U5      /5      nU(       aŠ  U R                  SU S35      nUR                  5         UR                  [        U5      5        UR                  5         U R                  S5        UR                  5       n	[        U5      [        U	5      ;   a  gg[        R                  " S5        [        R                  " 5       U:  a  Mß  g)	uŠ  Type value into the Purchase Price (Â£'s) field on the Stocklisting
step. That field carries no name/id/placeholder, only a label div
naming it, so the real input is located by finding the label's own
position on screen and then the geometrically nearest visible text
input to it, the same proven pattern _add_expense_items' own "Unit
Net" field already uses (2026-08-23), rather than walking up the DOM
from the label to the nearest ancestor holding an input (the original
approach, found live 2026-09-05 to grab the wrong field once
DealerKit added a "Purchase Invoice Ref #" field just above this one).

A second bug found the same day, once the position based rewrite
above still failed: `el.children.length <= 3` is not enough on its
own to tell a real, short label apart from a huge ancestor wrapper.
This page's own outer content container (everything on screen, the
whole vehicle list plus the open modal) has only 1 to 3 DIRECT
children of its own, so it passed that check too, and its own
innerText, which is the WHOLE PAGE's text, genuinely contains the
words "Purchase Price" buried somewhere inside it, thousands of
characters in. Since it comes earlier in raw DOM order than the real,
specific 20 character label and both report a real, nonzero bounding
box, the search's own `break` fired on that huge wrapper first. A
short text length cap, the same guard already used for the Unit Net
field (`.length > 20` there), rules this class of false match out for
good: a real field label is a handful of words, never a whole page.

A THIRD bug found the same day, once both of the above were fixed and
the right input was finally being typed into correctly (input.value
confirmed landing before Save): DealerKit's Save request itself still
carried purchase_cost: null. Captured live via a full request body
dump. This is a Vue component whose own reactive form state only
syncs from the raw DOM value on a genuine blur, not just on keystroke
events, so typing via raw mouse and keyboard events (which never
blurs the field before Save is clicked) leaves the framework's own
internal model unaware the value ever changed, even though the DOM
input.value itself reads back correctly, which is exactly why the
earlier "landed" confirmation kept reporting success. Fixed by
switching to Playwright's own locator.fill() plus an explicit
.blur(), the same proven, already working pattern set_retail_price
uses for this identical class of problem on a different DealerKit
panel.

Finds the input by marking it with a temporary, unique attribute
(rather than raw mouse coordinates) so Playwright's own Locator API,
not manual mouse and keyboard events, drives the actual fill and
blur. Reads the DealerKit-visible value back afterward, via the same
marker, to confirm the figure genuinely landed before ever clicking
Save; never raises, a field that cannot be confirmed is left for
Mark to fill by hand rather than risk a wrong or silently empty
figure.Fúdata-bb-purchase-priceaÖ  ([marker, labelKeys]) => {
                const els = document.querySelectorAll('div, span, label');
                let label = null;
                for (const el of els) {
                    if (el.children.length > 3) continue;
                    const raw = el.innerText || '';
                    if (raw.length > 40) continue;
                    const t = raw.toUpperCase().replace(/\s+/g, '');
                    if (!labelKeys.some(k => t.includes(k))) continue;
                    const r = el.getBoundingClientRect();
                    if (r.width > 0 && r.height > 0) { label = {x: r.x + r.width/2, y: r.y + r.height/2}; break; }
                }
                if (!label) return false;
                const inputs = Array.from(document.querySelectorAll('input[type=text]'));
                let best = null, bestDist = Infinity;
                for (const inp of inputs) {
                    const r = inp.getBoundingClientRect();
                    if (r.width === 0 || r.height === 0) continue;
                    const cx = r.x + r.width/2, cy = r.y + r.height/2;
                    const dist = Math.hypot(cx - label.x, cy - label.y);
                    if (dist < bestDist) { bestDist = dist; best = inp; }
                }
                if (!best) return false;
                document.querySelectorAll('[' + marker + ']').forEach(e => e.removeAttribute(marker));
                best.setAttribute(marker, '1');
                return true;
            }Ú[Ú]é,  Tr	   )r&   ÚfloatÚ
is_integerÚintr   r.   Úlistr   r   Úfillr'   ÚblurÚwait_for_timeoutÚinput_valuer   )
r   Úvaluer   Ú
label_keysÚmarkerÚnumberr   ÚmarkedÚfieldÚlandeds
             r   Ú_fill_purchase_pricerU   ¸   s
  € ðd �}ØÜ% e¬U×3Ñ3¸×8HÑ8H×8JÑ8JŒS�ŒZÐPU€FØ%€FÜ�yŠy‹{˜WÑ$€HÜ
�)Š)‹+˜Ó
 Ø—‘ðð6 ”T˜*Ó%Ð&ó9
ˆö< Ø—L‘L 1 V H¨A Ó/ˆEØ�K‰KŒMØ�J‰J”s˜6“{Ô#Ø�J‰JŒLØ×!Ñ! #Ô&Ø×&Ñ&Ó(ˆFÜ�6‹{œc &›kÓ)ØØÜ�
Š
�4ÔôS �)Š)‹+˜Õ
 ðT r"   c                   ó   • \ rS rSrSrSrg)ÚDealerkitMissingMileagei  a  Raised by add_due_in when it is about to create a genuinely NEW Due
In record with no known mileage to give it (2026-08-26, Mark: "never
create a listing in DK unless we have the mileage as this causes DK to
estimate the mileage which is dangerous"). Only ever raised on a first
push, an already existing record is returned early before this check
ever runs ("this is only for the first push if its already present in
DK dont over write"), matching every other hard gate in this project,
play safe rather than let a wrong, DealerKit guessed mileage sit on a
real business record. There is NO bypass (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.© N©Ú__name__Ú
__module__Ú__qualname__Ú__firstlineno__Ú__doc__Ú__static_attributes__rX   r"   r   rW   rW     s   † ô5r"   rW   c                 ó–   • U R                  S5      =(       d    SR                  5       nU(       a  U$ U R                  S5      S:X  a  S$ S$ )aÒ  The supplier to attribute a car's price to on DealerKit, when
nothing more specific applies: the row's own recorded supplier where
one was set (2026-08-31, a hand added car's own supplier wins so it
is never wrongly filed against Motorway), else CarWow or Motorway by
platform. Shared by add_due_in's own new Bought From selection
(2026-09-05) and push_purchase's "already exists, correct a price"
branch, which used to compute this same fallback inline, on its own.Úsupplierr5   ÚplatformÚCarwowÚCarWowÚMotorway)ÚgetÚstrip)Úpurchasera   s     r   Ú_default_supplierri   +  sC   € ð —‘˜ZÓ(×.¨B×5Ñ5Ó7€HÞØˆØ—|‘| JÓ/°8Ó;ˆ8ÐKÀÐKr"   c           
      óú  ^ ^
• [        T S5      nU(       d  [        S5      eT R                  R                  US   US   5        T R	                  S5        U 4S jnT R                  S5      nU(       d  [        S5      eT R                  S	5      nUR                  5         UR                  U5        UR                  5         T R	                  S
5        U" S5      nU(       d  [        S5      eT R                  R                  US   US   5        T R	                  S5        [        T S5        U" S5      nU(       d  [        S5      eT R                  R                  US   US   5        T R	                  S5        [        T S5        0 m
U
4S jnT R                  SU5        T R                  S5      n	U	(       d  T R                  SU5        [        S5      eT R                  R                  U	S   U	S   5        T R	                  S5        T R                  SU5        T
R                  S5      S;  aQ  [        SU< ST
R                  S5      (       a(  [        R                  " T
R                  S5      5      SS
 OS S35      eg)a<  Creates a new DealerKit contact for a car's own supplier, via the
real "+" button beside the Stocklisting step's "Bought From" field
(2026-09-05, Steven: "the fix needs to be that it actually adds the
supplier first ... so it works", so pressing Price on a genuinely new
supplier never again needs someone to go and create the contact in
DealerKit by hand first).

The "+" button is found by its own visible text ("add", the material
icon's own ligature) rather than a class, since a page wide class
search for the same green button style landed on a COMPLETELY
different, unrelated "+ Add Invoice Item" button once the Bought
From field's own dropdown had already been opened (found live), and
the two happened to share a screen position at that moment. Checked
scoped by its own nearby text ("Bought From") instead, found reliable
across several fresh attempts.

Opening it reveals a real "New Contact" modal STACKED on top of the
still-open Stocklisting one (both report the same z-index, so a
naive "first visible dialog" check finds the WRONG one, the one
underneath; scoped to `.contact-modal` specifically here). Company
Name and Contact Type (set to "Supplier") are the two fields this
actually needs; Referral Source LOOKS optional (Save stays enabled
with it blank) but is not, DealerKit's own server refuses the save
with a real 422 ("The referral source id field is required.") that
the button's own enabled state gives no hint of, found by checking
the real network response rather than trusting the click. "Other" is
used there, a neutral choice for a contact recorded purely as a
supplier, not a marketing lead.

Verifies via the real save response (never a guess): raises loudly
if DealerKit refuses it, for whatever reason, rather than reporting
a false success and leaving the caller to type a price with nowhere
real to attribute it.aî  () => {
            const btns = Array.from(document.querySelectorAll('button'));
            for (const b of btns) {
                const r = b.getBoundingClientRect();
                if (r.width === 0 || r.height === 0) continue;
                const icon = b.querySelector('i, .material-icons');
                const text = (b.innerText || icon?.textContent || '').trim();
                if (text !== 'add') continue;
                const node = b.closest('.row') || b.parentElement?.parentElement;
                const ctx = node ? (node.innerText || '') : '';
                if (!ctx.includes('Bought From')) continue;
                return {x: r.x + r.width / 2, y: r.y + r.height / 2};
            }
            return null;
        }z@DealerKit: the Bought From + (add contact) button was not found.r   r   éè  c                 ó(   >• TR                  SU 5      $ )NaP  (label) => {
                const modal = document.querySelector('.contact-modal');
                if (!modal) return null;
                const labels = Array.from(modal.querySelectorAll('.ui-select__label-text'));
                for (const l of labels) {
                    if (l.textContent.trim() !== label) continue;
                    const container = l.closest('.ui-select');
                    const r = container.getBoundingClientRect();
                    return {x: r.x + r.width / 2, y: r.y + r.height * 0.75};
                }
                return null;
            })r.   )Úlabelr   s    €r   Ú
_field_posÚ,_create_supplier_contact.<locals>._field_posr  s   ø€ Ø�}‰}ðð ó
ð 	
r"   a  () => {
            const modal = document.querySelector('.contact-modal');
            if (!modal) return false;
            const labels = Array.from(modal.querySelectorAll('.ui-textbox__label-text'));
            for (const l of labels) {
                if (l.textContent.trim() !== 'Company Name') continue;
                const input = l.closest('label').querySelector('input');
                input.setAttribute('data-bb-new-contact-name', '1');
                return true;
            }
            return false;
        }zDDealerKit: the New Contact modal's Company Name field was not found.z[data-bb-new-contact-name]rE   zContact TypezDDealerKit: the New Contact modal's Contact Type field was not found.é�  ÚSupplierzReferral SourcezGDealerKit: the New Contact modal's Referral Source field was not found.ÚOtherc                 óÄ   >• SU R                   ;   a?  U R                  R                  S:X  a$   U R                  TS'   U R	                  5       TS'   g g g ! [
         a     g f = f)Nz/api/contactsÚPOSTÚstatusÚbody)ÚurlÚrequestÚmethodru   Újsonr   )Úrespr2   s    €r   Ú_on_saveÚ*_create_supplier_contact.<locals>._on_save©  s_   ø€ Ø˜dŸh™hÓ&¨4¯<©<×+>Ñ+>À&Ó+HðØ#'§;¡;��xÑ Ø!%§¡£��v’ð ,IÐ&øô ó Ùðús   ­"A Á
AÁAÚresponseaŸ  () => {
            const modal = document.querySelector('.contact-modal');
            if (!modal) return null;
            const footer = modal.querySelector('.ui-modal__footer');
            const btn = footer ? footer.querySelector('button') : null;
            if (!btn) return null;
            const r = btn.getBoundingClientRect();
            return {x: r.x + r.width / 2, y: r.y + r.height / 2};
        }z=DealerKit: the New Contact modal's Save button was not found.éÐ  ru   ©éÈ   éÉ   z DealerKit refused to create the z
 contact: rv   Nzno response capturedÚ.)r3   ÚRuntimeErrorr   r   rL   r.   r   rJ   rK   Ú_click_dropdown_optionÚonÚremove_listenerrf   rz   Údumps)r   Úcompany_nameÚposrn   rR   rS   Útype_posÚreferral_posr|   Úsave_posr2   s   `         @r   Ú_create_supplier_contactrŽ   9  sD  ù€ ôD Øð	ó€Cö$ ÜÐ]Ó^Ð^Ø‡J�J×Ñ�S˜‘X˜s 3™xÔ(Ø×Ñ˜$Ôõ
ð" �]‰]ð	ó€Fö ÜÐaÓbÐbØ�L‰LÐ5Ó6€EØ	‡K�K„MØ	‡J�Jˆ|ÔØ	‡J�J„LØ×Ñ˜#Ôá˜.Ó)€HÞÜÐaÓbÐbØ‡J�J×Ñ�X˜c‘] H¨S¡MÔ2Ø×Ñ˜#ÔÜ˜4 Ô,áÐ/Ó0€LÞÜÐdÓeÐeØ‡J�J×Ñ�\ #Ñ&¨°SÑ(9Ô:Ø×Ñ˜#ÔÜ˜4 Ô)à€Fõð 	‡G�GˆJ˜Ô!Ø�}‰}ð	ó
€Hö Ø×Ñ˜Z¨Ô2ÜÐZÓ[Ð[Ø‡J�J×Ñ�X˜c‘] H¨S¡MÔ2Ø×Ñ˜$ÔØ×Ñ˜ XÔ.à‡z�z�(Ó :Ó-ÜØ.¨|Ñ.>¸jØ7=·z±zÀ&×7IÑ7IŒt�zŠz˜&Ÿ*™* VÓ,Ó-¨d¨sÑ3ÐOeÐfÐfgðió
ð 	
ð .r"   c                 óB  ^ ^• UU 4S jnU" 5       nU(       d>  [        T T5        T R                  S5        T R                  ST5      nU(       a  gU" 5       nU(       d  [        ST< S35      eT R                  R                  US   US   5        T R                  S5        g)	a.  Picks an EXISTING contact in the Stocklisting step's own "Bought
    From" field (2026-09-05, Steven, testing by hand: "you need to add a
    supplier when adding a car before the purchase price will save",
    found after three other real bugs in this same day's session had
    already been fixed and Purchase Price still would not persist).

    "Bought From" opens the same .ui-select component family as the Add
    to Funding modal's own fields (found live, so _click_ui_select_field,
    already proven for that modal, opens it correctly), but this is a
    live CONTACT SEARCH, not a plain static list: opening it reveals a
    real `.ui-select__search-input` (placeholder "Start typing to
    search") with NO options showing at all until something is typed, so
    the already existing _click_dropdown_option (built for the Funding
    modal's own plain, always-populated list) finds nothing here. Once a
    name is typed, each real result is a multi-line
    `li.ui-select-option` (contact name, then its postcode, then its own
    category, e.g. "Motorway
EC1A 2BN
Supplier"), never a bare
    single-line match, so the option is matched on its FIRST LINE
    (the contact's own name) rather than the whole cell's text.

    Never creates a new contact (the green + button beside this field is
    left alone, same as it always has been); raises if contact_name has
    no matching result after typing, so the caller can fall back to
    leaving Purchase Price for a person to enter rather than typing a
    figure that will only look saved, never genuinely persisting.c                  ó  >• [        TS5        [        TS5      n U (       d  [        S5      eTR                  R	                  U S   U S   S-  -   U S   U S   S-  -   5        TR
                  R                  T5        [        TS	TS
S9$ )NzBought Froma`  () => {
                const els = document.querySelectorAll('.ui-select__search-input');
                for (const el of els) {
                    const r = el.getBoundingClientRect();
                    if (r.width > 0 && r.height > 0) return {x: r.x, y: r.y, w: r.width, h: r.height};
                }
                return null;
            }z4DealerKit: the Bought From search box was not found.r   r6   r   r   r7   a  (name) => {
                const opts = Array.from(document.querySelectorAll('.ui-select-option'));
                for (const el of opts) {
                    const norm = s => (s || '').toLowerCase().replace(/[^a-z0-9]/g, '');
                    const first = (el.innerText || '').split('\n')[0].trim();
                    // "Carwow" on DealerKit, "CarWow" here: the same
                    // contact (2026-09-16, the reason a price could be
                    // left untyped at creation).
                    if (norm(first) !== norm(name)) continue;
                    const r = el.getBoundingClientRect();
                    if (r.width > 0 && r.height > 0) return {x: r.x + r.width / 2, y: r.y + r.height / 2};
                }
                return null;
            }ç      @©r   )Ú_click_ui_select_fieldr3   r„   r   r   ÚkeyboardÚtype)Ú
search_boxÚcontact_namer   s    €€r   Ú	_try_pickÚ&_select_bought_from.<locals>._try_pickæ  s›   ø€ Ü˜t ]Ô3Ü$Øðó

ˆ
ö ÜÐUÓVÐVØ�
‰
×Ñ˜ C™¨:°c©?¸QÑ+>Ñ>À
È3ÁÐR\Ð]`ÑRaÐdeÑReÑ@eÔfØ�‰×Ñ˜<Ô(ÜØðð Øñ#
ð 	
r"   éô  al  (name) => {
                const labels = Array.from(document.querySelectorAll('.ui-select__label-text'));
                for (const l of labels) {
                    if (l.textContent.trim() !== 'Bought From') continue;
                    const container = l.closest('.ui-select');
                    const r = container.getBoundingClientRect();
                    if (r.width === 0) continue;
                    const disp = container.querySelector('.ui-select__display-value');
                    return !!disp && disp.textContent.trim() === name;
                }
                return false;
            }Nz(DealerKit: no Bought From contact named z was found.r   r   )rŽ   rL   r.   r„   r   r   )r   r—   r˜   rŠ   Úselecteds   ``   r   Ú_select_bought_fromrœ   Ì  sœ   ù€ ö4#
ñJ ‹+€CÞô 	!  |Ô4Ø×Ñ˜cÔ"Ø—=‘=ðð ó
ˆö Øñ ‹kˆÞÜÐEÀlÑEUÐU`ÐaÓbÐbØ‡J�J×Ñ�S˜‘X˜s 3™xÔ(Ø×Ñ˜#Õr"   c                 óV  •  [        U R                  5      R                  n SR	                  U R                  5       =(       d    SR                  5       5      SS nU R                   SU 3U(       a  SU 3-   $ S-   $ ! [         a    U R                  n Ntf = f! [         a    Sn NLf = f)zxOne line on a DealerKit reply that said no: the status, the path
and the first words of the body, for a failure message.Ú r5   Né    z on ú: )r   rw   Úpathr   Újoinr)   Úsplitru   )r{   r¡   rv   s      r   Ú_reply_wordsr¤   2  s¤   € ðÜ˜Ÿ™Ó!×&Ñ&ˆðØ�x‰x˜Ÿ™›×*¨×1Ñ1Ó3Ó4°T°cÐ:ˆð �k‰kˆ]˜$˜t˜fÐ%¾¨2¨d¨V¨ÑEÐEÀ"ÑEÐEøô ó Ø�x‰xŠðûô ó ØŠðús"   ‚A= ¢9B Á=BÂBÂB(Â'B(c                 óX   •  U R                  S5      =(       d    S$ ! [         a     gf = f)zÕWhat the open DealerKit window is showing right now, in DealerKit's
own words: its error or warning lines when it has any, otherwise the
whole window's text. Never raises, an empty string when nothing can
be read.aG  () => {
          const opens = document.querySelectorAll('.ui-modal.is-open');
          const m = opens.length ? opens[opens.length - 1] : document.body;
          const picks = m.querySelectorAll('[role=alert], .alert, .error, .invalid-feedback, .text-danger, .has-error, [class*=error], [class*=danger], [class*=warning]');
          const parts = [];
          for (const el of picks) { const t = (el.innerText || '').trim(); if (t && !parts.includes(t)) parts.push(t); }
          if (parts.length) return parts.join(' ');
          return (m.innerText || '').trim();
        }r5   ©r.   r   ©r   s    r   Ú_modal_wordsr¨   @  s;   € ð
Ø�}‰}ð ó ÷ ð ð	øô ó Ùðús   ‚ œ
)¨)c                 ó  • SR                  U=(       d    SR                  5       5      SU nSR                  S U=(       d    /  5       5      SU nSU  S3nU(       a  USU 3-  nU(       a  US	U 3-  nU(       d  U(       d  US
-  nU$ )z÷The sentence a send raises when DealerKit neither created the car
nor said it already had it (2026-09-06). Carries DealerKit's own
words, from the window and from its server, so Dealer OS's timeline
says why instead of "the page may have changed".rž   r5   Nz; c              3   ó6   #   • U  H  o(       d  M  Uv •  M     g 7f©NrX   ©Ú.0Úrs     r   Ú	<genexpr>Ú+_no_confirmation_message.<locals>.<genexpr>Y  s   é € Ð9¢N˜a³qŸ™¢Nùó   ‚
�	zDealerKit did not add z& after its registration was submitted.z The Add Vehicle window says: z DealerKit's server replied: z0 No reason was shown. The page may have changed.)r¢   r£   )ÚregÚsaidÚrefusalsÚcapÚrepliesÚmsgs         r   Ú_no_confirmation_messager¸   S  s•   € ð
 �8‰8�T—Z˜R×&Ñ&Ó(Ó)¨$¨3Ð/€DØ�i‰iÑ9 H§N° NÓ9Ó9¸$¸3Ð?€GØ" 3 %Ð'MÐ
N€CÞØÐ/°¨vÐ6Ñ6ˆÞØÐ.¨w¨iÐ8Ñ8ˆÞžØÐAÑAˆØ€Jr"   c                 ó¶  ^^• [        X[        U5      5      nU(       a  SSSUS.$ U(       a  UR                  S5      (       d  [        U S35      eU R	                  [
         S3SSS	9  U R                  S
5        [        X R                  S5      5      (       d  [        S5      eU R                  S5        [        U R                  S5      5      u  pVU(       d  [        S5      eU R                  R                  US   US   S-  -   US   US   S-  -   5        U R                  R                  U5        U R                  S5        SnU=(       d    0 R                  S5      nU(       a–  [        U R                  S5      SS9u  pšU
(       at  U R                  R                  U
S   U
S   S-  -   U
S   U
S   S-  -   5        U R                  R                  [!        [#        U5      5      5        U R                  S5        SnSSUS.m/ mUU4S jnU R%                  SU5         ['        U SSS9(       d  [        SU S 35      e[(        R(                  " 5       S!-   n[(        R(                  " 5       U:  aT  TS"   (       dJ  TS#   (       d@  U R                  S$5        [(        R(                  " 5       U:  a  TS"   (       d  TS#   (       d  M@  TS"   (       a  U R                  S%5        U R+                  SU5        TS"   (       dS  TS#   (       dI  S&n [        X[        U5      5      nU(       a  STS#'   UTS''   O[        [1        U[3        U 5      T5      5      eTS"   (       a0  U(       a)  TR                  S'5      (       a   [5        U TS'   5      TS('   TS"   (       GaG  S&nU(       aD  U(       a=  UR                  S)5      nUc)  UR                  S*5      =(       d    UR                  S+5      nSnS&nUbv  [7        U5      nS&n[9        S5       H  n [;        U U5        Sn  O   U(       dA  S,U< S-3U(       a.  S.S/R=                  [!        U5      R?                  5       5      S&S0  3OS1-   nS&n[A        X5      nSnUb	  U(       d  S2nU(       Ga  [C        U[D        5      (       a)  [E        U5      RG                  5       (       a  [#        U5      OUnTR                  S'5      n[9        S5       H¦  nU R                  S3S4S59nSn[9        URI                  5       5       H$  n URK                  U5      R                  S
S9  Sn  O   U(       d    OKU R                  S65        U(       a-  [M        U [
         S7U 35      nUR                  S85      b  Sn  OM¤  Sn  O   U(       a	  U(       d  S9nU(       d  ['        U S:SS;S<9  U R                  S=5        UTS>'   UTS?'   U(       a  [O        S@U SA35        T$ STS>'   S&TS?'   T$ ! U R+                  SU5        f = f! [,         a    e [.         a    S&n GNùf = f! [.         a	    S&TS('    GN¤f = f! [.         a  nUn S&nAGMD  S&nAff = f! [.         a     GMQ  f = f)Ba¾  Create the vehicle as a Due In stock record from its registration
alone. Returns {"created": True} for a genuinely new record, or
{"already_exists": True} when DealerKit already has this reg in its
stocklist (real state, not an error, someone on the team may have added
it by hand already). Raises if neither is confirmed.

purchase is used two separate ways, deliberately never coupled: its
mileage always gates whether a genuinely new record may be created at
all (see DealerkitMissingMileage below), regardless of what is being
pushed, since photos or documents alone still need a real vehicle_id
to upload to and so still create the record; type_price (2026-08-26,
a real bug caught building the mileage gate, a "photos only" push
used to pass purchase=None specifically to suppress typing the price,
which would have also suppressed the mileage check on a car that
genuinely had one on file) separately controls whether purchase's own
price actually gets typed into the Purchase Price field, matching
push_purchase's own "only price gates the typed figure".

Raises DealerkitMissingMileage before ever opening the Add Vehicle
modal when this would be a genuinely new record and purchase carries
no mileage. No bypass exists, see the class docstring above.

When purchase is given and this is a genuine new record, types
BidBrain's own already computed purchase price (winning_bid or price)
into the Purchase Price field before Save. DealerKit has its own live
Motorway integration that can fill this in on its own (proven live
twice, see CLAUDE.md), but only ever seen firing on a session Mark
drove himself by hand, never on an automated one (three separate
attempts, one waited on for 15+ minutes, all blank), so an automated
push types the figure in itself rather than leave it sitting empty
forever. Retail Price is left untouched, that is Mark's own market
call. Nothing else on the Stocklisting step is touched either (Bought
From needs matching or creating a DealerKit contact, not attempted
here). When purchase is not given, or its price could not be confirmed
landing in the field, just clicks Skip, same as leaving it for Mark.

Checks DealerKit's own stocklist for this reg FIRST, before ever
attempting to create anything (2026-08-24, found live the hard way: a
Carwow purchases batch created 13 genuine DUPLICATE Due In records
for cars DealerKit already held as a completed, disposed sale,
life_cycle_status 25). The bug was trusting DealerKit's own POST
/api/stocklist response alone to detect "this reg already exists" (a
422): that check IS real and does fire for a reg still active in some
other stage, but DealerKit does NOT return it for an already disposed
one, it just creates a second record instead, no error at all. A
reg lookup first (_find_vehicle_id_by_reg, already used elsewhere in
this module) closes that gap regardless of the vehicle's own status,
and is also simply cheaper: a genuine repeat call for an already
pushed reg no longer even opens the Add Vehicle modal.FT)ÚcreatedÚalready_existsÚpurchase_price_filledÚ
vehicle_idÚmileagez² has no mileage on file. Creating it on DealerKit now would let DealerKit estimate the mileage itself, which is never allowed. Enter the real mileage on the purchases page first.ú	/vehiclesÚdomcontentloadedéÈ¯  ©Ú
wait_untilr   r   za.pill-greenz^DealerKit: the + Vehicle button was not found on the vehicles list. The page may have changed.é°  ze.g. EO67 GJVzdDealerKit: the registration field was not found in the Add Vehicle modal. The page may have changed.r   r   r   r   r   éX  z-.ui-modal.is-open input[placeholder*="43000"]r‘   r’   rp   )rº   r»   Úmileage_typedc                 ó*  >• SU R                   ;   a¶  U R                  R                  S:X  aœ  U R                  S:X  a  STS'   g U R                  S;   a[  STS'    U R	                  5       nUR                  S5      =(       d)    UR                  S	5      =(       d    0 R                  S5      TS
'   g TR                  [        U 5      5        g SU R                   ;   a,  U R                  S:¼  a  TR                  [        U 5      5        g g g ! [         a     g f = f)Nz/api/stocklistrt   i¦  Tr»   r€   rº   ÚidÚdatar½   z/api/rp   )	rw   rx   ry   ru   rz   rf   r   Úappendr¤   )r{   rv   r´   r2   s     €€r   Ú_on_responseÚ add_due_in.<locals>._on_responseË  sè   ø€ Ø˜tŸx™xÓ'¨D¯L©L×,?Ñ,?À6Ó,IØ�{‰{˜cÓ!Ø+/�Ð'Ò(Ø—‘ 
Ó*Ø$(��yÑ!ð
ØŸ9™9›;�DØ+/¯8©8°D«>×+_¸d¿h¹hÀvÓ>N×>TÐRT×=YÑ=YÐZ^Ó=_�F˜<Ò(ð —‘¤¨TÓ 2Õ3Ø˜Ÿ™Ó  T§[¡[°CÓ%7ð �O‰OœL¨Ó.Õ/ð	 &8Ð øô	 !ó Ùðús   ÁAD Ä
DÄDr~   ÚNextr$   z6DealerKit: the Next button was not found after typing rƒ   é   rº   r»   rš   i¸  Nr½   Úmileage_on_recordÚ	car_priceÚwinning_bidÚpricezthe Bought From contact z could not be pickedú, rž   r�   r5   z8the Purchase Price box was not found on DealerKit's formÚbuttonÚSave©Úhas_textéÜ  ú/api/stocklist/Úpurchase_costz2the figure was typed but DealerKit did not save itÚSkipç      @©r%   r   rk   r¼   Úprice_reasonz)  Purchase price at creation: not saved, z&; it goes on the purchase expense next)(Ú_find_vehicle_id_by_regÚother_platerf   rW   ÚgotoÚDEALERKIT_BASErL   r!   r   r„   r   Úget_by_placeholderr   r   r”   r•   r'   rH   r†   r,   r   r‡   ÚDealerkitSessionExpiredr   r¸   r¨   Ú_vehicle_mileageri   r   rœ   r¢   r£   rU   r&   rF   rG   r   r   Ú	_get_jsonÚprint)r   r²   rh   Ú
type_priceÚexisting_idÚreg_elÚreg_boxrÆ   ÚmilesÚmil_elÚmil_boxrË   r   Úlate_idrÒ   Úbought_from_setrÞ   ra   Úlast_errÚ_ÚeÚtypedÚfilledr=   Únew_idÚbtnsÚclickedr   rÉ   r´   r2   s                                @@r   Ú
add_due_inrù   d  sB  ù€ ôd *¨$´[ÀÓ5JÓK€KÞØ °DÐSXØ)ñ+ð 	+ö ˜Ÿ™ i×0Ñ0Ü%Øˆeð <ð <ó
ð 	
ð 	‡I�I”Ð  	Ð*Ð7IÐSX€IÑYØ×Ñ˜$Ôä˜$§¡¨^Ó <×=Ñ=Üð)ó
ð 	
ð 	×Ñ˜$Ôä˜t×6Ñ6°ÓGÓH�O€FÞÜð)ó
ð 	
ð 	‡J�J×Ñ�W˜S‘\ G¨GÑ$4°qÑ$8Ñ8¸'À#¹,ÈÐQYÑIZÐ]^ÑI^Ñ:^Ô_Ø‡M�M×Ñ�sÔØ×Ñ˜#Ôð €MØ�^˜× Ñ  Ó+€EÞÜ" 4§<¡<Ð0_Ó#`ÐjmÑn‰ˆÞØ�J‰J×Ñ˜W S™\¨G°GÑ,<¸qÑ,@Ñ@À'È#Á,ÐQXÐYaÑQbÐefÑQfÑBfÔgØ�M‰M×Ñœs¤3 u£:›Ô/Ø×!Ñ! #Ô&Ø ˆMà°%È-ÑX€FØ€Hö0ð. 	‡G�GˆJ˜Ô%ð7Ü" 4¨°t×<ÜØHÈÈÈQÐOóð ô —9’9“; Ñ#ˆÜ�iŠi‹k˜HÓ$¨f°Y×.?À6ÐJZ×C[Ø×!Ñ! #Ô&ô �iŠi‹k˜HÓ$¨f°Y×.?À6ÐJZ×C[ÑC[à�)×Ø×!Ñ! $Ô'à×Ñ˜Z¨Ô6à�)× VÐ,<×%=ð ˆð	Ü-¨d¼ÀXÓ9NÓOˆGö
 Ø'+ˆFÐ#Ñ$Ø#*ˆF�<Ò äÔ7¸¼\È$Ó=OÐQYÓZÓ[Ð[àˆi×ž]¨v¯z©z¸,×/GÑ/Gð	/Ü*:¸4ÀÈÑAUÓ*VˆFÐ&Ñ'ð ˆi×Ðð ˆÞž
ð —L‘L Ó-ˆEØ‰}Ø Ÿ™ ]Ó3×L°x·|±|ÀGÓ7L�ð  ˆØˆØÑÜ(¨Ó2ˆHð ˆHÜ˜1–X�ðÜ'¨¨hÔ7Ø&*�OÙñ	 ö #ð #;¸8¹,ÐFZÐ [ÞS[ R¨¯©´°X³×1DÑ1DÓ1FÓ(GÈÈÐ(MÐ'NÑ#OÐacñ!e�à�ô % TÓ1ˆØˆØÑ¦UØUˆLßô $.¨e´U×#;Ñ#;ÄÀeÃ×@WÑ@W×@YÑ@Y”S˜”ZÐ_dˆFØ—Z‘Z Ó-ˆFÜ˜1–X�Ø—|‘| H°v�|Ð>�Ø�Ü˜tŸz™z›|Ö,�Að!ØŸ™ ›×)Ñ)°$Ð)Ñ7Ø"&˜Ùñ	 -ö ÙØ×%Ñ% dÔ+ÞÜ$ T¬nÐ-=¸_ÈVÈHÐ+UÓV�DØ—x‘x Ó0Ñ<Ø!%˜Ùñ =ð "�FÙñ3 ö4 žØOˆLÞÜ  f°DÀ#ÒFØ×!Ñ! $Ô'Ø*0ˆÐ&Ñ'Ø!-ˆˆ~ÑÞÜÐ=¸l¸^ÐKqÐrÔsð
 €Mð +0ˆÐ&Ñ'Ø!%ˆˆ~Ñà€Møðc 	×Ñ˜Z¨Õ6ûô 'ó 	ØÜó 	Ø‹Gð	ûô ó 	/Ø*.ˆFÐ&Ô'ð	/ûôf !ó Ø �HÞûðûôT %ó !Û ð!úsa   ÈB"W$ ËW$ ÌW; Í4X ÐX.Ô
 Y	×$W8×;XØXØX+Ø*X+Ø.
YØ8YÙYÙ	
YÙYc                 óJ  ^ • [        T X5      (       a  gT R                  [         SU S3SSS9  T R                  S5        [	        T S5      (       d  [        S	U S
35      eT R                  S5        [        T SSSS9(       d  [        SU S
35      eT R                  S5        [        T R                  S5      5      u  p4U(       d  [        SU S
35      e[        [        U5      5      nUR                  5         UR                  [        U5      5        UR                  5         T R                  S5        U 4S jn[        S5       HN  nU" 5       (       d  M  T R                  S5        [!        T [         SU 35      nUR#                  S5      U:X  d  MN    g   [        SU S35      e)uî  Set DealerKit's own Retail Price on the vehicle's Pricing edit
panel (2026-08-26, Mark: "the Retail Est is the retail price that
should be mapped to DK", confirmed: "BB calculates the retail est
based on cazana 115%, this is the retail price that is then used in
DK"). This reverses the earlier, deliberate "leave Retail Price for
Mark to type in by hand" decision recorded elsewhere in this project,
a real policy change confirmed explicitly before building this.

Built against the real live page (vehicle 336, GU68XPP): Edit
Vehicle > Pricing opens a panel whose big central price display is
genuinely a plain <input type=text placeholder="Â£RETAIL">, found by
searching for that exact placeholder text once a plain "RETAIL" text
search only ever found the unrelated AutoTrader valuation figure
sitting elsewhere on the same panel; the label only exists as an
input placeholder here, never as real text content anywhere in the
DOM. Typing into it fires a real, harmless GET to DealerKit's own
pricing-insights endpoint (the live "AutoTrader price position"
indicator recalculating), proof the field itself is genuinely wired
up before Save is ever clicked.

Saving needs Playwright's own real .click() with its full
actionability checks, not this module's usual _click_visible_text
(a raw mouse click at a bounding box centre): this panel renders
SEVEN "Save Changes" buttons at once, and unlike every other
duplicate-button case elsewhere in this file, more than one of the
hidden ones still reports a real, non-zero bounding box, the one
thing _click_visible_text's own visibility check relies on, so it
silently reported success while actually clicking a dead duplicate
(confirmed live: no PATCH request at all reached DealerKit's API
that way). Tries each match in turn with a real, strict .click()
until one genuinely succeeds. Verifies via the API afterward,
retrying once, raising loudly rather than reporting a false success,
the same lesson every other DealerKit write in this project has
already learned the hard way.Úapiú
/vehicles/ú	/overviewrÀ   rÁ   rÂ   éÄ	  ÚEDITú2DealerKit: the Edit button was not found (vehicle ú).rØ   ÚPricingTú.ui-modal.is-open©r%   r*   z3DealerKit: the Pricing tile was not found (vehicle u   Â£RETAILz9DealerKit: the Retail Price field was not found (vehicle rš   c                  óÆ   >• TR                  SSS9n [        U R                  5       5       H"  n U R                  U5      R	                  SS9    g   g! [
         a     M4  f = f)NrÔ   úSave ChangesrÖ   r   r’   TF©r   r   r   r   r   r   ©r÷   r   r   s     €r   Ú	_try_saveÚ#set_retail_price.<locals>._try_save¿  sd   ø€ Ø�|‰|˜H¨~ˆ|Ð>ˆÜ�t—z‘z“|Ö$ˆAðØ—‘˜“×!Ñ!¨$Ð!Ñ/Ùñ %ð øô ó Úðúó   ¯AÁ
A ÁA r   r   rÙ   Úretail_priceÚscreenz:DealerKit: Retail Price did not actually save for vehicle rƒ   )Úset_retail_price_apirá   râ   rL   r@   r„   r,   r   rã   rH   Úroundr   rJ   r'   rK   r   ræ   rf   )	r   r½   rÒ   rS   Úboxr=   r	  rò   rÉ   s	   `        r   Úset_retail_pricer  …  s�  ø€ ôL ˜D *×4Ñ4ØØ‡I�I”Ð  
¨:¨,°iÐ@ÐM_Ðin€IÑoØ×Ñ˜$ÔÜ˜T 6×*Ñ*ÜÐOÐPZÈ|Ð[]Ð^Ó_Ð_Ø×Ñ˜$ÔÜ˜t Y°dÐCV×WÜÐPÐQ[ÐP\Ð\^Ð_Ó`Ð`Ø×Ñ˜$Ôä˜$×1Ñ1°*Ó=Ó>�J€EÞÜÐVÐWaÐVbÐbdÐeÓfÐfÜ”�u“Ó€FØ	‡K�K„MØ	‡J�JŒs�6‹{ÔØ	‡J�J„LØ×Ñ˜#Ôõô �1ŽXˆÙ�;‹;Ø×!Ñ! $Ô'Ü˜T¤nÐ%5°_ÀZÀLÐ#QÓRˆDØ�x‰x˜Ó'¨6Õ1Ùñ ô ÐSÐT^ÐS_Ð_`ÐaÓ
bÐbr"   c                 óþ   • U (       d  g [         R                   R                  U R                  SS5      5      nU[         R                  " SS9-   R                  5       R                  5       U:H  $ ! [         a     gf = f)a  DealerKit stores delivery_on as UTC midnight of the LOCAL (UK) day
(confirmed live 2026-08-26: setting it to "2026-08-28" read back as
"2026-08-27T23:00:00+00:00" during BST), so a plain stored_iso[:10]
comparison is off by a day whenever the UK is on summer time. Adding
a small fixed buffer before taking the date is safe regardless of
which offset (GMT +0 or BST +1) applies, since the UK is never more
than an hour ahead of UTC, unlike branching on the stored hour, which
would need to know the exact DST boundary date to get right.FÚZz+00:00é   )Úhours)ÚdatetimeÚfromisoformatÚreplaceÚ
ValueErrorÚ	timedeltaÚdateÚ	isoformat)Ú
stored_isoÚdate_isoÚdts      r   Ú_delivery_date_matchesr   Ò  su   € ö ØðÜ×Ñ×,Ñ,¨Z×-?Ñ-?ÀÀXÓ-NÓOˆð ”×#Ò#¨!Ñ,Ñ,×2Ñ2Ó4×>Ñ>Ó@ÀHÑLÐLøô ó Ùðús   Š/A/ Á/
A<Á;A<c                 óÚ  ^ • [        T [         SU 35      n[        UR                  S5      U5      (       a  gT R	                  [         SU S3SSS9  T R                  S	5        [        T S
5      (       d  [        SU S35      eT R                  S5        [        T SSSS9(       d  [        SU S35      eT R                  S5        [        T S5        T R                  S5        [        T US5        U 4S jn[        S5       HY  nU" 5       (       d  M  T R                  S5        [        T [         SU 35      n[        UR                  S5      U5      (       d  MY    g   [        SU S35      e)a†  DealerKit's own "Due In / In-stock On" date, on the vehicle's
Miscellaneous edit panel (2026-08-26, Mark found it live: "on the car
listing page go to edit then miscellaneous... the due in / in-stock
date it there"). Its internal API field is delivery_on, the exact
field an earlier session (see CLAUDE.md, 2026-08-23) concluded had NO
editable UI path anywhere, checked at the time under Description,
Additional Details, and this same Miscellaneous tab's Location and
Delivery Details section, and even tried once as a raw API write,
refused by Claude Code's own safety check as untested and invasive.
That conclusion was wrong, or the field is newer, either way it is
real and working now, found by Mark navigating the live UI himself
rather than guessed at again.

date_iso should be BidBrain's own collection_date (Motorway's real
"Collected"/"Collection date" timeline read, or Carwow's own
"Collection date" card, see motorway.read_collection_date and
carwow.read_collection_date), the date the car is due at the
dealership. No op, returns False without touching anything, if the
stored date already matches (see _delivery_date_matches).

Built the same way as every other DealerKit date write in this
module: a real UI flow, never a raw API PATCH. Opens the field via
_click_ui_select_field (the same .ui-datepicker__label-text
component the Add to Funding modal's own date fields use) and picks
the day via _pick_calendar_date. The Miscellaneous panel's own SAVE
button is one of 24 elements on the page sharing that exact text,
almost all hidden duplicates from other Edit Vehicle tiles rendered
off screen (the same shape already seen and solved for Pricing's own
"Save Changes", 7 of which report a live but dead bounding box), so
every match is tried in turn with a real .click() until one
genuinely succeeds, then verified via a fresh API read, retrying
once, raising loudly rather than reporting a false success.rÙ   Údelivery_onFrü   rý   rÀ   rÁ   rÂ   rþ   rÿ   r   r  rØ   ÚMiscellaneousTr  r  z9DealerKit: the Miscellaneous tile was not found (vehicle zDue In / In-stock Oné   c                  óÆ   >• TR                  SSS9n [        U R                  5       5       H"  n U R                  U5      R	                  SS9    g   g! [
         a     M4  f = f)NrÔ   ÚSAVErÖ   r   r’   TFr  r  s     €r   r	  Ú$set_delivery_date.<locals>._try_save  sd   ø€ Ø�|‰|˜H¨vˆ|Ð6ˆÜ�t—z‘z“|Ö$ˆAðØ—‘˜“×!Ñ!¨$Ð!Ñ/Ùñ %ð øô ó Úðúr  r   r   zBDealerKit: Due In / In-stock On did not actually save for vehicle rƒ   )ræ   râ   r   rf   rá   rL   r@   r„   r,   r“   Ú_pick_calendar_dater   )r   r½   r  Úexistingr	  rò   rÉ   s   `      r   Úset_delivery_dater*  ä  si  ø€ ôB ˜¤.Ð!1°ÀÀÐMÓN€HÜ˜hŸl™l¨=Ó9¸8×DÑDØà‡I�I”Ð  
¨:¨,°iÐ@ÐM_Ðin€IÑoØ×Ñ˜$ÔÜ˜T 6×*Ñ*ÜÐOÐPZÈ|Ð[]Ð^Ó_Ð_Ø×Ñ˜$ÔÜ˜t _¸DÐI\×]ÜÐVÐWaÐVbÐbdÐeÓfÐfØ×Ñ˜$Ôä˜4Ð!7Ô8Ø×Ñ˜#ÔÜ˜˜hÐ(>Ô?õô �1ŽXˆÙ�;‹;Ø×!Ñ! $Ô'Ü˜T¤nÐ%5°_ÀZÀLÐ#QÓRˆDÜ% d§h¡h¨}Ó&=¸x×HÓHÙñ ô Ð[Ð\fÐ[gÐghÐiÓ
jÐjr"   c                 óf  ^• [         R                   " 5       U-   nS=pV[         R                   " 5       U:  aÒ  / nU4U4S jjnU R                  SU5         U R                  [         ST S3SSS9  U R	                  S	5        U R                  SU5        U H6  n	 U	R                  5       n
U
R                  S
5      nU
R                  S5      n  O   Uc  Ub  XV4$ [         R                  " U5        [         R                   " 5       U:  a  MÒ  XV4$ ! U R                  SU5        f = f! [         a     M›  f = f)aÖ  Poll DealerKit's own stocklist API (not the UI, which masks these
figures behind a Show toggle) for up to timeout seconds, waiting for
its own Motorway integration to fill in purchase_cost and retail_price
after a fresh due in creation. Returns (purchase_cost, retail_price),
either or both None if DealerKit had not filled them in by the
deadline (a real, normal outcome, not every reg matches a source DealerKit
recognises; the record itself is unaffected either way).Nc                 ó†   >• ST 3U R                   ;   a-  U R                  R                  S:X  a  UR                  U 5        g g g )NrÙ   ÚGET)rw   rx   ry   rÊ   )r®   Ú	_capturedr½   s     €r   rË   Ú3wait_for_dealerkit_financials.<locals>._on_response6  s>   ø€ Ø   Ð-°·±Ó6¸1¿9¹9×;KÑ;KÈuÓ;TØ× Ñ  Õ#ð <UÐ6r"   r~   rü   rý   rÀ   rÁ   rÂ   rþ   rÚ   r  )
r   r†   rá   râ   rL   r‡   rz   r   rf   r   )r   r½   r   Ú
poll_everyr   rÚ   r  ÚcapturedrË   r®   rÉ   s    `         r   Úwait_for_dealerkit_financialsr2  )  s1  ø€ ô �yŠy‹{˜WÑ$€HØ#'Ð'€MÜ
�)Š)‹+˜Ó
 Øˆà&.÷ 	$ð 	�‰�
˜LÔ)ð	;Ø�I‰IœÐ(¨
°:°,¸iÐHØ!3¸Uð ñ Dà×!Ñ! $Ô'à× Ñ  ¨\Ô:ãˆAðØ—v‘v“x�ð !ŸH™H _Ó5ˆMØŸ8™8 NÓ3ˆLÙñ ð Ñ$¨Ñ(@Ø Ð.Ð.Ü�
Š
�:Ôô3 �)Š)‹+˜Õ
 ð4 Ð&Ð&øð × Ñ  ¨\Õ:ûô
 ó Úðús   Á,D ÂD"ÄDÄ"
D0Ä/D0c           
      ó¢  ^^^• U R                  [         SU S3SSS9  U R                  S5        [        XS5        U R                  S5        [	        U[
        [        45      (       aE  U Vs/ s H7  oDR                  5       (       d  M  UR                  5       R                  5       PM9     snOUR                  5       R                  5       /nU R                  S	U5      n[        U5      S
:w  a  [        SU< SU S[        U5       S35      eUS   nU R                  SU5      nU(       d  [        SU S35      eU R                  R                  US   US   5        U R                  S5        U R                  S5      n	[        R                  " SU	=(       d    S5      n
U
(       d  [        SU S35      e[!        U
R#                  S
5      5      mU R                  ST5      nU(       d  [        ST SU S35      eU R                  R                  US   US   5        U R                  S5        SS0mUU4S jnU R%                  S U5         ['        U S!S"S#9(       d  [        S$T SU S35      eU R                  S5        U R)                  S U5        T(       d  gTS   (       d  [        S%T SU S35      eU R*                  R,                  R/                  [         S&U S'3S(S)9R1                  5       n[3        U4S* jUR/                  S+5      =(       d    /  5       5      nU(       a  [        S,T S-U S35      eg"s  snf ! U R)                  S U5        f = f).a#  Deletes a whole expense record (every line item on it, not just
one) via the vehicle's own Edit > Expenses list and that specific
expense's own "More" menu. match_text is real, distinguishing text
from the expense's own row on the plain list (for example
"CarWow" plus "Delivery Charge" for a standalone one item expense,
never "Buyers Premium" in the same search so it does not also match a
fuller combined expense), used to find and open the right one; raises
if none or more than one row matches, never guesses which expense a
human meant.

Built 2026-08-24 to remove a real duplicate: WX16AUE had two separate
CarWow expenses, one a standalone single Delivery Charge item left
over from however these older records were first entered, found live
by Mark ("if you find record with duplicate info WX16AUE delete it").
Real reconnaissance found two dead ends before this worked: the item's
own kebab menu (Edit / Duplicate / Delete) DOES let a single line item
be deleted, but only from the browser's own local form state, a
"Save & Approve" afterward silently did not persist an expense
emptied down to zero items (DealerKit likely refuses that, an expense
needs at least one line); this function goes through the EXPENSE's
own separate "More" button (bottom left of the Edit Expense modal,
distinct from the item row's own more_vert kebab, matched on its
real "more_horiz" icon text so it is never confused with the item
one) instead, whose own Delete removes the whole record in one action
and needs no separate Save, verified live: item 752 and expense 571
were both confirmed gone via the API immediately after, no second
save step needed. Triggers a real native window.confirm ("Are you
sure you want to delete this expense?"), handled the same way as
delete_vehicle's own dialog. Verifies via the API afterward that the
matched expense's own real numeric id (read from the modal's own
"Edit Expense #NNN" heading before deleting) no longer has any items,
the same lesson as every other write in this module, a click
reporting success is not proof something actually happened.rü   rý   rÀ   rÁ   rÂ   r   ÚExpensesrØ   aR  (needles) => {
            const els = Array.from(document.querySelectorAll('div'));
            const out = [];
            for (const el of els) {
                if (el.children.length > 4) continue;
                const t = (el.textContent || '').trim().toLowerCase();
                if (!t) continue;
                if (needles.every(n => t.includes(n))) {
                    const r = el.getBoundingClientRect();
                    if (r.width > 0 && r.height > 0) out.push({x: r.x, y: r.y, w: r.width, h: r.height});
                }
            }
            return out;
        }r   z5DealerKit: expected exactly one expense row matching ú
 (vehicle z	), found rƒ   r   aE  (row) => {
            const el = document.elementFromPoint(row.x + row.w / 2, row.y + row.h / 2);
            let node = el;
            for (let i = 0; i < 5 && node; i++) {
                const btn = node.querySelector && node.querySelector('button, svg, i.material-icons');
                if (btn) {
                    const r = btn.getBoundingClientRect();
                    if (r.width > 0 && r.height > 0) return {x: r.x + r.width / 2, y: r.y + r.height / 2};
                }
                node = node.parentElement;
            }
            return null;
        }zPDealerKit: could not find the edit control for the matched expense row (vehicle r  r   r   zï() => {
            const h = Array.from(document.querySelectorAll('*'))
                .find(e => e.children.length === 0 && (e.textContent || '').includes('Edit Expense #'));
            return h ? h.textContent.trim() : null;
        }zEdit Expense #(\d+)r5   z[DealerKit: opening the matched expense did not land on its own Edit Expense modal (vehicle aw  (expense_id) => {
            const modal = Array.from(document.querySelectorAll('[role=dialog]'))
                .find(d => (d.innerText || '').includes('Edit Expense #' + expense_id));
            if (!modal) return null;
            const els = Array.from(modal.querySelectorAll('button'));
            for (const el of els) {
                if ((el.textContent || '').includes('more_horiz')) {
                    const r = el.getBoundingClientRect();
                    if (r.width > 0 && r.height > 0) return {x: r.x + r.width / 2, y: r.y + r.height / 2};
                }
            }
            return null;
        }z@DealerKit: the expense's own More button was not found (expense z
, vehicle rk   ÚflagFc                 ó`   >• T(       a  STS'   U R                  5         g U R                  5         g ©NTr6  ©ÚacceptÚdismiss©ÚdÚacceptedÚconfirms    €€r   Ú
_on_dialogÚ"delete_expense.<locals>._on_dialogÉ  ó"   ø€ ÞØ#ˆH�VÑØ�H‰H�Jà�I‰I�Kr"   ÚdialogÚDeleteTr$   zBDealerKit: the expense's own Delete option was not found (expense z@DealerKit: the delete confirmation dialog was not seen (expense rÙ   ú0?with[]=expenseItems&with[]=expenseItems.expenseé N  r’   c              3   óz   >#   • U  H0  oR                  S 5      =(       d    0 R                  S5      T:H  v •  M2     g7f)ÚexpenserÈ   N©rf   )r­   ÚitÚ
expense_ids     €r   r¯   Ú!delete_expense.<locals>.<genexpr>à  s/   øé € ÐuÒSsÈB—v‘v˜iÓ(×.¨B×3Ñ3°DÓ9¸ZÖGÒSsùs   ƒ8;Úexpense_itemszDealerKit: expense z> still has items after delete, it may not have saved (vehicle )rá   râ   rL   Ú_open_edit_tiler&   rI   Útuplerg   Úlowerr.   Úlenr„   r   r   r9   ÚsearchrH   Úgroupr†   r,   r‡   Úcontextrx   rf   rz   Úany)r   r½   Ú
match_textr?  ÚtÚneedlesr>   ÚrowÚ
pencil_posÚtitler?   Úmore_posr@  ÚafterÚstill_therer>  rK  s      `           @@r   Údelete_expenser_  P  sÆ  ú€ ðD 	‡I�I”Ð  
¨:¨,°iÐ@Ø+°Uð ñ <à×Ñ˜$ÔÜ�D jÔ1Ø×Ñ˜$ÔäFPÐQ[Ô^bÔdiÐ]j×FkÑFk©*ÓBª* Q¿¹¿	Ó ˆq�w‰w‹y�‰Ö ©*ÒBØ×ÑÓ ×&Ñ&Ó(Ð)ð à�m‰mð	ð 	ó€Gô" ˆ7ƒ|�qÓÜØCÀJÁ>ð RØ"�| 9¬S°«\¨N¸!ð=ó
ð 	
ð �!‰*€CØ—‘ð	ð 	ó€Jö  ÜÐmÐnxÐmyÐy{Ð|Ó}Ð}Ø‡J�J×Ñ�Z ‘_ j°¡oÔ6Ø×Ñ˜$Ôà�M‰Mð	ó€Eô 	�	Š	Ð(¨%¯+°2Ó6€AÞÜÐxð  zDð  yEð  EGð  Hó  Ið  	IÜ�Q—W‘W˜Q“Z“€Jà�}‰}ð	ð 	ó€Hö  ÜÐ]Ð^hÐ]iÐisÐt~Ðsð  @Bð  Có  Dð  	DØ‡J�J×Ñ�X˜c‘] H¨S¡MÔ2Ø×Ñ˜$Ôà˜ˆ€Höð 	‡G�GˆH�jÔ!ð3Ü" 4¨¸×>ÜÐ!cÐdnÐcoÐoyð  {Eð  zFð  FHð   Ió  Jð  JØ×Ñ˜dÔ#à×Ñ˜X zÔ2æØØ�F×ÜÐ]Ð^hÐ]iÐisÐt~Ðsð  @Bð  Có  Dð  	Dà�L‰L× Ñ ×$Ñ$ÜÐ
˜/¨*¨Ð5eÐfØð %ð ç‘t“vð 
ô ÔuÐSX×S\ÑS\Ð]lÓSm×SsÐqsÐSsÓuÓu€KÞÜÐ0°°Ð<zð  |Fð  {Gð  GIð  Jó  Kð  	KØùòW Cøð| 	×Ñ˜X zÕ2ús   Á,L5Â"L5É	3L: Ì:Mc                 óŒ  • U R                  [         S3SSS9  U R                  S5        [        U R	                  S5      5      u  p#U(       d  [        S5      eU R                  R                  US   US	   S
-  -   US   US   S
-  -   5        U R                  R                  U5        U R                  S5        [        X5      (       d  [        SU S35      eU R                  S5        [        R                  " SU R                  5      nU(       d  [        SU SU R                   S35      e[        UR                  S5      5      $ )z‚Search DealerKit for reg and open its own record, returning the
numeric vehicle id from the resulting /vehicles/<id>/overview URL.r¿   rÀ   rÁ   rÂ   rØ   zinput[placeholder=Search]z(DealerKit: the search box was not found.r   r   r   r   r   rþ   úDealerKit: z" did not appear in search results.z/vehicles/(\d+)/overviewzDealerKit: opening z. did not land on its overview page (landed on z
 instead).r   )rá   râ   rL   r   r   r„   r   r   r”   r•   r@   r9   rR  rw   rH   rS  )r   r²   r–   r  r?   s        r   Úopen_vehicle_overviewrb  æ  s.  € ð 	‡I�I”Ð  	Ð*Ð7IÐSX€IÑYØ×Ñ˜$Ôä˜tŸ|™|Ð,GÓHÓI�O€JÞÜÐEÓFÐFØ‡J�J×Ñ�S˜‘X  G¡¨qÑ 0Ñ0°#°c±(¸SÀ¹]ÈQÑ=NÑ2NÔOØ‡M�M×Ñ�sÔØ×Ñ˜$Ôä˜T×'Ñ'Ü˜[¨¨Ð-OÐPÓQÐQØ×Ñ˜$Ôä
�	Š	Ð-¨t¯x©xÓ8€AÞÜØ! # ð 'ØŸ(™(˜ :ð/ó
ð 	
ô ˆq�w‰w�q‹z‹?Ðr"   c                 ó¨  ^^• U R                  [         SU S3SSS9  U R                  S5        U R                  S5      nU(       d  [	        SU S	35      eU R
                  R                  US
   US   5        U R                  S5        U R                  S5      nU(       d  [	        SU S	35      eSS0mUU4S jnU R                  SU5         U R
                  R                  US
   US   5        U R                  S5        U R                  SU5        T(       d  gTS   (       d  [	        SU S	35      eU R                  R                  R                  [         SU 3SS9nUR                  S:  a9   UR                  5       R                  S5      U:H  nU(       a  [	        SU S35      eg! U R                  SU5        f = f! [         a    Sn N<f = f)aC  Permanently deletes a vehicle record via its own kebab menu (found
live 2026-08-24, real reconnaissance: the top right more_vert opens a
small menu with Add to list / Write-Off / Delete, real DIVs of class
.ui-menu-option__text, not semantic menu items, so not findable via a
role or tag based search). Delete triggers a genuine native
window.confirm ("Are you sure you want to delete this vehicle?"), the
same mechanism already proven for the Funding Settle flow, handled the
same way (a real page.on("dialog", ...) listener, never the auto
dismiss a plain click would otherwise get). confirm=False stops right
before accepting the dialog, for a dry run; the dialog is always
dismissed either way in that case, nothing deleted. Verifies via the
API afterward that the vehicle genuinely no longer resolves, the same
lesson as every other write in this module: a click reporting success
is not proof something actually happened.

Built 2026-08-24 specifically to clean up 13 real duplicate "Due In"
records a Carwow purchases batch push accidentally created: add_due_in
only ever checked DealerKit's own POST /api/stocklist response for a
422 to detect "this reg already exists", which DealerKit does return
for a car still active in some other stage, but NOT for one already
disposed of (life_cycle_status 25, a genuinely completed sale), so a
repeat push for an already sold reg silently created a second,
spurious Due In record alongside the real, untouched sold one rather
than being refused. See push_purchase's own updated docstring for the
real fix (a reg lookup before ever attempting to create).rü   rý   rÀ   rÁ   rÂ   r   aº  () => {
            const els = Array.from(document.querySelectorAll('button, a, i, span'));
            for (const el of els) {
                if ((el.textContent || '').trim() === 'more_vert') {
                    const r = el.getBoundingClientRect();
                    if (r.width > 0 && r.height > 0 && r.y < 200) return {x: r.x + r.width / 2, y: r.y + r.height / 2};
                }
            }
            return null;
        }zEDealerKit: the vehicle's own menu (top right) was not found (vehicle r  r   r   rk   a»  () => {
            const els = Array.from(document.querySelectorAll('.ui-menu-option__text'));
            for (const el of els) {
                if ((el.textContent || '').trim().toLowerCase() === 'delete') {
                    const r = el.getBoundingClientRect();
                    if (r.width > 0 && r.height > 0) return {x: r.x + r.width / 2, y: r.y + r.height / 2};
                }
            }
            return null;
        }z9DealerKit: the Delete menu option was not found (vehicle r6  Fc                 ó`   >• T(       a  STS'   U R                  5         g U R                  5         g r8  r9  r<  s    €€r   r@  Ú"delete_vehicle.<locals>._on_dialog@  rB  r"   rC  z@DealerKit: the delete confirmation dialog was not seen (vehicle rÙ   rF  r’   rp   rÈ   TzDealerKit: vehicle ú4 still resolves after delete, it may not have saved.)rá   râ   rL   r.   r„   r   r   r†   r‡   rT  rx   rf   ru   rz   r   )	r   r½   r?  Ú	kebab_posÚdel_posr@  r{   r^  r>  s	     `     @r   Údelete_vehicleri     sã  ù€ ð4 	‡I�I”Ð  
¨:¨,°iÐ@Ø+°Uð ñ <à×Ñ˜$Ôà—‘ð		ó€Iö ÜÐbÐcmÐbnÐnpÐqÓrÐrØ‡J�J×Ñ�Y˜s‘^ Y¨s¡^Ô4Ø×Ñ˜$Ôà�m‰mð		ó€Gö ÜÐVÐWaÐVbÐbdÐeÓfÐfà˜ˆ€Höð 	‡G�GˆH�jÔ!ð3Ø�
‰
×Ñ˜ ™ w¨s¡|Ô4Ø×Ñ˜dÔ#à×Ñ˜X zÔ2æØØ�F×ÜÐ]Ð^hÐ]iÐikÐlÓmÐmà�<‰<×Ñ×#Ñ#¤~Ð&6°oÀjÀ\Ð$RÐ\aÐ#Ðb€DØ‡{�{�SÓð	ØŸ)™)›+Ÿ/™/¨$Ó/°:Ñ=ˆKö ÜÐ!4°Z°LÐ@tÐuÓvÐvØøð 	×Ñ˜X zÕ2ûô ó 	ØŠKð	ús   Ã3F+ Å2"G Æ+F?ÇGÇGc                 óz  ^^• U R                  [         SU 3SSS9  U R                  S5        U R                  S5      nU(       d  [	        SU S35      eS	S
0mUU4S jnU R                  SU5         U R                  R                  US   US   5        U R                  S5        U R                  SU5        T(       d  g
TS	   (       d  [	        SU S35      eU R                  R                  R                  [         SU 3SS9nUR                  S:  a  [	        SU S35      eg! U R                  SU5        f = f)aJ  Permanently deletes a DealerKit contact from its own real page
    (`/contacts/{id}`, found live 2026-09-05, Steven: "to delete a contact
    inside dealerway you go to contacts, click the contacts name then
    click delete"), a real, plain "DELETE" button, not a kebab menu the
    way a vehicle's own delete is.

    The button's own visible text shares one node with the material icon
    ligature ("delete
DELETE"), the same "no element's text is ever the
    plain word alone" pattern already seen elsewhere in this file (the
    Edit button), so it is matched by a CONTAINS check, not an exact one.

    Triggers a genuine native window.confirm, worded plainly: "Are you
    sure you want to delete '<name>' and their ALL past deals, invoices,
    bookings and other related items? This cannot be undone." Handled the
    same real way as delete_vehicle's own confirm, a real page.on("dialog",
    ...) listener, never the auto dismiss a plain click would otherwise
    get. confirm=False stops right before accepting the dialog, for a dry
    run; the dialog is always dismissed either way in that case, nothing
    deleted.

    Verifies via the real API afterward that the contact genuinely no
    longer resolves, the same discipline as every other delete in this
    module: a click reporting success is not proof something actually
    happened. Only ever use this on a contact known to be safe to remove
    (a test contact carrying no real deals, invoices or bookings); the
    confirm dialog's own wording is the one honest warning DealerKit
    itself gives that this is not always reversible.z
/contacts/rÀ   rÁ   rÂ   rþ   a¿  () => {
            const els = Array.from(document.querySelectorAll('button, a'));
            for (const el of els) {
                const t = (el.innerText || '').trim();
                if (!t.toUpperCase().includes('DELETE')) continue;
                const r = el.getBoundingClientRect();
                if (r.width > 0 && r.height > 0) return {x: r.x + r.width / 2, y: r.y + r.height / 2};
            }
            return null;
        }z4DealerKit: the Delete button was not found (contact r  r6  Fc                 ó`   >• T(       a  STS'   U R                  5         g U R                  5         g r8  r9  r<  s    €€r   r@  Ú"delete_contact.<locals>._on_dialogŽ  rB  r"   rC  r   r   r   z@DealerKit: the delete confirmation dialog was not seen (contact z/api/contacts/rF  r’   rp   zDealerKit: contact rf  T)rá   râ   rL   r.   r„   r†   r   r   r‡   rT  rx   rf   ru   )r   Ú
contact_idr?  rh  r@  r{   r>  s     `   @r   Údelete_contactrn  ^  sK  ù€ ð8 	‡I�I”Ð  
¨:¨,Ð7ÐDVÐ`e€IÑfØ×Ñ˜$Ôà�m‰mð		ó€Gö ÜÐQÐR\ÐQ]Ð]_Ð`ÓaÐaà˜ˆ€Höð 	‡G�GˆH�jÔ!ð3Ø�
‰
×Ñ˜ ™ w¨s¡|Ô4Ø×Ñ˜dÔ#à×Ñ˜X zÔ2æØØ�F×ÜÐ]Ð^hÐ]iÐikÐlÓmÐmà�<‰<×Ñ×#Ñ#¤~Ð&6°nÀZÀLÐ$QÐ[`Ð#Ða€DØ‡{�{�SÓÜÐ0°°Ð<pÐqÓrÐrØøð 	×Ñ˜X zÕ2ús   Á33D& Ä&D:c                 ó8  • [         R                  " SS9n/ n[        U5       GHb  u  pE[        R                  R                  [        U5      R                  5      =(       d    SU 3nSU;  a&  SnS H  nX…R                  5       ;   d  M  Un  O   Xg-  n[        R                  R                  U[        U5      5      n	[        R                  " U	SS9  [        R                  R                  X–5      n
U R                  R                  R                  US	S
9nUR                  S:w  a  [        SU SUR                   S35      e[!        U
S5       nUR#                  UR%                  5       5        SSS5        UR'                  U
5        GMe     U$ ! , (       d  f       N&= f)a–  Fetch each url to a local temp file via the browser's own request
context (page.context.request), not urllib: this Mac's Python has no
trusted local certificate store (a stock python.org install issue, not
a DealerKit or imgix problem), so a plain urllib.request.urlopen fails
every HTTPS fetch with SSLCertVerificationError, silently, the first
version of this function did not check for it and every image/document
upload quietly ended up empty. The browser's own network stack has no
such gap, it is the same one already rendering these exact photos
everywhere else in the app.

The local file is named after the URL's OWN basename (v5-172..., not a
random bidbrain_dk_0_xxxxx one), 2026-08-24, Mark noticed EY69KYV had
neither V5 nor service history attached despite both having genuinely
been pushed: DealerKit's own upload widget reads the uploaded file's
name to auto fill its Documents list "description" (Service History,
V5 Front, V5 Internal), proven live by comparing HG17NRF's own
documents, whose v5-...jpg and docs-service-history-...jpg names
(Motorway's own CDN naming, preserved through here) got a real
description, against every other pushed car's, all bidbrain_dk_N_xxxxx
named and all landing as "Other Document Type", DealerKit having
nothing to guess from. Fixed at the source, not by manually setting a
description after the fact, so both a fresh push and DealerKit's own
auto categorisation stay in sync.Úbidbrain_dk_)Úprefixzbidbrain-dk-rƒ   ú.jpg)rr  z.jpegz.pngz.heicT©Úexist_oki0u  r’   r�   zDealerKit: fetching z returned HTTP ÚwbN)ÚtempfileÚmkdtempÚ	enumerateÚosr¡   Úbasenamer   rP  r¢   r'   ÚmakedirsrT  rx   rf   ru   r„   ÚopenÚwriterv   rÊ   )r   ÚurlsÚtmp_dirÚpathsr   rw   ÚnameÚextÚcandÚitem_dirr¡   r{   Úfs                r   Ú_download_tempr†  ¦  sJ  € ô0 ×Ò nÑ5€GØ€EÜ˜D—/‰ˆÜ�w‰w×Ñ¤¨£× 2Ñ 2Ó3×I¸ÀaÀSÐ7IˆØ�d‹?ØˆCÛ:�ØŸ9™9›;Õ&Ø�CÙñ ;ð ‰KˆDô —7‘7—<‘< ¬¨Q«Ó0ˆÜ
�Š�H tÒ,Ü�w‰w�|‰|˜HÓ+ˆØ�|‰|×#Ñ#×'Ñ'¨°UÐ'Ð;ˆØ�;‰;˜#ÓÜÐ!5°c°U¸/È$Ï+É+ÈÐVWÐXÓYÐYÜ�$˜Ô Ø�G‰G�D—I‘I“KÔ ÷ à�‰�T×ñ1 "ð2 €L÷ Õús   Å FÆ
F	c                 ó
  • [        5       nU  HH  nUR                  [        R                  R	                  U5      5         [        R
                  " U5        MJ     [        5       nU HH  nUR                  [        R                  R	                  U5      5         [        R                  " U5        MJ     U H  n [        R                  " U5        M     g ! [         a     MÒ  f = f! [         a     M‹  f = f! [         a     MN  f = fr«   )ÚsetÚaddry  r¡   ÚdirnameÚremoveÚOSErrorÚrmdir)r€  ÚdirsÚpÚparentsr=  s        r   Ú_cleanup_tempr‘  Ü  sÎ   € Ü‹5€DÛˆØ�‰”—‘—‘ Ó#Ô$ð	Ü�IŠI�aŽLñ ô ‹e€GÛˆØ�‰”B—G‘G—O‘O AÓ&Ô'ð	Ü�HŠH�QŽKñ ó ˆð	Ü�HŠH�QŽKò øô ó 	Úð	ûô ó 	Úð	ûô
 ó 	Úð	ús5   ¿CÂC#Â7C4Ã
C ÃC Ã#
C1Ã0C1Ã4
DÄDc                 ó  • U R                   R                  R                  [         SU SU 3SS9nUR                  S:w  a  g UR                  5       n[        UR                  U5      =(       d    / 5      $ ! [         a     gf = f)züThe real count of images or documents DealerKit's own API reports for
this vehicle right now, via the browser's own authenticated session
(page.context.request), never trusted to a client side "N attached"
label alone. field is "images" or "documents".rÙ   z?with[]=rF  r’   r�   N)rT  rx   rf   râ   ru   rz   r   rQ  )r   r½   rS   r{   rÉ   s        r   Ú_server_countr“  ò  sŠ   € ð
 �<‰<×Ñ×#Ñ#ÜÐ
˜/¨*¨°X¸e¸WÐEØð $ð €Dð ‡{�{�cÓØðØ�y‰y‹{ˆô ˆt�x‰x˜‹×$ "Ó%Ð%øô ó Ùðús   ÁA6 Á6
BÂBc                 óz   • U R                  S5      (       + =(       a    [        R                  R                  U 5      $ )zòTrue when item is a real, already-existing local file rather than
a url to fetch, checked by actually existing on disk (not just "does
not start with http") so a stray malformed url is never silently
treated as a valid local path by accident.)zhttp://zhttps://)Ú
startswithry  r¡   Úisfile)Úitems    r   Ú_is_local_pathr˜    s)   € ð
 �‰Ð6Ó7Ô7×P¼B¿G¹G¿N¹NÈ4Ó<PÐPr"   c                 ó6  • U(       d  gU Vs/ s H  n[        U5      (       a  M  UPM     nnU Vs/ s H  n[        U5      (       d  M  UPM     nnU(       a  [        X5      O/ n [        XX'U-   U5      [        U5        $ s  snf s  snf ! [        U5        f = f)a  Upload a list that can freely mix real urls (downloaded fresh here
via _download_temp) and already-local file paths, used together in
one upload so DealerKit's own server side count check still sees the
real, combined total. Motorway's own photo and document urls are now
downloaded to real local files AT CAPTURE TIME (2026-08-26, Mark: "the
motorway v5 images have a '?' ... they dont load", their signed imgix
urls go dead within about an hour, confirmed live against a 3 day old
one, so nothing here can safely fetch one a second time), so purchase
rows read since then carry local paths in the exact same fields a
Carwow row still carries real urls in; this one function now handles
either, or a mix, without upload_images/upload_documents needing to
know or care which.r   )r˜  r†  Ú!_upload_local_files_via_edit_tiler‘  )	r   r½   Ú	tile_textÚitemsÚwhatr   r~  Úalready_localÚ
downloadeds	            r   Ú_upload_via_edit_tiler     s‚   € ö ØÙÓ6’u�!¤N°1×$5�A‘u€DÐ6Ù %Ó;¢˜1¬¸×):—Q¡€MÐ;Þ/3” Ô+¸€Jð"Ü0°À9Ð^hÑNhÐjnÓoä�jÕ!ùò 7ùÚ;øô
 	�jÕ!ús!   �B¥B±BÁ	BÁ&B ÂBc                 óÂ  • U(       d  gUS:X  a  SOSnU R                  [         SU S3SSS	9  U R                  S
5        [        XU5      n[	        U S5      (       d  [        SU S35      eU R                  S5        [        XSSS9(       d  [        SU S35      eU R                  S5        U R                  S5      R                  nUR                  5       S:X  a  U R                  S5      R                  nUR                  U5        [        R                  " 5       S-   n[        R                  " 5       U:  aJ  U R                  S5      n	U	(       d  O1[        R                  " S5        [        R                  " 5       U:  a  MJ  U=(       d    S[        U5      -   n
S H¢  n[        U SSS9(       d  [        SU S35      eU R                  S
5        [        XU5      nUb  XÊ:¼  a  [        U5      s  $ US:X  d  M\  [	        U S5      (       d    O7U R                  S5        [        XSS9(       d    OU R                  S5        M¤     [        SU S U S!Wb  UOS" S#U S$U
 S%35      e)&a÷  Shared body for the Images and Documents edit panels: both are the
same upload widget (drag and drop, a file picker, or DealerKit's own
in-tray), only reached through a different Edit Vehicle menu tile.
Takes files already sitting on disk (a generated PDF, for example, not
only something fetched from a url, see upload_local_document).

Verifies against DealerKit's own API afterward rather than trusting the
Save Changes click, the same lesson as the Add Expense Team field bug
(2026-08-23): a save can silently do nothing (no error shown anywhere
in the UI) while every client side signal, the attached file count, the
click itself, all look completely normal. Caught live on this exact
upload: a batch of 25 documents across several cars reported success
for all of them, but 4 had genuinely saved nothing, only found by
checking the API afterward. One retry of the Save Changes click if the
count has not moved; raises loudly if it still has not, rather than
reporting a false success like the old version did.r   ÚImagesÚimagesÚ	documentsrü   rý   rÀ   rÁ   rÂ   rþ   rÿ   z4DealerKit: the Edit button was not found (uploading r  rØ   Tr  r  úDealerKit: the z- tile was not found in the Edit Vehicle menu.z".ui-modal.is-open input[type=file]zinput[type=file]éZ   zð() => {
                const b = Array.from(document.querySelectorAll('.ui-modal.is-open button'))
                    .find(el => (el.textContent||'').includes('Save Changes'));
                return b ? b.disabled : false;
            }g      à?©r   r   r  r$   z6DealerKit: Save Changes was not found after uploading rƒ   r   ra  z# did not actually save for vehicle z (server still reports Úunknownrž   z, expected at least ú), even after a retry.)rá   râ   rL   r“  r@   r„   r,   r   Úfirstr   Úset_input_filesr   r.   r   rQ  )r   r½   r›  Úlocal_pathsr�  rS   Úbeforer=   r   Ústill_disabledÚexpectedÚattemptr]  s                r   rš  rš  #  sW  € ö" Øà! XÓ-‰H°;€Eà‡I�I”Ð  
¨:¨,°iÐ@Ø+°Uð ñ <à×Ñ˜$Ôä˜4¨UÓ3€Fä˜T 6×*Ñ*ÜÐQÐRVÐQWÐWYÐZÓ[Ð[Ø×Ñ˜$Ôä˜t°dÐCV×WÜ˜_¨Y¨KÐ7dÐeÓfÐfØ×Ñ˜$Ôð �\‰\Ð>Ó?×EÑE€FØ‡|�|ƒ~˜ÓØ—‘Ð0Ó1×7Ñ7ˆØ
×Ñ˜;Ô'ô �yŠy‹{˜RÑ€HÜ
�)Š)‹+˜Ó
 ØŸ™ðó
ˆö ØÜ�
Š
�3Œô �)Š)‹+˜Õ
 ð —˜!œs ;Ó/Ñ/€HÛˆÜ" 4¨¸t×DÜÐ!WÐX\ÐW]Ð]^Ð_Ó`Ð`Ø×Ñ˜dÔ#Ü˜d°Ó6ˆØÑ Ó!2Ü�{Ó#Ò#Ø�a�<ô
 % T¨6×2Ñ2ÙØ×!Ñ! $Ô'Ü& t¸d×CÙØ×!Ñ! $Ö'ñ# ô$ Ø
�d�VÐ>¸z¸lð K!Ø*/Ñ*;¡ÀÐ KÈ1Øˆ'Ð% h ZÐ/Eð	GóHð Hr"   c                 ó    • [        XSU/S5      $ )zÓAttach one file already on disk (a generated PDF, for example) as a
vehicle Document. Never deletes file_path, that is the caller's own
file, unlike the url based uploads which clean up their own temp
downloads.Ú	Documentsz
a document)rš  )r   r½   Ú	file_paths      r   Úupload_local_documentr´  ‚  s   € ô
 -¨T¸{ÈYÈKÐYeÓfÐfr"   c           	      óB   • [        XS[        U=(       d    / 5      S5      $ )an  Attach several files already on disk in one go: Carwow's own service
record and V5C logbook scans, downloaded and extracted locally by
carwow.read_purchase_documents, and, since 2026-08-26, Motorway's own
service history and V5 scans too (motorway.download_purchase_media,
see purchases_run.py); push_purchase still routes those through the
ordinary upload_documents call, which now detects a local path just
as well as a url (see _upload_via_edit_tile), this dedicated entry
point is for a caller that already knows every item is local, such as
the Delivery Report PDF. Never deletes the files, they are the
caller's own.r²  r¤  )rš  rI   )r   r½   Ú
file_pathss      r   Úupload_local_documentsr·  Š  s"   € ô -¨T¸{ÌDÐQ[×QaÐ_aÓLbÐdoÓpÐpr"   c                 ó6   • [        XSU=(       d    / S U S5      $ )Nr¢  r£  ©r   )r   r½   Ú
photo_urlsÚ
max_imagess       r   Úupload_imagesr¼  ˜  s"   € Ü  °8¸j×>NÈBÐP[ÐQ[Ð=\Ð^fÓgÐgr"   c                 ó0   • [        XSU=(       d    / S5      $ )Nr²  r¤  r¹  )r   r½   Údocument_urlss      r   Úupload_documentsr¿  œ  s   € Ü  °;À×@SÐQSÐU`ÓaÐar"   )rÒ   ÚfeeÚ	transportr¾   Úphotosr¤  Úretailc                 óD  • [        U R                  S5      =(       d    / 5      [        U R                  S5      =(       d    / 5      -   nX R                  S5      =(       d    /  Vs/ s H+  n[        U[        5      (       d  M  U" U5      (       d  M)  UPM-     sn-  nU$ s  snf )a-  Every document on this Mac's disk that a send attaches to the car:
Carwow's service records and V5 scans, and, since 2026-09-16, what
Dealer OS holds for the car (the MotorCheck history check PDF,
dealer_os_documents). A path no longer on disk is left out rather
than failing the upload. Pure, tested.Údocument_pathsÚv5_local_pathsÚdealer_os_documents)rI   rf   r&   r'   )rh   Úexistsr€  r�  s       r   Úlocal_documents_forrÉ  ´  sƒ   € ô �—‘Ð.Ó/×5°2Ó6¼¸h¿l¹lÐK[Ó>\×>bÐ`bÓ9cÑc€EØ	Ÿ,™,Ð'<Ó=×CÀÐCÓiÒC�AÌ
ÐSTÔVY×HZ‹aÑ_eÐfg×_h�aÑCÑiÑi€EØ€Lùò js   Á$BÂBÂBc                 ó"   • US:„  =(       a    X:¬  $ )zŸWhether a repeat send should leave DealerKit's documents alone: it
already holds at least as many as this send would add, and there is
something to add at all.r   rX   )ÚhaveÚsendings     r   Údocuments_already_thererÍ  ¿  s   € ð �Q‰;×*˜4™?Ð*r"   c                 óV  • [        U S5      (       d  [        SU S35      e[        U5       HX  nU R                  U5        [	        XSSS9(       a    g U R                  S5      R                  5       S:X  a  [        U S5        MX  MZ     [        S	U S
U S35      e! [         a     M{  f = f)aÕ  Open the vehicle's Edit menu and click one of its tiles (Expenses,
Images, ...), trying again while the menu draws (2026-09-15, found
live on HX67UMH: the buyer's fee step raised "the Expenses tile was
not found" on a record whose expenses were plainly there, the menu
had not finished drawing 1.5 seconds after Edit). Up to `attempts`
looks, `pause_ms` apart; Edit is pressed again if the menu is not
open. Raises with the same wording as before when it never appears.rÿ   r   r  Tr  r  Nr   r¥  z tile was not found (vehicle )r@   r„   r   rL   r,   r   r   r   )r   r½   ÚtileÚattemptsÚpause_msr°  s         r   rN  rN  Æ  sµ   € ô ˜T 6×*Ñ*ÜÐOÐPZÈ|Ð[]Ð^Ó_Ð_Ü˜–?ˆØ×Ñ˜hÔ'Ü˜t°Ð>Q×RÙð	Ø�|‰|Ð/Ó0×6Ñ6Ó8¸AÓ=Ü! $¨Ö/ñ >ñ #ô ˜¨¨Ð.KÈJÈ<ÐWYÐZÓ
[Ð[øô ó 	Úð	ús   Á/BÂ
B(Â'B(c                 ój  • US   nUb  UO1 Skn[        U=(       d    S5      n0 nUq[        XUSU;   S9n[        X5      n	Sn
SU;   a‰  [	        UR                  S	5      =(       d    / 5      nUS
   (       a  [        X	S5      OSnU(       a,  U(       a%  U[        [        U5      U5      :¼  a  SU S3US'   Sn
O[        X	X³S9n
U
(       a  U
 S3OSUS'   SnSU;   aÏ  [	        UR                  S5      =(       d    / 5      [	        UR                  S5      =(       d    / 5      -   n[        U5      nUS
   (       a  [        X	S5      OSn[        U[        U5      [        U5      -   5      (       a  SU S3US'   SnO3[        X	U5      nU(       a  U[        X	U5      -  nU(       a  U S3OSUS'   UR                  S5      nUc)  UR                  S5      =(       d    UR                  S5      nS=nnSnSU;   a¯  UR                  S5      nU(       d  SUS'   O‘UR                  S5      [        U5      :X  a  SnS[        U5      S S3US'   O][        X	U5      nSnUS   (       a2  S [        U5      S S!3UR                  S"5      b	  S#US"   S 3OS$-   US'   OS%[        U5      S S!3US'   S=nnUR                  S&5      nUR                  S'5      n0 nSU;   a.  Uc  S(US'   O%US)   (       a  US*   (       a  UnS+U S,3US'   OUUS-'   S.U;   a  Uc  S/US.'   OUUS0'   S1U;   a  Uc  S2US1'   OUUS3'   U(       aÞ  [!        U5      nS$nUS)   (       a%  S-U;   a  UR                  S45      (       a	  S5US4    S63n [#        X	UU5      nUR%                  5        H€  u  nn['        UU5      US-:X  a  UR                  S5      (       a  UOS$-   U[(        U   '   [+        U5      n US-:X  a  U (       a  US7   nM\  US0:X  a  U (       a  SnMm  US3:X  d  Mu  U (       d  M~  SnM‚     Sn#S;U;   aÉ  UR                  S<5      n$U$c  S=US;'   O¯Un%U%c&  [7        U [8         S>U	 35      n&U&R                  S?5      n%U%b/  [;        [=        U%5      [=        U$5      -
  5      S@:  a  U%nSAU% 3US;'   ORU%b  S;U;  a  SBU% SCU$ SD3US;'   O<[?        X	U$5      n#U#(       a$  U$nU#SE:X  a  SFOSGn'U%c  S+U$ U' 3O	SHU% SIU$ U' 3US;'   OSJUS;'   Sn(SKU;   aC  UR                  SL5      c  SMUSK'   O+[A        X	USL   5      n)Sn(U)(       a  S USL    3OS%USL    3USK'   [        5       n*SU;   a  Ub  U*RC                  S5        SU;   a  U
S:”  a  U*RC                  S5        SU;   a  US:”  a  U*RC                  S5        S;U;   a  U#(       a  U*RC                  S;5        SKU;   a  U((       a  U*RC                  SK5        SU;   a  U(       a  U*RC                  S5        S.U;   a  U(       a  U*RC                  S.5        S1U;   a  U(       a  U*RC                  S15        UU	US)   US
   US*   UUU#U(U
U[E        U5      [E        U*5      USN.$ ! [,         a    e [.         aY  n!S8R1                  [3        U!5      R5                  5       5      SS9 n"U H  nS:U" 3US-:X  a  UOS$-   U[(        U   '   M      Sn!A!GN¤Sn!A!ff = f)Oaò  Push one purchase (a row shaped like db.list_purchases()'s output)
into DealerKit: due in record, then images, then service history
documents. Returns a summary dict. Raises on the first step that fails
outright; a step with nothing to do (no photos, no service history) is
just skipped, not an error. Raises DealerkitMissingMileage (never
caught here) when this would be a genuinely new record and purchase
has no mileage; no bypass exists, see add_due_in.

items (2026-08-26, Mark: "choose which data to push", the row's own
kebab menu) is an optional subset of {"price","photos","documents",
"retail","due_in_date"} plus, since 2026-09-05, "mileage" and "fee"
(the two extra things a car sent whole carries, see SEND_ITEMS); None
(the default) pushes all five, the original all-in-one behaviour,
now including Retail Price (2026-08-26, "the Retail Est is the
retail price that should be mapped to DK", reversing the earlier
"always manual" decision) and Due In / In-stock On (2026-08-26,
"lets now wire up the due in date in BB to DK"). The due in record
itself still always gets created first regardless (photos and
documents both need a real vehicle_id to upload to), only whether
BidBrain's own purchase price gets typed into it is what "price"
actually gates.r²   N>   rÒ   rÂ  rÃ  r¤  rX   rÒ   )rh   rè   r   rÂ  rº  r»   r£  zalready on DealerKit, z photos there)r»  z	 uploadedzCnothing to send, DealerKit already has these or BidBrain holds noner¤  Úservice_history_photosÚ	v5_photosz documents thererÐ   rÑ   Fr¾   z5nothing to send, BidBrain has no mileage for this carrÏ   Tzentered as Ú,z miles when the car was addedÚchangedzset to z milesÚoldz, DealerKit had r5   zalready right on DealerKit, Úeffective_feeÚtransport_feeú<nothing to send, BidBrain has no purchase price for this carrº   r¼   õ	   set to Â£z when the car was createdÚchassisrÀ  z9nothing to send, BidBrain has no buyer's fee for this carÚbuyers_premiumrÁ  ú;nothing to send, BidBrain has no transport fee for this carÚdelivery_chargerÞ   z& (not saved when the car was created: Ú)Únewrž   rE   znot on DealerKit, rÃ  Úretail_estimatez=nothing to send, BidBrain has no retail estimate for this carrÙ   r  ç{®Gáz„?õ   already right on DealerKit, Â£u   left alone, DealerKit says Â£u    and BidBrain says Â£z6; tick Retail price on its own to make DealerKit matchrû   z through the APIz on the Pricing panelõ   changed from Â£õ    to Â£zcould not be set on DealerKitÚdue_in_dateÚcollection_datez9nothing to send, BidBrain has no due in date for this car)r²   r½   rº   Úalready_existedr¼   rÚ   r  Úretail_price_setÚdue_in_date_setÚimages_uploadedÚdocuments_uploadedÚitems_pushedÚ
items_doneÚoutcomes)#rˆ  ÚLAST_OUTCOMESrù   rb  rI   rf   r“  ÚminrQ  r¼  rÉ  rÍ  r¿  r·  rH   Úadd_mileage_log_entryri   Úsync_purchase_expenserœ  Úfigure_wordsÚFIGURE_OUTCOME_KEYÚfigure_landedrä   r   r¢   r'   r£   ræ   râ   ÚabsrF   r  r*  r‰  Úsorted)+r   rh   rœ  Ú
max_photosr²   ÚwantÚ	asked_forrð  r2   r½   Ún_imagesrÂ  rË  Ún_docsr¤  Ú
local_docsÚ	have_docsÚbb_pricerÚ   r  Úmileage_setÚbb_milesÚresÚfee_setÚtransport_setÚbb_feeÚbb_transportÚfiguresra   Úcreation_noteÚsyncedÚkeyrT   ró   Úwordsrê  Ú	bb_retailÚexisting_retailr)  Úhowrë  rÖ  rï  s+                                              r   Úpush_purchaser  Ü  s™  € ð, �5‰/€CØÑ%‰5Ò+U€Dô
 �E—K˜RÓ €IØ€Hà€MÜ˜¨HÀÈDÁÑQ€FÜ& tÓ1€Jà€HØ�4ÓÜ�h—l‘l <Ó0×6°BÓ7ˆð =CÐCS×<TŒ}˜T¨xÔ8ÐZ[ˆÞ–d˜t¤s¬3¨v«;¸
Ó'CÓCØ#9¸$¸¸}Ð!MˆH�XÑØ‰Hä$ T°vÑUˆHÞ<D X J¨iÑ"8Ø'lð �XÑð €FØ�dÓÜ˜Ÿ™Ð&>Ó?×EÀ2ÓFÌÈhÏlÉlÐ[fÓNg×NmÐkmÓInÑnˆ	ô )¨Ó2ˆ
ð EKÐK[×D\”M $°KÔ@Ðbcˆ	Ü" 9¬c°)«n¼sÀ:»Ñ.N×OÑOØ&<¸Y¸KÐGWÐ$XˆH�[Ñ!Ø‰Fä% d¸	ÓBˆFÞØÔ0°À:ÓNÑN�Þ=C¨ x¨yÑ%9Ø*oð �[Ñ!ð �|‰|˜KÓ(€HØÑØ—<‘< Ó.×G°(·,±,¸wÓ2Gˆà#'Ð'€M�Lð €KØ�DÓØ—<‘< 	Ó*ˆÞØ"YˆH�YÒØ�Z‰ZÐ+Ó,´°H³Ó=ØˆKØ$/´°H³¸aÐ/@Ð@]Ð"^ˆH�YÒä'¨¸(ÓCˆCØˆKØ�9�~Ø)0´°X³¸qÐ0AÀÐ'HØORÏwÉwÐW\Ë~ÑOiÐ,<¸SÀ¹ZÈ¸NÑ*KÐoqñ(s�˜Ò#ð )EÄSÈÃ]ÐSTÐDUÐU[Ð&\�˜Ñ#ð $Ð#€GˆmØ�\‰\˜/Ó*€FØ—<‘< Ó0€LØ€GØ�$ƒØÑØ ^ˆH�WÒØ�I× 6Ð*A×#Bð %ˆMØ"+¨H¨:Ð5NÐ OˆH�WÒà!)ˆG�IÑØ�ƒ}Ø‰>ØYˆH�UŠOà(.ˆGÐ$Ñ%Ø�dÓØÑØ$aˆH�[Ò!à)5ˆGÐ%Ñ&ÞÜ$ XÓ.ˆØˆØ�)× ¨gÓ!5¸&¿*¹*À^×:TÑ:TØDÀVÈNÑE[ÐD\Ð\]Ð^ˆMð	)Ü*¨4¸XÀwÓOˆFð #ŸL™LžN‘��SÜ4@ÀÀhÓ4OØ%(¨IÓ%5¸#¿'¹'À)×:LÑ:L‘MÐRTñ5V�Ô+¨CÑ0Ñ1ä& sÓ+�Ø˜)Ó#®Ø$'¨¡J’MØÐ,Ó,¶Ø"’GØÐ-Õ-·&°&Ø$(’Mñ +ð& ÐØ�4ÓØ—L‘LÐ!2Ó3ˆ	ØÑØ!`ˆH�XÒà*ˆOØÑ&Ü$ T¬nÐ-=¸_ÈZÈLÐ+YÓZ�Ø"*§,¡,¨~Ó">�ØÑ*¬s´5¸Ó3IÌEÐR[ÓL\Ñ3\Ó/]Ð`dÓ/dØ.�Ø'EÀoÐEVÐ%W�˜Ò"Ø Ñ,°ÀÓ1JØ(EÀoÐEVð W+Ø+4¨+Ð5kð'm�˜Ò"ô $4°DÀiÓ#PÐ Þ#Ø#,�LØ0@ÀEÓ0IÑ,ÐOf�CØJYÑJa¨I°i°[ÀÀÑ*FØ1@ÀÐ@QÐQWÐXaÐWbÐcfÐbgÐ/hð ˜XÒ&ð *I�H˜XÑ&ð €OØ˜ÓØ�<‰<Ð)Ó*Ñ2Ø&aˆH�]Ò#ä'¨¸(ÐCTÑ:UÓVˆGØ"ˆOÞRY¨°Ð:KÑ1LÐ0MÑ'NØ.JÈ8ÐTeÑKfÐJgÐ,hð �]Ñ#ô “€JØ�$ƒ˜=Ñ4Ø�‰�wÔØ�4Ó˜H q›LØ�‰�xÔ Ø�dÓ˜v¨›zØ�‰�{Ô#Ø�4ÓÖ,Ø�‰�xÔ Ø˜Ó¦Ø�‰�}Ô%Ø�DÓž[Ø�‰�yÔ!Ø�ƒ}žØ�‰�uÔØ�dÓž}Ø�‰�{Ô#ð Ø Ø˜)Ñ$Ø!Ð"2Ñ3Ø!'Ð(?Ñ!@Ø&Ø$Ø,Ø*Ø#Ø$Ü˜t›Ü˜ZÓ(ð
 ñ%ð øôK 'ó 	ØÜó 	cØ—H‘HœS ›VŸ\™\›^Ó,¨T¨cÐ2ˆEÛ�Ø7IÈ%ÈÐ5QØHKÈyÓHX±}Ð^`ñ6b�Ô+¨CÑ0Ó1÷ ûð	cús   Í(Y ÙZ2ÙAZ-Ú-Z2ÚChassiszBuyers PremiumzDelivery ChargezAssurance / Indemnity Fee)rÜ  rÝ  rß  Úindemnity_feerÜ  )zStock PurchasezCost of Purchasec                 óÞ   • [        U =(       d    S5      R                  5       nU(       a  US   S:X  a  [        U5      S:”  a  USS nOUS/:X  a  gSR                  U5      R	                  5       $ )z¼A DealerKit button's own label with the material icon glyph taken
    off: "add
EXPENSE" is "expense", "add
ADD" is "add", a bare "add"
    (the icon only + beside a contact picker) is "".r5   r   r‰  r   Nrž   )r'   r£   rQ  r¢   rP  )r)   r  s     r   Ú_add_button_labelr    sa   € ô �—
˜‹O×!Ñ!Ó#€EÞ��q‘˜UÓ"¤s¨5£z°A£~Ø�a�b�	‰Ø	�5�'Ó	ØØ�8‰8�E‹?× Ñ Ó"Ð"r"   c                 ó´  ^• [        U =(       d    / 5       VVs/ s H9  u  pUR                  S5      (       d  M  X[        UR                  S5      5      4PM;     nnnU(       d  gU V^s/ s H*  mSTS   ;   d  [        U4S jS 5       5      (       a  M(  TPM,     sn=(       d    UnU Vs/ s H  oDS   (       d  M  UPM     sn=(       d    UnU Vs/ s H  oDS   S:X  d  SUS   ;   d  M  UPM     sn=(       d    UnU Vs/ s H  oDS   R                  S	5      (       d  M  UPM!     sn=(       d    Un[	        US
 S9S   $ s  snnf s  snf s  snf s  snf s  snf )a‚  Which of the open modal's buttons with "add" in their text is the
expense item's own ADD (2026-09-16, seen live on Steven's account: the
form offers "+ EXPENSE", "+ CREDIT", an icon only "+" beside the
supplier picker and the item row's own "ADD" at once, and picking by
size or by the word "expense" pressed the wrong one twice). cands is
[{text, ui, area, ...}] as the page reports them. A button that starts
another expense, a credit or a contact is never it; the icon only "+"
is never it while anything else offers; a label reading "add" or
naming the item wins; then the smallest. Returns the index into cands,
None when nothing offers.r)   Nr—  r   c              3   ó2   >#   • U  H  oTS    ;   v •  M     g7f)r   NrX   )r­   r6   r®   s     €r   r¯   Ú%_choose_add_button.<locals>.<genexpr>  s   øé € Ð8xÒLwÀq¸aÀ¹d¾ÒLwùs   ƒ)rH  ÚcreditÚcontactÚinvoicer‰  r   Úuic                 óN   • [        U S   R                  S5      =(       d    S5      $ )Nr   Úarear   )rF   rf   )r®   s    r   Ú<lambda>Ú$_choose_add_button.<locals>.<lambda>   s   € ¤ q¨¡t§x¡x°Ó'7×'<¸1Ô!=r"   )r  r   )rx  rf   r  rU  rò  )Úcandsr   ÚcÚrowsr®   ÚrealÚnamedr  s       `   r   Ú_choose_add_buttonr&    s"  ø€ ô BKÈ5Ï;ÐTVÔAWÔiÒAW¹¸Ð[\×[`Ñ[`Ðag×[hÓ4ˆQÔ$ Q§U¡U¨6£]Ó3Ó4ÑAW€DÑiÞØÙÔy’t�!˜v¨¨1©›~´SÔ8xÑLwÓ8x×5x�A‘tÑy÷  Bð  ~B€DÙÓ$’t�! �t�A‘tÑ$×,¨€DÙÓ@š�1 !¡¨£°¸1¸Q¹4±�Q™Ñ@×HÀD€EÙÓ	-’U� ™dŸh™h tŸn�!‘UÑ	-×	6°€BÜˆrÑ=Ñ>¸qÑAÐAùó jùò zùÚ$ùÚ@ùÚ	-s:   ™E ¹ E Á)'EÂEÂ*EÂ;EÃEÃ*EÄ EÄ Ec                 óF  •  U R                  S5      nS He  n U R                  SUU(       a  UOSSS9nUc    g[        U5      S-  S-  U::  d  US	:X  d  MA  S
SKnUR                  U5      R                  S5      s  $    g! [         a    Sn Nzf = f! [         a    Sn Nkf = f)u  A small, low quality picture of the topmost open DealerKit window,
clipped to just that window so it stays small (2026-09-16, Steven:
"is there any way at all that you will be able to make it so you can
sign in to dealerkit from here?" â€” no browser session or password
lives outside the Mac, but the Mac can send back a real picture of
what it saw, not just words, every time a step fails or the survey
runs). Returns base64 JPEG bytes (a plain str) or None. Never raises;
quietly gives up rather than block or fail whatever called it.aD  () => {
                const opens = document.querySelectorAll('.ui-modal.is-open');
                const m = opens.length ? opens[opens.length - 1] : null;
                const r = (m || document.body).getBoundingClientRect();
                const vw = window.innerWidth, vh = window.innerHeight;
                const x = Math.max(0, r.x), y = Math.max(0, r.y);
                const w = Math.max(50, Math.min(r.width || vw, vw - x));
                const h = Math.max(50, Math.min(r.height || vh, vh - y));
                return {x, y, width: w, height: h};
            }N)é#   é   é   Újpegé@  )r•   ÚqualityÚclipr   r  é   r*  r   Úascii)r.   r   Ú
screenshotrQ  Úbase64Ú	b64encodeÚdecode)r   Ú	cap_bytesr  r-  rÉ   r2  s         r   Ú_capture_shotr6  #  sÀ   € ðØ�m‰mð	ó
ˆó  ˆð	Ø—?‘?¨¸Þ/2©¸Àdð #ð LˆDð ‰<ÙÜˆt‹9�q‰=˜AÑ Ó*¨g¸­mÛØ×#Ñ# DÓ)×0Ñ0°Ó9Ò9ñ  ð øô ó ØŠðûô ó 	ØŠDð	ús"   ‚A? ™BÁ?BÂBÂB ÂB c           	      óÐ  • [        U5      nU(       d  g [        R                  " U SS9  [        [        R                  R                  X5      SSS9 nUR                  SU S[        U5       S	35        [        S
[        U5      S5       H  nUR                  SXFUS-    -   S-   5        M!     UR                  S5        SSS5        g! , (       d  f       g= f! [         a     gf = f)züBase64 JPEG lines appended to a log file, ASCII only so
diagnostics.tail_lines (a plain text reader) carries it intact: a
header the puller recognises ("image: <label> jpeg") then the base64
in 300 character lines, blank line to close it. Never raises.NTrs  Úaúutf-8©Úencodingzimage: z jpeg z	b64chars
r   rE   zimgb64:Ú
z
imageend:
)
r6  ry  r{  r|  r¡   r¢   r}  rQ  r   r   )Úlogs_dirÚfilenamerm   r   Úb64r…  r   s          r   Ú_append_shotr@  I  sÂ   € ô
 ˜Ó
€CÞØðÜ
�Š�H tÒ,Ü”"—'‘'—,‘,˜xÓ2°CÀ'ÒJÈaØ�G‰G�g˜e˜W F¬3¨s«8¨*°JÐ?Ô@Ü˜1œc #›h¨Ö,�Ø—‘˜	 C¨!¨c©' NÑ2°TÑ9Ö:ñ -à�G‰G�MÔ"÷	 K×JÖJûô
 ó Ùðús0   •=C ÁA,CÂ>C Ã
CÃC ÃC Ã
C%Ã$C%c                 óz  ^ • SnU 4S jn SR                  T R                  SU-   S-   5      =(       d    SR                  5       5      nU(       d2   SR                  [	        T 5      =(       d    SR                  5       5      nU" S5      nU" S5      n[
        R                  R                  [
        R                  R                  [
        R                  R                  [
        R                  R                  [        5      5      5      S	S
5      n [
        R                  " USS9  [        [
        R                  R                  US5      SSS9 nUR                  S[        R                  R                  5       R                  SS9 ST R                    S35        UR                  SSR                  U5      SS -   S-   5        [#        S[%        U5      S5       H1  n	UR                  SSR                  XiU	S-    5      SS -   S-   5        M3     [#        S['        [%        U5      S5      S5       H  n	UR                  SXIU	S-    -   S-   5        M!     SSS5        [)        USST 5        U(       a  S USU  3$ S!$ ! [         a    Sn GN=f = f! [         a    Sn GNf = f! , (       d  f       NS= f! [         a     Nbf = f)"a1  The open DealerKit window's own words, for a failure message, and
the whole window to data/logs/dealerkit_step.log so the next
diagnostics pull shows what the Mac was looking at (2026-09-16): its
buttons, its field labels and placeholders, and all of its text, the
topmost open window first. Never raises.z†() => { const opens = document.querySelectorAll('.ui-modal.is-open'); return opens.length ? opens[opens.length - 1] : document.body; }c                 ó^   >•  TR                  U 5      =(       d    / $ ! [         a    / s $ f = fr«   r¦   )Újsr   s    €r   ÚevÚ_screen_words.<locals>.evc  s0   ø€ ð	Ø—=‘= Ó$×*¨Ð*øÜó 	ØŠIð	ús   ƒ �,«,rž   Ú(z)().innerText || ''r5   a�  () => { const opens = document.querySelectorAll('.ui-modal.is-open'); const m = opens.length ? opens[opens.length - 1] : document.body;
        return Array.from(m.querySelectorAll('button'))
            .filter(b => { const r = b.getBoundingClientRect(); return r.width > 0 && r.height > 0; })
            .map(b => (b.innerText || '').trim().replace(/\s+/g, ' ')).filter(Boolean).slice(0, 40); }a,  () => { const opens = document.querySelectorAll('.ui-modal.is-open'); const m = opens.length ? opens[opens.length - 1] : document.body;
        const out = [];
        for (const el of m.querySelectorAll('.ui-select__label-text, .ui-select__display-value, .ui-select__display, .ui-textbox__label-text, label, [placeholder], select')) {
            const r = el.getBoundingClientRect(); if (r.width === 0 || r.height === 0) continue;
            const t = ((el.innerText || '') || el.getAttribute('placeholder') || '').trim().replace(/\s+/g, ' ');
            const tag = el.tagName.toLowerCase() + (el.className && typeof el.className === 'string' ? '.' + el.className.split(' ')[0] : '');
            if (t && !out.includes(tag + ': ' + t)) out.push(tag + ': ' + t);
        }
        return out.slice(0, 60); }rÉ   ÚlogsTrs  zdealerkit_step.logr8  r9  r:  z--- Úseconds)Útimespecr<  ú	buttons: ú | Né|  r   é   úfields: ép  é^  zscreen: ÚfailurezThe window says: zThe window showed no words.)r¢   r.   r£   r   r¨   ry  r¡   rŠ  ÚabspathÚ__file__r{  r|  r}  r  Únowr  rw   r   rQ  rò  r@  )
r   rµ   Újs_toprD  r³   r÷   ÚfieldsrG  r…  r   s
   `         r   Ú_screen_wordsrW  \  sm  ø€ ð Z€Fõð
Ø�x‰x˜Ÿ™ s¨V¡|Ð6KÑ'KÓL×RÐPR×YÑYÓ[Ó\ˆö ð	Ø—8‘8œ\¨$Ó/×5°2×<Ñ<Ó>Ó?ˆDñ ð jó k€Dñ ð &ó '€Fô �7‰7�<‰<œŸ™Ÿ™¬¯©¯©¼¿¹¿¹ÌÓ8QÓ(RÓSÐU[Ð]cÓd€Dð
Ü
�Š�D 4Ò(Ü”"—'‘'—,‘,˜tÐ%9Ó:¸CÈ'ÒRÐVWØ�G‰G�dœ8×,Ñ,×0Ñ0Ó2×<Ñ<ÀiÐ<ÐPÐQÐQRÐSW×S[ÑS[ÐR\Ð\^Ð_Ô`Ø�G‰G�K %§*¡*¨TÓ"2°4°CÐ"8Ñ8¸4Ñ?Ô@Ü˜1œc &›k¨1Ö-�Ø—‘˜
 U§Z¡Z°¸¸Q¹°Ó%@ÀÀ#Ð%FÑFÈÑMÖNñ .ä˜1œc¤# d£)¨TÓ2°CÖ8�Ø—‘˜
 T¨A°©G _Ñ4°tÑ;Ö<ñ 9÷ Sô �Ð+¨Y¸Ô=Þ/3Ð˜t D S˜z˜lÐ+ÐVÐ9VÐVøôI ó Ø‹ðûô
 ó 	Ø‹Dð	ú÷$ SÕRûô ó ÙðúsT   ‹=I6 Á1J	 Ä>J- ÅDJÉJ- É6JÊJÊ	JÊJÊ
J*Ê&J- Ê*J- Ê-
J:Ê9J:c                 ó8   • U R                  S5      R                  $ )z0The topmost open DealerKit window, as a locator.r  )r   Úlastr§   s    r   Ú
_top_modalrZ  ‘  s   € à�<‰<Ð+Ó,×1Ñ1Ð1r"   c                 óH   •  U R                  SU5      $ ! [         a     gf = f)zÍWhat the ui-select labelled `label` in the topmost open window shows
now ("Cost of Purchase > Chassis", "Z - Zero"), None when there is no
such field. Read live on Steven's account 2026-09-16 (the survey).aã  (label) => {
                const opens = document.querySelectorAll('.ui-modal.is-open');
                const m = opens.length ? opens[opens.length - 1] : document.body;
                for (const sel of m.querySelectorAll('.ui-select')) {
                    const lab = sel.querySelector('.ui-select__label-text');
                    if (!lab || lab.textContent.trim() !== label) continue;
                    const r = sel.getBoundingClientRect();
                    if (r.width === 0 || r.height === 0) continue;
                    const d = sel.querySelector('.ui-select__display-value, .ui-select__display');
                    return d ? d.textContent.trim() : '';
                }
                return null;
            }Nr¦   )r   rm   s     r   Ú_select_showsr\  –  s6   € ðØ�}‰}ðð ó
ð 	
øô  ó Ùðús   ‚ ”
! !c                 ó|   • U R                  SU5      nU(       d  gU R                  R                  US   US   5        g)z¢Open the ui-select labelled `label` in the topmost open window by
clicking low in its box, the way _click_ui_select_field does; False
when there is no such field.aI  (label) => {
            const opens = document.querySelectorAll('.ui-modal.is-open');
            const m = opens.length ? opens[opens.length - 1] : document.body;
            for (const sel of m.querySelectorAll('.ui-select')) {
                const lab = sel.querySelector('.ui-select__label-text');
                if (!lab || lab.textContent.trim() !== label) continue;
                const r = sel.getBoundingClientRect();
                if (r.width > 0 && r.height > 0) return {x: r.x + r.width / 2, y: r.y + r.height * 0.75};
            }
            return null;
        }Fr   r   T)r.   r   r   )r   rm   rŠ   s      r   Ú_click_select_by_labelr^  ¯  sC   € ð �-‰-ð
	ð 	ó€Cö ØØ‡J�J×Ñ�S˜‘X˜s 3™xÔ(Ør"   c                 ó   • [        U S5      nUb1  UR                  5       R                  5       UR                  5       :X  a  g[        U S5      =(       d    U=(       a    [	        XSS[        U 5      S9nU(       d  [        SU S[        U 5       35      eU R                  S5         [        X5        U R                  S5        g! [         a1    [	        XSSS	9(       d  [        S
U< SU S[        U 5       35      e NLf = f)a,  Make the item window's VAT Tariff read `wanted` ("N - No VAT" for the
chassis, "S - Standard" for a fee). Steven's account presets "Z - Zero"
on a chassis line (the survey, 2026-09-16), so the tariff is always set
outright, never assumed. Raises with the window's words when the
tariff cannot be set.z
VAT TariffNTç      @©r%   r   r*   z5DealerKit: the VAT Tariff box was not found (vehicle ú). rš   rÝ   zDealerKit: the VAT option z was not offered (vehicle )r\  rg   rP  r^  r,   rZ  r„   rW  rL   r…   r   )r   Úwantedr½   ÚshownÚopeneds        r   Ú_set_vat_tariffrf  Ç  s  € ô ˜$ Ó-€EØÑ˜UŸ[™[›]×0Ñ0Ó2°f·l±l³nÓDØÜ# D¨,Ó7÷ fØ×dÔ% d¸ÀsÔS]Ð^bÓScÑdð æÜÐRÐS]ÐR^Ð^aÔboÐptÓbuÐavÐwÓxÐxØ×Ñ˜#ÔðFÜ˜tÔ,ð 	×Ñ˜#Õøô ó FÜ" 4°tÀS×IÜÐ!;¸F¹:ÐE_Ð`jÐ_kÐknÔo|ð  ~Bó  pCð  oDð   Eó  Fð  Fñ JðFús   Â%C Ã8C=Ã<C=c                 ó¸	  • UR                  5        GH’  u  p4[        U   nU R                  S5      =(       d    / n[        U5      nUc  [	        SU S[        U 5       35      eU R                  R                  Xg   S   Xg   S   5        U R                  S5        [        U S5      nUc7  [        U S	S
SS9(       d#  [	        SU SXg   S   < S[        U 5       35      eSn	OpUR                  5       R                  5       R                  UR                  5       5      (       a  S
n	O-[        U S5      (       d  [	        SU S[        U 5       35      eSn	U	(       GaH  U R                  S5        [        U 5      n
[!        ["        5       H2  u  p¼[        XS
U(       a  SOSU
S9(       d  M!  U R                  S5          O/    U R$                  R'                  U5        U R                  S5        [        XS
S[        U 5      S9(       d  [	        SU< SU S[        U 5       35      eU R                  S5        [        U S5      nUbb  UR                  5       R                  5       R                  UR                  5       5      (       d"  [	        SU< SU< SU S[        U 5       35      eU R                  S5      nU(       d  [	        SU S[        U 5       35      eU R+                  S5      R,                  nUR                  5         UR/                  US  5        UR1                  5         U R                  S!5        [3        UR5                  5       =(       d    S"5      nUS  U;  a*  US# U;  a"  [	        S$U< S%US  SU S[        U 5       35      e[7        X[8        ;   a  S&OS'U5        S( H�  n[        U S)SS[        U 5      S9(       d  [	        S*U S[        U 5       35      eU R                  S5        [        U S5      c    OIUS+:X  a  [	        S,U S-U S[        U 5       35      e UR1                  5         U R                  S.5        MŸ      U R                  S/5      =(       d    S"nU(       d  GMS  UR                  5       UR                  5       ;  d  GMx  [	        S0U S1U S[        U 5       35      e   g! [(         a     GNÑf = f! [(         a     Nœf = f! [(         a    S"n N~f = f)2u˜  Add one or more line items (items is a dict keyed by
"chassis"/"buyers_premium"/"delivery_charge"/"indemnity_fee", value the
real amount) to whichever expense's own "Add Expense Item" flow is
currently open on the page (an existing supplier's expense being
edited, or a genuinely new one just created, both use the identical
widget). Pulled out of add_missing_expense_items 2026-08-24 so
create_expense_for_vehicle (a genuinely new expense, needed for
Carwow, which has no live DealerKit integration auto creating one the
way Motorway does) can reuse the exact same proven item adding
mechanics rather than duplicate them.

Rewritten from the real screens of Steven's account (the costs screen
survey, 2026-09-16, after four presses on ND66XYJ each stopped one
step further in): the form's own "ADD" (never "+ EXPENSE", "+ CREDIT"
or the icon only "+") opens a separate "Add Expense Item" window with
Item Category (already set, "Cost of Purchase > Chassis" on a fresh
form), Item Description, Qty, Unit Net Â£'s, VAT Tariff (preset
"Z - Zero") and its own "Add". The category is opened only when it
does not already read the line wanted; the "Expense Categories"
window then takes the group and the line, scoped to that window so
the same words behind it are never clicked. The VAT tariff is set
outright every time. Raises loudly, with the window's words, on the
first step that cannot be found; never guesses a category or field.aT  () => {
                const btns = Array.from(document.querySelectorAll('.ui-modal.is-open button'));
                const out = [];
                for (const b of btns) {
                    const t = (b.innerText || '').trim();
                    if (!t.toLowerCase().includes('add')) continue;
                    const r = b.getBoundingClientRect();
                    if (r.width > 0 && r.height > 0) out.push({text: t, ui: b.classList.contains('ui-button'), x: r.x + r.width/2, y: r.y + r.height/2, area: r.width*r.height});
                }
                return out;
            }Nz6DealerKit: the item ADD button was not found (vehicle rb  r   r   rÄ   zItem CategoryzPick a categoryFg      @rÝ   zQDealerKit: neither an Item Category box nor a category picker was found (vehicle z) after pressing r)   z. Tz>DealerKit: the Item Category box could not be opened (vehicle é„  r‘   rÜ   ra  r$  zDealerKit: category z was not found (vehicle z#DealerKit: the Item Category reads ú, not r5  aÕ  () => {
                const opens = document.querySelectorAll('.ui-modal.is-open');
                const m = opens.length ? opens[opens.length - 1] : document.body;
                let label = null;
                for (const el of m.querySelectorAll('*')) {
                    if (el.children.length > 2 && (el.innerText||'').length > 20) continue;
                    if ((el.textContent||'').trim().startsWith('Unit Net')) {
                        const r = el.getBoundingClientRect();
                        if (r.width > 0 && r.height > 0) { label = {x: r.x + r.width/2, y: r.y + r.height/2}; break; }
                    }
                }
                if (!label) return false;
                const inputs = Array.from(m.querySelectorAll('input[type=text], input[type=number], input:not([type])'));
                let best = null, bestDist = Infinity;
                for (const inp of inputs) {
                    const r = inp.getBoundingClientRect();
                    if (r.width === 0 || r.height === 0) continue;
                    const dist = Math.hypot(r.x + r.width/2 - label.x, r.y + r.height/2 - label.y);
                    if (dist < bestDist) { bestDist = dist; best = inp; }
                }
                if (!best) return false;
                document.querySelectorAll('[data-bb-unit-net]').forEach(e => e.removeAttribute('data-bb-unit-net'));
                best.setAttribute('data-bb-unit-net', '1');
                return true;
            }z5DealerKit: the Unit Net field was not found (vehicle z[data-bb-unit-net]ú.2frE   r5   Úgz"DealerKit: the Unit Net box reads z after typing z
N - No VATzS - Standardr§  ÚAddz<DealerKit: the item's own Add button was not found (vehicle r   r¥  z6 line was not taken by the item window's Add (vehicle rÅ   z¥() => { const opens = document.querySelectorAll('.ui-modal.is-open'); const m = opens.length ? opens[opens.length - 1] : document.body; return (m.innerText || ''); }z$DealerKit: the form does not list a z line after Add (vehicle )rœ  Ú_EXPENSE_CATEGORY_LABELr.   r&  r„   rW  r   r   rL   r\  r,   rg   rP  Úendswithr^  rZ  rx  Ú_EXPENSE_CATEGORY_GROUPSr”   r•   r   r   rª  rJ   rK   r'   rM   rf  Ú_EXPENSE_CATEGORY_VAT_FREE)r   rœ  r½   r  Úamountrm   r!  Úpickrd  Ú	need_pickÚtopr   rS  r]  rR   rS   rT   r°  Ú	form_texts                      r   Ú_add_expense_itemsrv  Ý  sd  € ð0 —{‘{—}‰ˆÜ'¨Ñ,ˆØ—‘ð
ó
÷ ð ð 	ô " %Ó(ˆØ‰<ÜÐ!WÐXbÐWcÐcfÔgtÐuyÓgzÐf{Ð|Ó}Ð}Ø�
‰
×Ñ˜™ SÑ)¨5©;°sÑ+;Ô<Ø×Ñ˜dÔ#ä˜d OÓ4ˆØ‰=ô ' tÐ->ÀeÐUX×YÜ"ð &/Ø/9¨lÐ:KÈEÉKÐX^ÑL_ÑKbÐbdÔerÐswÓexÐdyð${ó |ð |à‰IØ�[‰[‹]× Ñ Ó"×+Ñ+¨E¯K©K«M×:Ñ:Ø‰Iä)¨$°×@Ñ@Ü"Ð%cÐdnÐcoÐorô  tAð  BFó  tGð  sHð  $Ió  Jð  JØˆIßØ×!Ñ! #Ô&Ü˜TÓ"ˆCÜ%Ô&>Ö?‘�Ü& t¸%ÖPQÉÐWZÐcf×gÑgØ×)Ñ)¨#Ô.Ùñ @ðØ—M‘M×&Ñ& uÔ-Ø×)Ñ)¨#Ô.ô ' t¸%ÈÔU_Ð`dÓUe×fÜ"Ð%9¸%¹ÐBZÐ[eÐZfÐfiÔjwÐx|Ój}Ði~Ð#ó  Að  AØ×!Ñ! #Ô&Ü! $¨Ó8ˆEØÑ ¨¯©«×)<Ñ)<Ó)>×)GÑ)GÈÏÉË×)VÑ)VÜ"Ð%HÈÉ	ÐQWÐX]ÑW`Ð`jÐkuÐjvÐvyô  {Hð  IMó  {Nð  zOð  $Pó  Qð  Qð —‘ðó
ˆö6 ÜÐ!VÐWaÐVbÐbeÔfsÐtxÓfyÐezÐ{Ó|Ð|Ø—‘Ð1Ó2×8Ñ8ˆØ�‰ŒØ�
‰
�f˜S�\Ô#Ø�
‰
ŒØ×Ñ˜cÔ"Ü�U×&Ñ&Ó(×.¨BÓ/ˆØ�Sˆ\ &Ó(°°q¨zÀ&Ó-HÜÐ!CÀFÁ:È^Ð\bÐcfÐ[gð h+Ø+5¨,°c¼-ÈÓ:MÐ9Nð Pó Qð Qô 	˜Ô5OÓ.O™lÐUcÐeoÔpó ˆGÜ& t¨U¸$ÈÔT^Ð_cÓTd×eÜ"Ð%aÐblÐamÐmpÔq~ð  @Dó  rEð  qFð  $Gó  Hð  HØ×!Ñ! $Ô'Ü˜T ?Ó3Ñ;ÙØ˜!‹|Ü" _°U°Gð </Ø/9¨l¸#¼mÈDÓ>QÐ=Rð$Tó Uð UðØ—
‘
”ð ×!Ñ! #Ö&ñ ð	ØŸ™ð |ó÷ àð ÷
 ‰9˜Ÿ™›¨i¯o©oÓ.?Ö?ÜÐ!EÀeÀWð M+Ø+5¨,°c¼-ÈÓ:MÐ9Nð Pó Qð QòA %øô^ !ó ÚðûôL ó Ùðûô ó 	ØŠIð	ús6   Æ9,R)ÐR:Ñ S
Ò)
R7Ò6R7Ò:
SÓSÓ
SÓSc           
      ó8  • U(       d  gU R                  [         SU S3SSS9  U R                  S5        [        XS5        U R                  S	5        U R	                  S
5      nU(       d  [        SU S35      eU R                  R                  US   US   5        U R                  S5        U R	                  S5      nU(       d  [        SU S35      eU R                  R                  US   US   5        U R                  S5        U(       dG  [        [        U 5      [        5       5      u  pGU(       d  [        SU S35      e[        SU SU S35        [        X5        U R                  S5        U R	                  S5      nU(       d  [        SU S35      eU R                  R                  US   US   5        U R                  S5        U R                  R                  U5        U R                  S	5        U R	                  SU5      n	U	(       d  [        SU< SU S[        U 5       35      eU R                  R                  U	S   U	S   5        U R                  S5        [!        XU5        U R"                  R$                  R'                  [         SU S 3S!S"9R)                  5       n
[+        U
R'                  S#5      =(       d    / 5      nS$ H­  n[-        U S%S&S'9(       d  [        S(U S35      eU R                  S5        U R"                  R$                  R'                  [         SU S 3S!S"9R)                  5       nUR'                  S#5      =(       d    / n[+        U5      U:”  a  [+        U5      s  $ US):X  d  M­  M¯     [        S*U S[+        W5       S+U S35      e),a`  Create a genuinely NEW expense on a vehicle that has none yet
(Carwow, 2026-08-24: unlike Motorway, DealerKit has no live
integration that auto creates one, so add_due_in's own single typed
Purchase Price is the only figure a Carwow due in ever gets on its
own, real reconnaissance against a real vehicle with "No Expenses"
found the "+ EXPENSE" button opens an "Add Expense" form with its own
Team, Supplier Ref # and Supplier (a searchable contact picker,
"CarWow" already exists as a real contact in this account, found live,
no need to create one) fields above the same Items table and
"Pick a category" > "Add" mechanics add_missing_expense_items already
uses to edit an existing expense, reused here via _add_expense_items.
supplier is matched by a live search (types it, clicks the first
matching option), never guessed or created fresh; raises if the exact
contact is not found rather than leaving a purchase order unattributed
or misattributed. Verifies the categories actually landed via the API
afterward, the same lesson as _upload_via_edit_tile and
add_missing_expense_items: a save can silently do nothing.r   rü   rý   rÀ   rÁ   rÂ   rþ   r4  rØ   áñ  () => {
            const btns = Array.from(document.querySelectorAll('.ui-modal.is-open button'));
            for (const b of btns) {
                const t = (b.innerText || '').toUpperCase();
                if (t.includes('EXPENSE') && !t.includes('CREDIT')) {
                    const r = b.getBoundingClientRect();
                    if (r.width > 0 && r.height > 0) return {x: r.x + r.width / 2, y: r.y + r.height / 2};
                }
            }
            return null;
        }z7DealerKit: the + EXPENSE button was not found (vehicle zE may already have an expense, use add_missing_expense_items instead).r   r   é  áÐ  () => {
            const els = document.querySelectorAll('div, span');
            for (const el of els) {
                if (el.children.length > 2) continue;
                if ((el.textContent || '').trim() === 'Select team') {
                    const r = el.getBoundingClientRect();
                    if (r.width > 0 && r.height > 0) return {x: r.x + r.width / 2, y: r.y + r.height / 2};
                }
            }
            return null;
        }z1DealerKit: the Team field was not found (vehicle r  r$  z@DealerKit: no team was offered on the Add Expense form (vehicle z/  DealerKit team for the new purchase expense: ú (rà  rÅ   áÓ  () => {
            const els = document.querySelectorAll('div, span');
            for (const el of els) {
                if (el.children.length > 2) continue;
                if ((el.textContent || '').trim() === 'Select contact') {
                    const r = el.getBoundingClientRect();
                    if (r.width > 0 && r.height > 0) return {x: r.x + r.width / 2, y: r.y + r.height / 2};
                }
            }
            return null;
        }z5DealerKit: the Supplier field was not found (vehicle a½  (supplier) => {
            // The picker's own options first (2026-09-16); the whole open
            // modal only when it renders them some other way. Never the
            // page behind the modal, whose vehicle list also says the
            // supplier's name.
            let els = Array.from(document.querySelectorAll('.ui-select-option, .ui-select__options li, [role=option]'));
            if (!els.some(el => { const r = el.getBoundingClientRect(); return r.width > 0 && r.height > 0; }))
                els = Array.from(document.querySelectorAll('.ui-modal.is-open li, .ui-modal.is-open [role=option], .ui-modal.is-open div'));
            const s = supplier.toLowerCase();
            let best = null, bestLen = Infinity;
            for (const el of els) {
                if (el.children.length > 2) continue;
                const t = (el.textContent || '').trim();
                if (t.toLowerCase().startsWith(s) && t.length < bestLen) {
                    const r = el.getBoundingClientRect();
                    if (r.width > 0 && r.height > 0) { best = {x: r.x + r.width / 2, y: r.y + r.height / 2}; bestLen = t.length; }
                }
            }
            return best;
        }z(DealerKit: no supplier contact matching z was found (vehicle z'); it may need creating by hand first. rÙ   úT?with[]=expenseItems&with[]=expenseItems.expense&with[]=expenseItems.expense.contactrF  r’   rM  r§  úSave & ApproveTr$   ú1DealerKit: Save & Approve was not found (vehicle r   z=DealerKit: the new expense did not actually save for vehicle z! items after, expected more than )rá   râ   rL   rN  r.   r„   r   r   Ú_choose_teamÚ_visible_dropdown_optionsÚ_preferred_teamrç   r…   r”   r•   rW  rv  rT  rx   rf   rz   rQ  r,   )r   r½   rœ  ra   ÚteamÚexpense_posÚteam_posÚwhyÚsup_posÚopt_posr­  Úbefore_nr°  r]  Úafter_itemss                  r   Úcreate_expense_for_vehicler‹  y	  sÎ  € ö$ Øà‡I�I”Ð  
¨:¨,°iÐ@Ø+°Uð ñ <à×Ñ˜$Ôä�D jÔ1Ø×Ñ˜$Ôà—-‘-ð
	ó€Kö ô ØEÀjÀ\ð RGð Gó
ð 	
ð 	‡J�J×Ñ�[ Ñ% {°3Ñ'7Ô8Ø×Ñ˜$Ôð �}‰}ð
	ó€Hö ÜÐNÈzÈlÐZ\Ð]Ó^Ð^Ø‡J�J×Ñ�X˜c‘] H¨S¡MÔ2Ø×Ñ˜#ÔÞô !Ô!:¸4Ó!@Ä/ÓBSÓT‰	ˆÞÜÐ!aÐblÐamÐmoÐpÓqÐqÜÐ?À¸vÀRÈÀuÈAÐNÔOô ˜4Ô&Ø×Ñ˜#Ôð
 �m‰mð
	ó€Gö ÜÐRÐS]ÐR^Ð^`ÐaÓbÐbØ‡J�J×Ñ�W˜S‘\ 7¨3¡<Ô0Ø×Ñ˜#ÔØ‡M�M×Ñ�xÔ Ø×Ñ˜$Ôð �m‰mð	ð( 	ó+€Gö. ÜØ6°x±lð CØ"�|Ð#JÌ=ÐY]ÓK^ÐJ_ðaó
ð 	
ð 	‡J�J×Ñ�W˜S‘\ 7¨3¡<Ô0Ø×Ñ˜#Ôä�t JÔ/à�\‰\×!Ñ!×%Ñ%ÜÐ
˜/¨*¨ð 6/ð 	/Ø8=ð &ð ?ç?C¹t»vð ô �6—:‘:˜oÓ.×4°"Ó5€HãˆÜ" 4Ð)9À×FÜÐ!RÐS]ÐR^Ð^`ÐaÓbÐbØ×Ñ˜dÔ#Ø—‘×$Ñ$×(Ñ(ÜÐ˜o¨j¨\ð :3ð 3Ø<Að )ð CçCGÁ4Ã6ð 	ð —i‘i Ó0×6°BˆÜˆ{Ó˜hÓ&Ü�u“:ÒØ�a�<Ùñ ô Ø
GÈ
À|ð TÜ�ÓÐÐ>¸x¸jÈð	LóMð Mr"   rÒ   rÀ  rÁ  )rÜ  rÝ  rß  zpurchase pricezbuyer's feec                 óZ   • S n[        U" U 5      5      =(       a    U" U 5      U" U5      :H  $ )z°Whether two supplier names are the same DealerKit contact: case and
spacing do not count (2026-09-16, "Carwow" on DealerKit, "CarWow" in
the code, "Car Wow" typed by a person).c                 ó`   • [         R                  " SSU =(       d    SR                  5       5      $ )Nz	[^a-z0-9]r5   )r9   r:   rP  ©r   s    r   r  Ú_same_contact.<locals>.<lambda>6
  s   € ”R—V’V˜L¨"¨q¯w°B¯o©oÓ.?Ô@r"   )Úbool)r8  r   Únorms      r   Ú_same_contactr’  2
  s*   € ñ A€DÜ‘�Q“‹=×/™T !›W©¨Q«Ñ/Ð/r"   c                 ó~  • U =(       d    S Vs/ s HD  o"(       d  M  [        U5      R                  5       (       d  M+  [        U5      R                  5       PMF     nnU(       d  gU(       aH  U HB  nUR                  5       [        U5      R                  5       R                  5       :X  d  M>  US4s  $    [        U5      S:X  a  US   S4$ U Vs/ s H  nSUR                  5       ;   d  M  UPM     nn[        U5      S:X  a  US   S4$ U(       a  US   S	[        U5       S
34$ US   S	[        U5       S34$ s  snf s  snf )aE  Which DealerKit team a new purchase expense is filed under, from
the teams the Add Expense form offers (2026-09-16): the dealer's own
dealerkit_team setting when it names one of them, else the only team,
else the one team with Sales in its name, else the first. Returns
(team, why); (None, why) when the form offered nothing.rX   )Nzno team was offeredzthe dealerkit_team settingr   r   zthe only teamÚsaleszthe one sales teamzthe first of z sales teamsz teams)r'   rg   rP  rQ  )ÚoptionsÚ	preferredÚoÚoptsr”  s        r   r€  r€  :
  s  € ð &-§]° ]ÓL¢]˜q³q‹N¼SÀ»V¿\¹\¿^‹NŒC�‹F�L‰LŽN¡]€DÐLÞØ*ÞÛˆAØ�w‰w‹yœC 	›N×0Ñ0Ó2×8Ñ8Ó:Õ:ØÐ6Ð6Ò6ñ ô ˆ4ƒy�Aƒ~Ø�A‰w˜Ð'Ð'ÙÓ5š�1 ¨1¯7©7«9Ñ 4�Q™€EÐ5Ü
ˆ5ƒz�QƒØ�Q‰xÐ-Ð-Ð-ÞØ�Q‰x˜=¬¨U«¨°LÐAÐAÐAØ�‰7�m¤C¨£I ;¨fÐ5Ð5Ð5ùò Mùò 6s   Ž
D5œD5¼D5ÃD:Ã(D:c                  ó’   •  SSK Jn   U R                  5       =(       d    0 R                  S5      =(       d    S$ ! [         a     gf = f)z;The dealer's own dealerkit_team setting, when one is saved.r   ©ÚdbÚdealerkit_teamN)Úbidbrainr›  Úget_settingsrf   r   rš  s    r   r‚  r‚  Q
  s?   € ðÝØ—‘Ó!×' R×,Ñ,Ð-=Ó>×FÀ$ÐFøÜó Ùðús   ‚69 ¹
AÁAc                 ó\   •  U R                  S5      =(       d    / $ ! [         a    / s $ f = f)z?The texts of the options an open ui-select dropdown is showing.aÞ  () => {
                const out = [];
                for (const el of document.querySelectorAll('.ui-select__options li, .ui-select-option, .ui-select-option__basic')) {
                    const r = el.getBoundingClientRect();
                    if (r.width === 0 || r.height === 0) continue;
                    const t = (el.textContent || '').trim();
                    if (t && !out.includes(t)) out.push(t);
                }
                return out;
            }r¦   r§   s    r   r�  r�  Z
  s=   € ðØ�}‰}ð	ó
÷ ð ð	øô ó ØŠ	ðús   ‚ œ+ª+c                 óV  • [        U [         SU S35      nU=(       d    0 R                  S5      n[        U5      nUc  gSnU=(       d    0 R                  S5      =(       d    /  H¿  nUR                  S5      U=(       d    0 R                  S5      :X  dC  UR                  S5      c  MD  UR                  S5      U=(       d    0 R                  S5      :X  d  Ms  UR                  S	5      =(       d    0 R                  S
5      =(       d    0 R                  S5      =(       d    Sn  XE4$    XE4$ )a  (amount, contact name) of the purchase cost DealerKit holds on this
car under ANY supplier, (None, None) when it holds none. The guard
against a second purchase expense: a car whose price a person typed
under a differently named contact is never given another.rÙ   zh?with[]=purchaseCost&with[]=expenseItems&with[]=expenseItems.expense&with[]=expenseItems.expense.contactrÚ   Nr
   rM  rÈ   rK  rH  r  r‰   )ræ   râ   rf   Ú_expense_item_amount)r   r½   rÉ   Úcostrq  ÚholderrJ  s          r   Ú_purchase_cost_holderr¤  m
  s  € ô
 ØÜÐ
˜/¨*¨ð 6Kð 	KóL€Dð �J�B×Ñ˜OÓ,€DÜ! $Ó'€FØ�~ØØ€FØ�z�r×Ñ˜Ó/×5°2Ò5ˆØ�6‰6�$‹<˜DŸJ B×+Ñ+¨DÓ1Ó1Ø—‘�|Ó$Ó0°R·V±V¸LÓ5IÈdÏjÐVX×M]ÑM]Ð^jÓMkÕ5kØŸ™˜yÓ)×/¨R×4Ñ4°YÓ?×EÀ2×JÑJÈ>ÓZ×cÐ_cˆFØØˆ>Ðñ 6ð
 ˆ>Ðr"   c                 ó2  • U=(       d    0 R                  5        VVs0 s H  u  pVUc  M
  U[        U5      _M     nnnU(       d  0 $ [        XU5      u  pxUb%  [        XX75      n	U	 H  nSX•   S'   SX•   S'   M     U	$ [	        X5      u  p«U
b6  UR                  5        VVs0 s H  u  pVUSUSSX«=(       d    S4S._M     snn$ [        XX2U5        [        XU5      u  pÍUR                  5        VVs/ s H)  u  pV[        UR                  U5      U5      (       a  M'  UPM+     nnnU(       a+  [        SSR                  S	 U 5       5       S
U SU S35      eUR                  5        VVs0 s H  u  pVUSUSSSS._M     snn$ s  snnf s  snnf s  snnf s  snnf )at  Put the purchase figures on the car's DealerKit record, whatever
state it is in (2026-09-16). items is {"chassis": price,
"buyers_premium": fee, "delivery_charge": transport}, any subset, None
values dropped. The supplier's own purchase expense is read first: a
line already at the figure is left alone, a line that differs is
corrected (set_expense_items), a missing line is added, and a car with
no expense from this supplier at all gets one created with every line
in one go (create_expense_for_vehicle), under the team _choose_team
picks. Never makes a second purchase expense: when DealerKit already
holds a purchase cost under some other contact, nothing is written and
each line says so. Returns {key: {"old", "new", "changed", "created",
"held": (amount, contact) or None}} for every item asked for; raises
with a plain sentence when DealerKit refused or a save did not stick.NFrº   Úheldzanother contact)r×  rá  rÖ  rº   r¦  z2DealerKit: the new purchase expense saved without rÓ   c              3   ó4   #   • U  H  n[         U   v •  M     g 7fr«   ©rm  ©r­   Úks     r   r¯   Ú(sync_purchase_expense.<locals>.<genexpr>¥
  s   é € ÐEºW¸Ô0°Ö3ºWùó   ‚r5  z, now r  T)
rœ  rF   Ú_read_supplier_itemsÚset_expense_itemsr¤  r‹  Ú_amount_matchesrf   r„   r¢   )r   r½   ra   rœ  rƒ  rª  Úvr  r­  r  rq  r£  rò   r]  Úmissings                  r   rô  rô  ƒ
  s¿  € ð ',§k¨r×%8Ñ%8Ô%:ÔLÒ%:™T˜Q¸a‹[ˆQ”�a“Š[Ñ%:€EÑLÞØˆ	Ü*¨4¸XÓF�O€GØÑÜ °%ÓAˆÛˆAØ %ˆC‰F�9ÑØ!ˆC‰F�6‹Nñ ð ˆ
Ü*¨4Ó<�N€FØÑàOTÏ{É{Ì}ô^ÚO\ÁtÀqð ˜4¨°eÈØ#×%@Ð/@ÐAñCò CÙO\ò^ð 	^ä˜t°À$ÔGÜ# D°hÓ?�H€AØ"Ÿ[™[œ]ÔSš]‘T�Q´/À%Ç)Á)ÈAÃ,ÐPQ×2R�q™]€GÑSÞÜØ@Ø�y‰yÑE¹WÓEÓEÐFÀjÐQ[ÐP\Ð\bÐchÐbiÐikðmónð 	nð di×cnÑcnÔcpÔqÒcpÑ[_Ð[\ˆA�t A°$À4ÐQUÑVÒVÑcpÒqÐqùó+ Mùó^ùó Tùó
 rs"   �	FªFÂFÃ0&FÄFÅ(Fc                 óp   • U R                  S5      nU(       a  [        US   U R                  S5      5      $ g)z“Whether one line from sync_purchase_expense is right on DealerKit
now: written, already matching, or held under another contact at the
same figure.r¦  r   rá  T)rf   r¯  )r  r¦  s     r   r÷  r÷  ©
  s0   € ð �7‰7�6‹?€DÞÜ˜t A™w¨¯©°«Ó7Ð7Ør"   c                 óŠ  • [        U S   5      nU R                  S5      nU(       a5  Uu  pE[        XB5      (       a	  SUS SU 3$ S[        U5      S SU SU S	U 3$ U R                  S
5      (       a
  SUS SU S3$ U R                  S5      (       d  SUS 3$ U R                  S5      nUb  S[        U5      S SUS 3$ SUS 3$ )z8One line's outcome from sync_purchase_expense, in words.rá  r¦  rä  rk  z, under u   left alone, DealerKit holds Â£ú under ri  z(; correct it in DealerKit or move it to rº   rÛ  z
 on a new z purchase expenserÖ  r×  rå  ræ  )rF   rf   r¯  )r  ra   rá  r¦  rq  Úwhor×  s          r   rõ  rõ  ³
  sý   € ä
��E‘
Ó
€CØ�7‰7�6‹?€DÞØ‰ˆÜ˜6×'Ñ'Ø3°C¸°7¸(À3À%ÐHÐHØ0´°v³¸qÐ0AÀÈÈÈVÐT\ÐS]ð ^9Ø9A¸
ðDð 	Eà
‡w�wˆy×ÑØ˜3˜q˜' ¨H¨:Ð5FÐGÐGØ�7‰7�9×ÑØ/°°A¨wÐ7Ð7Ø
�'‰'�%‹.€CØ<?¹Oˆ_œU 3›Z¨˜N¨&°°Q°Ð8ÐdÐS\Ð]`ÐabÐ\cÐQdÐdr"   c                 ó˜  • U R                  [         SU S3SSS9  U R                  S5        [        XS5        U R                  S5        U R	                  S	U5        U R                  S
5        U R	                  SU5      nU(       d  [        SU SU S35      eU R                  R                  US   US   5        U R                  S5        g)a   Open the given supplier's own expense inside Edit Vehicle Expenses,
landing on the open Edit Expense modal. Factored out of
add_missing_expense_items (2026-08-27) so set_purchase_price can reuse
the exact same proven open, every hard won detail included:

The supplier row's pencil is found by locating the smallest visible
text match then walking UP to the nearest ancestor that itself
contains a real button, never the nearest button by the text's own
y centre: for a vehicle with many expenses (found live 2026-08-23 on
one with 8+), the supplier text is only the row's own top line of
three, so its tight bounding box centres well above the true row
centre and a distant unrelated button won by that measure.

scrollIntoView first, THEN re measure (2026-08-24): a matching row at
the bottom edge of this scrollable list (found live on HJ17ECW) has a
real, in DOM pencil whose own bounding box is still effectively
unclickable, so the position must be measured AGAIN after the scroll
settles, never reused from before it.rü   rý   rÀ   rÁ   rÂ   rþ   r4  rØ   aŠ  (supplier) => {
            const els = document.querySelectorAll('div, span, li');
            const matches = [];
            for (const el of els) {
                if (el.children.length > 10) continue;
                const t = (el.innerText || '').trim();
                if (t !== supplier) continue;
                const r = el.getBoundingClientRect();
                if (r.width > 0 && r.height > 0) matches.push({el, area: r.width * r.height});
            }
            if (matches.length) {
                matches.sort((a, b) => a.area - b.area);
                matches[0].el.scrollIntoView({block: 'center'});
            }
        }rp   a"  (supplier) => {
            const els = document.querySelectorAll('div, span, li');
            const matches = [];
            for (const el of els) {
                if (el.children.length > 10) continue;
                const t = (el.innerText || '').trim();
                if (t !== supplier) continue;
                const r = el.getBoundingClientRect();
                if (r.width > 0 && r.height > 0) matches.push({el, area: r.width * r.height});
            }
            if (!matches.length) return null;
            matches.sort((a, b) => a.area - b.area);
            let node = matches[0].el;
            let btn = null;
            for (let i = 0; i < 6 && node; i++) {
                btn = node.querySelector('button');
                if (btn) break;
                node = node.parentElement;
            }
            if (!btn) return null;
            const r = btn.getBoundingClientRect();
            const best = (r.width > 0 && r.height > 0) ? {x: r.x + r.width / 2, y: r.y + r.height / 2} : null;
            return best;
        }úDealerKit: no ú$ expense was found to edit (vehicle r  r   r   N)rá   râ   rL   rN  r.   r„   r   r   )r   r½   ra   r=   s       r   Ú_open_supplier_expenser¹  Å
  sÛ   € ð& 	‡I�I”Ð  
¨:¨,°iÐ@Ø+°Uð ñ <à×Ñ˜$ÔÜ�D jÔ1Ø×Ñ˜$Ôà‡M�Mð	ð 	ô!ð$ 	×Ñ˜#ÔØ�]‰]ð	ð0 	ó3€Fö6 Ü˜^¨H¨:Ð5YÐZdÐYeÐegÐhÓiÐiØ‡J�J×Ñ�V˜C‘[ &¨¡+Ô.Ø×Ñ˜$Õr"   re   c           
      ó’  • [        U5      nU R                  [         SU S3SSS9  U R                  S5        [	        U [         SU S35      n[        U=(       d    0 R                  S	5      5      n[        XU5        [        S
   nU R                  SU5      nU(       d  [        SU S35      eU R                  R                  US   US   5        U R                  S5        [        U SSS9(       d  [        SU S35      eU R                  S5        [        U S
U0U5        S H¢  n[        U SSS9(       d  [        SU S35      eU R                  S5        [	        U [         SU S35      n	[        U	=(       d    0 R                  S	5      5      n
U
b  [        X¢-
  5      S:  a  XRS.s  $ US:X  d  MŽ  [        SU SU
< SU S35      e   g) a  Correct the purchase price on a vehicle ALREADY on DealerKit, the
writer that never existed (found 2026-08-27: add_due_in types the
price only at creation and every later push returns early at
already_exists, so a chip agreed after the car was pushed, which is
when chips are always agreed, had no code path to DealerKit at all,
and from there none to Xero).

The mechanism is the real UI route proven live on 2026-08-24 for the
same kind of correction (WG18EZE's Buyers Premium 299 to 339): the
purchase price on DealerKit IS the Chassis line of the supplier's own
expense, so this deletes that item client side via its own kebab and
re adds it at the new figure in the same unsaved modal session, then
one Save & Approve. Net item count never drops below one, which
matters: an expense emptied to zero items silently refuses to save
(WX16AUE, 2026-08-24). Verified against the API afterward, one retry,
loud failure, never a reported success that did not stick.

Steven's call (2026-08-27) is FLAG, never auto push: nothing calls
this on a schedule, only a person ticking the price box on a row whose
mismatch the page has already shown them. NOT yet verified against a
live DealerKit session; the first real use should be watched, the
same way every other writer in this module was proven.rü   rý   rÀ   rÁ   rÂ   rØ   rÙ   z?with[]=purchaseCostrÚ   rÜ  a±  (label) => {
            const els = document.querySelectorAll('div, span, td, li');
            const matches = [];
            for (const el of els) {
                if (el.children.length > 10) continue;
                const t = (el.innerText || '').trim();
                if (!t.includes(label)) continue;
                const r = el.getBoundingClientRect();
                if (r.width > 0 && r.height > 0) matches.push({el, area: r.width * r.height});
            }
            if (!matches.length) return null;
            matches.sort((a, b) => a.area - b.area);
            let node = matches[0].el;
            for (let i = 0; i < 8 && node; i++) {
                const btns = Array.from(node.querySelectorAll('button'));
                const k = btns.find(b => (b.innerText || '').includes('more_vert'));
                if (k) {
                    k.scrollIntoView({block: 'center'});
                    const r = k.getBoundingClientRect();
                    if (r.width > 0 && r.height > 0)
                        return {x: r.x + r.width / 2, y: r.y + r.height / 2};
                }
                node = node.parentElement;
            }
            return null;
        }z>DealerKit: the Chassis item's own menu was not found (vehicle r  r   r   rh  rD  Tr$   z?DealerKit: Delete was not offered on the Chassis item (vehicle r§  r~  r  rþ   Nrã  )Ú	old_valueÚ	new_valuer   z@DealerKit: the purchase price did not actually save for vehicle ú (still ú	, wanted )rF   rá   râ   rL   ræ   r¡  rf   r¹  rm  r.   r„   r   r   r,   rv  rø  )r   r½   rÒ   ra   r­  r»  Úchassis_labelÚkebabr°  r]  Úgots              r   Úset_purchase_pricerÂ    sü  € ô. �%‹L€EØ‡I�I”Ð  
¨:¨,°iÐ@Ø+°Uð ñ <à×Ñ˜$ÔÜØ”Ð  °
¨|Ð;OÐPóR€Fä$ f§l°×%7Ñ%7¸Ó%HÓI€Iä˜4¨XÔ6ô
 ,¨IÑ6€MØ�M‰Mð	ð4 	ó7€Eö: ÜØLÈZÈLÐXZÐ[ó]ð 	]à‡J�J×Ñ�U˜3‘Z  s¡Ô,Ø×Ñ˜#ÔÜ˜t X°T×:ÜØMÈjÈ\ÐY[Ð\ó^ð 	^à×Ñ˜#Ôä�t˜i¨Ð/°Ô<ãˆÜ" 4Ð)9À×FÜÐ!RÐS]ÐR^Ð^`ÐaÓbÐbØ×Ñ˜dÔ#ÜØ”^Ð$ O°J°<Ð?SÐTóVˆä" E§K¨R×#4Ñ#4°_Ó#EÓFˆØ‰?œs 3¡;Ó/°$Ó6Ø!*Ñ?Ò?Ø�a�<ÜØRØ�,˜h s¡g¨Y°u°g¸RðAóBð Bò r"   )ra   c          	      óx  • U(       d  gU R                  [         SU S3SSS9  U R                  S5        U R                  R                  R                  [         SU S	3S
S9R                  5       nUR                  S5      =(       d    /  Vs1 s HX  nUR                  S5      (       d  M  US   R                  S5      (       d  M6  US   S   R                  S5      U:X  d  MS  US   iMZ     nn[        XU5        [        XU5        S GH  n[        U SSS9(       d  [        SU S35      eU R                  S5        U R                  R                  R                  [         SU S	3S
S9R                  5       n	U	R                  S5      =(       d    /  Vs1 s HX  nUR                  S5      (       d  M  US   R                  S5      (       d  M6  US   S   R                  S5      U:X  d  MS  US   iMZ     n
nXz:  d	  X§-
  (       a  [        U5      s  $ US:X  d  GM    O   [        SU SW
 SU S35      es  snf s  snf )a#  Add one or more missing line items (missing is a dict keyed by
"chassis"/"buyers_premium"/"delivery_charge"/"indemnity_fee", value the
real amount) onto the vehicle's EXISTING expense from the given
supplier, found via the real live "Motorway" account wide audit
2026-08-23: every one of the 71 non "Due In" Motorway purchases already
has its own single Motorway expense record with at least a Chassis
line, so this always edits an existing expense, it never creates a new
one (add_due_in / a fresh "+ EXPENSE" is for a genuinely new purchase).
Verifies the categories actually landed via the API afterward, the same
lesson as _upload_via_edit_tile: a save can silently do nothing.
Raises loudly (never guesses) if the supplier's own expense cannot be
found, or if a save does not stick after one retry.r   rü   rý   rÀ   rÁ   rÂ   rþ   rÙ   r}  rF  r’   rM  rH  r  r‰   Úexpense_category_idr§  r~  Tr$   r  r  r   z;DealerKit: expense items did not actually save for vehicle z (categories still z, expected new ones beyond )rá   râ   rL   rT  rx   rf   rz   r¹  rv  r,   r„   rQ  )r   r½   r±  rƒ  ra   r­  rJ  Úbefore_catsr°  r]  Ú
after_catss              r   Úadd_missing_expense_itemsrÇ  o  su  € ö Øà‡I�I”Ð  
¨:¨,°iÐ@Ø+°Uð ñ <à×Ñ˜$Ôà�\‰\×!Ñ!×%Ñ%ÜÐ
˜/¨*¨ð 6/ð 	/Ø8=ð &ð ?ç?C¹t»vð ð 9?¿
¹
À?Ó8S×8YÐWYÐ8Yó QÒ8Y°Ø—f‘f˜Y×'ó -Ø,.¨y©M×,=Ñ,=¸i×,Hó -à˜)‘} YÑ/×3Ñ3°NÓCÀxÑOó -�2Ð+Ô,Ñ8Y€Kð Qô ˜4¨XÔ6ä�t jÔ1äˆÜ" 4Ð)9À×FÜÐ!RÐS]ÐR^Ð^`ÐaÓbÐbØ×Ñ˜dÔ#Ø—‘×$Ñ$×(Ñ(ÜÐ˜o¨j¨\ð :3ð 3Ø<Að )ð CçCGÁ4Ã6ð 	ð <A¿9¹9À_Ó;U×;[ÐY[Ð;[ó TÒ;[°BØŸ™ 	×*ó 0Ø/1°)©}×/@Ñ/@À×/Kó 0à˜Y™-¨	Ñ2×6Ñ6°~ÓFÈ(ÑRó 0�bÐ.Ô/Ñ;[ˆ
ð Tð Ó#¨
×(@Ü�w“<ÒØ�aŽ<Ùñ ô Ø
EÀjÀ\ð RØ'˜LÐ(CÀKÀ=ÐPRð	TóUð Uùò-QùòTs0   ÂH2Â+H2ÃH2Ã#	H2ÆH7Æ2H7ÇH7Ç*	H7c                 óB  • U R                   R                  R                  [         SU S3SS9R	                  5       nUR                  S5      =(       d    /  HE  nUR                  S5      =(       d    0 nUR                  S5      U:X  d  M4  UR                  S5      s  $    g	)
zšThe real, current tax_at (an ISO datetime) for one expense record,
read via DealerKit's own API, never the UI. None if the expense id is
not found at all.rÙ   rE  rF  r’   rM  rH  rÈ   Útax_atN)rT  rx   rf   râ   rz   )r   r½   rK  ÚjrJ  Úexps         r   Ú_read_expense_tax_atrÌ  ¡  s–   € ð 	�‰×Ñ× Ñ ÜÐ
˜/¨*¨Ð5eÐfØð 	!ð 	ç‘t“vð ð �e‰e�OÓ$×*¨Ò*ˆØ�f‰f�YÓ×% 2ˆØ�7‰7�4‹=˜JÕ&Ø—7‘7˜8Ó$Ò$ñ +ð r"   c           	      ób  • [        XU5      nU(       a
  USS U:X  a  gU R                  [         SU S3SSS9  U R                  S	5        [	        XS
5        [        U SU5      nU(       d  [        SU SU S35      eU R                  R                  US   US   5        U R                  S5        [        U S5      nU(       d  [        SU S35      eU R                  R                  US   US   5        U R                  S5        UR                  S5      u  p‰n
SSK
nUR                  [        U	5         nU SU 3n[        S5       Hí  nU R                  S5      nUc  [        SU S35      eXý:X  a    OÖUR                  SS5      u  nn[!        UR                  5      R#                  U5      n[        U5      [        U5      -
  S-  [        U	5      U-
  -   S:„  nU R                  SU5      nU(       d  [        SU S35      eU R                  R                  US   US   5        U R                  S5        Mï     [        S U S!U S35      e[%        [        U
5      5      nU R                  S"U5      nU(       d  [        S#U S$U S35      eU R                  R                  US   US   5        U R                  S%5        S& HY  n['        U S'S(S)9(       d  [        S*U S35      eU R                  S	5        [        XU5      nU(       a  USS U:X  a    g(US:X  d  MY    O   [        S+U S,W< S-U< S.35      e)/a^  Correct one expense's own Tax Date (DealerKit's internal field name
is tax_at) to date_iso ("YYYY-MM-DD"), 2026-08-23, Mark, live in this
exact modal: "i want the collected date to be the tax date as this is
when the payment was made and so this is used for the push to xero".
date_iso should be motorway.read_collection_date's own real value, the
date Motorway's transport collected the car from the seller, never the
delivery date (a different, wrong field for this purpose, learned live
the same session). No op, returns False without touching anything, if
the stored date already matches (compared by calendar date only, tax_at
itself carries a time component DealerKit set on its own, not worth
perturbing when the day is already right). Never a raw API write: this
drives the real Edit Expense modal, the same route as
add_missing_expense_items, because DealerKit's other date field
(delivery_on, at the vehicle level) has no UI path to edit at all and a
direct API write to it was refused by Claude Code's own safety check as
untested and invasive; this one is neither, since it is a real UI flow,
not a bypass.Né
   Frü   rý   rÀ   rÁ   rÂ   rþ   r4  a*  (supplier) => {
            const els = document.querySelectorAll('div, span, li');
            const matches = [];
            for (const el of els) {
                if (el.children.length > 10) continue;
                const t = (el.innerText || '').trim();
                if (t !== supplier) continue;
                const r = el.getBoundingClientRect();
                if (r.width > 0 && r.height > 0) matches.push({el, area: r.width * r.height});
            }
            matches.sort((a, b) => a.area - b.area);
            for (const m of matches) {
                let node = m.el;
                let btn = null;
                for (let i = 0; i < 6 && node; i++) {
                    btn = node.querySelector('button');
                    if (btn) break;
                    node = node.parentElement;
                }
                if (!btn) continue;
                const r = btn.getBoundingClientRect();
                if (r.width === 0 || r.height === 0) continue;
                const cx = r.x + r.width / 2, cy = r.y + r.height / 2;
                const top = document.elementFromPoint(cx, cy);
                if (top && (top === btn || btn.contains(top) || top.contains(btn))) {
                    return {x: cx, y: cy};
                }
            }
            return null;
        }r·  r¸  r  r   r   rk   aL  () => {
            // Every open modal, not just the first: the same duplicate
            // modal instance seen opening this one can still be present,
            // and document.querySelector only ever returns the first.
            const modals = document.querySelectorAll('.ui-modal.is-open');
            for (const modal of modals) {
                const els = Array.from(modal.querySelectorAll('*'));
                const candidates = els.filter(e => e.children.length === 0 &&
                    /^\d{2}\/\d{2}\/\d{4}$/.test((e.textContent||'').trim()));
                for (const el of candidates) {
                    const r = el.getBoundingClientRect();
                    if (r.width === 0 || r.height === 0) continue;
                    const cx = r.x + r.width/2, cy = r.y + r.height/2;
                    const top = document.elementFromPoint(cx, cy);
                    if (top && (top === el || el.contains(top) || top.contains(el))) {
                        return {x: cx, y: cy};
                    }
                }
            }
            return null;
        }z5DealerKit: the Tax Date field was not found (vehicle r$  Ú-r   rž   é$   ág  () => {
                const el = Array.from(document.querySelectorAll('*')).find(e =>
                    e.children.length === 0 &&
                    /^(January|February|March|April|May|June|July|August|September|October|November|December) \d{4}$/.test((e.textContent||'').trim()));
                return el ? el.textContent.trim() : null;
            }zADealerKit: the date picker's month header was not found (vehicle r   r*  á~  (forward) => {
                const headerEl = Array.from(document.querySelectorAll('*')).find(e =>
                    e.children.length === 0 &&
                    /^(January|February|March|April|May|June|July|August|September|October|November|December) \d{4}$/.test((e.textContent||'').trim()));
                if (!headerEl) return null;
                const hr = headerEl.getBoundingClientRect();
                const cy = hr.y + hr.height / 2;
                const btns = Array.from(document.querySelectorAll('button')).filter(b => {
                    const r = b.getBoundingClientRect();
                    if (r.width === 0 || r.height === 0 || r.width > 40 || r.height > 40) return false;
                    return Math.abs((r.y + r.height / 2) - cy) < hr.height;
                });
                if (btns.length < 2) return null;
                btns.sort((a, b) => a.getBoundingClientRect().x - b.getBoundingClientRect().x);
                const b = forward ? btns[btns.length - 1] : btns[0];
                const r = b.getBoundingClientRect();
                return {x: r.x + r.width/2, y: r.y + r.height/2};
            }z?DealerKit: the month navigation arrows were not found (vehicle rp   z1DealerKit: could not navigate the date picker to r5  áz  (day) => {
            const btns = Array.from(document.querySelectorAll('button.ui-calendar-week__date'));
            const el = btns.find(b => b.textContent.trim() === day && !b.className.includes('is-in-other-month'));
            if (!el) return null;
            const r = el.getBoundingClientRect();
            return {x: r.x + r.width/2, y: r.y + r.height/2};
        }úDealerKit: day z+ was not found in the date picker (vehicle rš   r§  r~  Tr$   r  z6DealerKit: Tax Date did not actually save for vehicle r½  z, expected r©  )rÌ  rá   râ   rL   rN  r3   r„   r   r   r£   ÚcalendarÚ
month_namerH   r   r.   ÚrsplitrI   Úindexr'   r,   )r   r½   rK  r  ra   Úcurrentr=   Údate_posÚyearÚmonthÚdayÚ_calÚtarget_month_nameÚtarget_headerrò   rd  Úshown_monthÚ
shown_yearÚshown_month_numÚforwardÚ
nav_targetÚday_numÚday_posr°  r]  s                            r   Úset_expense_tax_daterè  ¯  s�  € ô$ # 4°ZÓ@€GÞ�7˜3˜B�< 8Ó+Øà‡I�I”Ð  
¨:¨,°iÐ@Ø+°Uð ñ <à×Ñ˜$Ôä�D jÔ1ô Øð	ð< 	óA!€FöD Ü˜^¨H¨:Ð5YÐZdÐYeÐegÐhÓiÐiØ‡J�J×Ñ�V˜C‘[ &¨¡+Ô.Ø×Ñ˜$Ôô Øð	ó€Hö0 ÜÐRÐS]ÐR^Ð^`ÐaÓbÐbØ‡J�J×Ñ�X˜c‘] H¨S¡MÔ2Ø×Ñ˜#Ôà—~‘~ cÓ*Ñ€D�ÛØŸ™¬¨E«
Ñ3ÐØ(Ð)¨¨4¨&Ð1€Mô �2ŽYˆØ—‘ðó
ˆð ‰=ÜÐ!bÐcmÐbnÐnpÐqÓrÐrØÓ!ÙØ"'§,¡,¨s°AÓ"6Ñˆ�ZÜ˜tŸ™Ó/×5Ñ5°kÓBˆÜ�t“9œs :›Ñ.°"Ñ4¼¸E»
À_Ñ8TÑUÐXYÑYˆð —]‘]ðð$ ó'
ˆ
ö* ÜÐ!`ÐakÐ`lÐlnÐoÓpÐpØ�
‰
×Ñ˜ C™¨*°S©/Ô:Ø×Ñ˜cÖ"ña ôd ÐNÈ}ÈoÐ]gÐhrÐgsÐsuÐvÓwÐwä”#�c“(‹m€GØ�m‰mð	ð 	ó	€Gö Ü˜_¨W¨IÐ5`ÐakÐ`lÐlnÐoÓpÐpØ‡J�J×Ñ�W˜S‘\ 7¨3¡<Ô0Ø×Ñ˜#ÔãˆÜ" 4Ð)9À×FÜÐ!RÐS]ÐR^Ð^`ÐaÓbÐbØ×Ñ˜dÔ#Ü$ T°zÓBˆÞ�U˜3˜B�Z 8Ó+ÙØ�a�<Ùñ ô Ø
@ÀÀð MØ‘˜+ h¡\Ð1Gð	IóJð Jr"   c                 ól  • UR                  S5      u  p4nSSKnUR                  [        U5          SU 3n[	        S5       Hì  nU R                  S5      n	U	c  [        SU S35      eX—:X  a    OÕU	R                  SS	5      u  p«[        UR                  5      R                  U
5      n[        U5      [        U5      -
  S
-  [        U5      U-
  -   S:„  nU R                  SU5      nU(       d  [        SU S35      eU R                  R                  US   US   5        U R                  S5        Mî     [        SU SU S35      e[        [        U5      5      nU R                  SU5      nU(       d  [        SU SU S35      eU R                  R                  US   US   5        U R                  S5        g)ab  Navigates and clicks a day on the SAME calendar popup component
(button.ui-calendar-week__date, a "Month YYYY" header, confirmed live
2026-08-24 to be shared between the Edit Expense modal's Tax Date field
and the Add to Funding modal's own two date fields) assuming a click
has already opened it. Same month navigation technique as
set_expense_tax_date.rÏ  r   Nrž   rÐ  rÑ  r¥  z* date picker's month header was not found.r   r*  rÒ  z6 date picker's month navigation arrows were not found.r   r   rp   z"DealerKit: could not navigate the z date picker to rƒ   rÓ  rÔ  z was not found in the z date picker.rš   )r£   rÕ  rÖ  rH   r   r.   r„   r×  rI   rØ  r   r   rL   r'   )r   r  r�  rÛ  rÜ  rÝ  rÞ  rà  rò   rd  rá  râ  rã  rä  rå  ræ  rç  s                    r   r(  r(  €  sÄ  € ð  —~‘~ cÓ*Ñ€D�ÛØ—‘¤s¨5£zÑ2Ð3°1°T°FÐ;€Mä�2ŽYˆØ—‘ðó
ˆð ‰=Ü °°Ð6`ÐaÓbÐbØÓ!ÙØ"'§,¡,¨s°AÓ"6ÑˆÜ˜tŸ™Ó/×5Ñ5°kÓBˆÜ�t“9œs :›Ñ.°"Ñ4¼¸E»
À_Ñ8TÑUÐXYÑYˆØ—]‘]ðð$ ó'
ˆ
ö* Ü °°Ð6lÐmÓnÐnØ�
‰
×Ñ˜ C™¨*°S©/Ô:Ø×Ñ˜cÖ"ñQ ôT Ð?À¸vÐEUÐVcÐUdÐdeÐfÓgÐgä”#�c“(‹m€GØ�m‰mð	ð 	ó	€Gö Ü˜_¨W¨IÐ5KÈDÈ6ÐQ^Ð_Ó`Ð`Ø‡J�J×Ñ�W˜S‘\ 7¨3¡<Ô0Ø×Ñ˜#Õr"   c                 ó®   • [        U SUUS9nU(       d  [        SU S35      eU R                  R                  US   US   5        U R	                  S5        g)	a±  Clicks a labelled field on the Add/Edit to Funding modal by its own
stable structure: a .ui-select__label-text (Vehicle, Finance House,
Unit Type) or .ui-datepicker__label-text (Plan Start Date, Funding
Expiry Date, a genuinely different component, found live 2026-08-24)
holding the field's own name, inside a .ui-select or .ui-datepicker
container that also holds the current value or placeholder just below
it. Far more reliable than matching the placeholder text directly: the
Vehicle placeholder ("Search by VRM, VIN, Make or Model") wraps across
a real newline in the underlying markup, so a plain .trim() equality
check against it never matches, and none of these fields are a real
<input> at all (real <input> elements on this modal are almost all
zero sized decoys), so a placeholder attribute lookup finds nothing
either. Every match is checked, not just the first: this page renders
more than one label reading the same text at once (two "Vehicle"
labels found live, the first inside a zero sized hidden duplicate),
DOM order alone cannot be trusted to put the real one first. Clicked
low in the container (75% down), landing on the value/placeholder row
rather than the label row above it.a/  (label) => {
            // Not the FIRST match: this page carries multiple .ui-select
            // fields sharing the same label text at once (found live
            // 2026-08-24, exactly two "Vehicle" labels, the first inside a
            // zero sized hidden duplicate container), so every match is
            // checked for a real, visible container rather than trusting
            // whichever one DOM order puts first.
            const labelEls = Array.from(document.querySelectorAll(
                '.ui-select__label-text, .ui-datepicker__label-text'));
            const matches = labelEls.filter(e => e.textContent.trim() === label);
            for (const match of matches) {
                const container = match.closest('.ui-select, .ui-datepicker');
                if (!container) continue;
                const r = container.getBoundingClientRect();
                if (r.width > 0 && r.height > 0) {
                    return {x: r.x + r.width / 2, y: r.y + r.height * 0.75};
                }
            }
            return null;
        }r’   r¥  z field was not found.r   r   rš   N©r3   r„   r   r   rL   )r   Ú
label_textr   rŠ   s       r   r“   r“   È  sb   € ô& Øð	ð( 	Øñ/€Cö2 Ü˜_¨Z¨LÐ8MÐNÓOÐOØ‡J�J×Ñ�S˜‘X˜s 3™xÔ(Ø×Ñ˜#Õr"   c                 ó&  • [        U SU5      nU(       d  [        SU< S35      eU R                  S5        U R                  SU5      nU(       d  [        SU< S35      eU R                  R                  US   US   5        U R                  S	5        g
)aH  Clicks an option inside an OPEN ui-select dropdown (ul.ui-select__options,
li.ui-select-option), scoped to that list specifically. Found live
2026-08-24, the reason Finance House silently never got set: the real
Finance House value ("LE Capital") ALSO appears, genuinely visible,
12 times over in the funding grid sitting behind the modal (every
other row already funded through LE Capital), so a page wide "find
the smallest visible match" search reliably picked one of those grid
cells instead of the real dropdown option, no error, just the wrong
click landing on inert table text.al  (text) => {
            const opts = Array.from(document.querySelectorAll(
                '.ui-select__options li, .ui-select-option, .ui-select-option__basic'));
            const match = opts.find(el => el.textContent.trim() === text);
            if (!match) return false;
            match.scrollIntoView({block: 'center'});
            return true;
        }zDealerKit: the dropdown option z was not found.rE   a–  (text) => {
            const opts = Array.from(document.querySelectorAll(
                '.ui-select__options li, .ui-select-option, .ui-select-option__basic'));
            const match = opts.find(el => el.textContent.trim() === text);
            const r = match.getBoundingClientRect();
            return (r.width > 0 && r.height > 0) ? {x: r.x + r.width / 2, y: r.y + r.height / 2} : null;
        }z! was not visible after scrolling.r   r   rš   N)r3   r„   rL   r.   r   r   )r   Úoption_textÚfoundrŠ   s       r   r…   r…   ú  s    € ô  Øð	ð 	ó€Eö ÜÐ<¸[¹OÈ?Ð[Ó\Ð\Ø×Ñ˜#ÔØ
�-‰-ð	ð 	ó	€Cö ÜÐ<¸[¹OÐKlÐmÓnÐnØ‡J�J×Ñ�S˜‘X˜s 3™xÔ(Ø×Ñ˜#Õr"   c           	      ó  • U R                  [         S3SSS9  U R                  S5        U R                  S5      nU(       d  [	        S5      eU R
                  R                  US   US	   5        [        U S
SS9  U R                  R                  U5        U R                  S5        [        XR                  SS5      SS SS9(       d  [	        SU S35      eU R                  S5        [        U S5        [        X5        [        U S5        [        XS5        [        U S5        [        XS5        U R                  S5      R                  nUR                  5         UR!                  US 5        U R                  S5        U R                  S5      n	U	(       d  [	        SU S 35      eU R
                  R                  U	S   U	S	   5        U R                  S!5        [#        X5      n
U
(       d  [	        S"U S#35      eU R$                  R&                  R)                  [         S$U
 S%3S&S9R+                  5       R)                  S'5      nU(       a*  [-        UR)                  S(5      =(       d    S)U-
  5      S*:”  a  [	        S+U S,U< S-U S 35      eU$ ).aK  Adds a new funding record to DealerKit's own Finance > Funding
section (rightdrive.app.dealerkit.uk/finance/funding), via the real
"Add to Funding" modal, proven live 2026-08-24 (Mark: "LE captial is
the source of truth ... i need DK funding to match the LE portal").
plan_starts_at and funding_expires_at are "YYYY-MM-DD" strings.
Deliberately never lets the modal's own auto calculated Funding Expiry
Date stand: setting Plan Start Date alone was found live to fill in a
default about 8 months out, not the 4 month term every real funding
record on this account actually uses, so the expiry is always set
explicitly rather than trusted. Verifies via DealerKit's own API
afterward (GET /api/stocklist/{id}?with[]=fundingRecord), the same
lesson as every other write in this module: a save can look right
client side and still not have taken.
z/finance/fundingrÀ   rÁ   rÂ   r   a  () => {
            const btn = document.querySelector('a[title="Add Vehicle"]');
            if (!btn) return null;
            const r = btn.getBoundingClientRect();
            return (r.width > 0 && r.height > 0) ? {x: r.x + r.width / 2, y: r.y + r.height / 2} : null;
        }z1DealerKit: the Fund Vehicle button was not found.r   r   ÚVehiclerÎ   r’   rØ   rž   r5   Nr  rM  z,DealerKit: no vehicle search result matched rƒ   r$  zFinance HousezPlan Start Datez
Plan StartzFunding Expiry DatezFunding Expiryzinput[placeholder="e.g. 13200"]rj  rš   aX  () => {
            const btns = Array.from(document.querySelectorAll('.ui-button--type-primary'));
            for (const btn of btns) {
                const r = btn.getBoundingClientRect();
                if (r.width > 0 && r.height > 0) return {x: r.x + r.width / 2, y: r.y + r.height / 2};
            }
            return null;
        }z9DealerKit: the modal's own Add button was not found (reg r  rþ   zDealerKit: could not find z-'s own vehicle id to verify the funding save.rÙ   ú?with[]=fundingRecordrF  Úfunding_recordÚfunded_valuer   rã  zDealerKit: funding record for ú did not actually save (got z, expected funded_value )rá   râ   rL   r.   r„   r   r   r“   r”   r•   r@   r  r…   r(  r   rª  rJ   rß   rT  rx   rf   rz   rø  )r   r²   rô  Úplan_starts_atÚfunding_expires_atÚfinance_houseÚ	unit_typeÚfund_btnÚamount_inputÚadd_posr½   Úrecords               r   Úadd_funding_recordrþ  )  s�  € ð  	‡I�I”Ð Ð 0Ð1Ð>PÐZ_€IÑ`Ø×Ñ˜$Ôð �}‰}ð	ó€Hö ÜÐNÓOÐOØ‡J�J×Ñ�X˜c‘] H¨S¡MÔ2ô ˜4 °BÒ7Ø‡M�M×Ñ�sÔØ×Ñ˜$ÔÜ˜T§;¡;¨s°BÓ#7¸¸Ð#;ÀQ×GÜÐIÈ#ÈÈaÐPÓQÐQØ×Ñ˜#Ôä˜4 Ô1Ü˜4Ô/ä˜4Ð!2Ô3Ü˜¨lÔ;ä˜4Ð!6Ô7Ü˜Ð2BÔCà—<‘<Ð AÓB×HÑH€LØ×ÑÔØ×Ñ˜ cÐ*Ô,Ø×Ñ˜#Ôð �m‰mð	ó	€Gö ÜÐVÐWZÐV[Ð[]Ð^Ó_Ð_Ø‡J�J×Ñ�W˜S‘\ 7¨3¡<Ô0Ø×Ñ˜$Ôä(¨Ó3€JÞÜÐ7¸°uÐ<iÐjÓkÐkØ�\‰\×!Ñ!×%Ñ%ÜÐ
˜/¨*¨Ð5JÐKÐUZð &ð \ß\`Ñ\`Ó\b×cfÑcfÐgwÓcxð æ”S˜&Ÿ*™* ^Ó4×9¸¸\ÑIÓJÈTÓQÜØ,¨S¨Eð 2Ø‘:Ð5°l°^À2ðGóHð 	Hð €Mr"   c                 ó°   • [        U SU5      nU(       d  [        SU S35      eU R                  R                  US   US   5        U R	                  S5        g)a8  Clicks the 3 dot menu at the right of a car's own row on the real
Finance > Funding page (rightdrive.app.dealerkit.uk/finance/funding),
found by walking up from the row's own reg cell rather than a fixed
column offset, since the grid is horizontally scrollable and the
kebab's own screen x position is not fixed.am  (reg) => {
            const cell = Array.from(document.querySelectorAll('*')).find(e =>
                e.children.length === 0 && (e.textContent||'').replace(/\s+/g, '') === reg.replace(/\s+/g, ''));
            if (!cell) return null;
            let node = cell, row = null;
            for (let i = 0; i < 8 && node; i++) {
                if (node.querySelector && node.querySelector('[class*=more_vert], button')) { row = node; }
                node = node.parentElement;
            }
            if (!row) return null;
            const btn = Array.from(row.querySelectorAll('button')).find(b => (b.textContent||'').includes('more_vert'))
                || row.querySelector('button');
            if (!btn) return null;
            const r = btn.getBoundingClientRect();
            return (r.width > 0) ? {x: r.x + r.width/2, y: r.y + r.height/2} : null;
        }zDealerKit: the row menu for z# was not found on the Funding page.r   r   rš   Nrë  )r   r²   rÀ  s      r   Ú_open_funding_row_kebabr   �  s`   € ô Øð	ð  	ó%€Eö( ÜÐ9¸#¸Ð>aÐbÓcÐcØ‡J�J×Ñ�U˜3‘Z  s¡Ô,Ø×Ñ˜#Õr"   c                 ó>  ^• [        X5        0 mU4S jnU R                  SU5         [        U SSS9(       d  [        SU S35      e[        R                  " 5       S-   nST;  aK  [        R                  " 5       U:  a2  U R                  S	5        ST;  a  [        R                  " 5       U:  a  M2  U R                  SU5        TR                  S5      nU(       d  [        S
U S35      eSUR                  R                  5       ;  a  [        SU SUR                  < 35      eU R                  S5        [        U S5      nU(       d  [        SU S35      eU R                  R                  US   US   5        U R                  S5        [        XS5        U(       d  gU R                  S5      nU(       d  [        SU S35      eU R                  R                  US   US   5        U R                  S5        [        X5      n	U	(       aK  U R                   R"                  R                  [$         SU	 S3SS9R'                  5       R                  S5      OSn
U
(       a  U
R                  S5      (       d  [        S U S!U
< S"35      eU
$ ! U R                  SU5        f = f)#aC  Marks an existing DealerKit funding record as settled (the loan
has genuinely been repaid), via the real Finance > Funding row's own
kebab menu, proven live 2026-08-24 with Mark walking the flow through
by hand first since there was no car both live in DealerKit and
genuinely settled on LE Capital's own side to test against safely:
kebab > Settle > a real "Are you sure you want to settle 1 funding
records?" confirmation > a Settlement Date modal (defaults to today,
a real editable date field, same calendar component as the Add to
Funding modal) > a SETTLE button that finalises it. settled_on is a
"YYYY-MM-DD" string, the real date LE Capital's own Stock History
shows for the loan, not just today's date, so a loan settled days ago
still gets recorded with the date it actually happened.

confirm=False stops right after the Settlement Date is set, before
the final SETTLE click, for safely proving the mechanism reaches the
right point without actually finalising anything, used to verify
this function against a real still-active record without incorrectly
marking it settled.

DealerKit's own kebab menu here has no Edit option at all, only
Settle and Delete, confirmed live: correcting a wrong funded value on
an existing record has to go through the caller deleting it and
add_funding_record adding it back, there is no in place edit.c                 óŽ   >• U TS'   SU R                   R                  5       ;   a  U R                  5         g U R                  5         g )NrC  Úsettle)ÚmessagerP  r:  r;  )rC  Úseens    €r   r@  Ú)settle_funding_record.<locals>._on_dialogÖ  s4   ø€ ØˆˆX‰Ø�v—~‘~×+Ñ+Ó-Ó-Ø�M‰M�Oà�N‰NÕr"   rC  ÚSettleTr$   z*DealerKit: no Settle option was found for z's funding row.rÎ  r�   zEDealerKit: no confirmation dialog appeared after clicking Settle for rƒ   r  z.DealerKit: unexpected confirmation dialog for r    r$  aM  () => {
            const els = document.querySelectorAll('.ui-datepicker__display-value');
            for (const el of els) {
                const r = el.getBoundingClientRect();
                if (r.width > 0 && r.height > 0) return {x: r.x + r.width / 2, y: r.y + r.height / 2};
            }
            return null;
        }z7DealerKit: the Settlement Date field was not found for r   r   rš   Ú
SettlementNa�  () => {
            const btns = Array.from(document.querySelectorAll('button'));
            for (const btn of btns) {
                if ((btn.textContent||'').trim().toUpperCase() !== 'SETTLE') continue;
                const r = btn.getBoundingClientRect();
                if (r.width > 0 && r.height > 0) return {x: r.x + r.width / 2, y: r.y + r.height / 2};
            }
            return null;
        }zKDealerKit: the Settlement Date modal's own SETTLE button was not found for rþ   rÙ   rò  rF  r’   ró  Úfunding_settled_atzDealerKit: settling rõ  r  )r   r†   r,   r„   r   rL   r‡   rf   r  rP  r3   r   r   r(  r.   rß   rT  rx   râ   rz   )r   r²   Ú
settled_onr?  r@  r   rC  rÚ  rü  r½   rý  r  s              @r   Úsettle_funding_recordr  ­  s�  ø€ ô0 ˜DÔ&ð €Dõð 	‡G�GˆH�jÔ!ð3Ü" 4¨¸×>ÜÐ!KÈCÈ5ÐP_Ð`ÓaÐaÜ—9’9“; Ñ#ˆØ˜dÓ"¤t§y¢y£{°XÓ'=Ø×!Ñ! #Ô&ð ˜dÓ"¤t§y¢y£{°XÕ'=ð 	×Ñ˜X zÔ2à�X‰X�hÓ€FÞÜÐbÐcfÐbgÐghÐiÓjÐjØ�v—~‘~×+Ñ+Ó-Ó-ÜÐKÈCÈ5ÐPRÐSY×SaÑSaÑRdÐeÓfÐfØ×Ñ˜#Ôô Øð	ó
€Hö ÜÐTÐUXÐTYÐYZÐ[Ó\Ð\Ø‡J�J×Ñ�X˜c‘] H¨S¡MÔ2Ø×Ñ˜#ÔÜ˜¨,Ô7æØà�m‰mð	ó
€Gö ÜÐhÐilÐhmÐmnÐoÓpÐpØ‡J�J×Ñ�W˜S‘\ 7¨3¡<Ô0Ø×Ñ˜$Ôä(¨Ó3€Jö ð �\‰\×!Ñ!×%Ñ%ÜÐ
˜/¨*¨Ð5JÐKÐUZð &ð \ß\`Ñ\`Ó\b×cfÑcfÐgwÔcxØð ö ˜Ÿ™Ð$8×9Ñ9ÜÐ1°#°Ð6RÐSYÑR\Ð\^Ð_Ó`Ð`Ø€Møðw 	×Ñ˜X zÕ2ús   ¨BJ ÊJc                 ó(  • / nSu  p# U R                   R                  R                  [         SU SU SX#-  S-    SU 3	SS9nUR	                  5       nUR                  S	5      =(       d    / nU GH  nUR                  S
5      nUR                  S5      =(       d    0 R                  S5      =(       d    0 R                  S5      n	U(       a  U	(       d  Mf  UR                  U	R                  SS5      R                  5       UR                  S5      UR                  S5      UR                  S5      =(       d    SSS =(       d    SUR                  S5      =(       d    SSS =(       d    SS.5        GM     X#-  nX%R                  SS5      :¼  a   U$ GMŒ  )aL  Every vehicle DealerKit's own Finance > Funding page currently
lists, read via its real API (found live 2026-08-24 while building the
LE Capital sync: GET /api/funding?with[]=fundingRecord, the same
request the page itself makes on load), not the page's own rendered
grid, since the grid needs scrolling to reveal more than a screenful.
Each vehicle's own registration lives at vehicle.registration.vrm, a
real stable field, more reliable than parsing it back out of
search_description (which happens to start with the reg but is not
guaranteed to). Returns a list of dicts: reg, funding_status,
funded_value, plan_starts_at ("YYYY-MM-DD" or None), funding_settled_at
("YYYY-MM-DD" or None). A vehicle with no funding_record at all (should
not happen, this list is filtered server side to funded vehicles, but
never trusted blindly) is skipped.)r   r�   zD/api/funding?with[]=fundingRecord&sort_order=created_at+DESC&offset=z&limit=z&filters={}&page=r   z
&per_page=rF  r’   Úresultsró  ÚvehicleÚregistrationÚvrmrž   r5   Úfunding_statusrô  rö  NrÎ  r	  )r²   r  rô  rö  r	  Útotalr   )rT  rx   rf   râ   rz   rÊ   r  r;   )
r   ÚoutÚoffsetÚlimitr{   rÉ   r#  r®   Úfrr  s
             r   Úread_dk_funding_listr  #  s~  € ð €CØ�M€FØ
Ø�|‰|×#Ñ#×'Ñ'ÜÐð Ø�h˜g e WÐ,?ÀÁÐRSÑ@SÐ?TÐT^Ð_dÐ^eðgàð (ð 
ˆð
 �y‰y‹{ˆØ�x‰x˜	Ó"×( bˆÜˆAØ—‘Ð'Ó(ˆBØ—E‘E˜)Ó$×*¨×/Ñ/°Ó?×EÀ2×JÑJÈ5ÓQˆCÞžSÙØ�J‰JØ—{‘{ 3¨Ó+×1Ñ1Ó3Ø"$§&¡&Ð)9Ó":Ø "§¡ ~Ó 6Ø#%§6¡6Ð*:Ó#;×#A¸rÀ3ÀBÐ"G×"OÈ4Ø')§v¡vÐ.BÓ'C×'IÀrÈ3ÈBÐ&O×&WÐSWñ÷ ñ ð 	‰ˆØ—X‘X˜g qÓ)Ó)ØØ€Jò/ r"   c                 óÀ  • 0 nSn [        U [         SU S35      nUR                  S5      =(       d    / nU(       d   U$ U H{  nUR                  S5      =(       d    0 R                  S5      =(       d    0 R                  S5      =(       d    SnU(       d  MV  US	   XR                  S
S5      R	                  5       '   M}     US-  nX#R                  SS5      :¼  a   U$ MÚ  )aq  Every real DealerKit vehicle currently on the stocklist, keyed by
normalised registration, one full paginated read (2026-08-26, built
for check_all_dealerkit_records so a bulk check reads the whole
stocklist once rather than repeating _find_vehicle_id_by_reg's own
per reg scan for every single car, which its own docstring already
warns is "not meant for a hot path").r   ú/api/stocklist?limit=25&offset=ú&with[]=vehicler  r  r  r  r5   rÈ   rž   é   r  )ræ   râ   rf   r  r;   )r   r  r  rÊ  r#  r°  r  s          r   Ú_stocklist_reg_mapr  M  sÛ   € ð €CØ€FØ
Ü�dœ~Ð.Ð.MÈfÈXÐUdÐeÓfˆØ�u‰u�YÓ×% 2ˆÞØð €Jó ˆAØ—E‘E˜)Ó$×*¨×/Ñ/°Ó?×EÀ2×JÑJÈ5ÓQ×WÐUWˆCßˆsØ45°d±G�—K‘K  RÓ(×.Ñ.Ó0Ó1ñ ð 	�"‰ˆØ—U‘U˜7 AÓ&Ó&ØØ€Jñ r"   c                   ó   • \ rS rSrSrSrg)ÚDealerkitRecordGoneie  a  One car's own DealerKit record no longer answers (removed there,
2026-09-07: SD66XWV's /api/stocklist/242 served a page instead of
JSON while the session itself was fine, and every run stopped on it
as if logged out). One car's problem, never the session's.rX   NrY   rX   r"   r   r  r  e  s   † ôBr"   r  c                 óF   • [        U[        5      =(       a    [        U 5      $ )z³Whether a DealerkitSessionExpired raised on one car's own record
is really that record gone: the session is re probed, and a live one
means the car, not the login, is the problem.)r&   rä   Úsession_alive)r   Úexcs     r   Úrecord_gone_not_sessionr"  l  s   € ô �cÔ2Ó3×K¼ÀdÓ8KÐKr"   c                   ó   • \ rS rSrSrSrg)rä   is  a   Raised when a DealerKit API call comes back HTTP 200 but with the
sign in page's own HTML instead of real JSON, DealerKit's own way of
saying the saved session has died rather than a clean 401 (found live
2026-08-26 diagnosing why a real check on KF66GJZ silently produced
nothing: _find_vehicle_id_by_reg's own bare resp.json() crashed with a
raw JSONDecodeError, which purchases_run.check_dealerkit never caught,
so nothing was ever saved and the failure was invisible). Callers
should surface this message directly rather than a stack trace.rX   NrY   rX   r"   r   rä   rä   s  s   † ôGr"   rä   c                 ó"   ^ • [        U 4S j5      $ )a‰  Whether the browser's saved DealerKit session still works for the
API, not only for the page (2026-09-07: after the 15:30 login job the
stock list page rendered its search box, so every opener took the
session as good, while every API call came back as the sign in page
and the quiet stage check and the daily run both failed). One cheap
stocklist read; False on the sign in page or any error.c                 ó   >• [        TU SS9$ )Ni˜:  r’   )ræ   )rw   r   s    €r   r  Úsession_alive.<locals>.<lambda>…  s   ø€ ¤i°°cÀ5Ò&Ir"   )Ú_session_prober§   s   `r   r   r   ~  s   ø€ ô ÔIÓJÐJr"   c                 ó‚  •  U " [          S35      n[        U[        5      (       a$  [        UR	                  S5      [
        5      (       d  gUR	                  S5      =(       d    / nU(       d  g U " [          SUS   R	                  S5       35      n[        U[        5      $ ! [         a     gf = f! [         a     gf = f)a™  The two reads every DealerKit pass makes, in order: the stocklist
(must answer with its results list) and then ONE car's own record
(/api/stocklist/{id}, must answer with JSON). 2026-09-07: a session
from the automatic login answered the list and still served the sign
in page for every car's record, so a list only probe passed and every
run died on its first car. get(url) returns the parsed JSON or raises.z/api/stocklist?limit=1&offset=0Fr  TrÙ   r   rÈ   )râ   r   r&   Údictrf   rI   )rf   rÊ  r#  Úones       r   r'  r'  ˆ  s¹   € ðÙ”>Ð"Ð"AÐBÓCˆô �qœ$×Ñ¤J¨q¯u©u°YÓ/?Ä×$FÑ$FØØ�5‰5�Ó×!˜r€DÞØðÙ”^Ð$ O°D¸±G·K±KÀÓ4EÐ3FÐGÓHˆô �cœ4Ó Ð øô ó Ùðûô ó Ùðús#   ‚B! Á.#B1 Â!
B.Â-B.Â1
B>Â=B>c                 óž   •  U " 5       n[        U[        5      =(       a    [        UR                  S5      [        5      $ ! [          a     gf = f)a  Whether a stocklist read is the real thing: a JSON object carrying
the "results" list DealerKit's stocklist always has (2026-09-07: a
signed out API can answer with a tidy JSON message rather than the
sign in page, which parsed fine and passed the first probe).Fr  )r   r&   r)  rf   rI   )ÚfetchrÊ  s     r   Ú_stocklist_answerr-  Ÿ  sF   € ð
Ù‹Gˆô �aœÓ×E¤:¨a¯e©e°IÓ.>ÄÓ#EÐEøô ó Ùðús   ‚? ¿
AÁAc                 ó  • U R                   R                  R                  XS9n[        USS5      nUR	                  [
        S5      nUS:X  a  [        SU S35      e UR                  5       $ ! [         a    [        SU S	U S
35      Sef = f)a$  A GET via the browser's own authenticated session, raising
DealerkitSessionExpired with a clear, actionable message instead of a
raw JSONDecodeError when the response is not really JSON (the session
has died and DealerKit quietly served its own sign in page instead,
still with a 200 status).r’   ru   Nr5   é”  zDealerKit has no record at z any more (HTTP 404).z`DealerKit's saved login has expired (the API returned its sign in page instead of real data for z, HTTP zn). Log in again: python3 login.py dealerkit manual, or Settings > Integrations on the cockpit, then try again.)
rT  rx   rf   Úgetattrr  râ   r  rz   r   rä   )r   rw   r   r{   ru   Úshorts         r   ræ   ræ   «  sª   € ð �<‰<×Ñ×#Ñ# CÐ#Ð9€DÜ�T˜8 TÓ*€FØ�K‰Kœ¨Ó+€EØ�ƒ}Ü!Ð$?À¸wÐF[Ð"\Ó]Ð]ðØ�y‰y‹{ÐøÜó Ü%ð(Ø(- w¨g°f°Xð >IðIó
ð ð		ðús   ÁA- Á-Bc                 ó   • U (       d  gS H_  nU R                  U5      n[        U[        [        45      (       a  [        U5      s  $ [        U[        5      (       d  MS   [        U5      s  $    g! [
         a     Mq  f = f)a=  The money on a DealerKit expense item, tried against the plausible
field names rather than one guessed key, None when none carries a
number (2026-08-27, needed for the purchase price value comparison:
the exact key has never been pinned down in a live session, and a
presence tick was silently passing a WRONG price as green). Safe for
the Chassis line specifically because it is N No VAT, so net and
gross cannot disagree whichever of these the API actually uses. The
first real check against a live record should confirm which key fires
and this can then be pinned to it.N)rN   Úunit_netÚnetrq  r  )rf   r&   rH   rF   r'   r  )r—  r  r°  s      r   r¡  r¡  À  so   € ö ØÛ>ˆØ�H‰H�S‹MˆÜ�aœ#œu˜×&Ñ&Ü˜“8ŠOÜ�aœ×ÓðÜ˜Q“x’ñ ?ð øô ó Úðús   Á
A/Á/
A=Á<A=c                 ó  • [        U [        5      (       d  g[        U 5      nUcÉ  U R                  S5      =(       d    0 R                  S5      =(       d    / n[        S   R                  5       nU Hz  n[        U[        5      (       d  M  [        UR                  S5      =(       d    UR                  S5      =(       d    S5      R                  5       nX5;   d  Mj  [        U5      nUc  Mz    O   Uc  S$ [        [        U5      5      $ )aD  The pounds DealerKit holds as the car's purchase price (2026-09-10):
the amount on the purchase_cost object itself when it carries one, else
the Chassis line inside its expense items (the purchase price on
DealerKit IS the Chassis line, see set_purchase_price). None when the
record has neither, never a guess. Whole pounds.NrH  rœ  rÜ  Údescriptionr�  r5   )	r&   r)  r¡  rf   rm  rP  r'   rH   r  )rÚ   rq  rœ  rÜ  rJ  Údescs         r   Úpurchase_cost_figurer8  Ø  sÕ   € ô �m¤T×*Ñ*ØÜ! -Ó0€FØ�~Ø×#Ñ# IÓ.×4°"×9Ñ9¸'ÓB×IÀrˆÜ)¨)Ñ4×:Ñ:Ó<ˆÛˆBÜ˜b¤$×'Ñ'ÙÜ�r—v‘v˜mÓ,×D°·±°v³×DÀ"ÓE×KÑKÓMˆDØ�Ü-¨bÓ1�ØÓ%Ùñ ð ‘>ˆ4Ð9¤s¬5°«=Ó'9Ð9r"   c                 óš  • U (       d
  [        5       $ U R                  S5      =(       d    0 R                  S5      =(       d    / n[        5       nU Hw  nUR                  S5      =(       d    SR                  5       n[        R	                  5        H4  u  pVUS:w  d  M  UR                  5       U;   d  M#  UR                  U5        M6     My     U$ )aÈ  Which of the itemised expense categories genuinely appear on this
car's own purchase expense right now (2026-08-26, completing check_
dealerkit_record's own auction_fee/delivery/indemnity fields now that
a real logged in session finally showed the actual shape: purchase_
cost.expense.items, each carrying a plain description). Matched by
that description TEXT against the exact label strings _EXPENSE_
CATEGORY_LABEL already uses to ADD these same lines elsewhere in this
file, never by expense_category_id (only ever observed live on one
real vehicle, KF66GJZ, not confidently generalised to every id on the
account), so this reuses the one mapping already audited and proven
correct rather than a fresh guess.rH  rœ  r6  r5   rÜ  )rˆ  rf   rP  rm  rœ  r‰  )rÚ   rœ  ÚpresentrJ  r7  r  rm   s          r   Ú_expense_categories_presentr;  ï  s�   € ö Ü‹uˆØ×Ñ 	Ó*×0¨b×5Ñ5°gÓ>×EÀ2€EÜ‹e€GÛˆØ—‘�}Ó%×+¨×2Ñ2Ó4ˆÜ1×7Ñ7Ö9‰JˆCØ�iÕ E§K¡K£M°TÕ$9Ø—‘˜CÖ ó :ñ ð
 €Nr"   c                 ó$  • [        5       nU  H  nUR                  S5      =(       d    SS-   UR                  S5      =(       d    S-   R                  5       nSU;   a  UR                  S5        SU;   d  SU;   d  Mn  UR                  S5        M�     U$ )a@  Which of Service History / V5 genuinely appear among this car's own
DealerKit documents right now (2026-08-26, Mark on AY66WFW: "showing
missing service and v5, however whe i checked the record in DK both
are present"). Matched two ways, both real signals, never a guess:
the document's own "description" field, DealerKit's real per-document
label, but only ever set when a person has actually picked it from
DealerKit's own dropdown (confirmed live on HG17NRF: its two hand
added documents read "Service History"/"V5 Front"/"V5 Internal", every
BidBrain pushed one alongside them, including literal duplicates of
the same two files, reads the default "Other Document Type", the
labelling step flagged unbuilt on 2026-08-24 is still unbuilt); and
the document's own filename, since BidBrain's own uploads (this file,
motorway.py, carwow.py) keep the real source name, docs-service-
history-.../v5-..., for anything pushed since the 2026-08-24 filename
fix in this file. A document pushed before that fix kept a random
bidbrain-dk-N-... name and carries neither signal, so it can still
legitimately be a genuine Service History or V5 scan that this cannot
identify (confirmed live: AY66WFW's own 2 V5 photos, pushed
2026-08-23, are exactly this case). This only ever adds a category
in from a positive match, never rules one out from an absence.r>  r5   rž   r6  ÚserviceÚv5Úlogbook)rˆ  rf   rP  r‰  )Údocsr:  r=  r)   s       r   Ú_document_types_presentrA    s~   € ô* ‹e€GÛˆØ—‘�zÓ"×( b¨CÑ/°1·5±5¸Ó3G×3MÈ2ÑN×UÑUÓWˆØ˜ÓØ�K‰K˜	Ô"Ø�4‹<˜9¨Õ,Ø�K‰K˜Öñ ð €Nr"   c                 óÚ  ^^• Uc  [        XU5      nU(       d  g[        XS5      n[        U [         SU S35      nUR	                  S5      =(       d    / n[        U5      m[        U5      mUR	                  S5      nUR	                  S5      nUR	                  S5      S	:H  n	[        U5      n
USLnUU4S
 jn0 SU_SU_ST_SU_S[        U5      _SU_SU	_SUR	                  S5      SL_SUR	                  S5      _S[        U5      _SUR	                  S5      _SUR	                  S5      _SU(       a  SU
;   OS_SU(       a  SU
;   OS_SU(       a  SU
;   OS_SU" S5      _SU" S5      _$ )a  What is REALLY on this car's DealerKit record right now (2026-08-26,
Mark: "add a check DK record for whats been added or present on the
record"), independent of whatever BidBrain's own dk_*_pushed_at flags
say was attempted: a push can succeed here but the record can still be
edited or have something deleted in DealerKit afterward, or a step can
partially fail in a way the push itself did not catch. Pure reads via
the browser's own authenticated API session (_server_count for images,
the same primitive _upload_via_edit_tile already trusts over any
client side "N attached" label, plus the vehicle's own core record for
its purchase price, retail price, expense line items, documents and
life cycle stage), no page navigation or clicking needed, so this is
fast even checking many cars in one pass. vehicle_id can be passed in
already resolved (see _stocklist_reg_map and vehicle_id_from_map) to
skip the reg lookup on a bulk check; looked up fresh otherwise, in
which case alt_regs carries any other plate the car is known by (the
plate underneath a kept private plate, see find_vehicle_by_plates).
Returns None when the car has no DealerKit record under ANY of its
plates yet. Raises DealerkitSessionExpired if the session has died.

service/v5 can only ever be confirmed True (a real filename or label
match, see _document_types_present) or, with no documents at all,
confirmed False; with documents present but no match either way they
stay None, since an older or foreign document can still genuinely be
one without this being able to tell.Nr£  rÙ   z?with[]=documentsr¤  rÚ   r  Úlife_cycle_statusé   c                 ó&   >• U T;   a  gTS:X  a  S$ S $ )NTr   FrX   )r  Ú	doc_typesr¤  s    €€r   Ú
_doc_stateÚ*check_dealerkit_record.<locals>._doc_stateW  s   ø€ Ø�)ÓØØ! Q›ˆuÐ0¨DÐ0r"   r½   Úpurchase_cost_valueÚdue_inrç  r"  ÚstageÚdisposed_onÚauction_feerÝ  FÚdeliveryrß  Ú	indemnityr  r=  r>  )
rß   r“  ræ   râ   rf   rQ  rA  r;  r¡  Ústage_label)r   r²   r½   Úalt_regsr£  rÉ   r@  rÚ   r  rJ  ÚcatsÚhas_expenserG  rF  r¤  s                @@r   Úcheck_dealerkit_recordrT  &  sí  ù€ ð2 ÑÜ,¨T¸ÓAˆ
ÞØÜ˜4¨XÓ6€FÜØ”Ð  °
¨|Ð;LÐMóO€Dà�8‰8�KÓ ×& B€DÜ�D“	€IÜ'¨Ó-€IØ—H‘H˜_Ó-€MØ—8‘8˜NÓ+€Lð �X‰XÐ)Ó*¨aÑ/€FÜ& }Ó5€Dð   tÐ+€Kö1ð
"Ø�jð"à�&ð"ð 	�Yð"ð 	˜ð	"ð 	Ô3°MÓBð"ð 	˜ð"ð 	�&ð"ð( 	�t—x‘x Ó.°dÐ:ð)"ð0 	˜TŸX™XÐ&9Ó:ð1"ð2 	”˜TÓ"ð3"ð4 	�t—x‘x Ó.ð5"ð8 	�t—x‘x Ó.ð9"ð: 	¶[Ð(¨DÒ0Àeð;"ð< 	¶;Ð&¨$Ò.ÀEð="ð> 	¶+�o¨Ò-À5ð?"ð@ 	‘:˜iÓ(ðA"ðB 	‰j˜ÓðC"ð "r"   úDue InúIn StockÚSold)rD  rÎ  r  rD  rÎ  é   r  éd   )rU  rV  zAwaiting DeliveryrW  z	Non-Stockc                 óð   •  U b(  [        U 5      R                  5       S:w  a  [        U 5      $ [
        R                  [        U=(       d    S5      R                  5       5      $ ! [        [        4 a     NGf = f)zƒDealerKit's life_cycle_status as a number from either the number
itself or the stage's words; None when neither says. Pure, tested.r5   )r'   rg   rH   Ú	TypeErrorr  ÚSTAGE_NUMBERSrf   )ru   rK  s     r   Ústage_numberr]  ‡  si   € ðØÑ¤# f£+×"3Ñ"3Ó"5¸Ó";Ü�v“;Ðô ×ÑœS §¨"Ó-×3Ñ3Ó5Ó6Ð6øô ”zÐ"ó Ùðús   ‚*A" Á"A5Á4A5c                 ó  • [        SU 05      (       d  g [        [        [        U5      5      5      n Ub  [        [        [        U5      5      5      OSnXC:X  a  S$ U$ ! [        [
        4 a     gf = f! [        [
        4 a    Sn N3f = f)a  Whether DealerKit's price should come here: only on a stage where
the price is DealerKit's alone (Ordered, Sold), only when DealerKit
gives one, and only when it differs from the figure shown here.
Returns the whole pound figure to set, or None. Pure, tested.rC  N)Úprice_lockedrH   r  rF   r[  r  )ru   ÚhereÚthererW  r7   s        r   Úlocked_price_to_followrb  ’  s“   € ô
 Ð,¨fÐ5×6Ñ6ØðÜ””e˜E“lÓ#Ó$ˆðØ'+Ñ'7ŒC””e˜D“kÓ"Ô#¸Tˆð “6ˆ4Ð ˜qÐ øô ”zÐ"ó Ùðûô ”zÐ"ó ØŠðús"   •A ³"A4 ÁA1Á0A1Á4B	ÂB	c                 ó¾   • U =(       d    0 R                  S5      nU[        ;   a	  [        U   $ U =(       d    0 R                  S5      =(       d    Ub  [        U5      $ S$ )znDealerKit's stage as words: Due In (5), In Stock (10), Sold (25),
else DealerKit's own label, else the number.rC  Úlife_cycle_status_labelN)rf   ÚSTAGE_LABELSr'   )rÉ   ru   s     r   rP  rP  ¤  sV   € ð �j�b×ÑÐ1Ó2€FØ”ÓÜ˜FÑ#Ð#Ø�J�B×ÑÐ5Ó6×gÈ&ÑJ\¼3¸v»;ÐgÐbfÐgr"   )rX  r  )rD  rÎ  rX  r  c                 ó„   •  [        U =(       d    0 R                  S5      5      [        ;   $ ! [        [        4 a     gf = f)z`Whether this car's price is ours to keep in step at all, from its
record or the stored dk_check.rC  F)rH   rf   ÚPRICE_SYNC_STATUSESr[  r  ©rÉ   s    r   Úprice_syncableri  ¿  s?   € ðÜ�D—J˜B×#Ñ#Ð$7Ó8Ó9Ô=PÑPÐPøÜ”zÐ"ó Ùðúó   ‚), ¬?¾?c                 ó„   •  [        U =(       d    0 R                  S5      5      [        ;   $ ! [        [        4 a     gf = f)zÉWhether a car's price is DealerKit's alone, from its record or the
stored dk_check (either carries life_cycle_status). Unknown reads as
not locked so a Due In car is never held back by a missing field.rC  F)rH   rf   ÚPRICE_LOCKED_STATUSESr[  r  rh  s    r   r_  r_  È  s?   € ðÜ�D—J˜B×#Ñ#Ð$7Ó8Ó9Ô=RÑRÐRøÜ”zÐ"ó Ùðúrj  )Ú
sale_priceÚselling_priceÚ
sold_priceÚ
price_soldÚsale_amountÚsold_forÚ
deal_priceÚinvoice_total)ÚsaleÚdealr  ÚsoldÚorderÚsale_detailsÚdeal_details)
rm  rn  r  rq  rÒ   rN   r4  ÚgrossÚ
cash_priceÚtotal_pricec                 ó  • [        U [        5      (       a  g[        U [        [        45      (       a  U S:”  a  [        [	        U 5      5      $ S$ [        U [
        5      (       aX  U R                  SS5      R                  SS5      R                  5       n [        U5      nUS:”  a  [        [	        U5      5      $ S$ [        U [        5      (       a.  [         H$  n[        U R                  U5      5      nUc  M"  Us  $    g! [         a     gf = f)zJA price as a whole number of pounds, or None for anything that is not one.Nr   ô   Â£r5   rÕ  )r&   r�  rH   rF   r  r'   r  rg   r  r)  ÚSALE_FIGURE_KEYSÚ_figurerf   )r   rW  r°  rª  r…  s        r   r�  r�  Ü  sä   € ä�!”T×ÑØÜ�!”cœ5�\×"Ñ"Ø ! A£Œs”5˜“8‹}Ð/¨4Ð/Ü�!”S×ÑØ�I‰I�d˜BÓ×'Ñ'¨¨RÓ0×6Ñ6Ó8ˆð	Ü�a“ˆAð !" A£Œs”5˜“8‹}Ð/¨4Ð/Ü�!”T×Ñß!ˆAÜ˜Ÿ™˜a›Ó!ˆAØ‹}Ø’ñ "ð øô ó 	Ùð	ús   ÂC? Ã?
DÄDc                 ó6  ^^• U =(       d    0 n/ mU4S jn[          H+  nX1;   d  M
  U" X1U   5        [        X   5      nUc  M'  UT4s  $    [         HJ  n[        UR	                  U5      [
        5      (       d  M)  U" X1U   5        [        X   5      nUc  MF  UT4s  $    UR                  5        Hd  u  p5[        U5      R                  5       m[        U4S jS 5       5      (       d  M:  U[         ;  d  MF  U[        ;  d  MR  U" [        U5      U5        Mf     ST4$ )aÏ  What a sold car went for, as its DealerKit record says, and the keys
that were looked at: (price or None, ["sale_price=10995", ...]).
A plain figure under one of the known names wins; failing that a
figure inside an object named for the sale; failing that, any key
whose name mentions the sale carrying a figure, so the report says
what the record had even when nothing was recognised. Never a VIN,
never a name: the keys reported are prices and their names only.c                 óœ  >• [        U5      nUb  TR                  U  SU 35        g [        U[        5      (       aH  TR                  U  SSR	                  [        S UR                  5        5       5      S S 5       S35        g UbF  [        U[        [        45      (       d*  TR                  U  S[        U5      R                   35        g g g )NÚ=z={rÕ  c              3   ó8   #   • U  H  n[        U5      v •  M     g 7fr«   ©r'   r©  s     r   r¯   Ú2sale_price_figure.<locals>.note.<locals>.<genexpr>   s   é € Ð3OÂJ¸q´C¸·F°FÂJùó   ‚r*  Ú}Ú:)
r�  rÊ   r&   r)  r¢   rù  ÚkeysrI   r•   rZ   )r¡   Úvalr…  r  s      €r   ÚnoteÚsale_price_figure.<locals>.noteû  s±   ø€ Ü�C‹LˆØ‰=Ø�K‰K˜4˜&  ! ˜Õ&Ü˜œT×"Ñ"Ø�K‰K˜4˜&  C§H¡H¬VÑ3OÀCÇHÁHÄJÓ3OÓ-OÐPSÐQSÐ-TÓ$UÐ#VÐVXÐYÕZØ‰_¤Z°´d¼D°\×%BÑ%BØ�K‰K˜4˜& ¤$ s£)×"4Ñ"4Ð!5Ð6Õ7ð &Cˆ_r"   Nc              3   ó,   >#   • U  H	  oT;   v •  M     g 7fr«   rX   )r­   r6   Úkls     €r   r¯   Ú$sale_price_figure.<locals>.<genexpr>  s   øé € ÐDÒ C˜1�BŽwÒ Cùs   ƒ)ru  rw  rv  r  )
ÚSALE_PRICE_KEYSr�  ÚSALE_OBJECT_KEYSr&   rf   r)  rœ  r'   rP  rU  )rý  r=  r�  rª  r…  r°  r�  r  s         @@r   Úsale_price_figurer”  ñ  sñ   ù€ ð 	��"€AØ€Dõ8÷ ˆØ�6Ù��a‘DŒMÜ˜™“ˆAØ‹}Ø˜$�w’ñ ÷ ˆÜ�a—e‘e˜A“h¤×%Ó%Ù��a‘DŒMÜ˜™“ˆAØ‹}Ø˜$�w’ñ ð —‘–	‰ˆÜ�‹V�\‰\‹^ˆÜÔDÑ CÓD×DÓDÈÔRaÕIaÐfgÔoÕfÙ”�Q“˜ŽOñ ð �ˆ:Ðr"   c           	      óÌ  • / SpC[        U5       HÑ  n[         SU S3nUb  US[        U5       3-  n[        X5      nUR	                  S5      =(       d    / nU(       d    U$ U HT  n	U	R	                  S5      n
Ub  U
SSU[        U5      4;  a  M,  U	R	                  S	5      c  M@  UR                  U	S	   5        MV     US
-  nXGR	                  S5      =(       d    S:¼  d  MÐ    U$    U$ )a<  Every vehicle id on DealerKit's stocklist, 25 a page, filtered to one
life cycle stage when the stocklist honours the same query its own
Vehicles page sends (instance.life_cycle_status). Rows that carry a
stage of their own and do not match are dropped either way, so an
ignored filter costs pages, never wrong cars.r   r  r  Nz&instance.life_cycle_status=r  rC  r5   rÈ   r  r  )r   râ   rH   ræ   rf   r'   rÊ   )r   ru   Ú	max_pagesr  r  rò   rw   rÊ  r#  r°  Ústs              r   Úlist_stocklist_idsr˜    s÷   € ð �aˆÜ�9ÖˆÜÐ Ð ?À¸xÀÐWˆØÑØÐ1´#°f³+°Ð?Ñ?ˆCÜ�dÓ ˆØ�u‰u�YÓ×% 2ˆÞØð €Jó ˆAØ—‘Ð*Ó+ˆBØÑ! b°°r¸6Ä3ÀvÃ;Ð0OÓ&OÙØ�u‰u�T‹{Ó&Ø—
‘
˜1˜T™7Ö#ñ ð 	�"‰ˆØ—e‘e˜G“n×)¨Õ*ØØ€Jñ# ð" €Jr"   )Ú
arrived_onÚarrival_dateÚin_stock_onÚin_stock_atÚ
stock_dateÚdate_in_stockr"  Údelivered_onÚfirst_advertised_onÚfirst_photographed_onÚ
created_at)	Úsold_onÚsold_atÚ	sold_dateÚ	date_soldÚ	sale_daterL  Údisposal_dateÚinvoice_dateÚ
updated_at)Úcover_imageÚ
main_imageÚprimary_imageÚimager£  rÂ  ÚmediaÚgalleryc                 óú  ^• U4S jm/ nX R                  S5      U R                  S5      [        U R                  S5      [        5      (       a)  U R                  S5      =(       d    0 R                  S5      OS4 H~  n[        U[        5      (       d  M  [        U5      U;  d  M+  UR	                  [        U5      5        [
         H/  nUS:X  a  M  T" UR                  U5      5      nU(       d  M+  Us  s  $    M€     g)zóThe first photo url on a DealerKit record, wherever it keeps it:
cover_image / main_image / primary_image / image as a url or {url},
the first of images / photos / gallery, the same again under media
and under vehicle. None when there is none.c                 ó¼  >• [        U [        5      (       a  U R                  S5      (       a  U $ S $ [        U [        5      (       ap  U R	                  S5      =(       d)    U R	                  S5      =(       d    U R	                  S5      n[        U[        5      (       a  UR                  S5      (       a  U$ S $ [        U [
        5      (       a  U (       a  T" U S   5      $ g )NÚhttprw   ÚsrcÚhrefr   )r&   r'   r•  r)  rf   rI   )r   Úurª  s     €r   rª  Úphoto_url.<locals>.firstH  s¢   ø€ Ü�aœ×ÑØŸ™ V×,Ñ,�1Ð6°$Ð6Ü�aœ×ÑØ—‘�e“×= §¡ e£×=°·±°f³ˆAÜ" 1¤c×*Ñ*¨q¯|©|¸F×/CÑ/C�1ÐMÈÐMÜ�aœ×Ñ¦1Ù˜˜1™“;ÐØr"   r¯  r  N)rf   r&   r)  rÈ   rÊ   Ú
IMAGE_KEYS)rý  r  r=  rª  r¶  rª  s        @r   Ú	photo_urlr¹  C  sï   ø€ õ
ð €DØ—j‘j Ó)¨6¯:©:°iÓ+@Ôpzð  |B÷  |Fñ  |Fð  GPó  |Qô  SW÷  qXñ  qXÀ6Ç:Á:ÈiÓCX×C^Ð\^×BcÑBcÐdkÔBlð  ^bó  cˆÜ�aœ×Ó¤2 a£5°Õ#4Ø�K‰Kœ˜1›Ôß�Ø˜“<ÙÙ˜!Ÿ%™% ›(“O�ß�1Ø”Hó  ñ cð r"   c                 ó,  • [        U [        5      (       ab  U R                  S5      =(       dJ    U R                  S5      =(       d2    U R                  S5      =(       d    U R                  S5      =(       d    Sn U c  S$ [        U 5      R	                  5       $ )Nr�  rm   rN   r[  r5   )r&   r)  rf   r'   rg   rŽ  s    r   Ú
_name_textr»  ^  sb   € Ü�!”T×ÑØ�E‰E�&‹M×U˜QŸU™U 7›^×U¨q¯u©u°W«~×UÀÇÁÀwÃ×UÐSUˆØ‘ˆ2Ð.¤ A£§¡£Ð.r"   c                 ól   •  [        [        U 5      5      nUS:¼  a  U$ S $ ! [        [        4 a     g f = f)Nr   )rH   rF   r[  r  )r   Úns     r   Ú_int_or_noner¾  d  s@   € ðÜ”�a“‹Mˆð �Q“ˆ1Ð ˜DÐ øô ”zÐ"ó Ùðús   ‚   3²3c                 ó²   • [        U [        5      (       a  [        U 5      S:  a  gU SS nUS   S:X  a#  US   S:X  a  USS R                  5       (       a  U$ S$ )z;YYYY-MM-DD out of an ISO stamp or a date string, else None.rÎ  Nr  rÏ  é   )r&   r'   rQ  Úisdigit)r   r=  s     r   Ú_dayrÂ  l  sV   € ä�aœ×Ñ¤ Q£¨"£ØØ	ˆ#ˆ2ˆ€AØ�!‘˜“  !¡¨£°°"°1°·±·±ˆ1ÐIÀTÐIr"   c                 ó˜  ^^• U=(       d    0 m [        TR                  S5      5      nU[        S   :w  a  gTR                  S5      =(       d    0 nUR                  S5      n[        U[        5      (       a  UR                  S5      OU=(       d2    TR                  S5      =(       d    TR                  S5      =(       d    SnSR                  [        U5      R                  5       R                  5       5      n[        UR                  S5      =(       d)    UR                  S	5      =(       d    TR                  S5      5      n[        UR                  S
5      =(       d    TR                  S
5      5      n[        UR                  S5      =(       d    UR                  S5      5      nUR                  S5      =(       dG    UR                  S5      =(       d/    [        UR                  S5      5      =(       d    SSS =(       d    Sn	SR                  S X–R                  5       (       a  UR                  5       OUU=(       d'    UR                  5       (       a  UR                  5       OU4 5       5      n
UR                  S5      =(       d    UR                  S5      n U(       a  [        [!        U5      S-  5      S-  S OSn[        UR                  S5      =(       d    UR                  S5      5      =(       d    Sn[        UR                  S5      =(       d)    UR                  S5      =(       d    UR                  S5      5      =(       d    SnSR                  S XÍU4 5       5      =(       d    Sn[#        T5      n[%        U4S j[&         5       S5      n[%        U4S  j[(         5       S5      n[+        T5      u  nnTR-                  5        VVs/ s Hw  u  nn[        U5      (       d  M  S![        U5      ;   d@  [        U5      R/                  S"5      (       d!  [        U5      R/                  S#5      (       d  Mg  U S$[        U5       3PMy     nnnTR-                  5        V^Vs/ s H<  u  mn[1        U4S% jS& 5       5      (       d  M"  T S'[3        U5      R4                   3PM>     nnn[        U[        5      (       a_  UUR-                  5        V^Vs/ s H=  u  mn[1        U4S( jS& 5       5      (       d  M"  S)T S'[3        U5      R4                   3PM?     snn-  n0 S*S+U  3_S,U_S-U
_SUR                  5       (       a  UR                  5       OU_S
UR                  5       (       a  UR                  5       OU_S.U_S/U_S0[7        UR                  S05      5      _S1S2 " [        UR                  S15      =(       d    UR                  S35      5      5      =(       d    S_S4U_S5U_S6U_S7U_S8S9_S:S;_S<UU-   U-   SS= _S>Ub  S_$ S?_$ ! [        [        4 a     gf = f! [        [        4 a    Sn GNtf = fs  snnf s  snnf s  snnf )@a  One sold car as its DealerKit record describes it, in the shape of
Dealer OS's /api/bidbrain/sales (2026-09-13): the plate, the car, its
spec and photo, the day it came in, the day it went, and what it went
for (sold_for). None when the record does not read Sold (25). Pure.rC  NrW  r  r  r  r5   ÚmakeÚmanufacturerÚmodelÚ
derivativeÚtrimrÛ  Úyear_of_manufactureÚregistration_dater  rž   c              3   óH   #   • U  H  o(       d  M  [        U5      v •  M     g 7fr«   r†  ©r­   r   s     r   r¯   Ú%sold_record_report.<locals>.<genexpr>ˆ  s*   é € ð  Xò  %R˜qó  VW“F”C˜—F�Fò  %Rùs   ‚
"�"Úengine_sizeÚ	engine_ccrY  rÎ  z.1fÚ	fuel_typeÚfuelÚtransmission_typeÚtransmissionÚgearboxu    Â· c              3   ó6   #   • U  H  o(       d  M  Uv •  M     g 7fr«   rX   rÌ  s     r   r¯   rÍ  �  s   é € Ð?Ò"9˜Q»Q—q‘qÒ"9ùr±   c              3   óš   >#   • U  H@  n[        TR                  U5      5      (       d  M$  [        TR                  U5      5      v •  MB     g 7fr«   ©rÂ  rf   ©r­   rª  r=  s     €r   r¯   rÍ  ’  s/   øé € ÐMª]¨¼dÀ1Ç5Á5ÈÃ8¿n“^”T˜!Ÿ%™% ›(—^�^ª]ùó
   ƒ#Aª!Ac              3   óš   >#   • U  H@  n[        TR                  U5      5      (       d  M$  [        TR                  U5      5      v •  MB     g 7fr«   r×  rØ  s     €r   r¯   rÍ  “  s/   øé € ÐKªL q¼DÀÇÁÀqÃ¿N“N”D˜Ÿ™˜q›—N�NªLùrÙ  r  Ú_onÚ_atr„  c              3   óZ   >#   • U  H   o[        T5      R                  5       ;   v •  M"     g 7fr«   ©r'   rP  ©r­   r6   rª  s     €r   r¯   rÍ  ™  s    øé € ÐbÒ0a¨1œ˜A›Ÿ™›Ö&Ò0aùó   ƒ(+)r®  Úphotor¯  Úpicturer°  rŠ  c              3   óZ   >#   • U  H   o[        T5      R                  5       ;   v •  M"     g 7fr«   rÞ  rß  s     €r   r¯   rÍ  œ  s    øé € ÐgÒ5f°œS ›VŸ\™\›^Ö+Ò5fùrà  zvehicle.Úapi_idzdk:r²   r�  Úspecrá  r¾   Úcolourc                 óP   • U R                  5       (       a  U R                  5       $ U $ r«   )Úisupperr[  )r"  s    r   r  Ú$sold_record_report.<locals>.<lambda>¢  s   € ¨!¯)©)¯+©+˜QŸW™W›YÐ<¸1Ð<r"   Úmanufacturer_colourÚin_sincer£  ro  ru   Úprice_sourcerv  ÚsourceÚ	dealerkitÚrecord_keysé<   r�  z"no figure recognised on the record)rH   rf   r[  r  r\  r&   r)  r¢   r'   r;   r£   r»  rÂ  rè  r[  r  rF   r¹  ÚnextÚIN_SINCE_KEYSÚSOLD_ON_KEYSr”  rœ  rn  rU  r•   rZ   r¾  )r½   rý  ru   r°  Úreg_objr²   rÄ  rÆ  rÇ  rÛ  r�  ÚccÚlitresrÑ  rÔ  rå  rá  rë  r£  rÒ   r‹  rª  rŒ  ÚdatesÚpicsr=  s                        `   @r   Úsold_record_reportrù  t  sÅ  ù€ ð
 	��"€AðÜ�Q—U‘UÐ.Ó/Ó0ˆð ”˜vÑ&Ó&ØØ	�‰ˆiÓ×˜B€AØ�e‰e�NÓ#€GÜ!+¨G´T×!:Ñ!:ˆ7�;‰;�uÔÀ×
wÈAÏEÉEÐRWËL×
wÐ\]×\aÑ\aÐbpÓ\q×
wÐuw€CØ
�'‰'”#�c“(—.‘.Ó"×(Ñ(Ó*Ó
+€CÜ�a—e‘e˜F“m×M q§u¡u¨^Ó'<×MÀÇÁÀfÃÓN€DÜ�q—u‘u˜W“~×7¨¯©¨w«Ó8€EÜ˜AŸE™E ,Ó/×@°1·5±5¸³=ÓA€JØ�5‰5�‹=×r˜AŸE™EÐ"7Ó8×r¼dÀ1Ç5Á5ÐI\ÓC]Ó>^×>dÐbdÐfhÐghÐ=i×=qÐmq€DØ�8‰8ñ  X T¿<¹<¿>¹>¨4¯:©:¬<ÈtÐU_÷  VQÐuz÷  vCñ  vC÷  vEñ  vEÐdi×doÑdoÔdqð  KPñ  %Ró  Xó  X€DØ	
�‰ˆ}Ó	×	3 §¡ {Ó!3€BðÞ9;”Eœ% ›) c™/Ó*¨RÑ/°Ñ4Àˆô �a—e‘e˜KÓ(×9¨A¯E©E°&«MÓ:×B¸d€DÜ˜Ÿ™Ð2Ó3×`°q·u±u¸^Ó7L×`ÐPQ×PUÑPUÐV_ÓP`Óa×iÐei€GØ�;‰;Ñ? 6°Ñ"9Ó?Ó?×GÀ4€DÜ�a‹L€EÜÔM­]ÓMÈtÓT€HÜÔK­LÓKÈTÓR€GÜ# AÓ&�K€Eˆ4Ø./¯g©g¬iô  Qªi¡F A s¼4À¿9ÓÈ&ÔTWÐXYÓTZÓJZÔ^aÐbcÓ^d×^mÑ^mÐns×^tÑ^tÔx{Ð|}Óx~÷  yHñ  yHð  IN÷  yOÓ�ˆs�!”D˜“I�;Ó©i€Eñ  Qð 78·g±g´iõ d²i©F¨A¨sÜÔbÑ0aÓb×bó (ˆqˆc�”4˜“9×%Ñ%Ð&Ó'±i€Dñ dä�!”T×ÑØÀ1Ç7Á7Ä9õ iÂ9¹¸¸CÜÔgÑ5fÓg×gó 5�8˜A˜3˜a¤ S£	× 2Ñ 2Ð3Ó4Á9ò iñ 	iˆð	Ø�C˜
�|Ð$ð	Ø&+¨Sð	Ø28¸$ð	Ø@FÐX\×XdÑXd×XfÑXfÈÏ
É
ÌÐlpð	à %§-¡-§/¡/�—‘”°uð	à>DÀdð	àLSÐUZð	ð 	”< §¡ iÓ 0Ó1ð		ð
 	Ò<¼jÈÏÉÈxË×IxÐ\]×\aÑ\aÐbwÓ\xÓ>yÓz÷  Cð  Cð	ð 	�Hð	ð (¨ð	ð 3?Àð	ð HPÐQWð	ð 	˜ð	ð !)¨+ð	ð 8EÀtÈeÁ|ÐVZÑGZÐ\_Ð]_ÐF`ð	ð 	˜Ñ)�ð	ð 	ð 0Tð	ð 	øôC ”zÐ"ó Ùðûô  ”zÐ"ó Ø‹ðüó QùódùóisN   �X Ê%X! ÏX:Ï.AX:Ð=X:Ñ(!Y ÒY Ó!YÓ=YØXØXØ!X7Ø6X7c                 ó>   • [        U [         SU 35      =(       d    0 $ )zµOne read of a car's DealerKit record, the whole thing (2026-09-07).
The stage check reads this once per car and takes both the stage and
the retail price from it, one call, not two.rÙ   )ræ   râ   ©r   r½   s     r   Úread_recordrü  ©  s"   € ô �TœnÐ-¨_¸Z¸LÐIÓJ×PÈbÐPr"   c                 óp   • [        X5      nUR                  S5      [        U5      UR                  S5      4$ )z›One read of a car's DealerKit record: (life_cycle_status, stage
words, delivery_on). For the automatic run's "DealerKit wins on
arrival" pass (2026-09-07).rC  r"  )rü  rf   rP  )r   r½   rÉ   s      r   Ú
read_stagerþ  °  s3   € ô �tÓ(€DØ�8‰8Ð'Ó(¬+°dÓ*;¸T¿X¹XÀmÓ=TÐTÐTr"   c                 óâ   •  U R                  S5      =(       d    0 nSSS.nUR                  S5      (       a  US   US'   UR                  S5      (       a  US   US'   U$ ! [         a    0 s $ f = f)	aK  The three headers DealerKit's own screens put on every call they
make (found 2026-09-07 in the front end's bundle: X-CSRF-TOKEN and
X-API-TOKEN from the page's own marque.ui, plus X-Requested-With), so
a call made through the same signed in browser looks exactly like one
of DealerKit's own. Empty when the page has no such tokens.z/() => (window.marque && window.marque.ui) || {}zapplication/jsonÚXMLHttpRequest)ÚAcceptzX-Requested-WithÚ	csrfTokenúX-CSRF-TOKENÚapiTokenzX-API-TOKEN)r.   r   rf   )r   r  r  s      r   Ú_page_api_headersr  ¸  s~   € ðØ�]‰]ÐLÓM×SÐQSˆð (Ð=MÑ
N€CØ	‡v�vˆk×ÑØ  ™oˆˆNÑØ	‡v�vˆj×ÑØ 
™^ˆˆMÑØ€Jøô ó ØŠ	ðús   ‚A ÁA.Á-A.c                 óv  • [        [        U5      5      n[        U 5      nSU;  a  g[         SU 3nS Hq  n [	        U R
                  R                  U5      " USU0USS9nUR                  S;   a"  [        X5      nUR                  S5      U:X  a    g	UR                  S
;  d  Mq    g   g! [         a     Mƒ  f = f)uÁ  Set the Retail Price by asking DealerKit's own API the way its
Pricing panel does when Save is pressed, instead of driving the
panel (2026-09-07, Steven: "i just want the prices to sync on both
sides as instant as possible"). The panel route failed on cars whose
Pricing panel did not show the "Â£RETAIL" box (DN18JNU, EK68WGV) and on
one where the search click landed on the Forecourt list (YB67ULM); a
plain API call has no panel to find. Returns True when the record
reads back with the new figure, False when DealerKit did not take it
(a 405, 422 or anything else), so the caller can fall back to the
panel. Never raises for a refusal; raises only when the session is
dead, like every other read here.r  FrÙ   )ÚpatchÚputr  rF  )rÉ   Úheadersr   )r�   r‚   éÌ   T)i•  r/  )rH   r  r  râ   r0  rT  rx   r   ru   rü  rf   )	r   r½   rÒ   r=   r	  rw   ry   r®   rÉ   s	            r   r  r  Ê  sÇ   € ô ”�u“Ó€FÜ Ó%€GØ˜WÓ$ØÜÐ˜O¨J¨<Ð
8€CÛ"ˆð	Ü˜Ÿ™×,Ñ,¨fÔ5°cÀÐQWÐ@XÐbiÐsxÑyˆAð �8‰8�Ó&Ü˜tÓ0ˆDØ�x‰x˜Ó'¨6Ó1ÙØ�8‰8˜:Õ%ØØñ #ð øô ó 	Úð	ús   ¸)B*Â*
B8Â7B8c                 ó2  • [         R                  " U6 nU(       d  g0 nSn [        U [         SU S35      nUR	                  S5      =(       d    / nU(       d  O©U Hm  n[         R
                  " UR	                  S5      =(       d    0 R	                  S5      =(       d    0 R	                  S5      5      nX‚;   d  M_  Xƒ;  d  Mf  US	   X8'   Mo     [        U5      [        U5      :X  a  OUS
-  nXER	                  SS5      :¼  a  OMá  U H  n	X“;   d  M
  X9   U	4s  $    g)a:  (vehicle_id, the plate it was found under) for the first of these
plates DealerKit holds, scanning the real stocklist (paginated 25 at a
time) since there is no direct reg lookup endpoint proven yet. Cheap
enough for a one off correction; not meant for a hot path. Raises
DealerkitSessionExpired (rather than a raw JSONDecodeError, found live
2026-08-26 crashing a real check with no useful message) if the session
has died.

Several plates because one car can genuinely have two (2026-09-20,
Steven's carried over item 1). A seller who keeps their private plate
leaves the car listed under that plate while it sells, and so reaches
DealerKit, under the plate underneath, so searching only the plate
BidBrain recorded read every one of those cars as having no DealerKit
record at all: no check, no chip, no transport invoice, nothing, for
good. The whole stocklist is still read only once however many plates
are given, and the search is ordered, so the recorded plate always
wins over the plate underneath when DealerKit somehow holds both.
(None, None) when none of them is there.r
   r   r  r  r  r  r  r  rÈ   r  r  )r   Ú
candidatesræ   râ   rf   ÚtidyrQ  )
r   Úregsrc  rï  r  rÊ  r#  r°  r  Úplates
             r   Úfind_vehicle_by_platesr  é  s  € ô& ×Ò Ð%€FÞØØ€EØ€FØ
Ü�dœ~Ð.Ð.MÈfÈXÐUdÐeÓfˆØ�u‰u�YÓ×% 2ˆÞØÛˆAÜ—+’+ §¡ iÓ 0× 6°B×;Ñ;¸NÓK×QÈr×VÑVÐW\Ó]Ó^ˆCØ�} Õ!1Ø˜t™W�“
ñ ô ˆu‹:œ˜V›Ó$ØØ�"‰ˆØ—U‘U˜7 AÓ&Ó&Øñ ó ˆØ�>Ø‘< Ð&Ò&ñ ð r"   c                 óP   • [        U[        5      (       a  U4n[        X/UQ76 S   $ )zÒThe DealerKit vehicle id for a car, by the plate it is recorded
under and then by any other plate it is genuinely known by (see
find_vehicle_by_plates, which does the work). alt_regs takes one plate
or several.r   )r&   r'   r  )r   r²   rQ  s      r   rß   rß     s-   € ô
 �(œC× Ñ Ø�;ˆÜ! $Ð7¨hÒ7¸Ñ:Ð:r"   c                 ó¬   • [        U[        5      (       a  U4n[        R                  " U/UQ76  H!  nU R	                  U5      (       d  M  X   U4s  $    g)a  The same lookup against an already read stocklist map (see
_stocklist_reg_map), for a bulk pass that must not rescan per car.
Ordered the same way: the recorded plate first, then the plate
underneath. Returns (vehicle_id, the plate it was found under), or
(None, None).r
   )r&   r'   r   r  rf   )Úreg_mapr²   rQ  r  s       r   Úvehicle_id_from_mapr    sS   € ô �(œC× Ñ Ø�;ˆÜ×"Ò" 3Ð2¨Õ2ˆØ�;‰;�u×ÓØ‘> 5Ð(Ò(ñ 3ð r"   c                 ó„   • U (       d  g U S   n[        R                  " U5      $ ! [         [        [        4 a    Sn N/f = f)a  The plate underneath a purchase, as something to pass as alt_regs:
the plate the car sells with when its seller kept a private plate
(db.purchases.selling_vrm). () when there is none, which is most cars.
A dict or a sqlite row, since both reach the push passes.rX   Úselling_vrmN)ÚKeyErrorÚ
IndexErrorr[  r   r  )rh   rN   s     r   rà   rà   -  sI   € ö
 ØðØ˜Ñ'ˆô ×Ò˜UÓ#Ð#øô ”j¤)Ð,ó ØŠðús   Š% ¥?¾?c                 óÖ  • [        U [         SU S35      nU=(       d    0 R                  S5      =(       d    0 nUR                  S5      UR                  S5      =(       d    0 R                  S5      4 Hr  n[        U[        [
        45      (       a  [	        U5      s  $ [        U[        5      (       d  MB  UR                  5       R                  5       (       d  Mg  [	        U5      s  $    g)z¶What DealerKit itself holds as this vehicle's mileage right now
(vehicle.mileage, confirmed live 2026-09-05 on vehicle 320 alongside
vehicle.latest_mileage_log_entry.miles), or None.rÙ   z?with[]=vehicler  r¾   Úlatest_mileage_log_entryrì   N)	ræ   râ   rf   r&   rH   rF   r'   rg   rÁ  )r   r½   rÉ   r°  rƒ  s        r   rå   rå   D  s¶   € ô �TœnÐ-¨_¸Z¸LÈÐXÓY€DØ	��×Ñ˜Ó#×) r€AØ—‘�yÓ! Q§U¡UÐ+EÓ%F×%LÈ"×$QÑ$QÐRYÓ$ZÓ\ˆÜ�dœS¤%˜L×)Ñ)Ü�t“9ÒÜ�dœC× Ó  T§Z¡Z£\×%9Ñ%9×%;Ó%;Ü�t“9Òñ	 ]ð
 r"   c           
      óð  • [        U5      nU R                  [         SU S3SSS9  U R                  S5        [	        X5      nXB:X  a  XBSS.$ [        U S	5      (       d  [        S
U S35      eU R                  S5        [        U SSSS9(       d  [        SU S35      eU R                  S5        [        U S5      nU(       d  [        SU S35      eU R                  R                  US   US   5        U R                  S5        [        U S5      nU(       d  [        SU S35      eU R                  S5      nUR                  5         UR                  [        U5      5        UR                  5         U R                  S5        U(       am  U R                  SU5        U R                  S5      nUR!                  5       (       a5  UR"                  R                  U5        UR"                  R                  5         S H{  n	[        U SSS 9n
U
(       a"  U R                  R                  U
S   U
S   5        U R                  S5        [	        X5      nX²:X  a  XBSS.s  $ U	S!:X  d  Mg  [        S"U S#U< S$U S35      e   g%)&aÂ  Write the real mileage onto a DealerKit record. DealerKit fills in
its OWN figure the moment a car is created ("Added to stock", 36000 on
the fake car this was built against, a guess by DealerKit, not a fact
anyone typed), and Steven's standing rule is that DealerKit is never
allowed to guess the mileage, so every car sent whole gets BidBrain's
own figure written over it here. The real UI path, found live
2026-09-05: Edit Vehicle > "Mileage Log" tile > ADD > "Add Mileage Log
Entry" (Date defaults to today, Mileage, Notes, an "Add service
entry?" switch left off) > SAVE. Verified through the API afterwards
(vehicle.mileage), retried once, raised loudly otherwise. Returns
{"old": ..., "new": miles}.rü   rý   rÀ   rÁ   rÂ   r   F©r×  rá  rÖ  rÿ   r   r  rØ   zMileage LogTr  r  z7DealerKit: the Mileage Log tile was not found (vehicle aO  () => {
            const els = document.querySelectorAll('.ui-modal.is-open button, .ui-modal.is-open a');
            const out = [];
            for (const el of els) {
                const t = (el.innerText || '').toUpperCase().replace(/\s+/g, '');
                if (!t.includes('ADD')) continue;
                const r = el.getBoundingClientRect();
                if (r.width > 0 && r.height > 0) out.push({x: r.x + r.width/2, y: r.y + r.height/2, area: r.width*r.height});
            }
            out.sort((a, b) => a.area - b.area);
            return out[0] || null;
        }z=DealerKit: the Mileage Log ADD button was not found (vehicle r   r   rÄ   a`  () => {
            const modals = Array.from(document.querySelectorAll('.ui-modal.is-open'));
            const dlg = modals.reverse().find(m => (m.innerText || '').includes('Mileage Log Entry')) || modals[0];
            if (!dlg) return false;
            let label = null;
            for (const el of dlg.querySelectorAll('*')) {
                if (el.children.length > 2) continue;
                if ((el.textContent || '').trim() !== 'Mileage') continue;
                const r = el.getBoundingClientRect();
                if (r.width > 0 && r.height > 0) { label = {x: r.x + r.width/2, y: r.y + r.height/2}; break; }
            }
            if (!label) return false;
            let best = null, bestDist = Infinity;
            for (const inp of dlg.querySelectorAll('input.ui-textbox__input, input[type=text], input[type=number]')) {
                const r = inp.getBoundingClientRect();
                if (r.width === 0 || r.height === 0) continue;
                const d = Math.hypot(r.x + r.width/2 - label.x, r.y + r.height/2 - label.y);
                if (d < bestDist) { bestDist = d; best = inp; }
            }
            if (!best) return false;
            document.querySelectorAll('[data-bb-mileage]').forEach(e => e.removeAttribute('data-bb-mileage'));
            best.setAttribute('data-bb-mileage', '1');
            return true;
        }z4DealerKit: the Mileage field was not found (vehicle z[data-bb-mileage]rE   aí  (note) => {
                const modals = Array.from(document.querySelectorAll('.ui-modal.is-open'));
                const dlg = modals.reverse().find(m => (m.innerText || '').includes('Mileage Log Entry')) || modals[0];
                const ta = dlg && Array.from(dlg.querySelectorAll('textarea')).find(t => t.getBoundingClientRect().width > 0);
                if (!ta) return false;
                ta.setAttribute('data-bb-mileage-note', '1');
                return true;
            }z[data-bb-mileage-note]r§  a6  () => {
                const modals = Array.from(document.querySelectorAll('.ui-modal.is-open'));
                const dlg = modals.reverse().find(m => (m.innerText || '').includes('Mileage Log Entry')) || modals[0];
                if (!dlg) return null;
                const out = [];
                for (const b of dlg.querySelectorAll('button')) {
                    const t = (b.innerText || '').toUpperCase().replace(/\s+/g, '');
                    if (t !== 'SAVE' && !t.endsWith('SAVE')) continue;
                    const r = b.getBoundingClientRect();
                    if (r.width > 0 && r.height > 0) out.push({x: r.x + r.width/2, y: r.y + r.height/2, area: r.width*r.height});
                }
                out.sort((a, b) => a.area - b.area);
                return out[0] || null;
            }r‘   r’   r   z9DealerKit: the mileage did not actually save for vehicle r½  r¾  N)rH   rá   râ   rL   rå   r@   r„   r,   r3   r   r   r   rJ   r'   rK   r.   r   rª  )r   r½   rì   r�  r×  rü  rR   rS   Únotesr°  r�   rÁ  s               r   ró  ró  R  s�  € ô �‹J€EØ‡I�I”Ð  
¨:¨,°iÐ@Ø+°Uð ñ <à×Ñ˜$ÔÜ
˜4Ó
,€CØ
ƒ|Ø°UÑ;Ð;ä˜T 6×*Ñ*ÜÐOÐPZÈ|Ð[]Ð^Ó_Ð_Ø×Ñ˜$ÔÜ˜t ]¸$ÐGZ×[ÜÐTÐU_ÐT`Ð`bÐcÓdÐdØ×Ñ˜$Ôô Øð	ó€Gö ÜÐZÐ[eÐZfÐfhÐiÓjÐjØ‡J�J×Ñ�W˜S‘\ 7¨3¡<Ô0Ø×Ñ˜$Ôô Øð	ó€Fö6 ÜÐQÐR\ÐQ]Ð]_Ð`ÓaÐaØ�L‰LÐ,Ó-€EØ	‡K�K„MØ	‡J�JŒs�5‹zÔØ	‡J�J„LØ×Ñ˜#ÔÞØ�‰ðð ô
	
ð —‘Ð5Ó6ˆØ�;‰;�=‰=Ø�K‰K×Ñ˜TÔ"Ø�K‰K×ÑÔãˆÜ"Øðð ñ!
ˆö$ Ø�J‰J×Ñ˜X c™]¨H°S©MÔ:Ø×Ñ˜dÔ#Ü˜tÓ0ˆØ‹<Ø¸Ñ>Ò>Ø�a�<ÜØKÈJÈ<ð XØ™ 	¨%¨°ð4ó5ð 5ò5 r"   c                 ó  • [        U [         SU S35      n0 SpTU=(       d    0 R                  S5      =(       d    /  GH  nUR                  S5      =(       d    0 R                  S5      =(       d    0 R                  S5      =(       d    Sn[        Xr5      (       d  M`  UnUR                  S	5      n [	        U5      nUR                  S
5      S:X  a  UR                  SU5        M¨  UR                  S5      =(       d    SR                  5       n	[        R                  5        H4  u  p«U
S:w  d  M  UR                  5       U	;   d  M#  UR                  X¨5        M6     GM     XT4$ ! [
        [        4 a    [        U5      n NÂf = f)a£  The line items DealerKit holds on this vehicle's expense from the
given supplier, keyed the way _EXPENSE_CATEGORY_LABEL keys them, each
the item's own unit_net (the figure a person types; DealerKit adds the
VAT itself). Returns (contact name as DealerKit spells it, {key:
amount}). The purchase price line is expense_category_id 2 whatever its
description says (confirmed live 2026-09-05: "FX68KAE chassis" on one,
the whole vehicle name on another); the fee lines are matched by their
description text, the same way _expense_categories_present already
does. Supplier matched case insensitively: this account spells the
contact "Carwow", the code's own default says "CarWow".rÙ   r}  NrM  rH  r  r‰   r5   r3  rÄ  r   rÜ  r6  )ræ   râ   rf   r’  rF   r[  r  r¡  Ú
setdefaultrP  rm  rœ  )r   r½   ra   rÉ   rï  Ú
name_foundrJ  r  rq  r7  r  rm   s               r   r­  r­  Û  s]  € ô ØÜÐ
˜/¨*¨ð 6Kð 	KóL€Dð ˜Dˆ:Ø�z�r×Ñ˜Ó/×5°2Ó5ˆØ—V‘V˜IÓ&×,¨"×1Ñ1°)Ó<×BÀ×GÑGÈÓW×]Ð[]ˆÜ˜W×/Ñ/ÙØˆ
Ø—‘˜
Ó#ˆð	.Ü˜6“]ˆFð �6‰6Ð'Ó(¨AÓ-Ø×Ñ˜Y¨Ô/ÙØ—‘�}Ó%×+¨×2Ñ2Ó4ˆÜ1×7Ñ7Ö9‰JˆCØ�iÕ E§K¡K£M°TÕ$9Ø× Ñ  Ö-ô :ñ 6ð" ÐÐøô œ:Ð&ó 	.Ü)¨"Ó-ŠFð	.ús   Â1E!Å!E?Å>E?c                 ót   • U S L=(       a.    US L=(       a#    [        [        U 5      [        U5      -
  5      S:  $ )Nrã  )rø  rF   )rË  rû  s     r   r¯  r¯  ÿ  s4   € Ø�tÐ×Z ¨DÐ 0×Z´S¼¸t»ÄuÈTÃ{Ñ9RÓ5SÐVZÑ5ZÐZr"   c                 ó  ^^• UR                  5        VVs0 s H  u  pEU[        U5      _M     nnnU(       d  0 $ U R                  [         SU S3SSS9  U R	                  S5        [        XU5      u  nmUc(  [        SU S	S
R                  S U 5       5       S35      e[        U4S jUR                  5        5       5      (       a9  UR                  5        VVs0 s H  u  pEUTR                  U5      USS._M     snn$ [        XU5        UR                  5        VVs0 s H)  u  pE[        TR                  U5      U5      (       a  M'  XE_M+     nnnU H»  nUT;  a  M  [        U   n	U R                  SU	5      n
U
(       d  [        SU	 SU S[        U 5       35      eU R                  R!                  U
S   U
S   5        U R	                  S5        [#        U SSS9(       d  [        SU	 SU S[        U 5       35      eU R	                  S5        M½     [%        XU5        S HÍ  n[#        U SSS9(       d  [        SU S[        U 5       35      eU R	                  S5        [        XU5      u  nm[        U4S  jUR                  5        5       5      (       a=  UR                  5        VVs0 s H  u  pEUTR                  U5      XTU;   S._M     snns  $ US!:X  d  Mº  [        S"U S#T S$U S%35      e   gs  snnf s  snnf s  snnf s  snnf )&aI  Set one or more lines on the supplier's purchase expense to exact
figures: a line already there under that category is deleted client
side and re added at the new figure in the same unsaved session (the
mechanism set_purchase_price proved live), a line not there yet is
simply added, then one Save & Approve and a read back through the API.
items is {"chassis"|"buyers_premium"|"delivery_charge"|"indemnity_fee":
amount}. A vehicle with no expense from this supplier at all is
refused with a plain sentence rather than guessed at: the price goes
on first, and that creates the expense.rü   rý   rÀ   rÁ   rÂ   rØ   Nz'DealerKit has no purchase expense from z on this car yet, so the rÓ   c              3   ó4   #   • U  H  n[         U   v •  M     g 7fr«   r¨  r©  s     r   r¯   Ú$set_expense_items.<locals>.<genexpr>  s   é € ÐCºU¸Ô0°Ö3ºUùr¬  z. cannot be set. Send the purchase price first.c              3   ó^   >#   • U  H"  u  p[        TR                  U5      U5      v •  M$     g 7fr«   ©r¯  rf   )r­   rª  r°  r­  s      €r   r¯   r$    s%   øé € Ð
Gº±°Œ?˜6Ÿ:™: a›=¨!×,Ð,ºùó   ƒ*-Fr  a‹  (label) => {
                // The Edit Expense window itself (the survey, 2026-09-16:
                // each line is a table row with its own more_vert), never
                // the page behind it.
                const opens = document.querySelectorAll('.ui-modal.is-open');
                const m = opens.length ? opens[opens.length - 1] : document.body;
                const els = m.querySelectorAll('td, tr, div, span, li');
                const matches = [];
                for (const el of els) {
                    if (el.children.length > 10) continue;
                    const t = (el.innerText || '').trim();
                    if (!t.toLowerCase().includes(label.toLowerCase())) continue;
                    const r = el.getBoundingClientRect();
                    if (r.width > 0 && r.height > 0) matches.push({el, area: r.width * r.height});
                }
                if (!matches.length) return null;
                matches.sort((a, b) => a.area - b.area);
                let node = matches[0].el;
                for (let i = 0; i < 8 && node; i++) {
                    const btns = Array.from(node.querySelectorAll('button'));
                    const k = btns.find(b => (b.innerText || '').includes('more_vert'));
                    if (k) {
                        k.scrollIntoView({block: 'center'});
                        const r = k.getBoundingClientRect();
                        if (r.width > 0 && r.height > 0)
                            return {x: r.x + r.width / 2, y: r.y + r.height / 2};
                    }
                    node = node.parentElement;
                }
                return null;
            }r¥  z( line's own menu was not found (vehicle rb  r   r   rh  rD  Tr$   z)DealerKit: Delete was not offered on the z line (vehicle r§  r~  r  rþ   c              3   ó^   >#   • U  H"  u  p[        TR                  U5      U5      v •  M$     g 7fr«   r&  )r­   rª  r°  r]  s      €r   r¯   r$  Y  s%   øé € ÐJºM±D°AŒ˜uŸy™y¨›|¨Q×/Ð/ºMùr'  r   z?DealerKit: the expense lines did not actually save for vehicle z (now r¾  r  )rœ  rF   rá   râ   rL   r­  r„   r¢   Úallrf   r¹  r¯  rm  r.   rW  r   r   r,   rv  )r   r½   rœ  ra   rª  r°  r  Útodor  rm   rÀ  r°  rò   r]  r­  s                @@r   r®  r®    s+  ù€ ð &+§[¡[¤]Ô3¢]™T˜QˆQ”�a“Š[¡]€EÑ3ÞØˆ	Ø‡I�I”Ð  
¨:¨,°iÐ@Ø+°Uð ñ <à×Ñ˜$ÔÜ*¨4¸XÓF�O€GˆVØ�ÜØ5°h°ZÐ?XØ�y‰yÑC¹UÓCÓCÐDÐDrðtóuð 	uô Ô
G¸¿¹¼Ó
G×GÑGØRW×R]ÑR]ÔR_Ô`ÒR_É$È!�˜6Ÿ:™: a›=°¸uÑEÒEÑR_Ò`Ð`ä˜4¨WÔ5ð #Ÿ[™[œ]ÔTš]‘T�Q´/À&Ç*Á*ÈQÃ-ÐQR×2S‹DˆAŠD™]€DÑTÛˆØ�fÓÙÜ'¨Ñ,ˆØ—‘ðð> óA!
ˆöD ÜØ! % Ð(PÐQ[ÐP\Ð\_Ô`mÐnrÓ`sÐ_tÐuówð wà�
‰
×Ñ˜˜s™ U¨3¡ZÔ0Ø×Ñ˜cÔ"Ü" 4¨¸×>ÜØ;¸E¸7À/ÐR\ÐQ]Ð]`ÔanÐosÓatÐ`uÐvóxð xà×Ñ˜cÖ"ñ] ô` �t :Ô.ãˆÜ" 4Ð)9À×FÜÐ!RÐS]ÐR^Ð^aÔboÐptÓbuÐavÐwÓxÐxØ×Ñ˜dÔ#Ü'¨¸(ÓC‰ˆˆ5ÜÔJ¸E¿K¹K¼MÓJ×JÑJð [`×ZeÑZeÔZgÔhÒZgÑRVÐRS�A˜vŸz™z¨!›}°QÈÁ9ÑMÒMÑZgÒhÒhØ�a�<ÜØQÐR\ÐQ]ð ^Ø�w˜i¨ w¨bð2ó3ð 3ò ùóO 4ùó aùó Uùóv is   –K4Ã"K:Ä &L Å
L Ê*$Lc           	      óp  ^ ^• [        T [         ST 35      nU=(       d    0 R                  S5      nUS:X  a  gUS:w  a0  [        SU=(       d    0 R                  S5      =(       d    U S35      eU=(       d,    [        R
                  R                  5       R                  5       nU U4S	 jnU=(       d    0 R                  S
5      (       a=  [        T TU5        [        S5       H!  nU" 5       S:X  a    gT R                  S5        M#     T R                  [         ST S3SSS9  T R                  S5        [        S5       HH  nT R                  S5      (       a    O1T R                  R                  SS5        T R                  S5        MJ     SnT R                  S[         R"                  " S[         R$                  5      S9n [        UR'                  5       5       HL  n	UR)                  U	5      n
U
R+                  5       (       d  M+  U
R-                  5         U
R/                  SS9  Sn  O    U(       d?  U" 5       S:X  a  gT R                  S5      n[        ST SUS S!  S"T R2                   S#35      eT R                  S5        [5        T S$5        [7        T US$5        T R                  S%5        S& Hs  n[9        T S'S(S9nU(       a"  T R                  R/                  US)   US*   5        T R                  S5        U" 5       S:X  a    gUS+:X  d  M]  [        S,T S-U" 5       < S.35      e   g ! [0         a    Sn GN
f = f)/a  Check a Due In car in on DealerKit, so it reads In Stock there
(life_cycle_status 5 to 10). What "checked in" means on the DealerKit
side, found live 2026-09-05: the car has arrived and is stock, no
longer expected, with the arrival date on its record. DealerKit has two
ways there and this uses whichever the record allows:

- A record with no Due In / In-stock On date (the normal case since
  v3.14.2, the due in date is no longer sent) shows a Due In banner on
  its overview with a CHECK-IN button ("Check-in / Move": Delivered On,
  Physical Location defaulting to Onsite, CHECK-IN). DealerKit's own
  code behind that button (read from its page script 2026-09-05) saves
  the chosen day as the record's delivery_on, so the button and the
  date field are the same action in DealerKit's eyes.
- A record that already carries a due in date hides that banner, and
  DealerKit moves the car to In Stock by itself the moment the date is
  today or past. So for such a record the check in is written as the
  arrival date on the Due In / In-stock On field (set_delivery_date).

date_iso is the arrival day (today when not given). Returns True when
the car moved, False when DealerKit already had it In Stock; raises a
plain sentence for any other stage (a sold car is never touched).rÙ   rC  rÎ  FrD  úDealerKit shows this car as rd  z', not Due In, so it was not checked in.c                  ó^   >• [        T [         ST 35      =(       d    0 R                  S5      $ )NrÙ   rC  )ræ   râ   rf   rû  s   €€r   Ú_statusÚ!check_in_vehicle.<locals>._statusƒ  s.   ø€ Ü˜$¤>Ð"2°/À*ÀÐ NÓO×UÐSU×ZÑZÐ[nÓoÐor"   r"  r  TrØ   rü   rý   rÀ   rÁ   rÂ   rþ   rM  z;() => /Vehicle Details/.test(document.body.innerText || '')r   rÄ   rÔ   z	check-?inrÖ   r,  r’   zå() => Array.from(document.querySelectorAll('button, a'))
                .filter(e => e.getBoundingClientRect().width > 0)
                .map(e => (e.innerText || '').replace(/\s+/g, ' ').trim()).filter(t => t && t.length < 30)z6DealerKit: the CHECK-IN button was not found (vehicle z); the page showed Né   z at rƒ   zDelivered Onrp   r§  aa  () => {
                const out = [];
                for (const b of document.querySelectorAll('.ui-modal.is-open button')) {
                    const t = (b.innerText || '').toUpperCase().replace(/\s+/g, '');
                    if (!t.includes('CHECK-IN') && !t.includes('CHECKIN')) continue;
                    const r = b.getBoundingClientRect();
                    if (r.width > 0 && r.height > 0) out.push({x: r.x + r.width/2, y: r.y + r.height/2, area: r.width*r.height});
                }
                out.sort((a, b) => a.area - b.area);
                return out[0] || null;
            }r‘   r   r   r   z:DealerKit: the check in did not actually save for vehicle z (status still r  )ræ   râ   rf   r„   r  r  Útodayr  r*  r   rL   rá   r.   r   Úwheelr   r9   ÚcompileÚIr   r   Ú
is_visibleÚscroll_into_view_if_neededr   r   rw   r“   r(  r3   )r   r½   r  rÉ   ru   r.  rò   rø   Ú
banner_btnr   r   r  r°  rŠ   s   ``            r   Úcheck_in_vehicler8  c  s  ù€ ô, �TœnÐ-¨_¸Z¸LÐIÓJ€DØ�j�b×ÑÐ1Ó2€FØ�ƒ|ØØ�ƒ{ÜØ*¨D¯J°B×+;Ñ+;Ð<UÓ+V×+`ÐZ`Ð*að b4ð 4ó5ð 	5ð ×<œ8Ÿ=™=×.Ñ.Ó0×:Ñ:Ó<€Höpð 	�
�×Ñ˜×&Ñ&Ü˜$ 
¨HÔ5Ü�q–ˆAÙ‹y˜B‹ÙØ×!Ñ! $Ö'ñ ð
 	‡I�I”Ð  
¨:¨,°iÐ@Ø+°Uð ñ <à×Ñ˜$Ôô �1ŽXˆØ�=‰=ÐV×WÑWÙØ�
‰
×Ñ˜˜DÔ!Ø×Ñ˜dÖ#ñ	 ð €GØ—‘˜h´·²¸LÌ"Ï$É$Ó1O�ÐP€Jð	Ü�z×'Ñ'Ó)Ö*ˆAØ—‘˜qÓ!ˆAØ�|‰|�~‹~Ø×,Ñ,Ô.Ø—‘ �Ñ%Ø�Ùò +ö Ù‹9˜‹?ØØ�}‰}ðoópˆô ÐSÐT^ÐS_ð `.Ø.2°3°B¨i¨[¸¸T¿X¹X¸JÀaðIó Jð 	Jà×Ñ˜$ÔÜ˜4 Ô0Ü˜˜h¨Ô7Ø×Ñ˜#ÔãˆÜØð
ð ñ
ˆö Ø�J‰J×Ñ˜S ™X s¨3¡xÔ0Ø×Ñ˜dÔ#Ù‹9˜‹?ÙØ�a�<ÜØLÈZÈLð Y!Ù!(£¡¨Rð1ó2ð 2ò- øô! ó Ø‹ðús   Ç A L% È"L% È'L% Ì%L5Ì4L5c                 ó  •  [        U [         SU 35      nU=(       d    0 R                  S5      nU=(       d    0 R                  S5      =(       d    UnUS:w  a  [	        SU S35      e[        XS	S
9  SU S3$ ! [         a	    SU S3s $ f = f)a‹  Remove a cancelled sale's record from DealerKit. Only a car still
Due In is deleted (delete_vehicle, the real kebab > Delete > confirm
path); a car DealerKit already shows as In Stock or beyond has been
handled on that side and is left alone with a plain sentence, the
closest safe thing to a delete that cannot be done. Returns a
sentence saying what happened; raises only on a genuine failure.rÙ   z<already gone from DealerKit, removed there by hand (vehicle rà  rC  rd  rD  r,  zW, not Due In, so it was left in place. Remove it in DealerKit by hand if that is right.T)r?  z/removed from DealerKit (it was Due In, vehicle )ræ   râ   r  rf   r„   ri  )r   r½   rÉ   ru   rm   s        r   Úremove_vehicle_recordr:  Õ  s¹   € ð\Ü˜¤.Ð!1°ÀÀÐMÓNˆð �j�b×ÑÐ1Ó2€FØ�Z�R×ÑÐ6Ó7×A¸6€EØ�ƒ{ÜØ*¨5¨'ð 2?ð ?ó@ð 	@ô �4¨TÒ2Ø<¸Z¸LÈÐJÐJøô ó \ð NÈjÈ\ÐYZÐ[Ò[ð	\ús   ‚A9 Á9BÂB)ÚchiprÁ  Úcheckinr‹  r	  c              #   ó<   #   • U  H  o[         ;  d  M  Uv •  M     g 7fr«   )Ú
SEND_ITEMS)r­   r8  s     r   r¯   r¯   ó  s   é € ÐIÒ!1˜A¼jÑ5H—a‘aÒ!1ùs   ‚“	c           	      ó\  • US   n[        X[        U5      5      nU(       d  USSSS.$ [        U5      nUS;   Gai  UR                  S5      nUc  XESSS.$ UR                  S	5      nS
U0n	Ub  X‰S'   Sn
U(       ae  [	        XU5      u  p¼UR                  S
5      bE  [        US
   U5      (       d1  U	R                  S
5        S[        US
   5      S S[        U5      S S3n
[        XXi5      nU
(       a  U
/O/ nS H3  nXý;   d  M
  UR                  [        U    S[        Xß   U5       35        M5     [        S UR                  5        5       5      =(       a    U
(       + nXEU[        U
5      =(       d     [        S UR                  5        5       5      SR!                  U5      S.$ US:X  ac  UR                  S5      nUc  XESSS.$ [        XUSU05      nUS   nXE[#        U5      [        UR                  S5      5      S[        UU5      -   S.$ US:X  aw  UR                  S5      =(       d    SSS =(       d    Sn[%        XU5      nU(       a  XES S!U=(       d    S" 3S.$ ['        X5      u    nnXES US#U(       a  S$USS  3-   S%.$ S-   S%.$ US&:X  a  XES [)        X5      S.$ [+        S'U< 35      e)(a   One of the four things that follow a car already on DealerKit
(FOLLOWUP_ACTIONS). Never creates a record: a car with no DealerKit
record is reported as nothing to do. Returns {"vehicle_id", "done",
"outcome"}; raises with a plain sentence when DealerKit refused or
the save did not stick.r²   NFznot on DealerKit, nothing to do)r²   r½   ÚdoneÚoutcome)r;  r	  rÐ   rÚ  rØ  rÜ  rÝ  u-   purchase price left alone, DealerKit holds Â£rk  u!    typed there and BidBrain says Â£z.; press Update the price and fee to replace it)rÜ  rÝ  rž   c              3   ó8   #   • U  H  n[        U5      v •  M     g 7fr«   )r÷  r¬   s     r   r¯   Ú push_followup.<locals>.<genexpr>   s   é € Ð:ª\¨”= ×#Ð#ª\ùrˆ  c              3   óB   #   • U  H  oR                  S 5      v •  M     g7f)r¦  NrI  r¬   s     r   r¯   rC  "  s   é € Ð)NÂ¸A¯%©%°¯-¨-Âùs   ‚rÓ   )r²   r½   r@  ÚleftrA  rÁ  rÙ  rÞ  rß  r¦  z
transport r<  Úchecked_in_atr5   rÎ  Tz7checked in on DealerKit, Due In to In Stock, delivered r1  zalready In Stock on DealerKitz, delivered )r²   r½   r@  rŸ  rA  r‹  zunknown DealerKit follow up )rß   rà   ri   rf   r­  r¯  ÚpoprF   rô  rÊ   ÚFIGURE_WORDSrõ  r)  Úvaluesr�  rU  r¢   r÷  r8  rþ  r:  r  )r   rh   ÚactionÚ	fill_onlyr²   r½   ra   rÒ   rÀ  rœ  rE  rò   rÙ  r  Úpartsr  r@  r®   ÚwhenÚmovedÚ	delivereds                        r   Úpush_followuprP  ö  s   € ð �5‰/€CÜ(¨´KÀÓ4IÓJ€JÞØ¨$¸Ø<ñ>ð 	>ä  Ó*€HàÐ$Ô$ð
 —‘˜[Ó)ˆØ‰=ØÀ%Ø]ñ_ð _à�l‰l˜?Ó+ˆØ˜EÐ"ˆØ‰?Ø&)Ð"Ñ#ØˆÞô
 .¨dÀÓI‰JˆAØ�{‰{˜9Ó%Ñ1¼/È'ÐR[ÑJ\Ð^c×:dÑ:dØ—	‘	˜)Ô$ØGÌÈgÐV_ÑN`ÓHaÐbcÐGdð e/Ü/4°U«|¸AÐ.>Ð>lðn�ä# D°hÓFˆÞ�‘ BˆÛ0ˆCØ�zØ—‘¤¨SÑ 1Ð2°!´LÀÁÈ8Ó4TÐ3UÐVÖWñ 1ô Ñ:¨S¯Z©Z¬\Ó:Ó:×GÀ4¼xˆØ¸dÜ˜T›
×N¤cÑ)NÀÇÁÄÓ)NÓ&NØŸ9™9 UÓ+ñ-ð 	-ð �ÓØ�l‰l˜?Ó+ˆØ‰;ØÀ%Ø\ñ^ð ^ä# D°hÐARÐTWÐ@XÓYˆØÐ!Ñ"ˆØ¼mÈAÓ>NÜ˜QŸU™U 6›]Ó+Ø'¬,°q¸(Ó*CÑCñEð 	Eð �ÓØ—‘˜_Ó-×3°°S°bÐ9×A¸TˆÜ  °4Ó8ˆÞØÀ$Ø!XÐY]×YhÐahÐXiÐjñlð lô % TÓ6‰ˆˆ1ˆiØ¸dÐT]Ø:ÖajÀÈYÐWZÐXZÈ^ÐL\Ð>]Ñsñuð 	uØprÑsñuð 	uð �ÓØ¸dÜ0°ÓBñDð 	Dô Ð3°F±:Ð>Ó
?Ð?r"   c           
      ó°  ^ • SnU 4S jnU" SU ST R                    35        SR                  [        U" SU-   S-   5      =(       d    S5      R                  5       5      n[	        S	[        [        U5      S
5      S5       H  nU" SXxUS-    -   5        M     U" SU-  5      n	[	        S	[        U	5      S5       H%  nU" SSR                  X˜US-    5      SS -   5        M'     U" SU-  5      n
[	        S	[        U
5      S5       H%  nU" SSR                  X¨US-    5      SS -   5        M'     U" S5      n[	        S	[        U5      S5       H%  nU" SSR                  X¸US-    5      SS -   5        M'     U(       a  U(       a  [        USUT 5        U" SU-  5      n[	        S	[        U5      S5       H%  nU" SSR                  XÈUS-    5      SS -   5        M'     g)a¾  Everything the topmost open DealerKit window shows right now, in
lines of at most 380 characters: its text, every visible button with
its class, every field label, hint and value with its tag and class,
every visible dropdown option, and every visible input. For the
survey and for nothing else. shot=True (2026-09-16, the key screens
only, to keep the file small enough for diagnostics' own tail limit)
also appends a real picture of the window.zŠ(() => { const opens = document.querySelectorAll('.ui-modal.is-open'); return opens.length ? opens[opens.length - 1] : document.body; })()c                 óŽ   >•  TR                  U 5      =(       d    / $ ! [         a  nS[        U5      S S  S3/s S nA$ S nAff = f)Nz(not read: éP   rà  )r.   r   r'   )rC  ró   r   s     €r   rD  Ú_survey_dump.<locals>.evP  sH   ø€ ð	2Ø—=‘= Ó$×*¨Ð*øÜó 	2Ø!¤# a£&¨¨" + ¨aÐ0Ð1Õ1ûð	2ús   ƒ �
A§?¹A¿Az== z  rž   rF  z).innerText || ''r5   r   rO  rP  ztext: a’  Array.from((%s).querySelectorAll('button, a.btn, a.pill-green, [role=button]'))
        .filter(b => { const r = b.getBoundingClientRect(); return r.width > 0 && r.height > 0; })
        .map(b => (b.tagName.toLowerCase() + '.' + (typeof b.className === 'string' ? b.className.split(' ').slice(0, 3).join('.') : '') + ' "' + (b.innerText || '').trim().replace(/\s+/g, ' ') + '"'))
        .slice(0, 60)r  rJ  rK  NrL  aw  Array.from((%s).querySelectorAll('.ui-select, .ui-textbox, .ui-datepicker, select, textarea, input'))
        .filter(el => { const r = el.getBoundingClientRect(); return r.width > 0 && r.height > 0; })
        .map(el => {
            const lab = el.querySelector ? (el.querySelector('.ui-select__label-text, .ui-textbox__label-text, .ui-datepicker__label-text') || {}).textContent : '';
            const disp = el.querySelector ? (el.querySelector('.ui-select__display-value, .ui-select__display') || {}).textContent : '';
            const inp = el.tagName.toLowerCase() === 'input' ? el : (el.querySelector ? el.querySelector('input') : null);
            const ph = inp ? (inp.getAttribute('placeholder') || '') : (el.getAttribute('placeholder') || '');
            const val = inp ? (inp.value || '') : (el.value || '');
            return el.tagName.toLowerCase() + '.' + (typeof el.className === 'string' ? el.className.split(' ').slice(0, 2).join('.') : '')
                + ' label="' + (lab || '').trim() + '" shows="' + (disp || '').trim() + '" hint="' + ph + '" value="' + String(val).slice(0, 30) + '"';
        }).slice(0, 60)r/  rN  aW  Array.from(document.querySelectorAll('.ui-select-option, .ui-select__options li, .ui-select-option__basic, [role=option], .dropdown-menu li, .dropdown-item'))
        .filter(el => { const r = el.getBoundingClientRect(); return r.width > 0 && r.height > 0; })
        .map(el => (el.textContent || '').trim().replace(/\s+/g, ' ')).slice(0, 80)rM  z	options: zdealerkit_survey.logaT  Array.from((%s).querySelectorAll('*'))
        .filter(el => el.children.length <= 2 && /categor/i.test((el.innerText || '') + ' ' + (el.getAttribute('placeholder') || '') + ' ' + (el.getAttribute('aria-label') || '')))
        .filter(el => { const r = el.getBoundingClientRect(); return r.width > 0 && r.height > 0; })
        .map(el => el.tagName.toLowerCase() + '.' + (typeof el.className === 'string' ? el.className.split(' ').slice(0, 3).join('.') : '') + ' "' + ((el.innerText || el.getAttribute('placeholder') || '').trim().replace(/\s+/g, ' ')).slice(0, 60) + '"')
        .slice(0, 20)zcategory words: )rw   r¢   r'   r£   r   rò  rQ  r@  )r   Úsayrm   Úshotr=  rt  rD  r)   r   r÷   rV  r˜  rR  s   `            r   Ú_survey_dumprW  G  s÷  ø€ ð [€Cõ2ñ
 ˆ#ˆeˆW�B�t—x‘x�jÐ!Ô"Ø�8‰8”C™˜3 ™9Ð':Ñ:Ó;×A¸rÓB×HÑHÓJÓK€DÜ�1”cœ#˜d›) TÓ*¨CÖ0ˆÙˆH�t˜a #™g�Ñ&Ö'ñ 1áð ð ñó  €Dô �1”c˜$“i Ö#ˆÙˆK˜%Ÿ*™* T¨A°©E ]Ó3°D°SÐ9Ñ9Ö:ñ $áð 
ð !ñ
!ó 
"€Fô �1”c˜&“k 1Ö%ˆÙˆJ˜Ÿ™ F¨Q°©U OÓ4°T°cÐ:Ñ:Ö;ñ &áð Xó Y€Dô �1”c˜$“i Ö#ˆÙˆK˜%Ÿ*™* T¨A°©E ]Ó3°D°SÐ9Ñ9Ö:ñ $æ–Ü�XÐ5°u¸dÔCÙð ð ñ	ó  €Dô
 �1”c˜$“i Ö#ˆÙÐ §¡¨D°1°q±5¨MÓ!:¸4¸CÐ!@Ñ@ÖAò $r"   c                 ó`  • [         R                  R                  [         R                  R                  [         R                  R                  [         R                  R	                  [
        5      5      5      SS5      n [        X5      nU(       d  U" U S35        g U" SU SU SU< 35        [        XU5      u  pg[        X5      u  p‰U" SU< SU< S	U S
U< SU	< 3
5        U R                  [         SU S3SSS9  U R                  S5        [        XS5        U R                  S5        [        XS5        U R                  S5      n
U
(       d	  U" S5        g U R                  R!                  U
S   U
S   5        U R                  S5        [        XS5        U R                  S5      nU(       a“  U R                  R!                  US   US   5        U R                  S5        [        XS5        [#        [%        U 5      ['        5       5      u  pÍU" SU< SU S 35        U(       a   [)        X5        U R                  S#5        OU" S$5        U R                  S%5      nU(       aÈ  U R                  R!                  US   US   5        U R                  S5        U R.                  R1                  U5        U R                  S5        [        XS&U< 35        U R                  S'U5      nU" S(U< 35        U(       a3  U R                  R!                  US   US   5        U R                  S5        OU" S)5        [        XS*5        U R                  S+5      =(       d    / nU" S,S-R                  S. U 5       5      S S/ -   5        [3        U5      nU" Ub  S0UU   S1   < 3OS25        Uc  g U R                  R!                  UU   S   UU   S   5        U R                  S5        [        XS3S4US59  U R                  S65      nU" S7U< 35        U(       aÎ  U R                  R!                  US   US   5        U R                  S85        [        XS95        [4         HC  n[7        U US:S;S<9(       d  M  U" S=U< 35        U R                  S5        [        XS>U< 35          OL   U" S?5         U R.                  R1                  S@5        U R                  SA5        [        XSB5        OU" SC5        Ubz   U R                  [         SU S3SSS9  U R                  S5        [9        XU5        [        XSDU< 3S4US59  U R                  SE5      =(       d    / nU H  nU" SFUS S/ -   5        M     U" SH5        U R                  [         SU S3SSS9  U R                  S5        [        XU5      u  nn[        X5      u  nnU" SIU SJU< SK35        g ! [*         a"  nU" S![-        U5      S S"  35         S nAGN·S nAff = f! [*         a     GN0f = f! [*         a!  nU" SG[-        U5      S S"  35         S nANÈS nAff = f)LNrÉ   rG  z( is not on DealerKit, nothing to survey.z
Survey of z, DealerKit vehicle z, supplier zAPI: expense from r    rž   z; purchase cost on record: r´  rü   rý   rÀ   rÁ   rÂ   rþ   r4  rØ   zS1 Edit Vehicle > Expensesrx  z)No + EXPENSE button found; stopping here.r   r   ry  z)S2 after + EXPENSE (the Add Expense form)rz  r$  zS3 team list openzteam chosen: r{  rà  zteam click failed: r�   rÅ   zNo 'Select team' field seen.r|  zS4 supplier list after typing a  (supplier) => {
                let els = Array.from(document.querySelectorAll('.ui-select-option, .ui-select__options li, [role=option]'));
                if (!els.some(el => { const r = el.getBoundingClientRect(); return r.width > 0 && r.height > 0; }))
                    els = Array.from(document.querySelectorAll('.ui-modal.is-open li, .ui-modal.is-open [role=option], .ui-modal.is-open div'));
                const s = supplier.toLowerCase();
                let best = null, bestLen = Infinity;
                for (const el of els) {
                    if (el.children.length > 2) continue;
                    const t = (el.textContent || '').trim();
                    if (t.toLowerCase().startsWith(s) && t.length < bestLen) {
                        const r = el.getBoundingClientRect();
                        if (r.width > 0 && r.height > 0) { best = {x: r.x + r.width / 2, y: r.y + r.height / 2, text: t}; bestLen = t.length; }
                    }
                }
                return best;
            }zsupplier option: zNo 'Select contact' field seen.zS5 form after team and suppliera>  () => {
            const btns = Array.from(document.querySelectorAll('.ui-modal.is-open button'));
            const out = [];
            for (const b of btns) {
                const t = (b.innerText || '').trim();
                if (!t.toLowerCase().includes('add')) continue;
                const r = b.getBoundingClientRect();
                if (r.width > 0 && r.height > 0) out.push({text: t, ui: b.classList.contains('ui-button'), cls: b.className, x: r.x + r.width/2, y: r.y + r.height/2, area: r.width*r.height});
            }
            return out;
        }zadd buttons: rK  c              3   ón   #   • U  H+  oS    < SUS    SUS   SS < S[        US   5       3v •  M-     g7f)	r)   z ui=r  z cls=ÚclsNé(   z area=r  )rH   )r­   r"  s     r   r¯   Ú&survey_expense_form.<locals>.<genexpr>û  sL   é € Ð$~Òx}Ðst¨&©	¡}°D¸¸4¹¸	ÀÀqÈÁxÐPSÐQSÀ}ÑFWÐW]Ô^aÐbcÐdjÑbkÓ^lÐ]mÕ%nÒx}ùs   ‚35rL  zADD chosen: r)   zno ADD chosenzS6 after the item ADDT)rV  r=  aÝ  () => {
            const opens = document.querySelectorAll('.ui-modal.is-open'); const m = opens.length ? opens[opens.length - 1] : document.body;
            for (const el of m.querySelectorAll('*')) {
                if (el.children.length > 3) continue;
                const words = (el.innerText || '') + ' ' + (el.getAttribute('placeholder') || '') + ' ' + (el.getAttribute('aria-label') || '');
                if (!/categor/i.test(words)) continue;
                const r = el.getBoundingClientRect();
                if (r.width > 0 && r.height > 0) return {x: r.x + r.width / 2, y: r.y + r.height / 2, tag: el.tagName, cls: el.className, words: words.trim().slice(0, 80)};
            }
            return null;
        }zcategory control: rk   zS7 category box openFr`  rÝ   zgroup clicked: zS8 after group z#no group text found; typing Chassisr  rh  zS8 after typing Chassiszno category words on the formzS9 Edit Expense for aŸ  () => { const opens = document.querySelectorAll('.ui-modal.is-open'); const m = opens.length ? opens[opens.length - 1] : document.body;
                    return Array.from(m.querySelectorAll('tr, .item, .row')).filter(r => /Chassis|Premium|Delivery/i.test(r.innerText || ''))
                        .filter(r => { const b = r.getBoundingClientRect(); return b.width > 0 && b.height > 0; })
                        .map(r => r.tagName.toLowerCase() + ' "' + (r.innerText || '').trim().replace(/\s+/g, ' ').slice(0, 120) + '" buttons: ' +
                            Array.from(r.querySelectorAll('button')).map(b => (b.innerText || '').trim()).join('/')).slice(0, 8); }zline: zS9 not reached: z-Survey done. Leaving the page without saving.zAfter: expense lines z, purchase cost on record z (must be unchanged))ry  r¡   r¢   rŠ  rR  rS  rß   r­  r¤  rá   râ   rL   rN  rW  r.   r   r   r€  r�  r‚  r…   r   r'   r”   r•   r&  ro  r,   r¹  )r   r²   ra   rU  r=  r½   r  rœ  rq  r£  r„  r…  rƒ  r†  ró   r‡  Úoptr!  rr  ÚcatrS  Ú	rows_infor®   rò   Úitems_afterÚamount_afters                             r   Úsurvey_expense_formrb  |  s=  € Ü�w‰w�|‰|œBŸG™GŸO™O¬B¯G©G¯O©O¼B¿G¹G¿O¹OÌHÓ<UÓ,VÓWÐY_ÐagÓh€Hðô )¨Ó3€JÞÙˆsˆeÐ;Ð<Ô=ØÙˆ*�S�EÐ-¨j¨\¸ÀXÁLÐQÔRÜ)¨$¸HÓE�N€GÜ*¨4Ó<�N€FÙÐ
˜X™L¨¨7©+°Q°u°gÐ=XÐY_ÑXbÐbiÐjpÑisÐtÔuØ‡I�I”Ð  
¨:¨,°iÐ@ÐM_Ðin€IÑoØ×Ñ˜$ÔÜ�D jÔ1Ø×Ñ˜$ÔÜ�Ð8Ô9Ø—-‘-ð
	ó€Kö ÙÐ7Ô8ØØ‡J�J×Ñ�[ Ñ% {°3Ñ'7Ô8Ø×Ñ˜$ÔÜ�ÐGÔHØ�}‰}ð
	ó€Hö Ø�
‰
×Ñ˜ #™¨°©Ô6Ø×Ñ˜cÔ"Ü�TÐ 3Ô4Ü Ô!:¸4Ó!@Ä/ÓBSÓT‰	ˆÙˆm˜D™8 2 c U¨!Ð,Ô-Þð:Ü& tÔ2ð 	×Ñ˜cÕ"áÐ*Ô+Ø�m‰mð
	ó€Gö Ø�
‰
×Ñ˜ ™ w¨s¡|Ô4Ø×Ñ˜cÔ"Ø�‰×Ñ˜8Ô$Ø×Ñ˜dÔ#Ü�TÐ"@ÀÁÐ MÔNØ�m‰mðð  ó#
ˆñ& 	Ð ™wÐ'Ô(ÞØ�J‰J×Ñ˜S ™X s¨3¡xÔ0Ø×!Ñ! #Ô&øáÐ-Ô.Ü�Ð=Ô>Ø�M‰Mð
	ó÷ ð 
ð 
ñ ˆ˜%Ÿ*™*Ñ$~Ñx}Ó$~Ó~ð  @Dð  ADð  Eñ  	Eô  FÜ˜eÓ$€DÙ°$Ñ2Bˆ,�u˜T‘{ 6Ñ*Ñ-Ñ.ÈÔXØ�|ØØ‡J�J×Ñ�U˜4‘[ Ñ% u¨T¡{°3Ñ'7Ô8Ø×Ñ˜$ÔÜ�Ð3¸$ÈÒRØ
�-‰-ð
	ó€Cñ Ð
˜S™GÐ$Ô%Þ
Ø�
‰
×Ñ˜˜S™ 3 s¡8Ô,Ø×Ñ˜dÔ#Ü�TÐ 6Ô7ß-ˆEÜ" 4¨°eÀS×IÑIÙ�o e¡YÐ/Ô0Ø×%Ñ% cÔ*Ü˜T¨/¸%¹Ð(CÔDÙñ .ñ Ð5Ô6ðØ—‘×"Ñ" 9Ô-Ø×%Ñ% cÔ*ô ˜Ð$=Õ>áÐ+Ô,ØÑð	3Ø�I‰IœÐ(¨
°:°,¸iÐHÐUgÐqvˆIÑwØ×!Ñ! $Ô'Ü" 4°WÔ=Ü˜Ð&:¸7¹+Ð$FÈTÐ\dÒeØŸ™ðGó÷ ð ð ó �Ù�H˜q  #˜wÑ&Ö'ñ ñ Ð7Ô8Ø‡I�I”Ð  
¨:¨,°iÐ@ÐM_Ðin€IÑoØ×Ñ˜$ÔÜ)¨$¸HÓE�N€A€{Ü+¨DÓ=�O€L�!ÙÐ
 ˜}Ð,FÀ|ÑFVÐVjÐkÕløôC ó :ÙÐ)¬#¨a«&°°#¨,¨Ð8×9Ò9ûð:ûôH ó Úðûô, ó 	3ÙÐ"¤3 q£6¨$¨3 < .Ð1×2Ñ2ûð	3úsC   È'W Ò",W1 Ó'A9X ×
W.×W)×)W.×1
W?×>W?Ø
X-ØX(Ø(X-)ç      $@)Frc  N)Nrc  r8   )rÜ   )ÚPURCHASEPRICErB   )rE   )NT)g      >@r`  )T)r  )rM  rØ   )Nr  )rÁ   r«   )re   )z
LE CapitalÚUsed)rF  )NrX   )Nrp   )rX   )zMileage from BidBrain)F)FN)•r^   r  rz   ry  r9   rv  r   Úurllib.parser   Údealer_configr5   r   ÚDEALERKIT_BASE_URLrâ   r   r!   r,   r3   r@   rU   r„   rW   ri   rŽ   rœ   r¤   r¨   r¸   rù   r  r   r*  r2  r_  rb  ri  rn  r†  r‘  r“  r˜  r   rš  r´  r·  r¼  r¿  r>  rñ  r¡   rÈ  rÉ  rÍ  rN  r  rm  rp  ro  r  r&  r6  r@  rW  rZ  r\  r^  rf  rv  r‹  rö  rH  r’  r€  r‚  r�  r¤  rô  r÷  rõ  r¹  rÂ  rÇ  rÌ  rè  r(  r“   r…   rþ  r   r  r  r  r  r"  rä   r   r'  r-  ræ   r¡  r8  r;  rA  rT  re  r\  r]  rb  rP  rl  rg  ÚNON_STOCK_STATUSri  r_  r’  r“  r€  r�  r”  r˜  rò  ró  r¸  r¹  r»  r¾  rÂ  rù  rü  rþ  r  r  r  rß   r  rà   rå   ró  r­  r¯  r®  r8  r:  ÚFOLLOWUP_ACTIONSrO  ÚFOLLOWUP_ONLYrP  rW  rb  rX   r"   r   Ú<module>rl     sÇ  ðñ5ón Û Û 	Û 	Û Û Ý !ã å ð  ×1Ñ1€ôô&ôNôô("ôJaôH5˜lô 5òLòO
òfdòLFòô&ô"^òB	JcòZMò$BkôJ$'ôNSòlô4[ô|FòP3òlò,&ò"Qò"ò0\Hò~gòqôhòbð$ W€
ð €ð *,¯©¯©ô ò+ô\ô,Tðz Ø&Ø(Ø0ñ	Ð ð (˜[Ð ØAÐ ò	#òBô*#òLô&2Wòj2ò
ò2ò0ò,YQôxrMðj ")¸EÐVaÑbÐ Ø+¸}ÐalÑm€ò0ô6ò.òò&ô,#ròLòeò$J ôZZBðz LVõ /UòdôNJòbEôP/òd,ð` ?EôaòHô@sòl'òTô0B˜,ô BòLôG˜lô GòKò!ò.	Fôò*ò0:ò.ò0ô>Xðv  °Ñ8€ð ¨"À2ÈrÐ`cÑd€ô7ò!ò$hð !Ð ð &Ð ØÐ òòð H€Ø_Ð ð EÐ òò*"ôJðLn€ð.€àl€
òò6/ò!òJò2òjQòUòò$ò>)ôX;ôò$ò.ôF5òR!òH[ò]3ô@o2òdKð0 IÐ ñ ÑIÑ!1ÓIÓI€ôJ@ôb2BójAmr"   