ó
    “íÃj¥ ã                   óœ  • S r SSKrSSKrSSKrSSKrSSKrSSKJr  SSKJ	r	  \R                  R                  \R                  R                  \R                  R                  \R                  R                  \5      5      5      S5      r\R                  R                  \S5      rSr\4S	\S
\R&                  4S jjr\4S	\S
S4S jjrGSRS jrSrGSRS jrS
\4S jrS
\4S jr\4S	\S
S4S jjrS
\4S jrSrS\S\S
\4S jrGSRS jr\4S\S\S	\S
S4S jjr \S4S	\S
\!4S jjr"S\4S\S	\S
\#4S jjr$S\4S\S\%S	\S
\&4S jjr'S\4S\S\%S	\S
\#4S  jjr(\4S!\S	\S
S4S" jjr)\4S	\S
\#4S# jjr*\4S	\S
\4S$ jjr+1 S%kr,S&r-S'r.S(r/S) r0S
\%4S* jr1GSSS
\24S+ jjr3\4S	\S
\24S, jjr4S
\#4S- jr5S
\%4S. jr6S
\!4S/ jr7GSRS0 jr8S1\4S	\S
\&4S2 jjr9\4S	\S
\&4S3 jjr:\4S	\S
S4S4 jjr;S5S6S7S8S9S:S;S<S=S>.	r<S?S@SA.r=\4S	\S
S4SB jjr>SC\4S	\S
\#4SD jjr?\4S	\S
S4SE jjr@S
\24SF jrAS\S4S	\S
S4SG jjrBSH rCSI rD\4S	\S
S4SJ jjrE\4S	\S
S4SK jjrFSC\S4S	\S
S4SL jjrGSC\4S	\S
S4SM jjrH\4S	\S
S4SN jjrISOSPSQSRSSSTSUSVSWSXSYSZS[S\.rJS]S^S_S`SaSbScSdSeSf.	rKSgrLShrMSiSjSk.rNSlrOGSRSm jrPS
\#4Sn jrQSCSSSSSC\4S	\S
S4So jjrR\4S	\S
\&4Sp jjrS\4S	\S
\&4Sq jjrT\4S	\S
\&4Sr jjrU\4S	\S
\&4Ss jjrVSC\4S	\S
S4St jjrWS
\&4Su jrXSC\4S	\S
S4Sv jjrYS
\Z4Sw jr[Sxr\S
\24Sy jr]GSSSz jr^\4S	\S
\&4S{ jjr_SCSCSS\4S	\S
\#4S| jjr`SCSCS}\4S	\S
S4S~ jjraSC\4S	\S
S4S jjrbSC\S€S4S	\S�\S‚\S
S4Sƒ jjrcSC\4S	\S
\24S„ jjrd\4S	\S
\e4S… jjrf\4S	\S
\4S† jjrgSC\4S	\S
S4S‡ jjrhSC\4S	\S
\4Sˆ jjriS€S‰SŠS‹.rjSŒS�SŽ.rk\4S	\S
\24S� jjrl\S�4S	\S‘\24S’ jjrmS“\4S	\4S” jjrnSC\4S	\S
\%4S• jjroS– rpS—S˜S�S™.S€SšS�S›.S‰SŒS�S›.SŠS�S�S›./SœS�S�.SžSŸS�.S S¡S�./S¢S£S�S™.S¤S˜S�.S¥S¦S�.S§S¨S�.S©SšS�Sª.// / / S«S¡S�.S¬SŒS�.S­SšS�./S®S¡S�.S¯S¡S�.S°SŒS�./S±.rq\4S	\S
\#4S² jjrrS³ rsS´ rt\4S	\S
\#4Sµ jjru/ S¶Q/ S·Q/ S¸Q/ S¹Q/ SºQ/ S»Q/ S¼Q/ S½Q/ S¾Q/ S¿Q/ SÀQ/ SÁQ/ SÂQ/ SÃQ/ SÄQ/ SÅQ/ SÆQ/ SÇQ// SÈQ/ SÉQ/ SÊQ/ SËQ/ SÌQ/ SÍQ/ SÎQ/ SÏQ/ SÐQ/ SÑQ/ SÒQ/ SÓQ/ SÔQ/ SÕQ/ SÖQ/ S×Q/ SØQ/ SÙQ/ SÚQ/ SÛQ/ SÜQ/ SÝQ/ SÞQ/ SßQ/ SàQ/Sá.rv\4S	\S
\#4Sâ jjrw\4S	\4Sã jjrxS
\4Sä jryS
\4Så jrzS
\#4Sæ jr{\4S	\S
\&4Sç jjr|Sè r}S\4S	\S
\#4Sé jjr~\4S	\S
\e4Sê jjr\4S	\S
\&4Së jjr€\4S	\S
\&4Sì jjr�\4S	\S
\&4Sí jjr‚\4S	\S
\&4Sî jjrƒ\4S	\S
\&4Sï jjr„\4Sð\%S	\S
S4Sñ jjr…\4Sð\%S	\S
S4Sò jjr†\4S	\S
\&4Só jjr‡\4S	\S
\&4Sô jjrˆ\4S	\S
\&4Sõ jjr‰\4S	\S
\&4Sö jjrŠSSSS\4Sð\%S	\S
S4S÷ jjr‹SSSS\4Sð\%S	\S
S4Sø jjrŒ\4S	\S
\%4Sù jjr�\4S	\S
\%4Sú jjrŽ\4S	\S
\24Sû jjr�\4S	\S
\#4Sü jjr�\4S\S	\S
\&4Sý jjr‘Sþ\#S
\&4Sÿ jr’SCSCSCSC\S4S	\S
S4GS  jjr“\S4S	\S
S4GS jjr”\4S	\S
S4GS jjr•\GS.S	\4GS jjr–SSS\4S	\S
\%4GS jjr—\4S	\S
\&4GS jjr˜\4S	\S
S4GS jjr™GSršS\4S	\S
\%4GS	 jjr›\4S	\S
\&4GS
 jjrœ\4S	\S
\%4GS jjr�\4S	\S
S4GS jjrž\4S	\S
\#4GS jjrŸGS\4S	\S
\%4GS jjr \4S	\S
\&4GS jjr¡SCSSCSCSCSC\S4S	\S
S4GS jjr¢\4S	\4GS jjr£\4S	\S
\#4GS jjr¤S�\4S	\S
\&4GS jjr¥\4S	\S
\&4GS jjr¦\4S	\4GS jjr§\4S	\S
\e4GS jjr¨\4S	\S
\&4GS jjr©\4S	\S
\e4GS jjrª\4S	\S
\&4GS jjr«\4S	\S
\&4GS jjr¬\4S	\S
\e4GS jjr­S�\4S	\S
\&4GS jjr®\4S	\S
S4GS jjr¯GS r°S
\4GS  jr±S
\24GS! jr²S
\e4GS" jr³S�SCSCSCSSS\S4	S	\S
S4GS# jjr´\4S	\S
\&4GS$ jjrµ\S4S	\4GS% jjr¶\4S	\S
\#4GS& jjr·\4S	\S
\e4GS' jjr¸\4S	\S
S4GS( jjr¹\4S	\S
\#4GS) jjrº\4S	\4GS* jjr»\4S	\S
\#4GS+ jjr¼\S4GS,\#S	\S
S4GS- jjr½\4S	\S
\#4GS. jjr¾SC\4GS/\GS0\2GS1\S	\S
S4
GS2 jjr¿\4S	\S
\#4GS3 jjrÀSC\4GS4\GS0\2Sþ\S	\S
S4
GS5 jjrÁ\4GS6\S\GS7\GS8\2S	\S
S4GS9 jjrÂGSrÃS\4S\S	\S
\&4GS: jjrÄS\S14GS6\S\GS0\2GS;\S	\GS<\2S
S4GS= jjrÅ\4S	\S
\%4GS> jjrÆ\4GS?\&S\S	\S
S4GS@ jjrÇ\4S	\S
\&4GSA jjrÈ\S14S	\S
\&4GSB jjrÉ\SS1S4S	\S
\%4GSC jjrÊS�\S4S	\S
S4GSD jjrËS�\S4S	\S
S4GSE jjrÌ\S4S	\S
S4GSF jjrÍ\4S	\4GSG jjrÎ\S4S	\S
S4GSH jjrÏGSI\4S	\4GSJ jjrÐS\4S	\S
S4GSK jjrÑGSI\4S	\S
\%4GSL jjrÒGSMrÓ\Ó\4S	\4GSN jjrÔ\4S	\S
S4GSO jjrÕGSP\4S	\S
\%4GSQ jjrÖg(T  a‚  
The single local database on the Mac Studio. SQLite, one file, no server.

Phase 1 uses two of the tables: the cars entered by hand, and the assessment
results from each run. The other tables are created now so the shape is ready
for later phases (learned model figures, daily stock snapshots, sales history),
but nothing writes to them yet.

Everything stays local. Section 2 rule 5.
é    Né   )Úname_make_model)ÚplatesÚdatazbidbrain.dba�A  
CREATE TABLE IF NOT EXISTS cars (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    reg TEXT,
    make TEXT,
    model TEXT,
    derivative TEXT,
    year INTEGER,
    mileage INTEGER,
    owners INTEGER,
    grade INTEGER,
    reserve INTEGER,
    cap_clean INTEGER,
    distance_miles REAL,
    engine TEXT,
    service_history TEXT,
    vat_qualifying INTEGER,
    glass_retail INTEGER,
    cazana_retail INTEGER,
    photo_url TEXT,
    listing_url TEXT,
    source TEXT,
    actually_paid INTEGER,
    loaded_at TEXT
);

CREATE TABLE IF NOT EXISTS assessments (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    run_at TEXT,
    reg TEXT,
    status TEXT,
    governing_source TEXT,
    governing_value INTEGER,
    max_bid INTEGER,
    reasons TEXT,
    flags TEXT
);

-- Phase 3: per model learned figures, rebuilt by the monthly run from
-- sales_history. model_key groups naming styles (see readers/clickdealer.py).
CREATE TABLE IF NOT EXISTS learned_models (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    make TEXT,
    model TEXT,
    model_key TEXT,
    n_sold INTEGER,
    avg_days_to_sell REAL,
    avg_prep_total INTEGER,
    avg_margin INTEGER,
    prep_split TEXT,
    spread_held INTEGER,
    by_platform TEXT,
    updated_at TEXT
);

CREATE TABLE IF NOT EXISTS stock_snapshots (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    snapshot_date TEXT,
    make TEXT,
    model TEXT,
    in_stock_count INTEGER
);

-- Phase 3: one row per sold car, ingested monthly from the Clickdealer sold
-- vehicles and sales margins reports. source is the supplier the car was
-- bought from (Motorway, Carwow, an auction house...). prep_total is the SIV.
-- margin is net sales minus purchase minus prep, straight off the report.
CREATE TABLE IF NOT EXISTS sales_history (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    stock_number TEXT,
    reg TEXT,
    make TEXT,
    model TEXT,
    sold_date TEXT,
    days_to_sell INTEGER,
    sale_price INTEGER,
    purchase_price INTEGER,
    margin INTEGER,
    total_margin INTEGER,
    prep_total INTEGER,
    prep_split TEXT,
    source TEXT,
    UNIQUE(stock_number, sold_date)
);

-- Cars Steven has actually bought, read daily from each platform's purchases
-- screen after the sale closes. emailed_at records when the buy was sent to
-- accounts; the first ever capture is baselined so history is not re emailed.
CREATE TABLE IF NOT EXISTS purchases (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    platform TEXT,
    vehicle_ref TEXT,
    reg TEXT,
    name TEXT,
    status TEXT,
    price INTEGER,
    total_price INTEGER,
    bought_date TEXT,
    first_seen TEXT,
    retail_estimate INTEGER,
    listing_url TEXT,
    emailed_at TEXT,
    UNIQUE(platform, vehicle_ref)
);

-- A chip: an agreed reduction on a purchase, negotiated after the win
-- (a fault on the video, or one the driver found at the door). A stored
-- event, never live arithmetic: bid_before freezes what the effective bid
-- was at the moment of the chip, and new_fee/new_vat freeze the re banded
-- platform fee where one applies (Motorway on Motorway Pay; Carwow's fee
-- never moves, Steven confirmed 2026-08-27). Nothing here is ever
-- recomputed against a later fee table or a later platform read, which is
-- what makes the double count trap impossible (see CLAUDE.md).
CREATE TABLE IF NOT EXISTS purchase_chips (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    purchase_id INTEGER NOT NULL,
    amount REAL NOT NULL,
    reason TEXT,
    bid_before REAL,
    new_fee REAL,
    new_vat REAL,
    created_at TEXT,
    who TEXT
);
CREATE INDEX IF NOT EXISTS ix_chips_purchase ON purchase_chips(purchase_id);

-- The per purchase timeline (Steven 2026-08-27: "every change to any
-- vehicle record should be shown in the timeline and timestamped
-- including all comments and status changes"). Every mutation of a
-- purchase goes through set_purchase_field / record_chip /
-- cancel_purchase / add_purchase_comment, each of which writes here, so
-- nothing can change a record without the timeline knowing. kind is one
-- of field / comment / milestone / sync / chip / cancel.
CREATE TABLE IF NOT EXISTS purchase_events (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    purchase_id INTEGER NOT NULL,
    ts TEXT,
    kind TEXT,
    text TEXT,
    field TEXT,
    was TEXT,
    now TEXT,
    body TEXT,
    who TEXT
);
CREATE INDEX IF NOT EXISTS ix_events_purchase ON purchase_events(purchase_id);

-- Every car seen on each platform each day, so repeat appearances can be
-- flagged. Identity is the registration. One row per car per platform per
-- sale date.
CREATE TABLE IF NOT EXISTS sightings (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    reg TEXT,
    platform TEXT,
    sale_date TEXT,
    make TEXT,
    model TEXT,
    derivative TEXT,
    year INTEGER,
    reserve INTEGER,
    cap_clean INTEGER,
    listing_url TEXT,
    recorded_at TEXT,
    UNIQUE(reg, platform, sale_date)
);

-- The two plates one car is genuinely known by (2026-09-20): the plate the
-- listing used, and the plate underneath it sells with when the seller keeps
-- a private plate. BidBrain has read that second plate since June for the
-- valuations (Motorway's own sellingVrm, Carwow's plate transfer field) and
-- never kept it, so nothing that looks a car up by plate afterwards, the
-- DealerKit lookups above all, could ever try it. Written by
-- save_assessments, which every run path goes through after its detail
-- reads, so both platforms are covered wherever they happen to read it.
-- Kept for good: a car bought today can reach DealerKit weeks later, long
-- after the listing caches have rotated away.
CREATE TABLE IF NOT EXISTS plate_pairs (
    reg TEXT NOT NULL,
    selling_vrm TEXT NOT NULL,
    first_seen TEXT,
    last_seen TEXT,
    UNIQUE(reg, selling_vrm)
);

-- Cars Steven has hidden with a reason, suppressed from future daily lists.
-- One row per registration. active 1 means hidden, reviewed 0 means the reason
-- has not yet been turned into a rule.
CREATE TABLE IF NOT EXISTS hidden_cars (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    reg TEXT NOT NULL,
    reason TEXT,
    make TEXT,
    model TEXT,
    name TEXT,
    hidden_at TEXT,
    active INTEGER DEFAULT 1,
    reviewed INTEGER DEFAULT 0
);
CREATE UNIQUE INDEX IF NOT EXISTS ix_hidden_reg ON hidden_cars(reg);

-- Steven's own daily bid shortlist. The app never bids, this is his pick and a
-- learning signal. One row per car per sale date.
CREATE TABLE IF NOT EXISTS bids (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    reg TEXT NOT NULL,
    sale_date TEXT NOT NULL,
    make TEXT,
    model TEXT,
    name TEXT,
    max_bid INTEGER,
    reserve INTEGER,
    year INTEGER,
    mileage INTEGER,
    grade INTEGER,
    fuel TEXT,
    body_type TEXT,
    cap_clean INTEGER,
    platform TEXT,
    created_at TEXT,
    active INTEGER DEFAULT 1,
    UNIQUE(reg, sale_date, platform)
);

-- Cars bought on a platform where the sale was then cancelled or voided. Steven
-- never wants to see these again (he has already walked away once), so their
-- registrations are suppressed from the daily page for good.
CREATE TABLE IF NOT EXISTS walked_away (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    reg TEXT NOT NULL,
    platform TEXT,
    name TEXT,
    status TEXT,
    noted_at TEXT
);
CREATE UNIQUE INDEX IF NOT EXISTS ix_walked_away_reg ON walked_away(reg);

-- Buying rule overrides edited from the cockpit's settings page, layered on
-- top of pricing.DEFAULT_SETTINGS. Value is JSON so a row can hold a number,
-- a list or a list of objects (for example value_caps). Empty table means
-- every rule is still at its factory default.
CREATE TABLE IF NOT EXISTS settings (
    key TEXT PRIMARY KEY,
    value TEXT NOT NULL,
    updated_at TEXT NOT NULL
);

-- Glass's and Cazana valuations, cached so re running the daily job (for
-- example a genuine same day re run, or the same car reappearing across an
-- Auction4Cars auction's several days) does not repeat a lookup for a retail
-- figure that has not changed. Keyed on reg, mileage and source so a car
-- that has genuinely been driven since last seen still gets a fresh look up.
-- value is nullable, a confirmed no match is cached too, so an unmatched reg
-- is not re queried every day either. Rows older than 48 hours are never
-- used (and are pruned) regardless of what is in the table.
CREATE TABLE IF NOT EXISTS valuation_cache (
    reg TEXT NOT NULL,
    mileage INTEGER NOT NULL,
    source TEXT NOT NULL,
    make TEXT NOT NULL,
    model TEXT NOT NULL,
    value INTEGER,
    url TEXT,
    valued_at TEXT NOT NULL,
    UNIQUE(reg, mileage, source)
);

-- Whatever a platform's own detail page visit adds to a car (owners, service
-- history, disclosed issues, photos, and on Carwow/Auction4Cars the reserve
-- itself, which only that page ever discloses), cached so a car reappearing
-- across a multi day Auction4Cars, Carwow or DealerWay auction does not get
-- its own page re visited every single run just to read the same, mostly
-- unchanged, facts again (Mark 2026-08-24: "are these cars info... being
-- added to a database... rather than a full scrape", the slow part of a
-- refresh). detail_json is whichever car fields the detail read actually
-- changed (a genuine before/after diff, not a fixed field list, so this
-- never needs updating when a reader gains a new field), applied back onto
-- the fresh car object on a hit. Reserve rides along in the same cache
-- entry as everything else with the same short TTL (Mark's own choice, "yes
-- cache price too with a short TTL"), not a separate longer one, so a
-- cached figure is never more than a few hours stale. Dealer Auction is not
-- covered, it has no detail page visit at all, every field including its
-- own reserve stand in already comes straight off the list card. cache_key
-- is a registration wherever one is already known before the detail visit
-- (Carwow, DealerWay), but Auction4Cars' own reg is only ever read FROM its
-- detail page, so a candidate's stable listing_url stands in as the key
-- there instead, until the real reg lands in the cached fields themselves.
CREATE TABLE IF NOT EXISTS listing_detail_cache (
    cache_key TEXT NOT NULL,
    platform TEXT NOT NULL,
    detail_json TEXT NOT NULL,
    fetched_at TEXT NOT NULL,
    UNIQUE(cache_key, platform)
);

-- Saved filter and sort combinations Mark can name and star, so a common
-- slice of the list is one click away instead of setting several filters by
-- hand each time (2026-08-20). criteria is the whole sidebar filter state
-- (make, model, year, transmission, body type, the three price ranges, sort
-- and view) as one JSON blob. Starred ones show as a quick button next to
-- the combined sort and view control; unstarred ones are just kept, listed
-- in the sidebar so they can be starred or deleted later.
CREATE TABLE IF NOT EXISTS saved_views (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    criteria TEXT NOT NULL,
    starred INTEGER NOT NULL DEFAULT 0,
    created_at TEXT NOT NULL
);

-- Every real auction bid on and lost, Mark 2026-08-24: the original
-- BidBrain vision was to learn real prices from real data (best sellers,
-- prep cost, margin) to make more educated bids; a lost auction's own real
-- winning price is the other half of that picture purchases alone never
-- carry, how much it actually would have taken to win. Identity is the
-- platform's own vehicle reference (a plain reg can repeat across
-- unrelated cars over time; the platform's own numeric id cannot), a lost
-- outcome never changes once recorded so this is a plain insert once, no
-- fill-in-later fields the way purchases has.
CREATE TABLE IF NOT EXISTS lost_bids (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    platform TEXT,
    vehicle_ref TEXT,
    reg TEXT,
    name TEXT,
    sold_for INTEGER,
    our_bid INTEGER,
    date_bid TEXT,
    first_seen TEXT,
    listing_url TEXT,
    mileage INTEGER,
    cazana_retail INTEGER,
    UNIQUE(platform, vehicle_ref)
);

-- The one platform action BidBrain is ever allowed to take (approved by
-- Steven 2026-06-09, built 2026-08-27, Mark: "when we shortlist a car in
-- bidbrain add that to watched/tracked in auction sites"): starring or
-- unstarring a car in the cockpit adds or removes it from that platform's
-- own shortlist/watchlist. Never a bid, never anything else. This table
-- holds the DESIRED state per (reg, platform), not a log of events, so a
-- quick star then unstar before the sync ever runs just updates the one
-- row rather than queuing duplicate work. synced_at is cleared to NULL on
-- every queue (so a later toggle is always picked up again even if an
-- earlier one already synced) and only ever set back on a confirmed,
-- verified success; a failed attempt leaves it NULL (so the next pass
-- retries) and records the real reason in error, surfaced the same way a
-- dead login already is elsewhere in this project.
CREATE TABLE IF NOT EXISTS watchlist_sync (
    reg TEXT NOT NULL,
    platform TEXT NOT NULL,
    listing_url TEXT,
    watched INTEGER NOT NULL,
    queued_at TEXT NOT NULL,
    synced_at TEXT,
    error TEXT,
    UNIQUE(reg, platform)
);

-- Cars won or offered on but not yet a confirmed, accepted purchase (Mark
-- 2026-08-27: "add a under offer section to the main nav for cars that
-- are won but bids not accepts this is for all auctions"), read from each
-- platform's own real "Bids & offers" style screen, never the purchases
-- table (a genuinely earlier stage, before a car is even guaranteed to
-- become a real purchase at all). A live snapshot, not an accumulating
-- history: record_under_offer replaces the whole set for one platform on
-- every read, so a car that has since been accepted or lost simply is
-- not in the next read and drops out on its own, no separate cleanup
-- needed.
CREATE TABLE IF NOT EXISTS under_offer (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    platform TEXT NOT NULL,
    reg TEXT,
    name TEXT,
    max_bid INTEGER,
    noted_at TEXT,
    vehicle_ref TEXT,
    listing_url TEXT,
    read_at TEXT NOT NULL
);

-- DealerKit's own in stock cars (2026-09-10): one row per vehicle from
-- DealerKit's record, read with the stock list, so the purchase cost of
-- every car in stock is DealerKit's figure whether BidBrain bought it or not.
CREATE TABLE IF NOT EXISTS dealerkit_stock (
    vehicle_id TEXT PRIMARY KEY,
    reg TEXT,
    make TEXT,
    model TEXT,
    purchase_cost INTEGER,
    retail_price INTEGER,
    life_cycle_status INTEGER,
    read_at TEXT NOT NULL
);

-- What a sold car actually went for (2026-09-12, Steven: "cars dont always
-- sell for what we list them up for retail for"). Dealer OS asks for one
-- car at a time the moment DealerKit says it is Sold (a sale.price
-- action); the row waits here until the quiet read has looked at the
-- car's own DealerKit record and posted the figure back.
CREATE TABLE IF NOT EXISTS sale_price_reads (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    reg TEXT NOT NULL,
    api_id TEXT,
    name TEXT,
    sold_at TEXT,
    queued_at TEXT NOT NULL,
    done_at TEXT,
    result TEXT
);

-- Cars Steven released from a hold (2026-09-10): the rules dropped the
-- car, he said on Dealer OS "un hold it" and why. The car passes those
-- rules on every later run, the reason is kept for tuning the rules.
CREATE TABLE IF NOT EXISTS released_cars (
    reg TEXT PRIMARY KEY,
    reason TEXT,
    rules TEXT,
    make TEXT,
    model TEXT,
    name TEXT,
    who TEXT,
    released_at TEXT NOT NULL,
    active INTEGER NOT NULL DEFAULT 1,
    reviewed INTEGER NOT NULL DEFAULT 0,
    updated_at TEXT
);

-- The shortlist deep read (Dealer OS v4.1.0, 2026-09-17): one row per
-- starred car Dealer OS asked this Mac to read in full, keyed the way
-- Dealer OS keys a card (its platform and its key, the reg or the listing
-- url for a car with no plate). wanted is 1 from the ask until the page
-- has been read (or given up on), deep_json is the last answer and stays
-- on the row so a re read pushes the old answer, with its older asked_at,
-- until the new one lands, which is exactly how Dealer OS knows it is
-- still reading. attempts counts passes that started on the row, so a
-- page that keeps crashing the read is answered with an error after
-- DEEP_READ_MAX_ATTEMPTS rather than tried forever.
CREATE TABLE IF NOT EXISTS deep_reads (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    car_key TEXT NOT NULL,
    platform TEXT NOT NULL,
    sale_date TEXT NOT NULL,
    reg TEXT,
    listing_url TEXT,
    name TEXT,
    asked_at TEXT,
    queued_at TEXT NOT NULL,
    wanted INTEGER NOT NULL DEFAULT 1,
    attempts INTEGER NOT NULL DEFAULT 0,
    read_at TEXT,
    deep_json TEXT,
    UNIQUE(car_key, platform, sale_date)
);
ÚpathÚreturnc                 ó¾   • [         R                  " [         R                  R                  U 5      SS9  [        R
                  " U SS9n[        R                  Ul        U$ )NT)Úexist_oké   )Útimeout)ÚosÚmakedirsr   ÚdirnameÚsqlite3ÚconnectÚRowÚrow_factory©r   Úconns     Ú,/Users/stevendouglas/BidBrain/bidbrain/db.pyr   r   á  s@   € Ü‡K‚K”—‘—‘ Ó%°Ò5ô �?Š?˜4¨Ñ+€DÜ—{‘{€DÔØ€Kó    c                 óÆ   • [        U 5      n UR                  [        5        [        U5        UR	                  5         UR                  5         g ! UR                  5         f = f©N)r   ÚexecutescriptÚSCHEMAÚ_migrateÚcommitÚcloser   s     r   Úinit_dbr   ê  sA   € Ü�4‹=€DðØ×Ñœ6Ô"Ü�ŒØ�‰Œà�
‰
�øˆ�
‰
�ús   �0A ÁA c                 ó’  • U R                  S5       Vs1 s H  oS   iM	     nnSU;  a  U R                  S5        SU;  a  U R                  S5        U R                  S5       Vs1 s H  oS   iM	     nnS H#  u  pEXC;  d  M  U R                  S	U S
U 35        M%     SU;  a  U R                  S5        [        U 5        U R                  S5       Vs1 s H  oS   iM	     nnSU;  a  U R                  S5        U R                  S5       Vs1 s H  oS   iM	     nnU(       a°  SU;  a|  U R                  S5        U R                  S5      R                  5        H7  nU R                  S[        R                  " 5       R
                  US   45        M9     U R                  S5        SU;  a  U R                  S5        SU;  a  U R                  S5        U R                  S5       Vs1 s H  oS   iM	     nnU(       a.  SU;  a  U R                  S5        SU;  a  U R                  S5        S HŒ  u  pšU R                  SU	 S 35       Vs1 s H  oS   iM	     nnU(       d  M5  X¢;  d  M<  U R                  S!U	 35      R                  5       S"   nUS#:X  a  U R                  S$U	 35        M}  [        U	 S%U S&35      e   U R                  S'5       Vs1 s H  oS   iM	     nnS( H  nXL;  d  M
  U R                  S)U S*35        M!     S+U;  a  U R                  S,5        S-U;  a  U R                  S.5        S/U;  a  U R                  S05        S1U;  a  U R                  S25        S3U;  a  U R                  S45        S5U;  a  U R                  S65        S7U;  a  U R                  S85        S9 Vs/ s H  nXÜ;  d  M
  UPM     nnU H  nU R                  S)U S:35        M     U(       a  U R                  S;5        S<U;  a  U R                  S=5        S>U;  a  U R                  S?5        S@U;  a  U R                  SA5        SB H  nXL;  d  M
  U R                  S)U S:35        M!     SC H  nXL;  d  M
  U R                  S)U S*35        M!     SD H  nXL;  d  M
  U R                  S)U S:35        M!     [        U 5        SEU;  a  U R                  SF5        SGU;  a  U R                  SH5        SIU;  a  U R                  SJ5        SK H  nXL;  d  M
  U R                  S)U S*35        M!     U R                  SL5       Vs1 s H  oS   iM	     nnSMU;  a  U R                  SN5        SOU;  a  U R                  SP5        SQU;  a  U R                  SR5        U R                  SS5       Vs1 s H  oST   iM	     nnSUU;  a  U R                  SV5        U R                  S5       Vs1 s H  oS   iM	     nnSW H$  u  pEUU;  d  M  U R                  SXU S
U 35        M&     SYU;  a  U R                  SZ5        S[U;  a  U R                  S\5        S]U;  a  U R                  S^5        S_U;  a  U R                  S`5        SYU;  a  U R                  Sa5        U R                  S'5       Vs1 s H  oS   iM	     nnS[U;  a  U R                  Sb5        S]U;  a  U R                  Sc5        S_U;  a  U R                  Sd5        SYU;  a  U R                  Se5        SMU;  a  U R                  Sf5        SgU;  a  U R                  Sh5        SiU;  a  U R                  Sj5        SkU;  a  U R                  Sl5        Sm H   nUU;  d  M  U R                  S)U S:35        M"     Sn H   nUU;  d  M  U R                  S)U S:35        M"     So H$  u  pEUU;  d  M  U R                  S)U S
U 35        M&     SpU;   a"  U R                  Sq5        U R                  Sr5        U R                  Ss5        StU;  a  U R                  Su5        U R                  Sv5       Vs1 s H  oS   iM	     nnSwU;  a  U R                  Sx5        U R                  [        5        [        U 5        [        U 5        gys  snf s  snf s  snf s  snf s  snf s  snf s  snf s  snf s  snf s  snf s  snf s  snf s  snf )zzàAdd columns to existing tables that CREATE TABLE IF NOT EXISTS cannot add.
Idempotent: only adds a column when it is missing, so old databases upgrade
in place and existing rows keep their values (the new column reads NULL).zPRAGMA table_info(sightings)ÚnameÚreservez0ALTER TABLE sightings ADD COLUMN reserve INTEGERÚ	cap_cleanz2ALTER TABLE sightings ADD COLUMN cap_clean INTEGERúPRAGMA table_info(bids)))ÚyearÚINTEGER©Úmileager&   )Úgrader&   )ÚfuelÚTEXT)Ú	body_typer+   )r#   r&   )Úplatformr+   zALTER TABLE bids ADD COLUMN Ú Ú
updated_atz+ALTER TABLE bids ADD COLUMN updated_at TEXTzPRAGMA table_info(hidden_cars)z2ALTER TABLE hidden_cars ADD COLUMN updated_at TEXTzPRAGMA table_info(saved_views)Ú	remote_idz1ALTER TABLE saved_views ADD COLUMN remote_id TEXTzSELECT id FROM saved_viewsz-UPDATE saved_views SET remote_id=? WHERE id=?ÚidzQCREATE UNIQUE INDEX IF NOT EXISTS ix_saved_views_remote ON saved_views(remote_id)z2ALTER TABLE saved_views ADD COLUMN updated_at TEXTÚdeletedzEALTER TABLE saved_views ADD COLUMN deleted INTEGER NOT NULL DEFAULT 0z PRAGMA table_info(sales_history)Útotal_marginz9ALTER TABLE sales_history ADD COLUMN total_margin INTEGERÚ
prep_splitz4ALTER TABLE sales_history ADD COLUMN prep_split TEXT))Úlearned_modelsÚ	model_key)Úsales_historyÚregzPRAGMA table_info(Ú)zSELECT COUNT(*) AS n FROM Únr   zDROP TABLE z has z> rows but an old shape. Not dropping data, migrate it by hand.zPRAGMA table_info(purchases)©Úwinning_bidÚmotorway_feeÚtransport_feeÚprotect_feeÚ	vat_totalz!ALTER TABLE purchases ADD COLUMN z REALÚ
cazana_115z0ALTER TABLE purchases ADD COLUMN cazana_115 REALÚpayment_estimatedz:ALTER TABLE purchases ADD COLUMN payment_estimated INTEGERÚ	photo_urlz/ALTER TABLE purchases ADD COLUMN photo_url TEXTÚservice_history_photosz<ALTER TABLE purchases ADD COLUMN service_history_photos TEXTÚ
photo_urlsz0ALTER TABLE purchases ADD COLUMN photo_urls TEXTÚ	v5_photosz/ALTER TABLE purchases ADD COLUMN v5_photos TEXTÚdealerkit_pushed_atz9ALTER TABLE purchases ADD COLUMN dealerkit_pushed_at TEXT)Údk_price_pushed_atÚdk_photos_pushed_atÚdk_documents_pushed_atz TEXTz¶UPDATE purchases SET dk_price_pushed_at=dealerkit_pushed_at, dk_photos_pushed_at=dealerkit_pushed_at, dk_documents_pushed_at=dealerkit_pushed_at WHERE dealerkit_pushed_at IS NOT NULLÚdk_retail_pushed_atz9ALTER TABLE purchases ADD COLUMN dk_retail_pushed_at TEXTÚdk_due_in_date_pushed_atz>ALTER TABLE purchases ADD COLUMN dk_due_in_date_pushed_at TEXTÚdk_checkz.ALTER TABLE purchases ADD COLUMN dk_check TEXT)Údk_mileage_pushed_atÚdk_fee_pushed_atÚdk_chip_pushed_atÚdk_transport_pushed_atÚdk_checked_in_atÚdk_removed_at)Údk_price_sentÚdk_fee_sent)ÚmakeÚmodelÚdocument_pathsz4ALTER TABLE purchases ADD COLUMN document_paths TEXTÚv5_local_pathsz4ALTER TABLE purchases ADD COLUMN v5_local_paths TEXTÚdealer_os_documentsz9ALTER TABLE purchases ADD COLUMN dealer_os_documents TEXT)Úfinance_settlementÚnegative_equityzPRAGMA table_info(lost_bids)r(   z0ALTER TABLE lost_bids ADD COLUMN mileage INTEGERÚcazana_retailz6ALTER TABLE lost_bids ADD COLUMN cazana_retail INTEGERÚcazana_checkedz7ALTER TABLE lost_bids ADD COLUMN cazana_checked INTEGERz!PRAGMA table_info(watchlist_sync)r   ÚattemptszIALTER TABLE watchlist_sync ADD COLUMN attempts INTEGER NOT NULL DEFAULT 0)r'   )Úenginer+   )Útransmissionr+   z!ALTER TABLE sightings ADD COLUMN r)   z.ALTER TABLE sightings ADD COLUMN grade INTEGERr%   z-ALTER TABLE lost_bids ADD COLUMN year INTEGERra   z2ALTER TABLE lost_bids ADD COLUMN transmission TEXTr`   z,ALTER TABLE lost_bids ADD COLUMN engine TEXTz.ALTER TABLE lost_bids ADD COLUMN grade INTEGERz-ALTER TABLE purchases ADD COLUMN year INTEGERz2ALTER TABLE purchases ADD COLUMN transmission TEXTz,ALTER TABLE purchases ADD COLUMN engine TEXTz.ALTER TABLE purchases ADD COLUMN grade INTEGERz0ALTER TABLE purchases ADD COLUMN mileage INTEGERÚcollection_datez5ALTER TABLE purchases ADD COLUMN collection_date TEXTÚretail_estimate_overridezAALTER TABLE purchases ADD COLUMN retail_estimate_override INTEGERÚcollection_date_overridez>ALTER TABLE purchases ADD COLUMN collection_date_override TEXT)Úcancelled_atÚcancelled_reasonÚcancelled_source)
Ústock_statusÚ
on_financeÚvideo_statusÚperson_collectingÚnotesÚcollection_arrangedÚlogbook_stateÚlocationÚchecked_in_atÚsupplier))Úpaid_forr&   )Úmanual_entryr&   )Úfee_has_vatr&   )Útransport_has_vatr&   Úarchived_atzcUPDATE purchases SET stock_status='In stock' WHERE archived_at IS NOT NULL AND stock_status IS NULLz-ALTER TABLE purchases DROP COLUMN archived_atzEUPDATE purchases SET stock_status='Due in' WHERE stock_status IS NULLÚselling_vrmz1ALTER TABLE purchases ADD COLUMN selling_vrm TEXTz"PRAGMA table_info(valuation_cache)Úurlz/ALTER TABLE valuation_cache ADD COLUMN url TEXTN)ÚexecuteÚ_bids_one_per_listingÚfetchallÚuuidÚuuid4ÚhexÚfetchoneÚRuntimeErrorÚ_fill_purchase_make_modelr   r   Ú_move_sunday_sightingsÚ_restore_motorway_amendments)r   ÚrÚcolsÚbcolsÚcolÚtypÚhcolsÚvcolsÚscolsÚtableÚmarkerr:   ÚpcolsÚcÚ_dk_new_colsÚ_dkcolÚlbcolsÚwcolsÚsicolsÚpcols2Úvccolss                        r   r   r   ô  sÐ  € ð  $Ÿ|™|Ð,JÔKÓLÒK˜!ˆfŒIÑK€DÐLØ˜ÓØ�‰ÐGÔHØ˜$ÓØ�‰ÐIÔJà $§¡Ð-FÔ GÓHÒ G˜1ˆvŒYÑ G€EÐHó+‰ˆð ÕØ�L‰LÐ7¸°u¸A¸c¸UÐCÖDñ	+ð ˜5Ó Ø�‰ÐBÔCÜ˜$ÔØ $§¡Ð-MÔ NÓOÒ N˜1ˆvŒYÑ N€EÐOØ˜5Ó Ø�‰ÐIÔJØ $§¡Ð-MÔ NÓOÒ N˜1ˆvŒYÑ N€EÐOÞØ˜eÓ#Ø�L‰LÐLÔMØ—\‘\Ð">Ó?×HÑHÖJ�Ø—‘ÐLÌtÏzÊzË|×O_ÑO_ÐabÐcgÑahÐNiÖjñ Kà�L‰LÐlÔmØ˜uÓ$Ø�L‰LÐMÔNØ˜EÓ!Ø�L‰LÐ`Ôað
 !%§¡Ð-OÔ PÓQÒ P˜1ˆvŒYÑ P€EÐQÞØ Ó&Ø�L‰LÐTÔUØ˜uÓ$Ø�L‰LÐOÔPÛT‰ˆØ#'§<¡<Ð2DÀUÀGÈ1Ð0MÔ#NÓOÒ#N˜a�&”	Ñ#NˆÐOßˆ4�FÕ&Ø—‘Ð9¸%¸ÐAÓB×KÑKÓMÈcÑRˆAØ�A‹vØ—‘˜{¨5¨'Ð2Ö3ä"Ø�g˜U 1 #ð &*ð *ó+ð +ñ Uð !%§¡Ð-KÔ LÓMÒ L˜1ˆvŒYÑ L€EÐMÛ[ˆØÕØ�L‰LÐ<¸S¸EÀÐGÖHñ \ð ˜5Ó Ø�‰ÐGÔHð  %Ó'Ø�‰ÐQÔRð
 ˜%ÓØ�‰ÐFÔGð
   uÓ,Ø�‰ÐSÔTð ˜5Ó Ø�‰ÐGÔHð ˜%ÓØ�‰ÐFÔGð  EÓ)Ø�‰ÐPÔQñ  gó 'Òf˜!Ø‘~÷ Ñf€Lð 'ãˆØ�‰Ð8¸¸ÀÐFÖGñ æð 	�‰ð4ô	
ð  EÓ)Ø�‰ÐPÔQð "¨Ó.Ø�‰ÐUÔVð ˜ÓØ�‰ÐEÔFóOˆàÕØ�L‰LÐ<¸S¸EÀÐGÖHñOó 0ˆØÕØ�L‰LÐ<¸S¸EÀÐGÖHñ 0ó !ˆØÕØ�L‰LÐ<¸S¸EÀÐGÖHñ !ô ˜dÔ#ð ˜uÓ$Ø�‰ÐKÔLð ˜uÓ$Ø�‰ÐKÔLð  EÓ)Ø�‰ÐPÔQó 9ˆØÕØ�L‰LÐ<¸S¸EÀÐGÖHñ 9ð "&§¡Ð.LÔ!MÓNÒ!M˜A�ŒiÑ!M€FÐNØ˜ÓØ�‰ÐGÔHØ˜fÓ$Ø�‰ÐMÔNð ˜vÓ%Ø�‰ÐNÔOð  Ÿ<™<Ð(KÔLÓMÒL�aˆqŒTÑL€EÐMØ˜ÓØ�‰Ð`Ôað "&§¡Ð.LÔ!MÓNÒ!M˜A�ŒiÑ!M€FÐNÛZ‰ˆØ�fÕØ�L‰LÐ<¸S¸EÀÀ3À%ÐHÖIñ [ð �fÓØ�‰ÐEÔFð �VÓØ�‰ÐDÔEØ˜VÓ#Ø�‰ÐIÔJØ�vÓØ�‰ÐCÔDð �fÓØ�‰ÐEÔFØ!%§¡Ð.LÔ!MÓNÒ!M˜A�ŒiÑ!M€FÐNØ�VÓØ�‰ÐDÔEØ˜VÓ#Ø�‰ÐIÔJØ�vÓØ�‰ÐCÔDØ�fÓØ�‰ÐEÔFð ˜ÓØ�‰ÐGÔHð  Ó&Ø�‰ÐLÔMð "¨Ó/Ø�‰ÐXÔYð "¨Ó/Ø�‰ÐUÔVó HˆØ�fÕØ�L‰LÐ<¸S¸EÀÐGÖHñ Hó*-ˆð �fÕØ�L‰LÐ<¸S¸EÀÐGÖHñ	-óS‰ˆà�fÕØ�L‰LÐ<¸S¸EÀÀ3À%ÐHÖIñSð ˜ÓØ�‰ð Nô 	Oà�‰ÐDÔEØ‡L�LÐXÔYð ˜FÓ"Ø�‰ÐHÔIð "&§¡Ð.RÔ!SÓTÒ!S˜A�ŒiÑ!S€FÐTØ�FÓØ�‰ÐFÔGØ×Ñ”vÔÜ˜4Ô Ü  Õ&ùò} Mùò Iùò Pùò Pùò Rùò Pùò Nùòx'ùòZ Oùò& Nùò  Oùò4 Oùòt UsS   ”bÁ%bÃbÄbÇ'bÉ
b!Ëb&Î+	b+Î8b+Ô2b0Öb5×b:Úb?à5c))Ú14331500g     8¹@g      i@z
2026-10-05c                 ó`  • [        5       n[         GH  u  p#pEU R                  SU45      R                  5       nUb  US   c  M3  [	        US   X4-
  -
  5      S:¼  a  ML  U R                  SUS   45      R                  5       S   nU(       a  [	        Xt-
  5      S:¼  a  M�  U R                  SX6S   45        U(       a  M¬  U R                  S	US   US
USSUS45        U R                  SUS   USSUS SU 3SUS 3SX4-
  S 3S45        US   (       d  GM  U R                  SXS   45        GM     g)a+  Put each car in _MOTORWAY_AMENDMENTS_2026_10_05 right, once: the
bid goes back to what was won and the amendment becomes a chip, so
car_price, and the price paid, stay exactly as they are. Only while the
bid still reads the amended price, so it does nothing on any other Mac
or on a second start. A chip someone already typed in for the same
amount stays as the record and only the bid is restored; a car with any
other chip is left alone. A car already on DealerKit is stamped as
having had the chip sent, since DealerKit already holds the amended
price.zfSELECT id, winning_bid, dealerkit_pushed_at FROM purchases WHERE platform='Motorway' AND vehicle_ref=?Nr<   ç)\�Âõ(ð?úKSELECT COALESCE(SUM(amount),0) AS t FROM purchase_chips WHERE purchase_id=?r1   Útz-UPDATE purchases SET winning_bid=? WHERE id=?ú€INSERT INTO purchase_chips (purchase_id, amount, reason, bid_before, new_fee, new_vat, created_at, who) VALUES (?,?,?,?,?,?,?,?)zPrice amended on MotorwayÚBidBrainú_INSERT INTO purchase_events (purchase_id, ts, kind, text, was, now, who) VALUES (?,?,?,?,?,?,?)Úchipõ
   Chipped Â£ú,.0fu@    Â· Price amended on Motorway, from Motorway's payment email of ô   Â£ú,.2frG   z3UPDATE purchases SET dk_chip_pushed_at=? WHERE id=?)Ú_nowÚ_MOTORWAY_AMENDMENTS_2026_10_05ry   r   Úabs)r   ÚnowÚrefÚwonÚamendedÚpaidÚrowÚchipss           r   rƒ   rƒ   £  su  € ô ‹&€Cß#BÐ#BÑˆ�'Ø�l‰lð:Ø<?¸6óCçCKÁ8Ã:ð 	ð ‰;˜#˜mÑ,Ñ4ÙÜˆs�=Ñ! S¡]Ñ3Ó4¸Ó<ÙØ—‘ØYØ�‰YˆLóç"™(›* Sñ*ˆö ”S˜™Ó)¨TÓ1ÙØ�‰ÐDÀsÐPTÉIÐFVÔWÞÙØ�‰ðJà�‰Y˜Ð!<¸cÀ4ÈÈtÐU_Ð`ô	bð 	�‰ð%à�‰Y˜˜VØ˜' $˜Ð'gÐhlÐgmÐnØ�#�d�ˆ_  3¡=°Ð"6Ð7¸ðEô	Fð Ð$×%Ò%Ø�L‰LÐNÐQTÐZ^ÑV_ÐP`×aò7 $Cr   c                  óX   • [         R                   R                  5       R                  SS9$ )NÚseconds©Útimespec)Údatetimer§   Ú	isoformat© r   r   r¤   r¤   Ì  s%   € Ü×Ñ× Ñ Ó"×,Ñ,°iÐ,Ð@Ð@r   c                  óÆ   • [         R                   R                  [         R                  R                  5      R	                  SS9R                  5       R	                  SS5      $ )zuUTC, ISO, Z suffixed: the one clock the Dealer OS sync compares on,
since the Mac and Vercel are in different places.r   )Úmicrosecondz+00:00ÚZ)r²   r§   ÚtimezoneÚutcÚreplacer³   r´   r   r   Ú_utcnowr»   Ð  sI   € ô ×Ñ×!Ñ!¤(×"3Ñ"3×"7Ñ"7Ó8×@Ñ@ÈQÐ@ÐOß‰Y‹[Ÿ™ ¨3Ó/ð1r   c                 ó¾  • [        U5      n UR                  S5        [        5       nU  Hû  nUR                  SUR                  UR                  UR
                  UR                  UR                  UR                  UR                  UR                  UR                  UR                  UR                  UR                  UR                  UR                   (       a  SOSUR"                  UR$                  UR&                  UR(                  UR*                  UR,                  U45        Mý     UR/                  5         UR1                  5         g! UR1                  5         f = f)zEPhase 1 loads a fresh hand entered set each run, so clear and reload.zDELETE FROM carsa†  INSERT INTO cars
                   (reg, make, model, derivative, year, mileage, owners, grade,
                    reserve, cap_clean, distance_miles, engine, service_history,
                    vat_qualifying, glass_retail, cazana_retail, photo_url,
                    listing_url, source, actually_paid, loaded_at)
                   VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)r   r   N)r   ry   r¤   r8   rV   rW   Ú
derivativer%   r(   Úownersr)   r"   r#   Údistance_milesr`   Úservice_historyÚvat_qualifyingÚglass_retailr]   rC   Úlisting_urlÚsourceÚactually_paidr   r   )Úcarsr   r   r§   r�   s        r   Úreplace_carsrÇ   ×  sî   € ä�4‹=€DðØ�‰Ð'Ô(Ü‹fˆÛˆAØ�L‰LðIð —‘˜Ÿ™ §¡¨¯©°q·v±v¸q¿y¹yØ—‘˜1Ÿ7™7 A§I¡I¨q¯{©{¸A×<LÑ<LØ—‘˜1×,Ñ,°1×3C×3C©aÈØ—‘ §¡°!·+±+¸q¿}¹}Ø—‘˜1Ÿ?™?¨Cð	1öñ ð 	�‰Œà�
‰
�øˆ�
‰
�ús   �D,E
 Å
Ec                 ó€   • SR                  [        U =(       d    S5      R                  5       R                  5       5      $ ©NÚ )ÚjoinÚstrÚupperÚsplit)r8   s    r   Ú	_norm_regrÏ   ð  s+   € Ø�7‰7”3�s—y˜b“>×'Ñ'Ó)×/Ñ/Ó1Ó2Ð2r   )ÚCarwowr-   Ú	sale_datec                 ó  • U [         ;  d  U(       d  U$  [        R                  R                  U5      nUR                  5       S:X  a%  U[        R                  " SS9-   R                  5       $ U$ ! [         a    Us $ f = f)zìThe sale a lot read for sale_date really belongs to on platform: the
Monday after, when sale_date is a Sunday and the platform holds no
Sunday sale. Anything else, a blank or unreadable date included, is
returned as given. Pure, tested.é   r   ©Údays)ÚNO_SUNDAY_SALEr²   ÚdateÚfromisoformatÚ
ValueErrorÚweekdayÚ	timedeltar³   )r-   rÑ   Údays      r   Úplatform_sale_dayrÝ     sy   € ð
 ”~Ó%®YØÐðÜ�m‰m×)Ñ)¨)Ó4ˆð ‡{�{ƒ}˜ÓØ”h×(Ò(¨aÑ0Ñ0×;Ñ;Ó=Ð=ØÐøô	 ó ØÒðús   •A/ Á/A>Á=A>c                 óØ  • SR                  S[        [        5      -  5      nU R                  SU S3[        5      R	                  5       nU H›  n[        US   US   5      nXCS   :X  a  M  U R                  SUS   US   U45      R                  5       nU(       a6  U R                  S	US
   US   US   45        U R                  SUS   45        M…  U R                  SXCS   45        M�     g)al  Refile any sighting already logged under a Sunday for a platform with
no Sunday sale onto the Monday it really was (see NO_SUNDAY_SALE). When
the car was also logged on that Monday the two are one lot, so the
Monday row keeps any reserve or CAP clean only the Sunday row read, and
the Sunday row goes. Runs on every start and does nothing once no such
row is left.Ú,Ú?zeSELECT id, reg, platform, sale_date, reserve, cap_clean FROM sightings
           WHERE platform IN (z%) AND strftime('%w', sale_date) = '0'r-   rÑ   zCSELECT id FROM sightings WHERE reg=? AND platform=? AND sale_date=?r8   z^UPDATE sightings SET reserve=COALESCE(reserve, ?), cap_clean=COALESCE(cap_clean, ?) WHERE id=?r"   r#   r1   z DELETE FROM sightings WHERE id=?z+UPDATE sightings SET sale_date=? WHERE id=?N)rË   ÚlenrÖ   ry   r{   rÝ   r   )r   ÚmarksÚrowsr„   ÚmondayÚtwins         r   r‚   r‚     sÿ   € ð �H‰H�Sœ3œ~Ó.Ñ.Ó/€EØ�<‰<ðØ$˜gÐ%Jð	Näó÷ !™›
ð 	ó ˆÜ" 1 Z¡=°!°K±.ÓAˆØ�{‘^Ó#ÙØ�|‰|ØQØˆu‰X�q˜‘} fÐ-ó/ç/7©x«zð 	ö Ø�L‰LØpØ�9‘˜q ™~¨t°D©zÐ:ô<ð �L‰LÐ;¸aÀ¹g¸ZÖHà�L‰LÐFÈÐSWÑQXÐHYÖZò r   c                 ó¬  • [        X5      n[        U5      n [        5       nU  Hø  n[        [	        USS5      5      =(       d*    [
        R                  " [	        USS5      =(       d    S5      nU(       d  MS  UR                  SXqX&R                  UR                  UR                  UR                  [	        USS5      [	        USS5      UR                  U[	        USS5      [	        US	S5      =(       d    S[	        US
S5      =(       d    S[	        USS5      45        Mú     UR                  5         UR                  5         g! UR                  5         f = f)a]  Log every car seen on a platform for a given sale date. Cars without a
readable registration cannot be tracked for repeats, so they are skipped.
One row per car per platform per sale date, repeats on the same day ignored.

A car whose listing plate could not be read but whose original plate
was (Carwow's plate transfer field, Motorway's sellingVrm) is logged
under that original plate (Steven 2026-09-28: S30NNT on Carwow read as
never seen, because a listing with no readable plate was never logged
on any day it appeared). The list read runs before the detail pages,
so a reader calls this again once they are read; the same day's row is
ignored the second time round.

A platform with no Sunday sale files a Sunday read under the Monday it
is really for (platform_sale_day), so one lot read on Saturday evening
and again on Sunday is one sighting, not two.r8   rÊ   rw   a"  INSERT OR IGNORE INTO sightings
                   (reg, platform, sale_date, make, model, derivative, year,
                    reserve, cap_clean, listing_url, recorded_at,
                    mileage, engine, transmission, grade)
                   VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)r"   Nr#   r(   r`   ra   r)   )rÝ   r   r¤   rÏ   Úgetattrr   Útidyry   rV   rW   r½   r%   rÃ   r   r   )rÆ   r-   rÑ   r   r   r§   r�   r8   s           r   Úrecord_sightingsré   -  s  € ô  " (Ó6€IÜ�4‹=€DðÜ‹fˆÛˆAÜœG A u¨bÓ1Ó2×f´f·k²kÄ'È!È]Ð\^ÓB_×BeÐceÓ6fˆCÞÙØ�L‰Lð=ð
  	¯6©6°1·7±7¸A¿L¹LØ—‘œ  I¨tÓ4´g¸aÀÈdÓ6SØ—‘ Ü˜˜I tÓ,¬g°a¸À4Ó.H×.PÈDÜ˜˜N¨DÓ1×9°T¼7À1ÀgÈtÓ;Tð	Vöñ	 ð  	�‰Œà�
‰
�øˆ�
‰
�ús   ˜DE ÅEc                 ó   • [        U [        5      (       d  U c  U /O
[        U 5      n/ nU H/  n[        U5      nU(       d  M  Xd;  d  M  UR	                  U5        M1     U(       d  gUSL nU=(       d    [        U5      n SR                  S[        U5      -  5      n UR                  SU SU S3XD-   5      R                  5       n	U	 Hh  n
U
S   U
S	   4 HW  n[        R                  " U5      nU(       d  M"  [        R                  " U5      (       d  M?  Xd;  d  MF  UR	                  U5        MY     Mj     [        U5      U(       a  UR!                  5         $ $ ! [        R                   a    / n	 N©f = f! U(       a  UR!                  5         f f = f)
aØ  Every plate a car's sightings may be filed under (2026-09-28). reg is
one plate, or several (the listing plate and the original plate
underneath, in that order); each is widened with the other half of any
plate pair on file, so a car listed on a private plate one day and on
its original plate another, or with no readable listing plate at all,
still meets its own earlier sightings. Blank and not plate shaped
values are dropped, and () means there is nothing to look up.Nr´   rß   rà   z7SELECT reg, selling_vrm FROM plate_pairs WHERE reg IN (z) OR selling_vrm IN (r9   r8   rw   )Ú
isinstancerÌ   ÚlistrÏ   Úappendr   rË   rá   ry   r{   r   ÚOperationalErrorr   rè   Úis_plateÚtupler   )r8   r   r   ÚgivenÚoutÚvr›   Úownrâ   rã   r„   s              r   Úsighting_platesrõ   V  s^  € ô   ¤S×)Ñ)¨S©[ˆS‰E¼dÀ3»i€Eð €CÛˆÜ�a‹Lˆßˆ1�•Ø�J‰J�qŽMñ ö ØØ
�$ˆ,€CØ× ”7˜4“=€DðØ—‘˜œs 3›x™Ó(ˆð	Ø—<‘<ØIÈ%ÈÐPeÐfkÐelÐlmÐnØ‘	óç#™8›:ð ó
 ˆAØ˜‘h  -Ñ 0Ó1�Ü—K’K “N�ß�1œŸš¨×+Ó+°µØ—J‘J˜q–Mó 2ñ ô
 �S‹zæØ�J‰J�Lð øô ×'Ñ'ó 	ØŠDð	ûö Ø�J‰J�Lð úsB   Á?E3 Â)E Ã-E3 Ã7E3 ÄE3 Ä#E3 ÅE0Å-E3 Å/E0Å0E3 Å3FÚbefore_datec                 óH  • [        U 5      n U (       d  g[        U5      n U(       a"  UR                  SX45      R                  5       nO!UR                  SU 45      R                  5       nU(       a  [	        U5      OS UR                  5         $ ! UR                  5         f = f)a&  The most recent sighting on file for this registration (any platform),
optionally only counting a sighting on or before before_date (a car's
mileage only ever goes up, so a sighting from after a bid was won or
lost is not a safe stand in for its mileage at the time). None when
this reg has never been sighted. Used to backfill mileage, year, engine
and transmission for a lost bid or a purchase from BidBrain's own daily
reads, before ever falling back to a fresh live lookup (Mark 2026-08-25:
"populate this from our database as much as possible").NzTSELECT * FROM sightings WHERE reg=? AND sale_date<=? ORDER BY sale_date DESC LIMIT 1zCSELECT * FROM sightings WHERE reg=? ORDER BY sale_date DESC LIMIT 1)rÏ   r   ry   r   Údictr   )r8   rö   r   r   r¬   s        r   Úlatest_sighting_for_regrù   }  s�   € ô �C‹.€CÞØÜ�4‹=€DðÞØ—,‘,ð2Ø47Ð3EóGçGOÁxÃzñ ð —,‘,ð2Ø47°6ó;ç;C¹8»:ð ö  Œt�CŒy TÐ)à�
‰
�øˆ�
‰
�ús    AB ÂB!é   Úwindow_daysc           	      óš  • [        U5      n [        XS9nU(       d  / UR                  5         $ UR                  SSR	                  S[        U5      -  5       S3/ UQUPUPS[        U5       S3P75      R                  5       nU Vs/ s H  owS   US	   US
   S.PM     snUR                  5         $ s  snf ! UR                  5         f = f)aŠ  Every earlier time this registration was seen in the window before
sale_date, newest first, one row per sale day and platform:
[{sale_date, platform, reserve}]. For the card's Seen tab on Dealer OS
(Steven 2026-09-10: "if a car has been seen before i want to know how
many days ago and if theres more than one i want to see them all").
reg may carry both of a car's plates, see sighting_plates.©r   zoSELECT sale_date, platform, MAX(reserve) AS reserve
               FROM sightings
               WHERE reg IN (rß   rà   zŒ) AND sale_date < ? AND sale_date >= date(?, ?)
               GROUP BY sale_date, platform
               ORDER BY sale_date DESC, platformÚ-ú daysrÑ   r-   r"   )rÑ   r-   r"   )r   rõ   r   ry   rË   rá   Úintr{   )r8   rÑ   rû   r   r   Úregsrã   r„   s           r   Úsighting_historyr  ˜  sß   € ô �4‹=€DðÜ˜sÑ.ˆÞØð 	�
‰
�ð �|‰|ðà!Ÿh™h s¬S°«Y¡Ó7Ð8ð 91ð4ð
 GˆdÐF�IÐF˜yÐF¨A¬c°+Ó.>Ð-?¸uÐ*EÑFó
÷ ‰(‹*ð 	ñ lpÓpÒkoÐfg ™n¸!¸J¹-ÐTUÐV_ÑT`ÔaÑkoÑpà�
‰
�ùò qøà�
‰
�ús#   �B8 ¯AB8 ÂB3Â B8 Â3B8 Â8C
c                 ó2  • 0 / SS.n[        U5      n [        XS9nU(       d  UUR                  5         $ SUS'   SR                  S[	        U5      -  5      nUR                  SU S	3/ UQUPUPS
[        U5       S3P75      R                  5       nU H  n	U	S   (       d  M  U	S   US   U	S   '   M     UR                  SU S3/ UQUP75      R                  5       n
[        S U
 5       5      US'   UUR                  5         $ ! UR                  5         f = f)a  How many previous times this registration was seen on each platform in
the window before sale_date, plus whether it is on more than one platform
on sale_date itself.

Returns: {"previous": {platform: count}, "same_day_platforms": [..]}.
Counts distinct earlier sale dates, so two sightings on one day count once.
reg may carry both of a car's plates, see sighting_plates, and "known"
says whether there was any plate to look up at all: a car with none
has no history on file, which is not the same as a first sighting.
F)ÚpreviousÚsame_day_platformsÚknownrý   Tr  rß   rà   zkSELECT platform, COUNT(DISTINCT sale_date) AS n
               FROM sightings
               WHERE reg IN (zr)
                 AND sale_date < ?
                 AND sale_date >= date(?, ?)
               GROUP BY platformrþ   rÿ   r:   r  r-   zESELECT DISTINCT platform FROM sightings
               WHERE reg IN (z) AND sale_date = ?c              3   ó*   #   • U  H	  oS    v •  M     g7f)r-   Nr´   ©Ú.0r„   s     r   Ú	<genexpr>Ú!repeat_summary.<locals>.<genexpr>Ø  s   é € Ð*GÂ$¸Q¨Z®=Â$ùó   ‚r  )	r   rõ   r   rË   rá   ry   r   r{   Úsorted)r8   rÑ   rû   r   rò   r   r  râ   rã   r„   Úsames              r   Úrepeat_summaryr  ±  sE  € ð °¸eÑ
D€CÜ�4‹=€DðÜ˜sÑ.ˆÞØð4 	�
‰
�ð3 ˆˆG‰Ø—‘˜œs 4›y™Ó)ˆà�|‰|ðà#˜Wð %!ð$ð GˆdÐF�IÐF˜yÐF¨A¬c°+Ó.>Ð-?¸uÐ*EÑFó
÷ ‰(‹*ð 	ó ˆAØ��v‰vØ12°3±��J‘  *¡Ó.ñ ð
 �|‰|ðØ#˜WÐ$7ð;àˆdÐ�IÑó
÷ ‰(‹*ð	 	ô
 %+Ñ*GÁ$Ó*GÓ$GˆÐ Ñ!Øà�
‰
�øˆ�
‰
�ús   “D µA)D Â"AD ÄDÚsnapshot_datec                 óê   • [        U5      n UR                  SU45        U  H  u  pEUR                  SXU45        M     UR                  5         UR                  5         g! UR                  5         f = f)zStore today's Clickdealer in stock list, one row per vehicle as a
(make, model) pair. Re run safe: same date rows are replaced.z1DELETE FROM stock_snapshots WHERE snapshot_date=?zYINSERT INTO stock_snapshots (snapshot_date, make, model, in_stock_count) VALUES (?,?,?,1)N©r   ry   r   r   )Úpairsr  r   r   rV   rW   s         r   Úrecord_stock_snapshotr  Þ  se   € ô �4‹=€Dð	Ø�‰ÐHÈ=ÐJZÔ[Û ‰KˆDØ�L‰LØkØ eÐ,öñ !ð
 	�‰Œà�
‰
�øˆ�
‰
�ús   �AA  Á A2c                 ó˜  • SSK Jn  [        U 5      n UR                  S5      R	                  5       S   nU(       d  0 UR                  5         $ 0 nUR                  SU45       H>  nU" US   US   5      nU(       d  M  UR                  US5      US	   =(       d    S-   XF'   M@     UUR                  5         $ ! UR                  5         f = f)
z|The most recent stock snapshot as {model_key: count}. Empty dict when no
snapshot exists yet, so callers degrade gracefully.r   ©r6   ú3SELECT MAX(snapshot_date) AS d FROM stock_snapshotsÚdzMSELECT make, model, in_stock_count FROM stock_snapshots WHERE snapshot_date=?rV   rW   r   Úin_stock_count)Úreaders.clickdealerr6   r   ry   r   r   Úget)r   r6   r   ÚlastÚcountsr„   Úkeys          r   Úlatest_stock_countsr  î  s¼   € õ /Ü�4‹=€DðØ�|‰|ÐQÓR×[Ñ[Ó]Ð^aÑbˆÞØð 	�
‰
�ð ˆØ—‘Ø_ÐbfÐahöjˆAá˜A˜f™I q¨¡zÓ2ˆCßˆsØ$Ÿj™j¨¨aÓ0°AÐ6FÑ4G×4LÈ1ÑM�“ñ	jð
 à�
‰
�øˆ�
‰
�ús   “*B7 Á,B7 Á>(B7 Â7C	c                 óÔ   • [        U 5      n UR                  S5      R                  5       nU(       a  US   (       a  US   OS UR                  5         $ ! UR                  5         f = f)z’The date of the most recent stock snapshot (ISO string), or None. Used to
warn when the Clickdealer read has gone stale, so the gap flags are old.r  r  N©r   ry   r   r   ©r   r   r„   s      r   Úlatest_stock_dater#    sQ   € ô �4‹=€DðØ�L‰LÐNÓO×XÑXÓZˆÞ˜q Ÿvˆq�Šv¨4Ð/à�
‰
�øˆ�
‰
�ús   �6A ÁA'>   ÚCompleteÚ	DeliveredúChecks completeúDelivery requestedúReviewing documentsúAwaiting confirmationr;   )Úretail_estimaterA   r(   )	rC   rD   rE   rF   rX   rY   rb   r[   r\   c                 ó¶   • [        U5      nU(       a2  U R                  SU45      R                  5       nU(       a
  US   US   4$ [        U5      nU(       a  U$ S$ )zßThe make and model for a bought car: the newest sighting of its plate
(the very listing the card was drawn from, so the two agree), else the
name read the way Dealer OS and CompMatch read it. (None, None) when
neither says.zžSELECT make, model FROM sightings WHERE reg=? AND make IS NOT NULL AND make<>'' AND model IS NOT NULL AND model<>'' ORDER BY recorded_at DESC, id DESC LIMIT 1rV   rW   )NN)rÏ   ry   r   r   )r   r8   r!   ÚseenÚmms        r   Ú_purchase_make_modelr.  D  sf   € ô
 �C‹.€CÞ
Ø�|‰|ð]Ø_bÐ^dófçfnÑfnÓfpð 	ö Ø˜‘L $ w¡-Ð0Ð0Ü	˜Ó	€BÞˆ2Ð%˜Ð%r   c                 óÚ   • SnU R                  S5      R                  5        HE  n[        XS   US   5      u  p4U(       d  M   U(       d  M)  U R                  SX4US   45        US-  nMG     U$ )z¦Fill make and model on every purchase row still without them. Returns
how many rows were filled. Idempotent and cheap: only rows missing one
of the two are looked at.r   z^SELECT id, reg, name FROM purchases WHERE make IS NULL OR make='' OR model IS NULL OR model=''r8   r!   ú/UPDATE purchases SET make=?, model=? WHERE id=?r1   r   )ry   r{   r.  )r   r:   r„   rV   rW   s        r   r�   r�   T  sz   € ð 	
€AØ�\‰\ÐzÓ{÷  Eñ  Eö  GˆÜ*¨4°5±¸1¸V¹9ÓE‰ˆßˆ4—E�EØ�L‰LÐJÈTÐZ[Ð\`ÑZaÐLbÔcØ�‰FŠAñ	 Gð
 €Hr   c                 ó   • [         R                  " U5      [         R                  " U5      pT[         R                  " U5      (       a   [         R                  " U5      (       a  XE:X  a  gU=(       d
    [        5       nU R	                  SXEX345        g)z¡Remember that these two plates are the same car. Refuses anything
not plate shaped, and a plate paired with itself, so a bad read can
never widen a later search.Fz¦INSERT INTO plate_pairs (reg, selling_vrm, first_seen, last_seen) VALUES (?,?,?,?)
           ON CONFLICT(reg, selling_vrm) DO UPDATE SET last_seen=excluded.last_seenT)r   rè   rï   r¤   ry   )r   r8   rw   r§   ÚaÚbs         r   Ú_record_plate_pairr4  a  sk   € ô �;Š;�sÓœVŸ[š[¨Ó5€qÜ�OŠO˜A×Ñ¤6§?¢?°1×#5Ñ#5¸!»&ØØ
�-”“€CØ‡L�Lð	Wà	
ˆsÐôð r   c                 ó    • [        U5      n [        X0U5      nUR                  5         UUR                  5         $ ! UR                  5         f = f)zL_record_plate_pair with a connection of its own, for a caller
outside a run.)r   r4  r   r   )r8   rw   r   r   Úoks        r   Úrecord_plate_pairr7  p  s=   € ô �4‹=€DðÜ ¨;Ó7ˆØ�‰ŒØà�
‰
�øˆ�
‰
�ús	   �; »Ac                 ó.  ^• 0 mU4S jnU R                  S5       H8  n [        R                  " US   5      nU" US   UR                  S5      5        M:     U R                  S5       H  nU" US   US   5        M     T$ ! [        [        4 a     Mx  f = f)aÆ  {plate: the other plate} for every pair on file. plate_pairs is the
lasting record; the listing detail cache is read as well because it
already carried these pairs for Carwow before plate_pairs existed, and
it costs nothing (only rows that mention the field are parsed). Read
BOTH ways round on purpose: a purchase may be recorded under either
plate, depending which screen it came off, and either way the other
plate is the one DealerKit may be holding.c                 óì   >• [         R                  " U 5      [         R                  " U5      p[         R                  " U 5      (       a-  [         R                  " U5      (       a  X:w  a  XsTU '   TU'   g g g g r   )r   rè   rï   )r2  r3  rò   s     €r   ÚpairÚ_plate_pair_map.<locals>.pair…  sX   ø€ Ü�{Š{˜1‹~œvŸ{š{¨1›~ˆ1Ü�?Š?˜1×Ñ¤&§/¢/°!×"4Ñ"4¸»ØˆNˆC�‰F�C˜’Fð :@Ð"4Ðr   zvSELECT cache_key, detail_json FROM listing_detail_cache WHERE detail_json LIKE '%selling_vrm%' ORDER BY fetched_at ASCÚdetail_jsonÚ	cache_keyrw   z?SELECT reg, selling_vrm FROM plate_pairs ORDER BY last_seen ASCr8   )ry   ÚjsonÚloadsÚ	TypeErrorrÙ   r  )r   r:  r¬   r  rò   s       @r   Ú_plate_pair_maprA  |  s¦   ø€ ð €Cõ"ð �|‰|ðMöNˆð	Ü—
’
˜3˜}Ñ-Ó.ˆAñ 	ˆS�Ñ˜qŸu™u ]Ó3Ö4ñNð �|‰|Ð]Ö^ˆÙˆS�‰Z˜˜]Ñ+Ö,ñ _à€Jøô œ:Ð&ó 	Úð	ús   ŸB Â BÂBc                 ó  • U R                  S5      R                  5       nU(       d  g[        U 5      nSnU HO  nUR                  [        R
                  " US   5      5      nU(       d  M4  U R                  SXTS   45        US-  nMQ     U$ )aÎ  Fill selling_vrm on every purchase row still without one, from the
plate pairs on file (2026-09-20, see the column's own note in _migrate
and the plate_pairs table). Returns how many rows were filled.
Idempotent and cheap: only rows missing it are looked at, and the pairs
are only read when there is at least one such row. A row no pair covers
is simply left alone, same as make and model; nothing is ever guessed,
and a later read of that car fills it in then.zISELECT id, reg FROM purchases WHERE selling_vrm IS NULL OR selling_vrm=''r   r8   z-UPDATE purchases SET selling_vrm=? WHERE id=?r1   r   )ry   r{   rA  r  r   rè   )r   rã   r  r:   r„   Úothers         r   Ú_fill_purchase_selling_vrmrD  –  s‰   € ð �<‰<ØSóUßU]ÑU]ÓU_ð 	æØÜ˜DÓ!€EØ	€AÛˆØ—	‘	œ&Ÿ+š+ a¨¡hÓ/Ó0ˆßˆ5Ø�L‰LÐHÈ5ÐTXÑRYÐJZÔ[Ø�‰FŠAñ	 ð
 €Hr   c                 óœ   ^ • [        T [        5      (       a  T R                  OSU 4S jjn[        R                  " U" S5      U" S5      5      $ )zíEvery plate one purchase might be known by on DealerKit, the
recorded one first: the row's own reg, then the plate underneath. What
every DealerKit lookup searches on, so a car with two real plates is
found whichever one DealerKit holds.c                 ó:   >• U TR                  5       ;   a  TU    $ U$ r   )Úkeys)Úkr  r¬   s     €r   Ú<lambda>Ú!purchase_plates.<locals>.<lambda>±  s"   ø€ ÈQÐRU×RZÑRZÓR\Ë_À3ÀqÁ6ÐCcÐbcÐCcr   r8   rw   r   )rë   rø   r  r   Ú
candidates)r¬   r  s   ` r   Úpurchase_platesrL  ¬  s9   ø€ ô
   ¤T×*Ñ*ˆ#�'Š'Ö1c€CÜ×Ò™S ›Z©¨]Ó);Ó<Ð<r   c                 óP  • U(       d  gU R                  SX!45      R                  5       nU(       d  gSnU R                  SXSU S[        U Vs/ s H  owS   PM	     sn5       SU 3S	45        U H.  nU R                  SUS
   USU SU=(       d    S SU 3S	45        M0     gs  snf )ax  A win that lands on a plate some other LIVE sale already holds:
say so on both timelines at once, so a double win is on the record
the moment it happens rather than noticed weeks later when accounts
have already paid. A car with no readable plate cannot be matched to
anything, so it is never flagged. See db.double_wins for the rule and
for what is held back while it stands.NzQSELECT id, platform FROM purchases WHERE reg=? AND id<>? AND cancelled_at IS NULLzhNothing goes to accounts or to DealerKit for either sale until one of them is cancelled on its platform.úQINSERT INTO purchase_events (purchase_id, ts, kind, text, who) VALUES (?,?,?,?,?)Úsyncz was already won on r-   z", so this car has been won twice. r�   r1   z has now also been won on úanother platform)ry   r{   Ú_plain_list)r   Únew_idr8   r-   r§   ÚtwinsÚtailr›   s           r   Ú_note_double_winrU  µ  sã   € ö ØØ�L‰LØ[Ø	ˆóç‘x“zð 
ö Øð1€Dà‡L�LØ[Ø	�fØˆ5Ð$¤[ÉÓ1OÊÀA°J´-ÉÑ1OÓ%PÐ$Qð R,Ø,0¨6ð
3Ø4>ð	@ôAó
 ˆØ�‰Ø_Øˆt‰W�c˜6ØˆuÐ.¨x×/MÐ;MÐ.Nð O0Ø04¨vð7Ø8BðDö	Eò ùò 2Ps   ÁB#Fc                 óf  ^^^^^ • [        U5      n / n[        5       nSR                  [        5      nSR                  [        5      nSR                  [
        5      nU  GHß  mUR                  SU SU SU S3TS   [        TS   5      45      R                  5       mSn	T(       db  [        TR                  S5      5      (       aC  UR                  SU SU SU S	3[        TR                  S5      5      45      R                  5       n	U	(       GaN  [         V
s0 s H  oªXš   b  Xš   OTR                  U
5      _M     sn
m[
         V
s0 s H*  oªTR                  U
5      b  TR                  U
5      OXš   _M,     sn
m TR                  S
5      SLn[         V
s0 s H  oªU(       a  TR                  U
5      OXš   _M!     sn
mTR                  S5      =(       d    SnU(       a  U[        ;  a  UOU	S   =(       d    SnSR                  S / [        Q[        Q[
        Q7 5       5      nUR                  SU S3TS   [        TS   5      TR                  S5      TR                  S5      TR                  S5      TR                  S5      UTR                  S5      U(       a  TR                  S5      (       a  SOSOU	S   /	U4S j[         5       QU4S j[         5       QU 4S j[
         5       QU	S   P75        U	S   b	  SU	S   S 3OSnUR                  S U	S   US!TS    S"TS    S#U STS    S$3S%45        UR                  0 [        T5      ES&U	S   0E5        GM   T(       Ga  TS   =(       d    SnTR                  S5      =(       d    SnUS:H  =(       d	    U[        ;   nUS:g  =(       a	    U[        ;  nU(       a	  U(       a  UOUnTS   =(       d    TR                  S5      =(       d    Sn[        U4S' j[         5       5      nU=(       a    TS   (       + =(       a    TS(   SLnTR                  S
5      SLnUR                  S)TS   45      R                  5       S*   nU(       a|  U(       au  TS
   bo  TS
   U-
  n[        TS
   U-
  5      S+:  aR  TS    S,US S-3nUR                  S.TS   U45      R                  5       nU(       d  UR                  S TS   US!US%45        U(       d  U(       d  U(       d�  [         V
s0 s H  oªTU
   _M
     sn
mTS   nU(       aZ  TS(   cT  TR                  S(5      bB  TR                  S(5      TS('   TR                  S/5      TS/'   TR                  S5      (       a  SOSnOÓ[         V
s0 s H  oªTR                  U
5      _M     sn
mTR                  S5      (       a  SOSnU(       aŒ  TS
   b†  TS
   TS
   S+-
  :  aw  [!        TS
   TS
   -
  S05      nTS
   TS
'   UR                  S1TS   US2TS    3TS
   SSUS%45        UR                  S3TS   US4S5US6 S7TS    3STS
   S 3STS
   S 3S%45        [         V
s0 s H   n
U
TU
   b  TU
   OTR                  U
5      _M"     sn
m[
         V
s0 s H,  n
U
TR                  U
5      b  TR                  U
5      OTU
   _M.     sn
m SR                  S8 / [        Q[        Q[
        Q7 5       5      nUR                  S9U S:3TR                  S5      TR                  S5      TR                  S5      UUU/U4S; j[         5       QU4S< j[         5       QU 4S= j[
         5       QTS   P75        GM  UR                  S>U SU SU S?S@R                  SA[#        [        5      -  5       SBS@R                  SA[#        [        5      -  5       SBS@R                  SA[#        [
        5      -  5       SC3TS   [        TS   5      [        TR                  S5      5      TR                  S5      TR                  S5      TR                  S5      TR                  S5      TR                  S5      UTR                  S5      U(       a  UOSTR                  S5      (       a  SOS/U4SD j[         5       QU4SE j[         5       QU4SF j[
         5       Q75        [%        X3R                  SG5      R                  5       SH   [        TR                  S5      5      TR                  S5      U5        UR                  [        T5      5        GMâ     ['        U5        [)        U5        UR+                  5         UUR-                  5         $ s  sn
f s  sn
f s  sn
f s  sn
f s  sn
f s  sn
f s  sn
f ! UR-                  5         f = f)IaÀ  Store bought cars from the platform purchases screens. Returns the rows
that are NEW (not seen before), which are the ones to email to accounts.
Existing rows get their status refreshed, their name healed if it was
ever stored blank or as a status word (a name that already looks real is
never overwritten by a worse one), their bought_date filled in once, and
their payment breakdown replaced wholesale on every read UNTIL it is
confirmed (see _PURCHASE_PAYMENT_FIELDS), at which point it locks for
good. retail_estimate, cazana_115 and mileage fill in once and never
change after; everything in _PURCHASE_REFRESHABLE_FIELDS (photos,
documents, collection_date) always takes a fresh value when a read
actually finds one, since a stored url can go stale, a local file can
be replaced by a better one, and a scheduled date can become a
confirmed one or simply move. baseline=True marks new rows as already
emailed, used once to seed history without emailing it.ú, z1SELECT id, name, bought_date, payment_estimated, z2 FROM purchases WHERE platform=? AND vehicle_ref=?r-   Úvehicle_refNr8   z8SELECT id, name, price, bought_date, payment_estimated, zo FROM purchases WHERE reg=? AND COALESCE(manual_entry, 0) = 1 AND cancelled_at IS NULL ORDER BY id DESC LIMIT 1r<   r!   rÊ   c              3   ó*   #   • U  H	  o S 3v •  M     g7f©z=?Nr´   ©r	  Úfs     r   r
  Ú#record_purchases.<locals>.<genexpr>  ó   é € ð %xÚ%vð 34 s¨"¥XÚ%vùr  a!  UPDATE purchases SET platform=?, vehicle_ref=?, manual_entry=0, listing_url=?, status=?,
                          price=COALESCE(?, price), total_price=COALESCE(?, total_price), name=?,
                          bought_date=COALESCE(bought_date, ?), payment_estimated=?, emailed_at=NULL, z#
                        WHERE id=?rÃ   ÚstatusÚpriceÚtotal_priceÚbought_dateÚ	estimatedr   r   rB   c              3   ó.   >#   • U  H
  nTU   v •  M     g 7fr   r´   ©r	  r\  Úpayments     €r   r
  r]    ó   øé € ÐDÒ+C a�w˜q–zÒ+Cùó   ƒc              3   ó.   >#   • U  H
  nTU   v •  M     g 7fr   r´   ©r	  r\  rô   s     €r   r
  r]    ó   øé € Ð<Ò'; !�s˜1–vÒ';ùrh  c              3   ó.   >#   • U  H
  nTU   v •  M     g 7fr   r´   ©r	  r\  Úrefreshs     €r   r
  r]    ó   øé € ÐHÒ+G a�w˜q–zÒ+Gùrh  r1   r¢   r£   zno pricerN  rO  z9 now lists this car, so the row added by hand is now the zD purchase. Everything typed on it is kept; the hand typed price was z's figures now stand.r�   Ú
taken_overc              3   ó2   >#   • U  H  nTU   S Lv •  M     g 7fr   r´   )r	  r\  Úexistings     €r   r
  r]    s   øé € Ð!\ÒC[¸a (¨1¡+°TÕ"9ÒC[ùs   ƒr=   rš   r›   r™   u     now shows the chipped price (Â£z), matching the chips on filez<SELECT 1 FROM purchase_events WHERE purchase_id=? AND text=?r@   é   rœ   zPrice amended on rž   rŸ   r    r¡   u    Â· Price amended on c              3   ó*   #   • U  H	  o S 3v •  M     g7frZ  r´   r[  s     r   r
  r]  q  r^  r  z”UPDATE purchases SET status=?, price=?, total_price=COALESCE(?, total_price),
                          name=?, bought_date=?, payment_estimated=?, ú WHERE id=?c              3   ó.   >#   • U  H
  nTU   v •  M     g 7fr   r´   re  s     €r   r
  r]  x  rg  rh  c              3   ó.   >#   • U  H
  nTU   v •  M     g 7fr   r´   rj  s     €r   r
  r]  y  rk  rh  c              3   ó.   >#   • U  H
  nTU   v •  M     g 7fr   r´   rm  s     €r   r
  r]  z  ro  rh  zöINSERT INTO purchases (platform, vehicle_ref, reg, name, status,
                          price, total_price, bought_date, first_seen,
                          listing_url, emailed_at, payment_estimated, stock_status,
                          zd)
                        VALUES (?,?,?,?,?,?,?,?,?,?,?,?,'Due in',
                                rß   rà   z",
                                r9   c              3   óF   >#   • U  H  nTR                  U5      v •  M     g 7fr   ©r  ©r	  r\  r„   s     €r   r
  r]  Š  s   øé € ÐBÒ)A A�q—u‘u˜Q—x�xÒ)Aùó   ƒ!c              3   óF   >#   • U  H  nTR                  U5      v •  M     g 7fr   rz  r{  s     €r   r
  r]  ‹  s   øé € Ð>Ò)= A�q—u‘u˜Q—x�xÒ)=ùr|  c              3   óF   >#   • U  H  nTR                  U5      v •  M     g 7fr   rz  r{  s     €r   r
  r]  Œ  s   øé € ÐFÒ)E A�q—u‘u˜Q—x�xÒ)Eùr|  zSELECT last_insert_rowid() AS iÚi)r   r¤   rË   Ú_PURCHASE_PAYMENT_FIELDSÚ_PURCHASE_OWN_FIELDSÚ_PURCHASE_REFRESHABLE_FIELDSry   rÌ   r   rÏ   r  Ú_BAD_PURCHASE_NAMESrí   rø   Úanyr¦   Úroundrá   rU  r�   rD  r   r   )!rã   Úbaseliner   r   Únewr§   Úpay_colsÚown_colsÚrefresh_colsrp  r\  Úhas_new_paymentÚnew_nameÚ
final_nameÚset_colsÚtypedÚold_nameÚold_badÚnew_goodÚfinal_boughtÚhad_paymentÚold_confirmedÚ
chip_totalÚchippedÚnoticer,  Úfinal_estimatedÚdroprr  rô   rf  r„   rn  s!                               @@@@@r   Úrecord_purchasesr›  Ò  s*
  ü€ ô �4‹=€DðxØˆÜ‹fˆØ—9‘9Ô5Ó6ˆØ—9‘9Ô1Ó2ˆØ—y‘yÔ!=Ó>ˆÜˆAØ—|‘|ØCÀHÀ:ÈRÐPXÈzÐY[Ð\hÐ[ið jDð Eà�:‘¤ A mÑ$4Ó 5Ð6ó8÷ 9A¹»
ð ð ˆJÞ¤	¨!¯%©%°«,× 7Ñ 7ð "Ÿ\™\ØNÈxÈjÐXZÐ[cÐZdÐdfÐgsÐftð u/ð /ä1:¸1¿5¹5À»<Ó1HÐ0JóL÷ MUÉHËJð ÷ Ý^rÓsÒ^rÐYZ¨J©MÑ,E˜:š=È1Ï5É5ÐQRË8ÒSÑ^rÑs�Ý]yÓzÒ]yÐXY¨1¯5©5°«8Ñ+?˜qŸu™u QœxÀZÁ]ÒRÑ]yÑz�Ø"#§%¡%¨Ó"6¸dÐ"B�ÝXpÓqÒXpÐST®?˜qŸu™u QœxÀ
ÁÒMÑXpÑq�ØŸ5™5 ›=×.¨B�Þ*2°xÔGZÓ7Z™XÐblÐmsÑbt×bzÐxz�
ØŸ9™9ñ %xØ%vÔ'?Ð%vÔBVÐ%vÔYuÒ%vó%xó x�à—‘ðgàgoÐfpð q#ð&ð �z‘]¤C¨¨-Ñ(8Ó$9¸1¿5¹5ÀÓ;OÐQR×QVÑQVÐW_ÓQ`Ø—U‘U˜7“^ Q§U¡U¨=Ó%9¸:ÀqÇuÁuÈ]ÓG[Þ9H˜1Ÿ5™5 ×-Ñ-‘a±1ÈjÐYlÑNmð\ô EÕ+CÓDð\ô =Õ';Ó<ð	\ô
 IÕ+GÓHð\ð
 KUÐUYÑJZñ\ô
]ð <FÀgÑ;NÑ;Z˜"˜Z¨Ñ0°Ð6Ñ7Ð`j�Ø—‘ØgØ Ñ% s¨FØ˜*™�Ð&_Ð`aÐblÑ`mÐ_nð oQØQVÐPWÐWYÐZ[Ð\fÑZgÐYhÐh}ðàð!ô"ð —
‘
ÐFœd 1›gÐF |°ZÀÑ5EÑF×GßØ# FÑ+×1¨r�ØŸ5™5 ›=×.¨B�Ø" b™.×K¨HÔ8KÑ,K�Ø# r™>×Q¨hÔ>QÑ.Q�Þ*1¶h™XÀX�
Ø'¨Ñ6×V¸!¿%¹%ÀÓ:N×VÐRV�ä!Ô!\ÕC[Ó!\Ó\�ð
 !,× z°HÐ=PÑ4QÔ0Q× zÐV^Ð_mÑVnÐvzÐVz�Ø"#§%¡%¨Ó"6¸dÐ"B�ð "Ÿ\™\ØaØ˜d‘^Ð%ó'ç'/¡x£z°#ñ7�
ö ¦/°h¸}Ñ6MÑ6YØ& }Ñ5¸
ÑB�GÜ˜1˜]Ñ+¨gÑ5Ó6¸Ó=Ø%& z¡] Oð 4(Ø(/° ~Ð5Rð#T˜à#Ÿ|™|ØZØ% d™^¨VÐ4ó 6ç6>±h³jð ö  $Ø ŸL™Lð!5à!)¨$¡°°f¸fÀjÐ QôSö !¦J¶oÝ7OÓPÒ7O°! (¨1¡+š~Ñ7OÑP�GØ&.Ð/BÑ&C�Oö
 " h¨~Ñ&>Ñ&FÈ1Ï5É5ÐQ_ÓK`ÑKlØ23·%±%¸Ó2G˜ Ñ/Ø/0¯u©u°[Ó/A˜ Ñ,Ø/0¯u©u°[×/AÑ/A©!Àq˜øå4LÓMÒ4L¨q !§%¡%¨£(š{Ñ4LÑM�GØ+,¯5©5°×+=Ñ+=¡aÀ1�Oö (¨H°]Ñ,CÑ,OØ ! -Ñ 0°8¸MÑ3JÈTÑ3QÓ QÜ$ X¨mÑ%<¸qÀÑ?OÑ%OÐQRÓS˜Ø19¸-Ñ1H˜ Ñ.ØŸ™ðZà% d™^¨TÐ5FÀqÈÁ}ÀoÐ3VØ% mÑ4°d¸DÀ#ÀzðSôTð
 Ÿ™ð5à% d™^¨S°&Ø)¨$¨t¨Ð4IÈ!ÈJÉ-ÈÐYØ! (¨=Ñ"9¸$Ð!?Ð@ÀBÀqÈÑGWÐX\ÐF]ÐB^Ø'ð)ô*õ !5ó6Ú 4˜1ð ¨(°1©+Ñ*A˜8 Aš;ÀqÇuÁuÈQÃxÒOÙ 4ñ6�õ %AóBÚ$@˜qð ¨1¯5©5°«8Ñ+?˜qŸu™u QœxÀXÈaÁ[ÒPÙ$@ñB�ð  Ÿ9™9ñ %xØ%vÔ'?Ð%vÔBVÐ%vÔYuÒ%vó%xó x�à—‘ðGØGOÀjÐP[ð_à—U‘U˜8“_ a§e¡e¨G£n°a·e±e¸MÓ6JØ ¨ðZäDÕ+CÓDðZô =Õ';Ó<ðZô IÕ+GÓHð	Zð KSÐSWÉ.ñ	Z÷[ð —‘ðð $˜* B x j°°<°.ð A!à!$§¡¨#´Ô4LÓ0MÑ*MÓ!NÐ Oð P!Ø!$§¡¨#´Ô4HÓ0IÑ*IÓ!JÐ Kð L!Ø!$§¡¨#´Ô4PÓ0QÑ*QÓ!RÐ SÐSTðXð �z‘]¤C¨¨-Ñ(8Ó$9¼9ÀQÇUÁUÈ5Ã\Ó;RØ—U‘U˜6“] A§E¡E¨(£O°Q·U±U¸7³^ÀQÇUÁUÈ=ÓEYØ—U‘U˜=Ó)¨3Ø—U‘U˜=Ó)¶(©3ÀØŸ%™% ×,Ñ,‘Q°!ð	Hô
 CÕ)AÓBðHô ?Õ)=Ó>ðHô GÕ)EÓFñHôIô" ! §|¡|Ð4UÓ'V×'_Ñ'_Ó'aÐbeÑ'fÜ!*¨1¯5©5°«<Ó!8¸!¿%¹%À
Ó:KÈSôRà—
‘
œ4 ›7×#ñO ôV 	" $Ô'ô 	# 4Ô(Ø�‰ŒØà�
‰
�ùò tùÚzùâqùòD Qùò NùòB6ùòBøðX 	�
‰
�úsj   ’Df Ä$e;Ä=f Å1f Å9f Æ&fÆ=Mf Ôf
ÔA2f ÖfÖ!B8f Ù'fÚ f Ú3fÚ>J,f å;#f æf0c                 óH  • [        U 5      n UR                  S5      R                  5        Vs/ s H  n[        U5      PM     nn[	        U5      nU Vs/ s H  o"S   b  M
  US   U;  d  M  UPM     snUR                  5         $ s  snf s  snf ! UR                  5         f = f)a»  Rows the accounts email has not covered yet. A cancelled sale is
excluded outright (2026-08-27): emailing accounts about a car whose
sale has already fallen through would be a real wrong message to a
real inbox.

A DOUBLE WIN IS HELD BACK the same way (Steven 2026-09-20, "warn and
hold everything"): while two live sales share one registration,
neither goes to accounts, because telling them to pay twice for one
car is exactly the wrong message. Cancel one and the other is emailed
on the next run, on its own. Every live row is read here, not just
the unemailed ones, so a twin that went out days ago still holds the
new one back.

Nothing is stamped emailed while it is held, so nothing is lost.zHSELECT * FROM purchases WHERE cancelled_at IS NULL ORDER BY platform, idÚ
emailed_atr1   )r   ry   r{   rø   Údouble_winsr   )r   r   r„   ÚliveÚhelds        r   Úunemailed_purchasesr¡  �  sœ   € ô �4‹=€DðØ!%§¡ð$ó"%ç%-¡X£Zð"0ó 1ò "0˜A”�Q–ñ "0ˆð 1ô ˜4Ó ˆÙÓSš4�a \¡?“¸qÀ¹wÈdÑ?R—™4ÑSà�
‰
�ùò1ùò Tøà�
‰
�ús4   �!B ®BÁB Á	B
Á!B
Á,B
Á2B Â
B ÂB!c                 óÒ   • [        U5      n [        5       nU  H  nUR                  SX445        M     UR                  5         UR	                  5         g ! UR	                  5         f = f)Nz,UPDATE purchases SET emailed_at=? WHERE id=?)r   r¤   ry   r   r   )Úidsr   r   r§   r  s        r   Úmark_purchases_emailedr¤  ·  sN   € Ü�4‹=€DðÜ‹fˆÛˆAØ�L‰LÐGÈ#ÈÖRñ à�‰Œà�
‰
�øˆ�
‰
�ús   �6A ÁA&rH   rI   rJ   rK   rL   rN   rO   rP   rQ   )	r`  ÚphotosÚ	documentsÚretailÚdue_in_dater(   ÚfeerŸ   Ú	transportrR   rS   )ÚcheckinÚremovec                 ó‚  • [         U   n[        U5      n UR                  SU S3[        5       U 45        US:X  aO  SR	                  S [        [        R                  5       5      SS/-    5       5      nUR                  SU S3U 45        UR                  5         UR                  5         g	! UR                  5         f = f)
a  Stamp one of the two DealerKit event columns (checkin, remove) with
now, by purchase id. A removal also clears dealerkit_pushed_at and
every per item stamp, since nothing is on DealerKit any more: the
stamps say what is THERE, and a later re send starts clean.úUPDATE purchases SET ú=? WHERE id=?r¬  rW  c              3   ó*   #   • U  H	  o S 3v •  M     g7f)z=NULLNr´   ©r	  r�   s     r   r
  Ústamp_dk.<locals>.<genexpr>Û  s   é € ð   Dò  6C° # U¥ò  6Cùr  rG   rR   ru  N)
Ú_DK_EVENT_COLUMNSr   ry   r¤   rË   rì   Ú_DK_ITEM_COLUMNSÚvaluesr   r   )Úpurchase_idr  r   r‡   r   Úcleareds         r   Ústamp_dkr¸  Ñ  s»   € ô
 ˜CÑ
 €CÜ�4‹=€DðØ�‰Ð,¨S¨E°Ð?Ä$Ã&È+ÐAVÔWØ�(‹?Ø—i‘iñ   D´TÔ:J×:QÑ:QÓ:SÓ5TÐXmð  pBð  XCò  6Có   Dó  DˆGØ�L‰LÐ0°°	¸ÐEÈÀ~ÔVØ�‰Œà�
‰
�øˆ�
‰
�ús   –BB, Â,B>rÊ   c                 óä  • [        U5      n UR                  SU 45      R                  5       nUc  [        SU  35      eUR                  SX45      R                  5       nUc  [        SU SU  35      e[	        U5      nUR                  SU45        [        U5      nSnUR                  S	U 45      R                  5        H(  n	UR                  S
Ub  Xx-
  OSU	S   45        X‰S   -  nM*     UR                  5         UR                  5         UR                  S5      =(       d    Sn
[        U SSU
S S3UR                  S5      (       a  SUS    3OS-   S-   X#S9  U$ ! UR                  5         f = f)u�  Take a chip off a car again (2026-09-16, Steven: Donna recorded the
same Â£500 chip twice on FJ67WFR, once on inspection and once at the
check in box). The chip row goes, the chips left have their frozen
bid_before recomputed in order (each is the original less the chips
before it), and the timeline says what came off. Returns the chip
removed. Raises ValueError when the chip is not this car's.ú"SELECT * FROM purchases WHERE id=?Núno purchase with id z9SELECT * FROM purchase_chips WHERE id=? AND purchase_id=?zno chip z on purchase z%DELETE FROM purchase_chips WHERE id=?ç        zQSELECT id, amount FROM purchase_chips WHERE purchase_id=? ORDER BY created_at, idz1UPDATE purchase_chips SET bid_before=? WHERE id=?r1   Úamountr   rŸ   u
   Chip of Â£r¡   z removedÚreasonõ    Â· rÊ   z, the price goes back up©Úwhor   )r   ry   r   rÙ   rø   Ú_bid_originalr{   r   r   r  Úlog_purchase_event)r¶  Úchip_idrÁ  r   r   r¬   rŸ   ÚbaseÚpriorr�   Úamts              r   Úremove_chiprÈ  â  sš  € ô �4‹=€DðØ�l‰lÐ?À+ÀÓP×YÑYÓ[ˆØ‰;ÜÐ3°K°=ÐAÓBÐBØ�|‰|ÐWÐZaÐYoÓp×yÑyÓ{ˆØ‰<Ü˜x¨ y°¸k¸]ÐKÓLÐLÜ�D‹zˆØ�‰Ð<¸w¸jÔIÜ˜SÓ!ˆØˆØ—‘ÐqÐtð  tBó  C÷  Lñ  Lö  NˆAØ�L‰LÐLØ,0Ñ,<˜4š<À$ÈÈ$ÉÐPôRà�x‘[Ñ ŠEñ Nð 	�‰Œà�
‰
ŒØ
�(‰(�8Ó
×
! €CÜ�{ FØ# C¨ :¨XÐ6ÐUY×U]ÑU]Ð^f×UgÑUg¸DÀÀhÁÐ@PÑ:QÐmoÑpð  tNñ  NØò+ð €Køð 	�
‰
�ús   �C/E ÅE/c                 óÆ   • [         U   n[        U5      n UR                  SU S3U 45        UR                  5         UR	                  5         g! UR	                  5         f = f)z­Un-stamp one per item column, so the next automatic DealerKit run
sees it as pending again (a new chip on an already chipped car, a
transport fee changed after it was sent).r®  z=NULL WHERE id=?N)r´  r   ry   r   r   )r¶  r  r   r‡   r   s        r   Úclear_dk_stamprÊ    sS   € ô ˜3Ñ
€CÜ�4‹=€DðØ�‰Ð,¨S¨EÐ1AÐBÀ[ÀNÔSØ�‰Œà�
‰
�øˆ�
‰
�ús   –'A ÁA c                 óâ   • U R                  S5      (       a  gU R                  S5      (       a  gU R                  S5      n[        [        U[        5      =(       a    UR                  S5      5      $ )zþWhether this purchase has a live DealerKit record as far as the Mac
knows: it was sent (dealerkit_pushed_at) and not since removed
(dk_removed_at), or a check found a record. The Mac's own belief, not
a live read; check_dealerkit_record is the live read.rS   FrG   TrM   Ú
vehicle_id)r  Úboolrë   rø   )r¬   Údcs     r   Úon_dealerkitrÏ    sW   € ð
 ‡w�wˆ×ÑØØ
‡w�wÐ$×%Ñ%ØØ	�‰�Ó	€BÜ”
˜2œtÓ$×=¨¯©°Ó)=Ó>Ð>r   c                 óÜ  • [        5       nUb  UO[        [        5      n[        U5      n UR	                  SX@45        U H#  n[        U   nUR	                  SU S3X@45        M%     U=(       d    0  HB  nUS;   d  M  Xu;   d  SU;   d  M  X7   c  M  UR	                  SU S3[        X7   5      U 45        MD     UR                  5         UR                  5         g! UR                  5         f = f)	aÍ  Record that this purchase has been pushed into DealerKit as a Due In
record, so a later run does not push it again. items is an optional
subset of "price"/"photos"/"documents" (see dealerkit_write.push_purchase),
stamping only the matching per item columns; None (the default) stamps
all three, a genuine full push. sent ({"price": x, "fee": y}) remembers
the exact figures that went across for the items among them, or for a
chip, which sets both (2026-09-14).Nz6UPDATE purchases SET dealerkit_pushed_at=? WHERE reg=?r®  z=? WHERE reg=?)r`  r©  rŸ   zUPDATE purchases SET dk_z_sent=? WHERE reg=?)r¤   rð   r´  r   ry   Úfloatr   r   )	r8   Úitemsr   Úsentr§   Úwantr   r  r‡   s	            r   Úmark_dealerkit_pushedrÕ    sÞ   € ô ‹&€CØÑ%‰5¬5Ô1AÓ+B€DÜ�4‹=€Dð
Ø�‰ÐMÐPSÈzÔZÛˆCÜ" 3Ñ'ˆCØ�L‰LÐ0°°°^ÐDÀsÀjÖQñ ð —J˜B’JˆCØÐ&Õ&¨C«K¸6ÀT½>ÈtÉyÓOdØ—‘Ð7¸°uÐ<OÐPÔSXÐY]ÑYbÓScÐehÐRiÖjñ ð 	�‰Œà�
‰
�øˆ�
‰
�ús   «AC Á=	C Â
C Â7C ÃC+c                 óð  • U R                  S5      (       a  U R                  S5      (       a  / $ U R                  S5      (       a  U R                  S5      (       d  / $ / nSSU R                  S5      4SS	U R                  S
5      44 Hk  u  p#nU R                  U5      nUb  Uc  M  [        [        U5      [        U5      -
  5      S:¼  d  ME  UR                  U[        U5      [        U5      45        Mm     U$ )aJ  What DealerKit still holds wrong (2026-09-14): the price or the
buyer's fee on file now against the exact figure last sent, as
[(what, sent, now)], empty when nothing has moved or nothing was ever
sent. A chip changes the price too, but the chip follow up owns that
until it has gone across, so a pending chip means no drift here.rG   re   r­   rP   r`  rT   Ú	car_pricer©  rU   Úeffective_feer   )r  r¦   rÑ  rí   )r¬   rò   ÚwhatÚsent_colÚnow_valrÓ  s         r   Údk_figures_driftrÜ  5  sÞ   € ð �7‰7Ð(×)Ñ)¨S¯W©W°^×-DÑ-DØˆ	Ø
‡w�wˆw×Ñ §¡Ð(;× <Ñ <Øˆ	Ø
€CØ%,¨o¸s¿w¹wÀ{Ó?SÐ$TØ%*¨M¸3¿7¹7À?Ó;SÐ$Tó$VÑˆ˜à�w‰w�xÓ ˆØ‰<˜7™?ÙÜŒu�T‹{œU 7›^Ñ+Ó,°Õ1Ø�J‰J˜œe D›k¬5°«>Ð:Ö;ñ$Vð €Jr   c                 ó˜  • U R                  S5      (       a  U R                  S5      (       a  gU R                  S5      c  gU R                  S5      (       d  U R                  S5      b  gU R                  S5      (       a  U R                  S5      (       d  gU R                  S	5      =(       d    0 nU(       a  UR                  S
5      b  gg)a  A car sent to DealerKit whose purchase price never landed there
(2026-09-16, ND66XYJ and FJ67WFR: the box at creation did not save it
and nothing ever retried): on DealerKit, not cancelled, a price on
file, nothing ever stamped or sent for the price, no chip waiting,
and the Mac's last look at DealerKit, when it has had one, found no
purchase cost there. The automatic run fills it in; a price DealerKit
already holds is never overwritten by this (see push_followup's
fill_only), only by a press of Update the price and fee.rG   re   Fr×  rH   rT   r­   rP   rM   Úpurchase_cost_valueTrz  )r¬   Úchecks     r   Údk_figures_missingrà  J  s¤   € ð �7‰7Ð(×)Ñ)¨S¯W©W°^×-DÑ-DØØ
‡w�wˆ{ÓÑ#ØØ
‡w�wÐ#×$Ñ$¨¯©°Ó(@Ñ(LØØ
‡w�wˆw×Ñ §¡Ð(;× <Ñ <ØØ�G‰G�JÓ×% 2€EÞ�—‘Ð0Ó1Ñ=ØØr   c                 ó  • U(       a  [        U5      O0 n[        5       US'   [        U5      n UR                  S[        R
                  " U5      U 45        UR                  5         UR                  5         g! UR                  5         f = f)ae  Record what dealerkit_write.check_dealerkit_record actually found on
the real DealerKit record for reg just now (2026-08-26, "check DK
record"/"check all DK records"). result is that function's own dict,
or None when the reg has no DealerKit record at all yet, either way
stored as JSON with a checked_at timestamp folded in, decoded back in
list_purchases.Ú
checked_atz+UPDATE purchases SET dk_check=? WHERE reg=?N)rø   r¤   r   ry   r>  Údumpsr   r   )r8   Úresultr   Úpayloadr   s        r   Úset_dealerkit_checkræ  a  sf   € ö %Œd�6Œl¨"€GÜ ›F€GˆLÑÜ�4‹=€DðØ�‰ÐBÄTÇZÂZÐPWÓEXÐZ]ÐD^Ô_Ø�‰Œà�
‰
�øˆ�
‰
�ús   ®8A7 Á7B	c                 ó¬   • [        U5      n UR                  SX45        UR                  5         UR                  5         g ! UR                  5         f = f)Nz1UPDATE purchases SET retail_estimate=? WHERE id=?r  )r¶  r§  r   r   s       r   Úset_purchase_retailrè  r  s>   € Ü�4‹=€DðØ�‰ÐHÈ6ÐJ_Ô`Ø�‰Œà�
‰
�øˆ�
‰
�úó   �#A ÁAc                 óp  • [        U5      n UR                  SU 45      R                  5       nU(       a  US   OSnXq:X  a   UR                  5         gUR                  SX45        UR	                  5         UR                  5         [        U SU=(       d    Uc  SOSSXqX#S9  g! UR                  5         f = f)	ai  Mark's own manual Retail est., set from the purchases page row
(2026-08-26, "add the ability to edit it from the row if we want to
price it manually lower"). Always wins over both the formula's own
stored retail_estimate and the settings retail_price_cap, see
list_purchases. value=None clears the override, reverting to the
capped or plain formula figure.

Logged to the timeline like every other hand edit (2026-08-31): this
and the due in date below were the only two money/date figures a
person could change without leaving a trace, which was odd on its own
and untenable once the manual money fields joined them.z9SELECT retail_estimate_override FROM purchases WHERE id=?rc   Nz:UPDATE purchases SET retail_estimate_override=? WHERE id=?ÚfieldzRetail est. clearedzRetail est. set by hand©rë  ÚwasrÛ  rÁ  r   ©r   ry   r   r   r   rÃ  )r¶  ÚvaluerÁ  r   r¾  r   r¬   Úolds           r   Úset_retail_estimate_overriderñ  {  s®   € ô �4‹=€Dð	Ø�l‰lÐVØ'˜>ó+ß+3©8«:ð 	æ14ˆcÐ,Ò-¸$ˆØ‹<Øð 	�
‰
�ð 	�‰ÐQÐTYÐSgÔhØ�‰Œà�
‰
Œô �{ GØ×h¸5¹=Ñ"7ÐNgØ7¸SØó+øð 	�
‰
�ús   �4B# Á#B# Â#B5c                 ó^  • [        U5      n UR                  SU 45      R                  5       nU(       a  US   OSnXa:X  a   UR                  5         gUR                  SX45        UR	                  5         UR                  5         [        U SUc  SOSSXaX#S9  g! UR                  5         f = f)	a{  Mark's own manual Due in date, set from the purchases page row
(2026-08-27, "make the due in editable like the retail est"). Always
wins over whatever the platform itself read, see list_purchases.
value=None clears the override, reverting to the platform's own
collection_date (or blank, if it never read one). Logged to the
timeline, same reasoning as the retail override above.z9SELECT collection_date_override FROM purchases WHERE id=?rd   Nz:UPDATE purchases SET collection_date_override=? WHERE id=?rë  zDue in date clearedzDue in date set by handrì  rî  )r¶  rï  rÁ  r   r   r¬   rð  s          r   Úset_collection_date_overrideró  š  s§   € ô �4‹=€Dð	Ø�l‰lÐVØ'˜>ó+ß+3©8«:ð 	æ14ˆcÐ,Ò-¸$ˆØ‹<Øð 	�
‰
�ð 	�‰ÐQÐTYÐSgÔhØ�‰Œà�
‰
ŒÜ�{ GØ05±Ñ,ÐC\Ø7¸SØó+øð 	�
‰
�ús   �4B Á#B ÂB,c                 ó¬   • [        U5      n UR                  SX45        UR                  5         UR                  5         g! UR                  5         f = f)zvThe raw Cazana retail times 1.15, unrounded, a reference figure shown
next to retail_estimate on the purchases report.z,UPDATE purchases SET cazana_115=? WHERE id=?Nr  )r¶  rï  r   r   s       r   Úset_purchase_cazana115rõ  ²  s@   € ô �4‹=€DðØ�‰ÐCÀeÐEYÔZØ�‰Œà�
‰
�øˆ�
‰
�úré  ÚStatuszOn finance?ÚVideozPerson collectingÚNotesz	Paid for?zCollection arrangedzLogbook here?ÚLocationÚRegistrationÚVehicleÚMileagezSells on plate)rh   ri   rj   rk   rl   rr   rm   rn   ro   r8   r!   r(   rw   zPurchase priceÚFeeÚ	TransportÚVATzPurchase dateÚSupplierzVAT on the feezVAT on the transportÚPhotos)	r`  r=   r>   r@   rb  rq   rt   ru   rE   é(   )r`  r=   r>   r@   r=   r>   )rt   ru   gš™™™™™É?c                 ó&  • U R                  SU45      R                  5       nUb  US   c  US   c  gSn[        R                  5        H'  u  pEX$   (       d  M  X2U   =(       d    S[        -  -  nM)     [        US5      nU R                  SX145        g)	aU  Rewrite a hand added car's vat_total from its own per fee VAT flags
(2026-08-31). VAT is MAINTAINED here rather than derived on read, so
every existing reader (the receipt, the table, the DealerKit push, the
accounts email) keeps working untouched with no new derivation logic
anywhere, and the stored column is never out of step with the flags.

A row with both flags NULL is left completely alone: that is every
platform row, whose own invoiced VAT must stay exactly as read, and any
manual row added before per fee VAT existed, whose typed lump sum is
still the only figure anyone ever gave it.z\SELECT fee_has_vat, transport_has_vat, motorway_fee, transport_fee FROM purchases WHERE id=?Nrt   ru   r¼  r   rs  z+UPDATE purchases SET vat_total=? WHERE id=?)ry   r   Ú_MANUAL_VAT_FLAGSrÒ  ÚVAT_RATEr…  )r   r¶  r¬   ÚtotalÚflagÚ
amount_cols         r   Ú_recompute_manual_vatr	    s›   € ð �,‰,ð 3Ø5@°NóDßDLÁHÃJð à
�{�s˜=Ñ)Ñ1°cÐ:MÑ6NÑ6VØØ€EÜ-×3Ñ3Ö5ÑˆØ�9‰9Ø˜*‘o×*¨¬hÑ6Ñ6ŠEñ 6ô �%˜‹O€EØ‡L�LÐ>ÀÐ@TÕUr   c                 ó    • [        U [        5      (       a  U R                  S5      OU S   nU(       a  0 [        E[        E$ [        [        5      $ )z‚The fields this particular row allows a person to edit: the shared
list, plus the manual only ones when the row was added by hand.rs   )rë   rø   r  Ú_PURCHASE_EDITABLE_FIELDSÚ_MANUAL_EDITABLE_FIELDS)r¬   Úmanuals     r   Úeditable_fields_forr  "  sF   € ô *4°C¼×)>Ñ)>ˆc�g‰g�nÔ%ÀCÈÑDW€FÞØGÔ+ÐGÔ/FÐGÐGÜÔ)Ó*Ð*r   c	                 ó   • [        U5      n	 U	R                  SU [        5       XUUc  SO
[        U5      Uc  SO
[        U5      Xg4	5        U	R	                  5         U	R                  5         g! U	R                  5         f = f)zÌOne timeline entry. kind is field / comment / milestone / sync /
chip / cancel. was and now_val carry the before and after for a field
change, body carries a comment's or a cancellation reason's own text.zpINSERT INTO purchase_events (purchase_id, ts, kind, text, field, was, now, body, who) VALUES (?,?,?,?,?,?,?,?,?)N)r   ry   r¤   rÌ   r   r   )
r¶  ÚkindÚtextrë  rí  rÛ  ÚbodyrÁ  r   r   s
             r   rÃ  rÃ  +  so   € ô
 �4‹=€Dð	Ø�‰ð)àœ$›& $¨eØ‘[‰T¤c¨#£hØ‘_‰T¬#¨g«,¸ðCô	Dð 	�‰Œà�
‰
�øˆ�
‰
�ús   �AA+ Á+A=c                 óä   • [        U5      n UR                  SU 45      R                  5        Vs/ s H  n[        U5      PM     snUR	                  5         $ s  snf ! UR	                  5         f = f)z1The full timeline for one purchase, newest first.zKSELECT * FROM purchase_events WHERE purchase_id=? ORDER BY ts DESC, id DESC©r   ry   r{   rø   r   ©r¶  r   r   r„   s       r   Úpurchase_eventsr  =  sj   € ä�4‹=€DðØ!%§¡ØYØˆNó"ç$™H›Jð"'ó (ò "'˜A”�Q–ñ "'ñ (ð 	�
‰
�ùò	(øð 	�
‰
�úó   �#A °AÁA ÁA ÁA/c                 óà   • [        U 5      n UR                  S5      R                  5        Vs/ s H  n[        U5      PM     snUR	                  5         $ s  snf ! UR	                  5         f = f)z{Every purchase's timeline in one list, newest first, for the Dealer
OS push (2026-09-02). Each row carries its purchase_id.z7SELECT * FROM purchase_events ORDER BY ts DESC, id DESCr  r"  s      r   Úall_purchase_eventsr  H  sn   € ô �4‹=€DðØ!%§¡ØEó"GßGOÁxÃzð"Ró Sò "R˜A”�Q–ñ "Rñ Sð 	�
‰
�ùòSøð 	�
‰
�úó   �!A ®AÁA ÁA ÁA-c           
      óÎ  • [        U5      n UR                  SU 45      R                  5       nUc  [        SU  35      e [        R
                  " US   =(       d    S5      n[        U[        5      (       d  / nU Vs/ s HA  n[        U[        5      (       d  M  [        R                  R                  U5      U:X  d  M?  UPMC     nnU(       aP  UR                  S[        R                  " U Vs/ s H  ofU;  d  M
  UPM     sn5      U 45        UR                  5         UUR                  5         $ ! [        [        4 a    / n Nâf = fs  snf s  snf ! UR                  5         f = f)a>  Forget a document Dealer OS has taken back (2026-10-05, S77RUU: a
MotorCheck report on a private plate that had moved to another car).
Every path on the row whose file name is `file_name` comes off the
row's dealer_os_documents, so it never goes to DealerKit. Returns the
paths taken off; the caller deletes the files.ú4SELECT dealer_os_documents FROM purchases WHERE id=?r»  rZ   ú[]ú5UPDATE purchases SET dealer_os_documents=? WHERE id=?)r   ry   r   rÙ   r>  r?  r@  rë   rì   rÌ   r   r   Úbasenamerã  r   r   )r¶  Ú	file_namer   r   r¬   ÚhaveÚpÚgones           r   Úremove_dealer_os_documentr$  S  s/  € ô �4‹=€DðØ�l‰lÐQÐT_ÐSaÓb×kÑkÓmˆØ‰;ÜÐ3°K°=ÐAÓBÐBð	Ü—:’:˜cÐ"7Ñ8×@¸DÓAˆDô ˜$¤×%Ñ%ØˆDÙÓ[š4�a¤:¨a´×#5“¼"¿'¹'×:JÑ:JÈ1Ó:MÐQZÑ:Z—™4ˆÐ[ÞØ�L‰LÐPÜŸ*š*±Ó%G²¨AÀ$¹§a±Ñ%GÓHÈ+ÐVôXà�K‰KŒMØà�
‰
�øô œ:Ð&ó 	ØŠDð	üò \ùò &Høð 	�
‰
�úsY   �2E Á "D0 Á"E Á=EÂ!EÂ?EÃ)E Ã.	EÃ;EÄE Ä0EÅE ÅEÅE ÅE$c                 ó  • [        U5      n UR                  SU 45      R                  5       nUc  [        SU  35      e [        R
                  " US   =(       d    S5      n[        U[        5      (       d  / nX;  aI  UR                  U5        UR                  S[        R                  " U5      U 45        UR                  5         UUR                  5         $ ! [        [        4 a    / n N‹f = f! UR                  5         f = f)z§Remember a document fetched from Dealer OS for this car (2026-09-16):
the local file's path joins the row's dealer_os_documents list, once.
Returns the list as stored.r  r»  rZ   r  r  )r   ry   r   rÙ   r>  r?  r@  rë   rì   rí   rã  r   r   )r¶  Ú
local_pathr   r   r¬   r!  s         r   Úadd_dealer_os_documentr'  n  sê   € ô �4‹=€DðØ�l‰lÐQÐT_ÐSaÓb×kÑkÓmˆØ‰;ÜÐ3°K°=ÐAÓBÐBð	Ü—:’:˜cÐ"7Ñ8×@¸DÓAˆDô ˜$¤×%Ñ%ØˆDØÓ!Ø�K‰K˜
Ô#Ø�L‰LÐPÔSW×S]ÒS]Ð^bÓScÐepÐRqÔrØ�K‰KŒMØà�
‰
�øô œ:Ð&ó 	ØŠDð	ûð 	�
‰
�ús0   �2C1 Á "C Á"A&C1 ÃC.Ã+C1 Ã-C.Ã.C1 Ã1Dc                 óÔ  • [        U5      n UR                  SU 45      R                  5       nUc  [        SU  35      e[	        U5      nX;  a  [        U S35      eUS:X  a  [        U5      nOûUS:X  ab  [        R                  " U5      nU(       a)  [        R                  " U5      (       d  [        U S35      eU(       a  U[        US   5      :X  a  SnO“US:X  d
  U[        ;   a  US	;   a  S
OSnOxUS:X  a  US;   a  SO
[        U5      nO^US:X  a   [        R                  " [        U5      5      nO8U[        ;   a.  US;   a  SnO%[        U5      nU[        U5      :X  a  [        U5      nUS:X  a6  UR                  SU 45      R                  5       S   nU(       a  [        S5      eXa   n	US:X  aM   [        R                   " U	=(       d    S5      n
U
[        R                   " U5      :X  a   UR%                  5         gO#X’:X  d  U	S;   a  US;   a   UR%                  5         gUS:X  a7  [        R                   " U5      nUR                  SX+(       a  US   OSU 45        O2US:X  a  UR                  SX"U 45        OUR                  SU S3X 45        U[        ;   d  U[        R'                  5       ;   a  [)        XP5        UR+                  5         UR%                  5         US:X  aF  [-        W
5      n[-        [        R                   " U5      5      n[/        U SSU SU 3UU S3U S3X4S9  g[/        U SXq    S3UX’X4S9  g! ["        [        4 a    / n
 GN�f = f! UR%                  5         f = f)aã  The single mutation point for a person editing a purchase. Refuses
anything the row itself does not allow (see editable_fields_for: the
shared list, widened to the money and supplier fields only on a hand
added row), writes the change, and logs it to the timeline with the
before and after. A no op change (same value) writes nothing and logs
nothing.

Which list applies is decided from the row, so the check has to happen
after it is loaded rather than up front the way it used to.rº  Nr»  z is not editabler8   rw   z# does not look like a registration.rr   ©r   TÚ1Útruer   r   r(   rÉ   rE   r`  rš   r›   z^this car has a chip on it, so its price is frozen. Record another chip to change what it cost.r  z9UPDATE purchases SET photo_urls=?, photo_url=? WHERE id=?z6UPDATE purchases SET price=?, winning_bid=? WHERE id=?r®  r¯  rë  zPhotos changed, z to z photosrì  z changed)r   ry   r   rÙ   r  rÏ   r   rè   rï   r  r   r>  rã  Ú_photo_linksÚ_MANUAL_MONEY_FIELDSrÑ  r?  r@  r   rµ  r	  r   rá   rÃ  )r¶  rë  rï  rÁ  r   r   r¬   Úallowedr—  rð  Úold_listÚlinksÚwas_nÚnow_ns                 r   Úset_purchase_fieldr3  †  se  € ô �4‹=€DðRØ�l‰lÐ?À+ÀÓP×YÑYÓ[ˆØ‰;ÜÐ3°K°=ÐAÓBÐBÜ% cÓ*ˆØÓÜ ˜wÐ&6Ð7Ó8Ð8Ø�E‹>Ü˜eÓ$‰EØ�mÓ#ô —K’K Ó&ˆEÞœVŸ_š_¨U×3Ñ3Ü  E 7Ð*MÐ!NÓOÐOÞ˜¤)¨C°©JÓ"7Ó7Ø�øØ�jÓ  EÔ->Ó$>ØÐ"8Ó8‘A¸a‰EØ�iÓØ! ZÓ/‘D´S¸³Z‰EØ�lÓ"Ü—J’Jœ|¨EÓ2Ó3‰EØÔ*Ó*ð ˜
Ó"Ø‘ä˜e›�ØœC ›JÓ&Ü ›J�EØ�GÓð
 —l‘lØ]Ø�ó ç (¡£
¨3ñ0ˆGö Ü ð "Oó Pð Pà‰jˆØ�LÓ ðÜŸ:š: c§k¨TÓ2�ð œ4Ÿ:š: eÓ,Ó,Øð4 	�
‰
�ð7 -à‹\˜c ZÓ/°E¸ZÓ4GØð0 	�
‰
�ð/ �LÓ ô —J’J˜uÓ%ˆEØ�L‰LÐTØ­U  q¢¸¸kÐJõLà�gÓð
 �L‰LÐQØ¨Ð4õ6ð �L‰LÐ0°°°}ÐEÈÐG[Ô\ð
 Ô%Ó%¨Ô2C×2JÑ2JÓ2LÓ)LÜ! $Ô4Ø�‰Œà�
‰
ŒØ�Óä�H“ˆÜ”D—J’J˜uÓ%Ó&ˆÜ˜;¨Ð3CÀEÀ7È$ÈuÈgÐ1VÐ^cØ"' ¨Ð0¸U¸GÀ7Ð:KÐQTò	aàÜ�{ G°±Ð/?¸xÐ-HÐPUØ°3óCøôK œzÐ*ó Ø“ðûð: 	�
‰
�ús=   �F#M Æ1L< ÇM Ç<M È B,M Ì<MÍM ÍMÍM ÍM'c                 ó  • U S;   a  / $ [        U [        5      (       a   [        R                  " U 5      n [        U [
        5      (       d  [	        S5      e/ nU  Hk  n[        U[        5      (       a  UR                  S5      (       d  [	        S5      e[        U5      S:”  a  [	        S5      eX!;  d  MZ  UR                  U5        Mm     [        U5      [        :”  a  [	        S[         S35      eU$ ! [         a    [	        S5      ef = f)	a  The photo list a hand added car may be given: a list of https links
(a JSON string of one is taken too, the Mac's own page posts strings),
each a real web address, no repeats, at most MANUAL_PHOTO_LIMIT. An
empty list clears the gallery. Anything else is refused in words.rÉ   z"photos must be a list of web links)zhttps://zhttp://z/each photo must be a web link starting https://iX  za photo link is too longzat most z photos on one car)
rë   rÌ   r>  r?  rÙ   rì   Ú
startswithrá   rí   ÚMANUAL_PHOTO_LIMIT)rï  rò   Úitems      r   r,  r,  ï  sö   € ð
 �
ÓØˆ	Ü�%œ×Ñð	CÜ—J’J˜uÓ%ˆEô �eœT×"Ñ"ÜÐ=Ó>Ð>Ø
€CÛˆÜ˜$¤×$Ñ$¨D¯O©OÐ<S×,TÑ,TÜÐNÓOÐOÜˆt‹9�s‹?ÜÐ7Ó8Ð8Ø�?Ø�J‰J�tÖñ ô ˆ3ƒxÔ$Ó$Ü˜8Ô$6Ð#7Ð7IÐJÓKÐKØ€Jøô ó 	CÜÐAÓBÐBð	Cús   ŸC0 Ã0Dc           	      ót   • U=(       d    SR                  5       nU(       d  [        S5      e[        U SSUX#S9  g )NrÊ   za comment needs some textÚcommentzComment added©r  rÁ  r   )ÚstriprÙ   rÃ  )r¶  r  rÁ  r   s       r   Úadd_purchase_commentr<  
	  s6   € Ø�J�B×ÑÓ€DÞÜÐ4Ó5Ð5Ü�{ I¨ÀTØó+r   c                 ó"  • [        U [        5      (       a  U R                  S5      OU S   nUb  U$ [        U [        5      (       a  U R                  S5      OU S   n[        U [        5      (       a  U R                  S5      OU S   nUS:X  a  S$ U$ )aJ  What the car itself was won at, before any chips. winning_bid where
known; for a non Motorway row without one, price (which holds the car
alone on Carwow and on a manual entry). A Motorway row without a
winning_bid gets None, never the all in figure price holds there:
honest absence beats a number that quietly includes the fees.r<   Nr-   r`  ÚMotorway)rë   rø   r  )r¬   Úwbr-   r`  s       r   rÂ  rÂ  	  s�   € ô $.¨c´4×#8Ñ#8ˆ�‰�Ô	¸cÀ-Ñ>P€BØ	�~Øˆ	Ü&0°´d×&;Ñ&;ˆs�w‰w�zÔ"ÀÀZÁ€HÜ *¨3´× 5Ñ 5ˆC�G‰G�GÔ¸3¸w¹<€EØ˜zÓ)ˆ4Ð4¨uÐ4r   )r&  r'  r%  r$  c                 óT   • [        U =(       d    S5      R                  5       [        ;   $ )zvWhether Motorway's purchases list figure for a car at this status
already has the buyer's fee and its VAT in it. Pure.rÊ   )rÌ   r;  ÚMOTORWAY_FEE_ADDED_STATUSES)r_  s    r   Úmotorway_fee_in_list_pricerB  3	  s"   € ô ˆv�|˜Ó×"Ñ"Ó$Ô(CÑCÐCr   c                 óØ   • U c  g[        U5      (       d  [        U 5      $ U=(       d    /  H<  u  p4nUc  M  [        U S-   [        U5      S-  -
  5      nX6s=::  a  U::  d  M6   Us  $   M>     g)a‚  The car's own price behind Motorway's purchases list figure, until
the payment page confirms it.

Once the fee is in the figure (see motorway_fee_in_list_price), the
list shows the total to the pound (bid plus the banded fee plus VAT,
the pennies dropped: 6521 + 329 + 65.80 shows as 6915), so the bid is
the list figure less the band's fee and its VAT, the band being the one
the derived bid itself sits in. None when no band fits, honest absence
as before. Proven on eight confirmed purchases (2026-09-09): every one
lands on the exact winning bid.

Before that, the list figure already IS the bid and nothing is taken
off it. Pure, tested.Ng      à?g333333ó?)rB  r…  rÑ  )r`  Úbandsr_  ÚloÚhir©  Úbids          r   Úprovisional_motorway_bidrH  9	  sq   € ð �}ØÜ% f×-Ñ-Ü�U‹|ÐØ—{ ’{‰ˆ�Ø‰;ÙÜ�E˜C‘K¤%¨£*¨sÑ"2Ñ2Ó3ˆØ�?˜Ž?ØŠJò ñ	 #ð r   c                 óä   • [        U5      n UR                  SU 45      R                  5        Vs/ s H  n[        U5      PM     snUR	                  5         $ s  snf ! UR	                  5         f = f)NzHSELECT * FROM purchase_chips WHERE purchase_id=? ORDER BY created_at, idr  r  s       r   Úpurchase_chips_forrJ  T	  sj   € Ü�4‹=€DðØ!%§¡ØVØˆNó"ç$™H›Jð"'ó (ò "'˜A”�Q–ñ "'ñ (ð 	�
‰
�ùò	(øð 	�
‰
�úr  c                 óŠ  • [        U5      nUS::  a  [        S5      e[        U5      n UR                  SU 45      R	                  5       nUc  [        SU  35      e[        S UR                  SU 45       5       5      n	[        U5      n
U
b  X©-
  OSnUR                  SXU=(       d    SX´U[        5       U45        UR                  5         UR                  5         Ub  X±-
  OSn[        U S	S
US 3U(       a  SU 3OS-   Uc  SOSUS 3Uc  SOSUS 3X6S9  Ub  [        U S	SSUS 3SUS9  X¼S.$ ! UR                  5         f = f)a™  An agreed reduction, stored as a frozen event. bid_before captures
the effective bid at this moment (the original less any earlier chips)
so the sum is always frozen figures, never arithmetic against whatever
the platform says later. new_fee/new_vat, where given, freeze the re
banded platform fee, computed by the caller at chip time and never
recomputed (a fee table can change; a chip already made cannot).r   z a chip must be a positive amountrº  Nr»  c              3   ó*   #   • U  H	  oS    v •  M     g7f©r½  Nr´   r±  s     r   r
  Úrecord_chip.<locals>.<genexpr>n	  s   é € ð Vò *U A�h–Kò *Uùr  z5SELECT amount FROM purchase_chips WHERE purchase_id=?rœ   rŸ   r    r¡   r¿  rÊ   r¢   r£   )rí  rÛ  rÁ  r   z"Platform fee re banded by the chipr�   )rÛ  rÁ  r   )Ú
bid_beforeÚ	bid_after)rÑ  rÙ   r   ry   r   ÚsumrÂ  r¤   r   r   rÃ  )r¶  r½  r¾  rÁ  Únew_feeÚnew_vatr   r   r¬   rÆ  rÅ  rO  Úafters                r   Úrecord_chiprU  ^	  s�  € ô �6‹]€FØ�ƒ{ÜÐ;Ó<Ð<Ü�4‹=€DðØ�l‰lÐ?À+ÀÓP×YÑYÓ[ˆØ‰;ÜÐ3°K°=ÐAÓBÐBÜñ V¨¯©ØCÀkÀ^ô*Uó Vó Vˆä˜SÓ!ˆØ'+Ñ'7�d’l¸Tˆ
Ø�‰ðJà &§.¨D°*ÀwÜ‹V�Sðô	ð
 	�‰Œà�
‰
ŒØ%/Ñ%;ˆZÒ À€EÜØ�VØ
�V˜D�MÐ"¾¨¨V¨H¡oÀRÑHØÑ&‰D¨b°¸DÐ0AÐ,BØ™‘¨R°°d¨|Ð+<Øòð ÑÜ˜;¨Ð0TØ%'¨° ~Ð#6¸JÈTò	Sà$Ñ9Ð9øð 	�
‰
�ús   ©B"D0 Ä0EÚappc           	      óÀ  • [        U5      n UR                  SU 45      R                  5       nUc  [        SU  35      eUR                  S[	        5       U U(       a  SU 3OS-   X@45        UR                  5         US   US   US	   p©nUR                  5         [        U S
SU 3U=(       d    SX5S9  U(       a  [        X‰U
US./US9  gg! UR                  5         f = f)a@  The sale fell through. Recorded on the purchase row itself (never
derived from walked_away, which is keyed on the registration alone and
cannot say WHICH sale died, the Ford Puma trap). Also written to
walked_away so the car stops appearing at auction. No reinstate:
Steven 2026-08-27, a cancelled sale never comes back.rº  Nr»  zVUPDATE purchases SET cancelled_at=?, cancelled_reason=?, cancelled_source=? WHERE id=?r¿  rÊ   r8   r-   r!   Úcancelu   Sale cancelled Â· r:  )r8   r-   r!   r_  ©r   )	r   ry   r   rÙ   r¤   r   r   rÃ  Úrecord_walked_away)r¶  Úreason_typeÚreason_textrÁ  rÄ   r   r   r¬   r8   r-   r!   s              r   Úcancel_purchaser]  ‡	  sõ   € ô �4‹=€DðØ�l‰lÐ?À+ÀÓP×YÑYÓ[ˆØ‰;ÜÐ3°K°=ÐAÓBÐBØ�‰ØdÜ‹V˜�}Æ¨4°¨}Ñ)=ÐRTÑUØð"ô	#ð 	�‰ŒØ! %™j¨#¨j©/¸3¸v¹;�tˆà�
‰
ŒÜ�{ HÐ0BÀ;À-Ð.PØ'×/¨4°SòEæ
Ü CÀtØ'2ñ4ð 5Ø;?ó	Að øð 	�
‰
�ús   �A=C ÃCc                 ó†  • [        U5      n UR                  SU 45      R                  5       nUc  [        SU  35      eUS   (       d  [        S5      eUR                  SU 45        UR                  SU 45        US   =(       d    S	R	                  S
S	5      R                  5       nSnU(       aF  UR                  SU45      R                  5       nUc"  UR                  SU45      nUR                  S:„  nUR                  5         UR                  5         [        U SSU(       a  SOS	-   XS9  g! UR                  5         f = f)aí  Removes a hand added car and its whole history for good (2026-08-31,
"is there a way i can delete a manually added car?"). Only ever a
hand added row: a real platform purchase is a true historical fact,
Cancel is its own and only way off the working list, never a delete,
same reasoning cancel_purchase's own docstring already gives for
never reinstating one. Refuses outright on anything else, checked
here rather than trusted to the caller, the same defensive posture as
editable_fields_for.z2SELECT manual_entry, reg FROM purchases WHERE id=?Nr»  rs   z]only a car added by hand can be deleted, a real platform purchase can only ever be cancelled.z.DELETE FROM purchase_chips WHERE purchase_id=?z DELETE FROM purchases WHERE id=?r8   rÊ   r.   Fz<SELECT 1 FROM purchases WHERE REPLACE(UPPER(reg), ' ', '')=?zRDELETE FROM walked_away WHERE REPLACE(UPPER(reg), ' ', '')=? AND platform='Manual'r   r2   zRemoved from the purchases listzU, and its cancellation record with it, so the plate is not kept off future shortlistsrÀ  )
r   ry   r   rÙ   rº   rÍ   Úrowcountr   r   rÃ  )	r¶  rÁ  r   r   r¬   r8   Útrace_clearedÚothersÚcurs	            r   Údelete_manual_purchaserc  ¢	  sF  € ô �4‹=€Dð Ø�l‰lÐOÐR]ÐQ_Ó`×iÑiÓkˆØ‰;ÜÐ3°K°=ÐAÓBÐBØ�>×"Üð Dó Eð Eð 	�‰ÐEÈÀ~ÔVØ�‰Ð7¸+¸ÔHð �5‰z×˜R×(Ñ(¨¨bÓ1×7Ñ7Ó9ˆØˆÞØ—\‘\Ð"`Ø#& &ó*ß*2©(«*ð à‰~Ø—l‘lð $;Ø=@¸FóD�à #§¡¨qÑ 0�Ø�‰Œà�
‰
ŒÜ�{ IØ8æ(ñ pØ.0ñ2à7:óGøð 	�
‰
�ús   �C8D. Ä.E zIn stockr_  Úwhenc                 óœ  • [        U5      n UR                  SU 45      R                  5       nUc  [        SU  35      eUS   nUR                  SU=(       d
    [	        5       X045        UR                  5         UR                  5         [        U SU(       d  SOSUSS	  3XS
9  Xs:w  a  [        U SSSXsXS9  gg! UR                  5         f = f)a
  The car has arrived on site. Stamps checked_in_at and moves the car
to In stock (or the finished answer given), which is what clears it off
the working list (Steven: once it is in stock, everything else is
handled in DealerKit). The DealerKit half lives in serve.py.z-SELECT stock_status FROM purchases WHERE id=?Nr»  rh   z?UPDATE purchases SET checked_in_at=?, stock_status=? WHERE id=?Ú	milestonezChecked in on sitezChecked in, arrived é
   rÀ  rë  úStatus changedrì  )r   ry   r   rÙ   r¤   r   r   rÃ  )r¶  rÁ  r   r_  rd  r   r¬   rð  s           r   Úcheck_in_purchaseri  Ó	  sß   € ô
 �4‹=€Dð
Ø�l‰lÐJØ'˜>ó+ß+3©8«:ð 	à‰;ÜÐ3°K°=ÐAÓBÐBØ�.Ñ!ˆØ�‰ÐVØ—nœd›f fÐ:ô	<à�‰Œà�
‰
ŒÜ�{ KÎTÑ1EÐYmÐnrÐsvÐtvÐnwÐmxÐWyð  @Cò  OØ
ƒ}Ü˜;¨Ð1AÈØ"¸ó	Hð øð 	�
‰
�ús   �A,B9 Â9Cc                 óÄ  • U=(       d    SSS nU(       d  g[        U5      n UR                  SU 45      R                  5       nUc  [        SU  35      eUS   =(       d    SnUSS U:X  a   UR	                  5         gUR                  SX45        UR                  5         UR	                  5         [        U S	S
U 3SU=(       d    SXUS9  g! UR	                  5         f = f)zÈDealerKit's arrival date wins (2026-09-07): rewrite a checked in
car's arrival time to the day DealerKit holds, when the two differ.
Returns whether anything changed. `when` is a date or ISO datetime.rÊ   Nrg  Fz.SELECT checked_in_at FROM purchases WHERE id=?r»  rp   z/UPDATE purchases SET checked_in_at=? WHERE id=?rf  z#Arrival date taken from DealerKit, rì  T)r   ry   r   rÙ   r   r   rÃ  )r¶  rd  rÁ  r   r   r¬   rð  s          r   Úset_checked_in_atrk  ê	  sä   € ð �J�B˜˜Ð€DÞØÜ�4‹=€Dð
Ø�l‰lÐKÈkÈ^Ó\×eÑeÓgˆØ‰;ÜÐ3°K°=ÐAÓBÐBØ�/Ñ"×( bˆØˆs�ˆ8�tÓØð 	�
‰
�ð 	�‰ÐFÈÐH[Ô\Ø�‰Œà�
‰
ŒÜ�{ KÐ3VÐW[ÐV\Ð1]ÐetØŸ; $°ÀDòJàøð 	�
‰
�ús   £A	C Á>#C ÃCc                 óÂ   • [        U 5      R                  S5      =(       d    /  Vs1 s H,  nUR                  S5      (       d  M  UR                  S5      iM.     sn$ s  snf )zdThe Status answers flagged finished (In stock by default): the car
is done and off the working list.rh   Úfinishedrï  ©Úget_purchase_dropdownsr  ©r   Úos     r   Úfinished_statusesrr  
  sX   € ô &<¸DÓ%A×%EÑ%EÀnÓ%U×%[ÐY[Ð%[ó "Ò%[˜qØ�u‰u�Z× ó ˆA�E‰E�'ŽNÑ%[ñ "ð "ùò "s   ¦AÁAc                 ó¼   • [        U 5      R                  S5      =(       d    /  H5  nUR                  S5      (       d  M  UR                  S5      =(       d    Ss  $    g)z?The Status answer a new purchase starts at (Due in by default).rh   Údefaultrï  úDue inrn  rp  s     r   Údefault_statusrv  	
  sL   € ä# DÓ)×-Ñ-¨nÓ=×CÀÒCˆØ�5‰5�×ÓØ—5‘5˜“>×- XÒ-ñ Dð r   c                 óü  • [        U5      n UR                  SU 45      R                  5       nUc  [        SU  35      eUS   (       d,  US   =(       d    S[	        U5      ;  a  [        US    S35      eUS	   (       a  [        US    S
35      eUS   nUR                  SX45        UR                  5         UR                  5         [        U SSX#S9  Xa:w  a  [        U SSSXaX#S9  gg! UR                  5         f = f)a0  Take a check in back: the arrival time is cleared and the car
returns to the working list under `status`. Allowed only while
DealerKit has not been checked in (dk_checked_in_at empty); once
DealerKit shows the car In Stock, DealerKit is the truth and the
change has to be made there (Steven, 2026-09-06).zSSELECT reg, stock_status, checked_in_at, dk_checked_in_at FROM purchases WHERE id=?Nr»  rp   rh   rÊ   r8   z# is not checked in, nothing to undorR   zH is already In Stock on DealerKit. Change it there and BidBrain follows.zBUPDATE purchases SET checked_in_at=NULL, stock_status=? WHERE id=?rf  z,Check in undone, the arrival time is clearedrÀ  rë  rh  rì  )r   ry   r   rÙ   rr  r   r   rÃ  )r¶  r_  rÁ  r   r   r¬   rð  s          r   Úundo_check_inrx  
  s  € ô �4‹=€DðØ�l‰lÐpØ'˜>ó+ß+3©8«:ð 	à‰;ÜÐ3°K°=ÐAÓBÐBØ�?×#¨¨^Ñ)<×)BÀÔK\Ð]aÓKbÓ(bÜ  E¡
˜|Ð+NÐOÓPÐPØÐ!×"Ü  E¡
˜|Ð+sÐtÓuÐuØ�.Ñ!ˆØ�‰ÐYØÐ*ô	,à�‰Œà�
‰
ŒÜ�{ KÐ1_ÐehÒtØ
ƒ}Ü˜;¨Ð1AÈØ"¸ó	Hð øð 	�
‰
�ús   �B+C) Ã)C;c                 ó  • [        U5      n UR                  SU 45      R                  5       nUR                  5         Uc  [	        SU  35      eU=(       d    Sn[        U5      nX;   n[        US   5      =(       d    US   =(       d    SU;   nU(       a'  U(       d   US   (       a  [	        S5      e[        XX1S9  g	U(       a$  U(       d  [        X=(       d    [        U5      X#S
9  g[        U SXUS
9  g! UR                  5         f = f)aü  Status follows the check in (Steven, 2026-09-06): the one fact that
says a car has arrived is its arrival time, and the Status word is a
view of it, never typed on its own. Picking a finished answer on a car
that is not checked in IS the check in; picking any other answer on a
checked in car takes the check in back (undo_check_in's rule about
DealerKit applies); anything else is a plain field write. Returns what
happened: "checkin", "undo" or "set", so the caller can do the
DealerKit half of a check in.úOSELECT reg, stock_status, checked_in_at, cancelled_at FROM purchases WHERE id=?r»  rÊ   rp   rh   re   z+this sale is cancelled, nothing to check in)rÁ  r   r_  r«  rÀ  ÚundoÚset)r   ry   r   r   rÙ   rr  rÍ  ri  rx  rv  r3  )	r¶  rï  rÁ  r   r   r¬   rm  Úto_finishedÚarriveds	            r   Úset_stock_statusr  -
  sõ   € ô �4‹=€DðØ�l‰lÐlØ'˜>ó+ß+3©8«:ð 	ð 	�
‰
ŒØ
�{ÜÐ/°¨}Ð=Ó>Ð>Ø�K�R€EÜ  Ó&€HØÑ#€Kô �3�Ñ'Ó(×S¨S°Ñ-@×-FÀBÈ8Ñ,S€GÞž7Øˆ~×ÜÐJÓKÐKÜ˜+°TÒHØÞ–{Ü�k×#@¬N¸4Ó,@ÀcÒUØÜ�{ N°EÈÒNØøð' 	�
‰
�ús   �!C5 Ã5DÚOrderedÚSold)rg  é   é   ÚamberÚgrey)r€  r�  c                 ó>  ^ • [        [        U5      R                  S5      =(       d    0 5      nSU;  a  [        U 4S j[        S    5       5      $ [        US   5      n[        U 4S jU 5       5      (       a  gUR                  T USS.5        XCS'   [        SU0US9  g)	a  Add a Status answer to this dealership's own saved list when it is
missing (a list saved before Ordered and Sold existed keeps only what
it had, get_purchase_dropdowns replaces the column whole). Finished,
so the car counts as arrived. Returns whether anything was added.Úpurchase_dropdownsrh   c              3   óJ   >#   • U  H  oR                  S 5      T:H  v •  M     g7f©rï  Nrz  ©r	  rq  rï  s     €r   r
  Ú'ensure_status_option.<locals>.<genexpr>a
  s   øé € Ð_Ò4^¨q—5‘5˜“> UÖ*Ò4^ùó   ƒ #c              3   óJ   >#   • U  H  oR                  S 5      T:H  v •  M     g7fr‰  rz  rŠ  s     €r   r
  r‹  c
  s   øé € Ð
0ªC q�5‰5�‹>˜UÖ"ªCùrŒ  FT©rï  Úcolourrm  rY  )rø   Úget_settingsr  r„  ÚPURCHASE_DROPDOWN_DEFAULTSrì   rí   Úset_settings)rï  r�  r   Úsavedr‡   s   `    r   Úensure_status_optionr”  Y
  s•   ø€ ô
 ”˜dÓ#×'Ñ'Ð(<Ó=×CÀÓD€EØ˜UÓ"äÔ_Ô4NÈ~Ò4^Ó_Ó_Ð_Ü
ˆu�^Ñ$Ó
%€CÜ
Ô
0©CÓ
0×0Ñ0ØØ‡J�J˜¨&¸dÑCÔDØˆ.ÑÜÐ&¨Ð.°TÒ:Ør   TÚaddc                 ó&  • [         R                  U 5      nUc  gU[        U5      R                  S5      =(       d    /  Vs1 s H  oDR                  S5      iM     sn;  a,  U(       a$  [        U[        R                  US5      US9(       d  gU$ s  snf )zçThe Status word for a DealerKit stage number, or None when the stage
means nothing here (Due In, Non-Stock, unknown). With add, a word this
dealership's list lacks is added to it first, so DealerKit's stages can
always be followed.Nrh   rï  r…  rY  )ÚDK_STAGE_STATUSr  ro  r”  ÚDK_STAGE_COLOUR)r_  r   r•  Úwordrq  s        r   Ústage_status_wordrš  k
  s€   € ô
 ×Ñ˜vÓ&€DØ�|ØØÔ,BÀ4Ó,H×,LÑ,LÈ^Ó,\×,bÐ`bÐ,bÓcÒ,b q—E‘E˜'–NÑ,bÑcÓcÞÔ.¨t´_×5HÑ5HÈÈvÓ5VÐ]a×bØØ€Kùò ds   Á Br�   c                 ó”  • [        XS9nUc  g[        U5      n UR                  SU 45      R                  5       nUR	                  5         Ub
  US   (       a  g[        U5      n	[        US   5      =(       d    US   =(       d    SU	;   n
U=(       d    UnU
(       dj  U=(       d    SSS =(       d,    [        R                  R                  5       R                  5       n[        XXVUS	9  [        U S
US9  [        U SSU SU 3XES9  g
US   =(       d    SU:X  a  g[        U SXdUS9  [        U SSU 3XES9  g! UR	                  5         f = f)a”  DealerKit wins (2026-09-07, "the dealer os purchases list needs to
be in sync with dealerkit"): move a car's Status to DealerKit's stage.
A car not yet arrived here that DealerKit has In Stock, Ordered or Sold
is checked in with DealerKit's arrival date and its DealerKit check in
stamped; an arrived car whose word differs is moved with a timeline
line. Returns "checkin", "set" or None (nothing to do).rY  Nrz  re   rp   rh   rÊ   rg  )rÁ  r   r_  rd  r«  Ú	dealerkitz$Checked in, matched from DealerKit: z there since rÀ  zStatus matched from DealerKit: r|  )rš  r   ry   r   r   rr  rÍ  r²   r×   Útodayr³   ri  r¸  rÃ  r3  )r¶  r_  ÚstageÚ	deliveredrÁ  r   r™  r   r¬   rm  r~  Úlabelrd  s                r   Úmatch_dealerkit_stager¡  y
  sN  € ô ˜VÑ/€DØ�|ØÜ�4‹=€DðØ�l‰lÐlØ'˜>ó+ß+3©8«:ð 	ð 	�
‰
ŒØ
�{�c˜.×)ØÜ  Ó&€HÜ�3�Ñ'Ó(×S¨S°Ñ-@×-FÀBÈ8Ñ,S€GØ�M�T€EÞØ—˜R  "Ð%×J¬¯©×)<Ñ)<Ó)>×)HÑ)HÓ)JˆÜ˜+°TÈTÒRÜ�˜i¨dÒ3Ü˜;¨Ð7[Ð\aÐ[bÐboÐptÐouÐ5vÐ|ò  	LØØˆNÑ×!˜r dÓ*ØÜ�{ N°DÈÒMÜ�{ KÐ3RÐSXÐRYÐ1ZÐ`cÒoØøð! 	�
‰
�ús   š!D5 Ä5Ec                 ó4  • [        U R                  S5      5      nU(       d  [        S5      eU R                  S5      =(       d    SR                  5       (       d  [        S5      eU R                  S5      nUS;   a  [        S5      e[	        5       n[        U5      n [        U5      R                  S	5      =(       d    /  Vs1 s H,  nUR                  S
5      (       d  M  UR                  S5      iM.     nnSn	UR                  SU45       H  n
U
S	   =(       d    SU;  d  M  U
n	  O   U	b5  U	S   =(       d    SnU	S   (       a  SU	S    3OSn[        U SU U S35      eU R                  S5      S;   a  SOSnU R                  S5      S;   a  SOSn[        [        U R                  S5      5      =(       d    S[        -  U-  [        U R                  S5      5      =(       d    S[        -  U-  -   S5      nUR                  SSSU S[        R                  " 5       R                  SS   3X0S   R                  5       [        U5      [        U5      U R                  S!5      S;   a  SO[        U S!   5      U R                  S5      =(       d    USS" XUS#SU R                  S$5      =(       d    S[        U R                  S5      5      [        U R                  S5      5      UU R                  S%5      =(       d    SR                  5       =(       d    S[        U R                  S&5      5      U R                  S'5      =(       d    SR                  5       =(       d    SU R                  S(5      =(       d    SR                  5       =(       d    SXÞ45      nUR                   n[#        XcU S   5      u  nnU(       a  U(       a  UR                  S)UUU45        UR%                  5         UR'                  5         [)        US*S+XS,9  U$ s  snf ! UR'                  5         f = f)-aj  A car bought outside the platforms, a local trader for instance
(Steven 2026-08-27). The same record, hand fed: platform "Manual", a
generated reference, and with no platform behind it nothing ever
overwrites it. Marked already emailed, since the accounts email is for
platform buys landing overnight, not something typed in by hand.
Returns the new purchase id.r8   za registration is requiredr!   rÊ   za vehicle name is requiredr`  rÉ   za purchase price is requiredrh   rm  rï  NzjSELECT platform, bought_date, stock_status FROM purchases WHERE reg=? AND cancelled_at IS NULL ORDER BY idr-   rP  rb  z on z- is already on the working list, bought from z,. Cancel that one first if this replaces it.rt   r)  r   r   ru   r©  rª  rs  a½  INSERT INTO purchases (platform, vehicle_ref, reg, name, price,
                  winning_bid, mileage, bought_date, first_seen, emailed_at,
                  stock_status, manual_entry, notes, motorway_fee, transport_fee,
                  vat_total, supplier, retail_estimate_override, person_collecting,
                  collection_arranged, fee_has_vat, transport_has_vat)
               VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)ÚManualzmanual-rþ   é   r(   rg  ru  rl   rq   r*  rk   rm   r0  rf  zAdded by handrÀ  )rÏ   r  rÙ   r;  r¤   r   ro  ry   r…  Ú_num_or_noner  r|   r}   r~   rÑ  r   Ú	lastrowidr.  r   r   rÃ  )ÚfieldsrÁ  r   r8   r`  r§   r   rq  rm  Úclashr¬   Úwhererd  Úfee_vatÚtr_vatÚvatrb  ÚpidÚ_makeÚ_models                       r   Úadd_manual_purchaser°  ›
  sÇ  € ô �F—J‘J˜uÓ%Ó
&€CÞÜÐ5Ó6Ð6Ø�J‰J�vÓ×$ "×+Ñ+×-Ñ-ÜÐ5Ó6Ð6Ø�J‰J�wÓ€EØ�
ÓÜÐ7Ó8Ð8Ü
‹&€CÜ�4‹=€DðQô ,¨DÓ1×5Ñ5°nÓE×KÈÐKó*ÚKð ()à—u‘u˜Z×(ó #�A—E‘E˜'–NÙKð 	ð *ð ˆØ—<‘<ðCØEHÀFöLˆCð �NÑ#×) r°(Õ:Ø�ÙñLð ÑØ˜*Ñ%×;Ð);ˆEØ49¸-×4H�T˜% Ñ.Ð/Ñ0ÈbˆDÜ ˜uÐ$QÐRWÐQXÐY]ÐX^ð _Jð Jó Kð Kð —z‘z -Ó0Ð4JÓJ‘!ÐPQˆØ—j‘jÐ!4Ó5Ð9OÓO‘ÐUVˆÜ”\ &§*¡*¨UÓ"3Ó4×9¸¼XÑEÈÑOÜ! &§*¡*¨[Ó"9Ó:×?¸aÄ8ÑKÈfÑTñUØVWóYˆà�l‰lðGð ˜   Q¤t§z¢z£|×'7Ñ'7¸¸Ð';Ð&<Ð=¸sÈ6ÁN×DXÑDXÓDZÜ�5‹\œ5 ›<Ø—Z‘Z 	Ó*¨jÓ8‰T¼cÀ&ÈÑBSÓ>TØ�Z‰Z˜Ó&×2¨#¨c¨r¨(°CØ�q˜&Ÿ*™* WÓ-×5°Ü˜&Ÿ*™* UÓ+Ó,¬l¸6¿:¹:ÀkÓ;RÓ.Sð Ø�j‰j˜Ó$×*¨×1Ñ1Ó3×;°tô
 ˜&Ÿ*™*Ð%6Ó7Ó8ð �j‰jÐ,Ó-×3°×:Ñ:Ó<×DÀØ�j‰jÐ.Ó/×5°2×<Ñ<Ó>×FÀ$Øð/ó$ˆðJ �m‰mˆô -¨T¸¸v¹ÓG‰ˆˆvÞ–VØ�L‰LÐJÈUÐTZÐ\_ÐL`ÔaØ�‰Œà�
‰
ŒÜ�s˜K¨¸cÒMØ€JùòO*øðJ 	�
‰
�ús+   Â%P ÃP Ã P Ã5*P Ä#J?P Ð P ÐPc                 ó(   • U S;   a  S $ [        U 5      $ )NrÉ   )rÑ  ©ró   s    r   r¥  r¥    s   € Ø˜
“?ˆ4Ð0¬¨a«Ð0r   ru  Úblue)rï  r�  rt  ÚgreenrŽ  z
NO FINANCE©rï  r�  z
ON FINANCEÚnavyúNEGATIVE EQUITYÚredr)  ÚskyzMore info neededzVideo requestedÚteal2zVideo receivedÚtealzVideo signed off)rï  r�  Úgatez
No logbookzLogbook applied forzLogbook herezWe cancelled before collectionzWe cancelled at collectionz$Customer cancelled before collection)rh   ri   rj   rk   ro   rq   rn   rf   c                 óÞ   • [        U 5      R                  S5      =(       d    0 n0 n[        R                  5        H  u  p4X1;   a  X   OUX#'   M     [	        UR                  S5      5      US'   U$ )aS  The effective option lists: whatever the settings table holds per
column, falling back to the factory defaults per column (a saved column
replaces that column's whole list, the same per key merge shape as the
platforms settings, never a wholesale replace of the dict, which is the
exact bug class the DealerWay settings hit on 2026-08-24).r‡  ri   )r�  r  r‘  rÒ  Úwith_negative_equity_answer)r   r“  rò   r‡   Úfactorys        r   ro  ro  L  se   € ô ˜Ó×"Ñ"Ð#7Ó8×>¸B€EØ
€CÜ2×8Ñ8Ö:‰ˆØ!$£�5’:°7ˆ‹ñ ;ä3°C·G±G¸LÓ4IÓJ€CˆÑØ€Jr   c                 ón   • [        U =(       d    / 5      n[        S U 5       5      (       a  U$ USSS./-   $ )a"  The On finance? answers with NEGATIVE EQUITY among them (2026-09-16):
a dealership's saved list from before it gets the answer added, once,
so both dealerships can mark a car whose seller owes more than the
price. A list that already has an answer naming equity is left alone.
Pure, tested.c              3   óÜ   #   • U  Hb  nS UR                  S5      =(       d    SR                  5       ;   =(       d+    SUR                  S5      =(       d    SR                  5       ;   v •  Md     g7f)Úequityrï  rÊ   ÚnegativeN©r  Úlower)r	  rq  s     r   r
  Ú.with_negative_equity_answer.<locals>.<genexpr>a  sR   é € Ð
vÒquÐlmˆH˜Ÿ™˜w›×-¨2×4Ñ4Ó6Ñ6×f¸*ÈÏÉÈwË×I]Ð[]×HdÑHdÓHfÑ:fÔfÒquùs   ‚A*A,r·  r¸  rµ  )rì   r„  )ÚoptionsrŸ  s     r   r¾  r¾  Z  s=   € ô �—˜2Ó€DÜ
Ñ
vÑquÓ
v×vÑvØˆØÐ.¸%Ñ@ÐAÑAÐAr   c                 óÎ   • U =(       d    /  HV  nUR                  S5      =(       d    SR                  5       nUR                  S5      (       a  MC  SU;   d  SU;   d  MQ  US   s  $    g)z@The live On finance? answer that names negative equity, or None.rï  rÊ   ÚretiredrÂ  rÃ  NrÄ  )rÇ  rq  ró   s      r   Únegative_equity_answerrÊ  f  sW   € à�]˜Š]ˆØ�U‰U�7‹^×!˜r×(Ñ(Ó*ˆØ�u‰u�Y×Ó X°£]°jÀAµoØ�W‘:Òñ ð r   c                 óH   • [        U 5      R                  S5      =(       d    0 $ )NÚpurchase_columns©r�  r  rY  s    r   Úget_purchase_columnsrÎ  t  s   € Ü˜Ó×!Ñ!Ð"4Ó5×;¸Ð;r   )r   éÃ	  i  )éÄ	  é‡  i+  )éˆ  éK  iI  )éL  é'  ig  )é'  iÓ0  i…  )iÔ0  é—:  é�  )é˜:  i[D  i£  )i\D  éN  i­  )é N  é§a  éÕ  )é¨a  é/u  iß  )é0u  é?œ  é  )é@œ  éOÃ  é/  )éPÃ  é_ê  i‰  )é`ê  éo iã  )ép é8 i=  )é€8 é�_ i—  )é�_ éŸ† i¿  )é † éÿàõé#  )r   iç  éÇ   )iè  iÏ  éÛ   )iÐ  rÏ  éù   )rÐ  rÑ  i!  )rÒ  rÓ  i?  )rÔ  rÕ  iS  )rÖ  r×  iq  )rÙ  rÚ  rØ  )rÛ  rÜ  iÁ  )rÞ  rß  rÝ  )rà  i·ˆ  ió  )i¸ˆ  rá  râ  )rã  iÇ¯  rå  )iÈ¯  rä  iW  )ræ  i×Ö  i§  )iØÖ  rç  iÏ  )rè  içý  i  )ièý  ré  iG  )rê  rë  i«  )rì  rí  rò  )rî  rï  i_  )rð  i¯­ i¯  )i°­ i¿Ô i'  )iÀÔ iÏû iŸ  )iÐû rñ  iÛ  )r>  rÐ   c                 óÐ   • [        U 5      R                  S5      =(       d    0 n0 n[        R                  5        H  u  p4X1;   a  X   OUX#'   M     U H  nX2;  d  M
  X   X#'   M     U$ )a}  The effective fee band tables, per platform, saved table replacing
that platform's factory one (per key merge, same shape as
get_purchase_dropdowns). A platform with no table at all (Auction4Cars,
Dealer Auction, DealerWay, Manual) simply is not here: a chip on one of
those never computes a fee, it says the fee may change and to check
the invoice instead, never a guessed figure.Ú	fee_bands)r�  r  ÚPURCHASE_FEE_BAND_DEFAULTSrÒ  )r   r“  rò   Úplatr¿  s        r   Úget_fee_bandsrú  —  sf   € ô ˜Ó×"Ñ" ;Ó/×5°2€EØ
€CÜ3×9Ñ9Ö;‰ˆØ#'£=�E’K°gˆ‹	ñ <ãˆØ�?Ø™ˆC‹Iñ ð €Jr   c                 ó~   • Uc  g[        U5      R                  U / 5       H  u  p4nX1s=::  a  U::  d  M   Us  $   M     g)z¢The platform's purchase fee at this price, from the effective band
table, None when the platform has no table or the price sits outside
every band. Never guessed.N)rú  r  )r-   r`  r   rE  rF  r©  s         r   Úfee_forrü  ¨  sF   € ð �}ØÜ$ TÓ*×.Ñ.¨x¸Ö<‰ˆ�ØÕ˜"ÖØŠJò ñ =ð r   c                 óR   • U c  g[        U 5      [        U 5      :X  a  SU S 3$ SU S 3$ )zƒA money figure the way Steven reads one: no pence unless there are
pence. Only ever for a sentence on screen, never for arithmetic.zno price on filer¢   r¡   r£   )rÑ  r   r²  s    r   Ú_money_wordsrþ  ´  s8   € ð 	�yØ!Ü % a£¬C°«FÓ 2ˆV�A�d�8ÐÐI¸&ÀÀ4ÀÐ8IÐIr   c                 ó´   • U  Vs/ s H  o=(       d    SPM     n n[        U 5      S::  a  U (       a  U S   $ S$ SR                  U SS 5      S-   U S   -   $ s  snf )zwMotorway and Carwow. Motorway, Carwow and Dealer Auction. Steven's
house style, so no dashes and no serial comma games.rP  r   r   rW  Néÿÿÿÿú and )rá   rË   )ÚwordsÚws     r   rQ  rQ  ¼  sd   € ñ /4Ó4ªe¨×$Ð$Ò$©e€EÐ4Ü
ˆ5ƒz�QƒÞ ˆu�Q‰xÐ8Ð&8Ð8Ø�9‰9�U˜3˜B�ZÓ  7Ñ*¨U°2©YÑ6Ð6ùò 5s   …Ac                 óR  ^• 0 nU =(       d    S H_  nUR                  S5      (       a  M  [        UR                  S5      5      nU(       d  M>  UR                  U/ 5      R                  U5        Ma     0 nUR	                  5        GH…  u  p5[        U5      S:  a  M  U Vs0 s H  o"S   UR                  S5      _M     nn[        S UR	                  5        5       5      n[        U5      [        U5      :  nU(       + =(       a&    [        U5      S:„  =(       a    US	   S	   US   S	   :H  n	U(       a  U(       d  U	(       d  US	   S   OS
mTb  US   S	   US	   S	   -
  OS
n
U GH£  nU Vs/ s H=  o»S   US   :w  d  M  US   UR                  S5      XkS      UR                  S5      S.PM?     nnU Vs/ s H  o»S   PM	     nn[        UR                  S5      /U-   5      nTb¹  TUS   :H  n[        U4S jU 5       5      nU Vs/ s H  nUS   T:w  d  M  UPM     nnU SU SUR                  S5      =(       d    S S[        UT   5       S[        U Vs/ s H  nUR                  S5      PM     sn5       S[        U5      S:X  a  SOS S[        U
5       S3nO.U	(       a  S
nU SU S[        US	   S	   5       S3nO
S
nU SU S3nU[        U5      UUR                  S5      /U-   XbS      TXúX‰UU SU S3S.XBS   '   GM¦     GMˆ     U$ s  snf s  snf s  snf s  snf s  snf )a  {purchase id: its double win block} for every live purchase that
shares a registration with another live purchase. Cancelled sales are
not in the running, and a car with no readable plate cannot be
matched to anything so it never joins a group. Pure, so the page, the
email, the preflight and the tests read one rule.

The block carries `others` (the id, platform and all in of each
twin), `platforms` (every platform in the group, this row's first),
`all_in`, `cheapest_id`, `keep` (this is the one to keep), `saving`
(what cancelling the dearer sale saves), `unsure` (a price is
missing, so nothing is recommended), `tied` (they cost the same), and
two ready written sentences, `advice` for the screens and `hold` for
the DealerKit preflight, so both sides word it identically.
r´   re   r8   rs  r1   Úall_inc              3   ó6   #   • U  H  u  pUc  M
  X!4v •  M     g 7fr   r´   )r	  r  r�   s      r   r
  Údouble_wins.<locals>.<genexpr>ù  s   é € ÐIª<¡4 1¸1›˜�ª<ùs   ‚	�
r   r   Nr   r-   rÃ   )r1   r-   r  rÃ   c              3   ó<   >#   • U  H  oS    T:X  d  M  Uv •  M     g7f)r1   Nr´   )r	  ÚgÚcheapest_ids     €r   r
  r    s   øé € ÐG¢u !°$±¸;Ñ0FŸA™A¢uùs   ƒ“	z was won twice, on z. Keep the Úfirstz	 sale at z all in and cancel the r.   ÚsaleÚsalesz, which saves Ú.z, and both cost z< all in. Cancel whichever you like and the other one stands.zG. One of them has no price on file yet, so pick which to keep yourself.zS. Cancel one of the sales first, or DealerKit ends up with two records for one car.)r8   Úcountra  Ú	platformsr  r
  ÚkeepÚsavingÚunsureÚtiedÚadviceÚhold)
r  rÏ   Ú
setdefaultrí   rÒ  rá   r  rQ  Únextrþ  )rã   Úgroupsr„   r8   rò   ÚgroupÚcostÚpricedr  r  r  rq  ra  Úothers_namesÚeveryoner  Úbestr	  Úworstr  r  r
  s                        @r   rž  rž  Ý  sj  ø€ ð €FØ�Z�RŠZˆØ�5‰5�× Ñ ÙÜ˜Ÿ™˜e›Ó%ˆÞÙØ×Ñ˜#˜rÓ"×)Ñ)¨!Ö,ñ ð €CØ—l‘l—n‰
ˆÜˆu‹:˜‹>ÙÙ27Ó8²%¨Q�$‘˜Ÿ™˜x›Ò(±%ˆÐ8ÜÑI¨4¯:©:¬<ÓIÓIˆÜ�V“œs 5›zÑ)ˆØ”
×P¤ F£¨a¡×P°F¸1±I¸a±LÀFÈ1ÁIÈaÁLÑ4PˆÞ'-¶fÆT�f˜Q‘i ’lÐPTˆØ3>Ñ3J�&˜‘*˜Q‘- &¨¡)¨A¡,Ò.ÐPTˆÜˆAñ  %ó<â$˜!¨$©°1°T±7Ñ(:óU˜Q˜t™W°!·%±%¸
Ó2CØ!%¨¡g¡¸q¿u¹uÀ]Ó?SôUá$ð ð <ñ 4:Ó:²6¨a˜jœM±6ˆLÐ:Ü" A§E¡E¨*Ó$5Ð#6¸Ñ#EÓFˆHØÑ&Ø" a¨¡gÑ-�ÜÔG¡uÓGÓG�Ù$)ÓD¢E˜q¨Q¨t©W¸Ñ-CŸ¡E�ÐDØ ˜EÐ!4°X°J¸kØ!ŸX™X jÓ1×<°WÐ=¸YÜ)¨$¨{Ñ*;Ó<Ð=Ð=TÜ(ÁUÓ)KÂUÀ¨!¯%©%°
Ö*;ÁUÑ)KÓLÐMÈQÜ'*¨5£z°Q£™V¸GÐDÀNÜ)¨&Ó1Ð2°!ð5‘ö Ø�Ø ˜EÐ!4°X°JÐ>NÜ)¨&°©)°A©,Ó7Ð8ð 9?ð?‘ð �Ø ˜EÐ!4°X°Jð ?Oð O�ð ¤S¨£Z¸6ØŸe™e JÓ/Ð0°<Ñ?Ø ™w™-¸Ø¸&Ø Ø˜5Ð 3°H°:ð >$ð $ñ	ˆC�$‘ŒLô3 ñ %ðX €JùòS 9ùò<ùò ;ùò
 Eùò *Ls*   ÂLÅ)LÅ<1LÆ3LÈLÈLÉL$c                 óJ  • [        U 5      n UR                  S5      R                  5        Vs/ s H  n[        U5      PM     nn0 nUR                  S5       H0  nUR	                  US   / 5      R                  [        U5      5        M2     U Hq  nS H4  n [        R                  " UR                  U5      =(       d    S5      X&'   M6     UR                  S5      n U(       a  [        R                  " U5      OSUS'   Ms     [        U 5      R                  S5      nU Hn  nUR                  S	5      n	UR                  S
5      n
X’S'   U
b
  X¢S	'   SUS'   OU	b  U(       a  X˜:”  a
  X‚S	'   SUS'   OSUS'   UR                  S5      nUc  Mj  X²S'   Mp     [        U 5      R                  S/ 5      nU GHâ  nUR                  US   / 5      nXÒS'   [        S U 5       5      US'   [        U5      US'   SUS'   US   cK  UR                  S5      S:X  a6  [        UR                  S5      XÂR                  S5      5      nUb	  XâS'   SUS'   US   b  US   US   -
  OSUS'   UR                  S5      UR                  S5      nnU H  nUS   c  M  US   US   nnM     UUsUS '   US!'   XòR                  S"5      UR                  S#5      U/n[!        S$ U 5       5      (       a  [        S% U 5       5      OSUS&'   UR                  S5      S:H  =(       a    [#        UR                  S5      5      nUS   b+  US   (       a  U(       d  US   US&   =(       d    S'-   US('   GM«  UR                  S)5      b  UR                  S)5      OUR                  S5      US('   GMå     [%        U5      nU H  nUR                  US   5      US*'   M     UUR'                  5         $ s  snf ! [        [        4 a	    / X&'    GM\  f = f! [        [        4 a
    SUS'    GM~  f = f! UR'                  5         f = f)+a  Every recorded purchase, newest bought first (falling back to newest
recorded first for the handful of rows with no bought_date), for the
purchases working list. Cancelled sales stay in the table flagged with
cancelled_at (never deleted: deleting also clears emailed_at, letting a
later read email accounts about an old car again), and the page filters
them into their own view. service_history_photos and photo_urls are both
stored JSON encoded (a plain list of URLs each, see
motorway.read_service_history_photos and read_purchase_gallery),
decoded back to real lists here, [] when never read or genuinely
empty, same convention as saved_views.criteria.

This is also the single place the CANONICAL MONEY is derived, so every
reader (the page, the DealerKit push, the bias bid comparison, the
accounts email) agrees rather than each keeping its own fallback chain,
which is exactly how the page ended up with five disagreeing
definitions of "what was paid" (found 2026-08-27):

  bid_original   what the car was won at, before any chips. None for a
                 Motorway row with no winning_bid rather than the all
                 in figure price holds there: honest absence, never a
                 number that quietly includes the fees.
  chip_total     the sum of every chip, 0 when none.
  car_price      bid_original less the chips. What the car itself cost.
  effective_fee  the platform fee after any re banding chip froze a new
  effective_vat  one, else the figures as read. Never recomputed.
  fees_total     effective fee + transport + protect + effective VAT,
                 None when nothing at all is known.
  all_in         car_price + fees, what actually left the bank. Falls
                 back to total_price/price for a row whose bid was
                 never known.
zPSELECT * FROM purchases ORDER BY COALESCE(bought_date, first_seen) DESC, id DESCz4SELECT * FROM purchase_chips ORDER BY created_at, idr¶  )rD   rE   rF   rX   rY   rZ   r  rM   NÚretail_price_capr*  rc   Úretail_estimate_rawFÚretail_estimate_cappedTrd   rb   r>  r1   r­   c              3   ó*   #   • U  H	  oS    v •  M     g7frM  r´   r±  s     r   r
  Ú!list_purchases.<locals>.<genexpr>ƒ  s   é € Ð!=²u°! H¦+²uùr  r–  Úbid_originalÚbid_provisionalr-   r`  r_  r×  r=   r@   rR  rS  rØ  Úeffective_vatr>   r?   c              3   ó(   #   • U  H  oS Lv •  M
     g 7fr   r´   ©r	  r"  s     r   r
  r&  ™  s   é € Ð%CºU¸¨t¥mºUùs   ‚c              3   ó.   #   • U  H  oc  M  Uv •  M     g 7fr   r´   r+  s     r   r
  r&  ˜  s   é € Ð"Eªe¨§1¡1ªeùs   ‚Œ	Ú
fees_totalr   r  ra  Ú
double_win)r   ry   r{   rø   r  rí   r>  r?  r  r@  rÙ   r�  rú  rQ  rÂ  rH  r„  rB  rž  r   )r   r   r„   rò   Úchips_by_purchaser�   rë  Ú	raw_checkÚcapÚrawÚoverrideÚdate_overrideÚmw_bandsr­   Úestr©  r¬  ÚpartsÚlist_is_all_inÚfounds                       r   Úlist_purchasesr:  $  sC  € ô@ �4‹=€DðgØ $§¡Ø^ó!
ç
‰(‹*ð!ó ò !˜1Œt�AŽwñ !ˆð ð ÐØ—‘ÐTÖUˆAØ×(Ñ(¨¨=Ñ)9¸2Ó>×EÑEÄdÈ1ÃgÖNñ VãˆAó J�ð"Ü#Ÿzšz¨!¯%©%°«,×*>¸$Ó?�A“Hñ Jð Ÿ™˜jÓ)ˆIð%Þ9B¤§
¢
¨9Ô 5È��*“ñ ô0 ˜4Ó ×$Ñ$Ð%7Ó8ˆÛˆAØ—%‘%Ð)Ó*ˆCØ—u‘uÐ7Ó8ˆHØ'*Ð#Ñ$ØÑ#Ø'/Ð#Ñ$Ø.3�Ð*Ò+Ø‘¦S¨S«YØ'*Ð#Ñ$Ø.2�Ð*Ò+à.3�Ð*Ñ+ð ŸE™EÐ"<Ó=ˆMØÓ(Ø'4Ð#Ó$ñ) ô4 ! Ó&×*Ñ*¨:°rÓ:ˆÜˆAØ%×)Ñ)¨!¨D©'°2Ó6ˆEØˆg‰JÜ!Ñ!=±uÓ!=Ó=ˆAˆl‰OÜ -¨aÓ 0ˆAˆnÑð $)ˆAÐÑ Ø�Ñ Ñ(¨Q¯U©U°:Ó->À*Ó-LÜ.¨q¯u©u°W«~¸xÏÉÈxËÓY�Ø‘?Ø(+�nÑ%Ø+/�AÐ'Ñ(à!" >Ñ!2Ñ!>ð   Ñ/°!°L±/ÒAØDHð ˆk‰Nà—u‘u˜^Ó,¨a¯e©e°KÓ.@�ˆCÛ�Ø�Y‘<Ó+Ø  ™|¨Q¨y©\˜�C™ñ ð 69¸#Ð2ˆAˆoÑ  /Ñ 2ØŸ%™% Ó0°!·%±%¸Ó2FÈÐLˆEä"%Ñ%C¹UÓ%C×"CÑ"Cô  #Ñ"E©eÓ"EÔEØIMð ˆl‰OàŸe™e JÓ/°:Ñ=÷ NÜ"<¸Q¿U¹UÀ8»_Ó"Mð à�‰~Ñ)°1Ð5F×3GÎNØ ™n°°,±×0DÀ1ÑE��(”ð
 78·e±e¸MÓ6JÑ6V˜aŸe™e MÔ2Ð\]×\aÑ\aÐbiÓ\j��(”ñE ôN ˜CÓ ˆÛˆAØ#Ÿi™i¨¨$©Ó0ˆAˆl‹Oñ àà�
‰
�ùòMøô "¤:Ð.ó "Ø!�A•Hð"ûô œzÐ*ó %Ø $��*•ð%ûðh 	�
‰
�úst   �!P ®OÁAP Â0OÃP Ã"O3Ã?BP Æ	C<P Ê	D8P ÏP ÏO0Ï+P Ï/O0Ï0P Ï3PÐP ÐPÐP ÐP"c                 óâ  • U =(       d    /  Vs/ s H4  o"R                  S5      (       a  M  UR                  S5      (       d  M2  UPM6     nnS nS nU Vs/ s H  o$" US   5      (       d  M  US   PM     nnU Vs/ s H+  o$" US   5      (       a  M  U" US   5      (       a  M&  US   PM-     nnUSL a  [        U5      S:X  a  US   $ USL a  [        U5      S:X  a  US   $ g	s  snf s  snf s  snf )
a  Which of the dealership's own On finance? answers means `financed`:
the answer whose words say no (NO FINANCE, None, Clear) for False, the
other live answer for True. None when the answers do not split that
way, so nothing is ever set on a guess. Pure, tested.rÉ  rï  c                 ó�   • U R                  5       R                  5       nSU;   =(       d    SU;   =(       d    SU;   =(       d    SU;   $ )NÚnoÚnoneÚclearÚwithout)rÅ  rÎ   ©ró   r  s     r   Úsays_noÚfinance_answer.<locals>.says_noµ  s9   € Ø�G‰G‹I�O‰OÓˆØ�q‰y×I˜F a™K×I¨7°a©<×I¸9È¹>ÐIr   c                 óD   • U R                  5       nSU;   =(       d    SU;   $ )NrÂ  rÃ  )rÅ  rA  s     r   Úsays_negativeÚ%finance_answer.<locals>.says_negative¸  s    € Ø�G‰G‹IˆØ˜1‰}×/ 
¨a¡Ð/r   Tr   r   FN)r  rá   )rÇ  Úfinancedrq  rŸ  rB  rE  ÚnosÚyesess           r   Úfinance_answerrJ  ¯  sæ   € ð
  Ÿ 2˜ÓRš�!¯e©e°I×.>‹AÀ1Ç5Á5ÈÇ>�A™€DÐRòJò0ñ  $Ó
;št˜! w¨q°©z×':‹:ˆ1ˆWŒ:™t€CÐ
;ñ "&Óc¢˜A¨W°Q°w±Z×-@‹ZÉÐWXÐY`ÑWa×Ib‹ZˆQˆwŒZ¡€EÐcØ�4ÒœC ›J¨!›OØ�Q‰xˆØ�5ÒœS ›X¨›]Ø�1‰vˆØùò Sùò <ùò ds.   ŽC"«C"ÁC"ÁC'Á,	C'Á;C,ÂC,Â$	C,c                 óÄ  • [         R                   R                  5       [         R                  " US9-
  R                  SS9n[	        U5      n 0 nUR                  SU45       H>  n [        R                  " US   5      nX;   d  M$  Xp   c  M+  Xp   U[        US   5      '   M@     UUR                  5         $ ! [        [        4 a     Mg  f = f! UR                  5         f = f)a	  {reg: value} for one fact every platform's detail page read saved
in the last `days` days (listing_detail_cache holds the fields a read
changed, keyed by reg), newest wins. For facts worth keeping past the
cache's own short reuse window, the finance flag above all.rÔ   r¯   r°   zeSELECT cache_key, detail_json FROM listing_detail_cache WHERE fetched_at >= ? ORDER BY fetched_at ASCr<  r=  )r²   r§   rÛ   r³   r   ry   r>  r?  r@  rÙ   rÏ   r   )rë  rÕ   r   Úcutoffr   rò   r¬   r  s           r   Úrecent_detail_factsrM  Æ  sÔ   € ô
 ×Ñ×#Ñ#Ó%¬×(:Ò(:ÀÑ(EÑE×PÑPÐZcÐPÐd€FÜ�4‹=€DðØˆØ—<‘<ØwØ�	öˆCðÜ—J’J˜s =Ñ1Ó2�ð �z˜a™hÓ2Ø34±8�”I˜c +Ñ.Ó/Ó0ñð à�
‰
�øô œzÐ*ó Úðûð 	�
‰
�ús<   ÁC Á&B6Á?C ÂC ÂC Â6C
ÃC Ã	C
Ã
C ÃCc                 óÎ   • [        U5      n UR                  SU 45       Vs1 s H  n[        US   5      iM     snUR                  5         $ s  snf ! UR                  5         f = f)zuThe vehicle refs already on the Didn't win list for a platform, so a
sync can stop at the first car it already knows.z2SELECT vehicle_ref FROM lost_bids WHERE platform=?rX  )r   ry   rÌ   r   )r-   r   r   r„   s       r   Úlost_bid_refsrO  Ý  sk   € ô �4‹=€DðØ/3¯|©|Ø@À8À+ô0Oó Pò 0O¨!”�A�mÑ$Ö%ñ 0Oñ Pð 	�
‰
�ùòPøð 	�
‰
�ús   �A ¢AºA ÁA ÁA$c                 ór  • [        U5      n [        5       n/ nU  Hã  nUR                  SUS   [        US   5      45      R	                  5       nU(       a  M=  UR                  SUS   [        US   5      [        UR                  S5      5      UR                  S5      UR                  S5      UR                  S5      UR                  S	5      X5R                  S
5      4	5        UR                  [        U5      5        Må     UR                  5         UUR                  5         $ ! UR                  5         f = f)av  Stores every real lost auction (Mark 2026-08-24, the "didn't win"
section, motorway.read_didnt_win's own shape). A lost outcome never
changes once recorded, unlike a purchase's own status, so this is a
plain insert on a new (platform, vehicle_ref), skipping one already on
file rather than any fill-in-later or lock-once logic. Returns only
the rows that were genuinely new.z;SELECT id FROM lost_bids WHERE platform=? AND vehicle_ref=?r-   rX  z²INSERT INTO lost_bids (platform, vehicle_ref, reg, name, sold_for,
                      our_bid, date_bid, first_seen, listing_url)
                   VALUES (?,?,?,?,?,?,?,?,?)r8   r!   Úsold_forÚour_bidÚdate_bidrÃ   )r   r¤   ry   rÌ   r   rÏ   r  rí   rø   r   r   )rã   r   r   r§   r‡  r„   rr  s          r   Úrecord_lost_bidsrT  è  s  € ô �4‹=€DðÜ‹fˆØˆÛˆAØ—|‘|ØMØ�:‘¤ A mÑ$4Ó 5Ð6ó8ç8@¹»
ð ö ÙØ�L‰Lð1ð �:‘¤ A mÑ$4Ó 5´yÀÇÁÀuÃÓ7NØ—‘�v“ §¡ jÓ 1°1·5±5¸Ó3CØ—‘�zÓ" C¯©¨}Ó)=ð?ô	@ð �J‰J”t˜A“wÖñ ð 	�‰ŒØà�
‰
�øˆ�
‰
�ús   �DD$ Ä$D6c                 óà   • [        U 5      n UR                  S5      R                  5        Vs/ s H  n[        U5      PM     snUR	                  5         $ s  snf ! UR	                  5         f = f)zqEvery recorded lost auction, newest bid first, for the Didn't win
admin page. Mirrors list_purchases's own shape.zMSELECT * FROM lost_bids ORDER BY COALESCE(date_bid, first_seen) DESC, id DESCr  r"  s      r   Úlist_lost_bidsrV    sg   € ô �4‹=€DðØ!%§¡Ø[ó"
ç
‰(‹*ð"ó ò "˜A”�Q–ñ "ñ ð 	�
‰
�ùò	øð 	�
‰
�úr  c                 ó"  • [        U 5      n UR                  S5       Vs/ s H  o"S   US   4PM     nnX1R                  S5       Vs/ s H  o"S   US   4PM     sn-  nUUR                  5         $ s  snf s  snf ! UR                  5         f = f)aV  Every real winning price BidBrain knows of, for a suggested bid
(Mark 2026-08-24: "lets try and build a AI suggested bid... based on
both won and lost (but sold for) data"). A won purchase's own
winning_bid (or price, for the handful of older rows without a full
breakdown) is exactly what it took to win that specific car, no less
real a data point than a lost auction's own sold_for, the price
someone else paid to win it. Both are precise, real market clearing
prices, just for a car we ended up with versus one we did not.
Returns a plain list of (name, amount) pairs, name still the raw
"Make Model ..." free text neither table breaks out separately;
bid_suggest.model_key does the grouping. No make/model filtering here,
a genuinely small or malformed name is still handed on, left for the
caller to decide there is not enough to group it usefully.zqSELECT name, COALESCE(winning_bid, price) AS amount FROM purchases WHERE COALESCE(winning_bid, price) IS NOT NULLr!   r½  zISELECT name, sold_for AS amount FROM lost_bids WHERE sold_for IS NOT NULL©r   ry   r   ©r   r   r„   rã   s       r   Ú	bid_compsrZ    s¯   € ô �4‹=€DðØ26·,±,ð=ô3>ó ?ò 3>¨Q�6‘˜A˜h™KÓ(ñ 3>ˆð ?ð 	·<±<ØWô4Yó Zò 4Y¨a�F‘)˜Q˜x™[Ó)ñ 4Yñ Zñ 	Zˆàà�
‰
�ùò?ùòZøð 	�
‰
�ús'   �A<  A2³A< ÁA7ÁA< Á2
A< Á<Bc                 óÄ   • [        U 5      n UR                  S5       Vs/ s H  n[        U5      PM     snUR                  5         $ s  snf ! UR                  5         f = f)aP  Every lost bid with no mileage on file yet, for the retail
enrichment pass (Mark 2026-08-24: "% of cazana retail... should
improve the accuracy"). Each one's own listing_url is still a real,
viewable Motorway page even long after the sale (proven live), which
is where the mileage comes from, the Didn't win list itself never
showed it.z9SELECT * FROM lost_bids WHERE mileage IS NULL ORDER BY id©r   ry   rø   r   r"  s      r   Úlost_bids_missing_mileager]  -  sa   € ô �4‹=€DðØ!%§¡ØGô"Ió Jò "I˜A”�Q–ñ "Iñ Jð 	�
‰
�ùòJøð 	�
‰
�úó   �A  AµA ÁA ÁAc                 óÄ   • [        U 5      n UR                  S5       Vs/ s H  n[        U5      PM     snUR                  5         $ s  snf ! UR                  5         f = f)a¬  Every lost bid with a real mileage on file but never yet checked
against Cazana, ready for the second half of the enrichment pass (the
Cazana lookup itself). A car genuinely confirmed to have no Cazana
match is cazana_checked=1 with cazana_retail still NULL, and is
correctly never returned here again, the same "a confirmed no match
is cached too, not just a hit" rule already used for the daily
Glass's/Cazana valuation_cache.znSELECT * FROM lost_bids WHERE mileage IS NOT NULL AND (cazana_checked IS NULL OR cazana_checked=0) ORDER BY idr\  r"  s      r   Úlost_bids_missing_retailr`  <  sd   € ô �4‹=€DðØ!%§¡ðKô"Ló Mò "L˜A”�Q–ñ "Lñ Mð 	�
‰
�ùò	Møð 	�
‰
�úr^  Úrow_idc                 ó¬   • [        U5      n UR                  SX45        UR                  5         UR                  5         g ! UR                  5         f = f)Nz)UPDATE lost_bids SET mileage=? WHERE id=?r  )ra  r(   r   r   s       r   Úset_lost_bid_mileagerc  M  s>   € Ü�4‹=€DðØ�‰Ð@À7ÐBSÔTØ�‰Œà�
‰
�øˆ�
‰
�úré  c                 ó¬   • [        U5      n UR                  SX45        UR                  5         UR                  5         g! UR                  5         f = f)zÚRecords a Cazana lookup outcome, match or a confirmed no match
alike (cazana_retail stays NULL either way then, cazana_checked=1
marks it as settled either way, so lost_bids_missing_retail never
offers this row again).zAUPDATE lost_bids SET cazana_retail=?, cazana_checked=1 WHERE id=?Nr  )ra  r]   r   r   s       r   Úset_lost_bid_retailre  V  sC   € ô
 �4‹=€DðØ�‰ÐXØ$Ð-ô	/à�‰Œà�
‰
�øˆ�
‰
�úré  c                 ó8  • [        U 5      n UR                  S5       Vs/ s H  o"S   US   US   S-  4PM     nnX1R                  S5       Vs/ s H  o"S   US   US   4PM     sn-  nUUR                  5         $ s  snf s  snf ! UR                  5         f = f)	aã  Every real winning price BidBrain knows of AND can also relate to
a real Cazana retail value, for the % of retail model (Mark
2026-08-24: "if we also knew that a 3 series winning bid was 90% of
cazana retail and im always bidding 88% im lossing most of them...
having this info should improve the accuracy"). Purchases already
store a Cazana figure as cazana_115 (retail times 1.15, the buying
brain's own uplifted figure), divided back down here to the plain
retail value so it is directly comparable to a lost bid's own,
unscaled cazana_retail. Returns (name, amount, retail) triples, only
for a row that genuinely has both; most rows will not, this is a
strict subset of bid_comps, deliberately never a guess at a missing
retail figure.z˜SELECT name, COALESCE(winning_bid, price) AS amount, cazana_115 FROM purchases WHERE COALESCE(winning_bid, price) IS NOT NULL AND cazana_115 IS NOT NULLr!   r½  rA   gffffffò?zlSELECT name, sold_for, cazana_retail FROM lost_bids WHERE sold_for IS NOT NULL AND cazana_retail IS NOT NULLrQ  r]   rX  rY  s       r   Úbid_comps_with_retailrg  d  sÓ   € ô �4‹=€Dð	ØJNÏ,É,ðXôKYó Zò KYÀQ�6‘˜A˜h™K¨¨<©¸4Ñ)?Ó@ñ KYˆð Zð 	ÏÉðGôJHó Iò JHÀA�F‘)˜Q˜z™]¨A¨oÑ,>Ó?ñ JHñ Iñ 	Iˆð à�
‰
�ùòZùòIøð
 	�
‰
�ús'   �B  A=ºB ÁBÁ&B Á=
B ÂBc                 ó*  • [        U 5      n UR                  S5       Vs/ s H  n[        U5      PM     nnX1R                  S5       Vs/ s H  n[        U5      PM     sn-  nUUR                  5         $ s  snf s  snf ! UR                  5         f = f)a  Every real winning price, with whatever else is known about that
exact car, for the like for like suggested bid model (Mark 2026-08-25:
"compare year, transmission, engine size, mileage so the ai is
comparing as similar cars as it can", then "add the transmission and
year, engine cc, grade, from the scrape for better matching"). Returns
one dict per comp: name, amount, retail (may be None), mileage, year,
transmission, engine, grade (any of the last five may be None, never
guessed). Retail, mileage, year, transmission, engine and grade all
only ever fill in from a real BidBrain read (a Cazana lookup, or a
sighting/purchase record), so most rows here will still be missing
some of them; bid_suggest.py scores on whatever is actually present
rather than requiring all of it.a  SELECT name, COALESCE(winning_bid, price) AS amount, CASE WHEN cazana_115 IS NOT NULL THEN cazana_115 / 1.15 END AS retail, mileage, year, transmission, engine, grade, bought_date AS date FROM purchases WHERE COALESCE(winning_bid, price) IS NOT NULL AND COALESCE(manual_entry, 0) = 0z SELECT name, sold_for AS amount, cazana_retail AS retail, mileage, year, transmission, engine, grade, date_bid AS date FROM lost_bids WHERE sold_for IS NOT NULLr\  rY  s       r   Úbid_comps_fullri  ~  s    € ô �4‹=€Dðð "&§¡ð0ô"1ó 2ò "1˜A”�Q–ñ "1ˆð 2ð 	§,¡,ð8ô#9ó :ò #9˜Q”�a–ñ #9ñ :ñ 	:ˆð à�
‰
�ùò'2ùò:øð 	�
‰
�ús'   �B   A6µB  Á
A;ÁB  Á6
B  Â Bc                 óÄ   • [        U 5      n UR                  S5       Vs/ s H  n[        U5      PM     snUR                  5         $ s  snf ! UR                  5         f = f)z½Every lost bid missing year, transmission, engine or grade, for the
database first backfill pass (Mark 2026-08-25). No browser needed,
this is a pure join against sightings already on file.zqSELECT * FROM lost_bids WHERE year IS NULL OR transmission IS NULL OR engine IS NULL OR grade IS NULL ORDER BY idr\  r"  s      r   Úlost_bids_missing_fieldsrk  ¨  s]   € ô �4‹=€DðØ!%§¡ð=ô">ó ?ò ">˜A”�Q–ñ ">ñ ?ð 	�
‰
�ùò	?øð 	�
‰
�úr^  c                 óÄ   • [        U 5      n UR                  S5       Vs/ s H  n[        U5      PM     snUR                  5         $ s  snf ! UR                  5         f = f)zcEvery purchase missing year, transmission, engine or grade, same
shape as lost_bids_missing_fields.zqSELECT * FROM purchases WHERE year IS NULL OR transmission IS NULL OR engine IS NULL OR grade IS NULL ORDER BY idr\  r"  s      r   Úpurchases_missing_fieldsrm  µ  s]   € ô �4‹=€DðØ!%§¡ð=ô">ó ?ò ">˜A”�Q–ñ ">ñ ?ð 	�
‰
�ùò	?øð 	�
‰
�úr^  c                 ó°   • [        U5      n UR                  SXX4U 45        UR                  5         UR                  5         g! UR                  5         f = f)zíFills in whichever of year/transmission/engine/grade is still
missing on a lost bid, never overwrites a value already on file (a
sighting used to backfill an old row should not clobber a figure a
later, more specific pass already found).z”UPDATE lost_bids SET year=COALESCE(year, ?), transmission=COALESCE(transmission, ?), engine=COALESCE(engine, ?), grade=COALESCE(grade, ?) WHERE id=?Nr  ©ra  r%   ra   r`   r)   r   r   s          r   Úset_lost_bid_fieldsrp  Á  sN   € ô �4‹=€DðØ�‰ðNð  °Ð7ô		9ð
 	�‰Œà�
‰
�øˆ�
‰
�úó   �%A ÁAc                 ó°   • [        U5      n UR                  SXX4U 45        UR                  5         UR                  5         g ! UR                  5         f = f)Nz”UPDATE purchases SET year=COALESCE(year, ?), transmission=COALESCE(transmission, ?), engine=COALESCE(engine, ?), grade=COALESCE(grade, ?) WHERE id=?r  ro  s          r   Úset_purchase_fieldsrs  Ó  sL   € ä�4‹=€DðØ�‰ðNð  °Ð7ô		9ð
 	�‰Œà�
‰
�øˆ�
‰
�úrq  c                 óÀ  • SSK n[        U5      n SnU  GH  nUR                  S5      nUR                  SUR                  S5      [	        UR                  S5      5      UR                  S5      UR                  S5      UR                  S	5      UR                  S
5      UR                  S5      UR                  S5      UR                  S5      UR                  S5      UR                  S5      U(       a  UR
                  " U5      OSUR                  S5      45        US-  nGM     UR                  5         UUR                  5         $ ! UR                  5         f = f)a  Store sold cars from the monthly reports, one dict per car with keys
stock_number, reg, make, model, sold_date, days_to_sell, sale_price,
purchase_price, margin, prep_total, source. Re run safe via the
(stock_number, sold_date) unique key; existing rows are refreshed.r   Nr4   aY  INSERT INTO sales_history
                   (stock_number, reg, make, model, sold_date, days_to_sell,
                    sale_price, purchase_price, margin, total_margin,
                    prep_total, prep_split, source)
                   VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)
                   ON CONFLICT(stock_number, sold_date) DO UPDATE SET
                     reg=excluded.reg, make=excluded.make, model=excluded.model,
                     days_to_sell=excluded.days_to_sell, sale_price=excluded.sale_price,
                     purchase_price=excluded.purchase_price, margin=excluded.margin,
                     total_margin=excluded.total_margin, prep_total=excluded.prep_total,
                     prep_split=COALESCE(excluded.prep_split, sales_history.prep_split),
                     source=COALESCE(excluded.source, sales_history.source)Ústock_numberr8   rV   rW   Ú	sold_dateÚdays_to_sellÚ
sale_priceÚpurchase_priceÚmarginr3   Ú
prep_totalrÄ   r   )r>  r   r  ry   rÏ   rã  r   r   )rã   r   r>  r   r:   r„   rÎ   s          r   Úrecord_sales_historyr|  á  s  € ó
 Ü�4‹=€DðØˆÜˆAØ—E‘E˜,Ó'ˆEØ�L‰LðOð —‘�~Ó&¬	°!·%±%¸³,Ó(?ÀÇÁÀvÃØ—‘�w“ §¡ {Ó!3°Q·U±U¸>Ó5JØ—‘�|Ó$ a§e¡eÐ,<Ó&=¸q¿u¹uÀX»Ø—‘�~Ó&¨¯©¨lÓ(;Þ&+�—’˜EÔ"°°q·u±u¸X³ð	Hôð& �‰F‹Añ+ ð, 	�‰ŒØà�
‰
�øˆ�
‰
�ús   ‘D)E ÅEc                 óŠ  • SSK JnJn  SSKn[	        U 5      n UR                  S5      R                  5        Vs/ s H  n[        U5      PM     nn0 nU HT  nU" UR                  S5      UR                  S5      5      nU(       d  M3  UR                  U/ 5      R                  U5        MV     UR                  S5        [        5       n	S	 n
UR                  5        GHò  u  p‹[        U5      nU
" U Vs/ s H  oUS
   PM	     sn5      nU
" U Vs/ s H  oUS   PM	     sn5      n[        S U 5       5      n0 Snn/ nU H�  nUR                  S5      (       d  M   UR                  " US   5      nUS-  nU" U5      nUb  UR                  U5        UR                  5        H'  u  nnUR                  US5      U=(       d    S-   UU'   M)     M�     U
" U5      nU(       aL  UR$                  " ['        UR                  5       5       VVs0 s H  u  nnU[)        UU-  5      _M     snn5      OSn0 nU H“  nUR                  S5      =(       d    SR+                  5       nU(       d  M4  UR                  US/ / S.5      nUS==   S-  ss'   US
   b  US   R                  US
   5        US   c  M|  US   R                  US   5        M•     UR                  5        VVs0 s Hq  u  nnUUS   US   (       a'  [-        [        US   5      [        US   5      -  S5      OSUS   (       a&  [)        [        US   5      [        US   5      -  5      OSS._Ms     nnnUR                  SUS   R                  S5      US   R                  S5      XŒUb  [-        US5      OSUb  [)        U5      OSUb  [)        U5      OSUXóR$                  " U5      U	45        GMõ     UR/                  5         [        U5      UR1                  5         $ s  snf s  snf s  snf ! [         ["        4 a     GMä  f = fs  snnf s  snnf ! UR1                  5         f = f)a  Roll sales_history up into learned_models, one row per model_key:
how many sold, average days to sell, average prep, average margin, how many
kept a positive margin after prep (spread_held), and the per platform
split. Replaces the whole table each time, cheap at this scale.r   )r6   Úactual_prepr   NzSELECT * FROM sales_historyrV   rW   zDELETE FROM learned_modelsc                 óx   • U  Vs/ s H	  oc  M  UPM     n nU (       a  [        U 5      [        U 5      -  $ S $ s  snf r   )rQ  rá   )Úvalsró   s     r   ÚavgÚ#rebuild_learned_models.<locals>.avg  s5   € Ù#Ó5št˜!—A™tˆDÐ5Þ.2”C˜“I¤ D£	Ñ)Ð<¸Ð<ùò 6s   …7�7rw  rz  c              3   óL   #   • U  H  oS    =(       d    SS:”  d  M  Sv •  M     g7f)rz  r   r   Nr´   r  s     r   r
  Ú)rebuild_learned_models.<locals>.<genexpr>  s    é € ÐA¢$˜Q¨X©;×+;¸!¸qÑ*@—q‘q¢$ùs   ‚$›	$r4   rÄ   rÊ   )r:   rÕ   rz  r:   rÕ   )r:   Úavg_daysÚ
avg_margina  INSERT INTO learned_models
                   (make, model, model_key, n_sold, avg_days_to_sell,
                    avg_prep_total, avg_margin, prep_split, spread_held,
                    by_platform, updated_at)
                   VALUES (?,?,?,?,?,?,?,?,?,?,?))r  r6   r~  r>  r   ry   r{   rø   r  r  rí   r¤   rÒ  rá   rQ  r?  rÙ   r@  rã  r  r   r;  r…  r   r   )r   r6   r~  r>  r   r„   Úsoldr  r  r§   r�  rã   r:   rÕ   rz  r   Ú
split_sumsÚsplit_nÚactualsÚsÚaprˆ   r½  Úprepr›   ró   r4   ÚplatsÚsrcr"  Úby_platforms                                  r   Úrebuild_learned_modelsr‘    sý  € ÷
 <ÛÜ�4‹=€DðFØ!%§¡Ð.KÓ!L×!UÑ!UÔ!WÓXÒ!W˜A”�Q–Ñ!WˆÐXØˆÛˆAÙ˜AŸE™E &›M¨1¯5©5°«>Ó:ˆCÞÙØ×Ñ˜c 2Ó&×-Ñ-¨aÖ0ñ	 ð
 	�‰Ð1Ô2Ü‹fˆò	=ð  Ÿ™Ÿ‰IˆCÜ�D“	ˆAÙ±4Ó8²4¨a˜.Ô)±4Ñ8Ó9ˆDÙ©tÓ4ªt¨!˜Hœ+©tÑ4Ó5ˆFÜÑA¡$ÓAÓAˆDð #% a˜ˆJØˆGÛ�Ø—5‘5˜×&Ó&ð!Ø ŸJšJ q¨¡Ó7˜ð ˜q‘L�GÙ$ Q›�BØ‘~ØŸ™ rÔ*Ø'(§w¡w¦y™˜˜VØ*4¯.©.¸¸aÓ*@ÀFÇKÈaÑ*P˜
 3›ó (1ñ ñ �w“<ˆDæ$ð Ÿ*š*ÄfÈZ×M]ÑM]ÓM_ÔF`Ô%aÒF`¹d¸aÀ a¬¨Q°©[Ó)9Ò&9ÑF`Ò%aÔbØ*.ð àˆEÛ�Ø—u‘u˜X“×,¨"×3Ñ3Ó5�ÞÙØ×$Ñ$ S°¸2ÈÑ*LÓM�Ø�#“˜!‘“Ø�^Ñ$Ñ0Ø�f‘I×$Ñ$ Q ~Ñ%6Ô7Ø�X‘;Ó*Ø�h‘K×&Ñ& q¨¡{Ö3ñ ð $Ÿk™kœmô	ò ,‘F�C˜ð ˜1˜S™6ØOPÐQWÏy¤%¬¨A¨f©I«¼¸Q¸v¹Y»Ñ(GÈÔ"KÐ^bØPQÐRZ×P[¤C¬¨A¨h©KÓ(8¼3¸qÀ¹{Ó;KÑ(KÔ$LÐaeñgò gñ ,ð	 ñ ð �L‰Lð5ð
 �a‘—‘˜VÓ$ d¨1¡g§k¡k°'Ó&:¸CØ#'Ñ#3”�t˜Q”¸Ø"Ñ.”�T”°DØ &Ñ 2”�V”¸Ø˜T§:¢:¨kÓ#:¸Cð	A÷ñU (ðl 	�‰ŒÜ�6‹{à�
‰
�ùòK Yùò 9ùÚ4øô '¬	Ð2ó !Û ð!üó &bùóøð* 	�
‰
�ús{   ™!P0 ºO=ÁB#P0 Ã2P
Ä P0 ÄP
Ä8P0 ÅPÅ-BP0 ÈP$È BP0 Ê*.P0 ËA8P*ÍBP0 Ï=P0 ÐP!ÐP0 Ð P!Ð!P0 Ð0Qc                 ór  • SSK Jn  [        U 5      n UR                  S5      R	                  5       (       d   UR                  5         gUR                  S5       H4  nU" US   US   5      US   =(       d    S	:w  d  M$    UR                  5         g
    UR                  5         g! UR                  5         f = f)aÂ  True when learned_models holds keys built by an older model_key rule,
so a live car no longer keys to its own sold record. The key is derived
data, the make and model on each row are the real record, so the fix is
simply to rebuild the table. One small scan, and it says no when there is
no sales history to rebuild from, so a copy that has never ingested a
sales report (Mark's) is never touched. Added 2026-09-20 with the
CompMatch grouping change.r   r  z#SELECT 1 FROM sales_history LIMIT 1Fz1SELECT make, model, model_key FROM learned_modelsrV   rW   r6   rÊ   T)r  r6   r   ry   r   r   )r   r6   r   r„   s       r   Úlearned_keys_staler“  W  s™   € õ /Ü�4‹=€DðØ�|‰|ÐAÓB×KÑK×MÑMØð 	�
‰
�ð —‘ÐQÖRˆAÙ˜˜6™ A g¡JÓ/°A°k±N×4HÀbÕIØð 	�
‰
�ñ Sð à�
‰
�øˆ�
‰
�ús   “$B$ Á	1B$ Á>B$ ÂB$ Â$B6c                 óæ   • [        U 5      n UR                  S5      R                  5        Vs0 s H  o"S   [        U5      _M     snUR	                  5         $ s  snf ! UR	                  5         f = f)zGAll learned model figures keyed by model_key, for the daily card notes.zSELECT * FROM learned_modelsr6   r  r"  s      r   Úlearned_forr•  l  s\   € ä�4‹=€DðØ15·±Ð>\Ó1]×1fÑ1fÔ1hÓiÒ1h¨A�+‘¤ Q£Ò'Ñ1hÑià�
‰
�ùò jøà�
‰
�ús   �!A ®AÁA ÁA ÁA0c           	      ór  • [        U5      n [        XS9nU(       d  / UR                  5         $ UR                  SSR	                  S[        U5      -  5       S3/ UQUP75      R                  5       nU Vs/ s H  n[        U5      PM     snUR                  5         $ s  snf ! UR                  5         f = f)a  Previous sightings of this registration with a recorded reserve, before
sale_date, newest first. Used to spot a reserve that has been lowered on a
car doing the rounds. Returns a list of {sale_date, reserve, cap_clean}.
reg may carry both of a car's plates, see sighting_plates.rý   z`SELECT sale_date, reserve, cap_clean
               FROM sightings
               WHERE reg IN (rß   rà   zR) AND sale_date < ? AND reserve IS NOT NULL
               ORDER BY sale_date DESC)r   rõ   r   ry   rË   rá   r{   rø   )r8   rÑ   r   r   r  rã   r„   s          r   Úreserve_historyr—  u  s¯   € ô
 �4‹=€DðÜ˜sÑ.ˆÞØð 	�
‰
�ð �|‰|ðà!Ÿh™h s¬S°«Y¡Ó7Ð8ð 9'ð*ð ˆdÐ�IÑó
÷ ‰(‹*ð 	ñ "&Ó&¢˜A”�Q–¡Ñ&à�
‰
�ùò 'øà�
‰
�ús#   �B$ ¯AB$ Á7BÂB$ ÂB$ Â$B6Úsummaryc           	      óˆ  • / nU R                  S0 5      nU(       a_  / n[        U5       H(  nX$   nUR                  U SU SUS:X  a  SOS 35        M*     UR                  SSR                  U5      -   S	-   5        U R                  S
/ 5      n[	        U5      S:”  a&  UR                  SSR                  U5      -   S-   5        U$ )zzTurn a repeat_summary into plain card lines, house style, no hyphens.
Returns an empty list when there is nothing to flag.r  z
 previous r.   r   r  r  zSeen before within 30 days: rW  r  r  zIn both r  z today.)r  r  rí   rË   rá   )r˜  ÚlinesÚprevr7  r-   r:   r  s          r   Úrepeat_flag_linesrœ  ‹  s¾   € ð €EØ�;‰;�z 2Ó&€DÞØˆÜ˜tžˆHØ‘ˆAØ�L‰L˜A˜3˜j¨¨
°!¸aÀ1»f±FÈ'Ð3RÐSÖTñ %ð 	�‰Ð3°d·i±iÀÓ6FÑFÈÑLÔMØ�;‰;Ð+¨RÓ0€DÜ
ˆ4ƒy�1ƒ}Ø�‰�Z '§,¡,¨tÓ"4Ñ4°yÑ@ÔAØ€Lr   c                 ó  • [        U 5      n U (       d  g[        U5      n UR                  SXX#U[        5       U=(       d
    [	        5       45        UR                  5         UR                  5         g! UR                  5         f = f)a  Hide a car from all future daily lists, with a reason. Upserts one row
per registration and resets reviewed so the new reason is surfaced again.
updated_at is the moment of the change (UTC ISO); an action applied
from Dealer OS passes the action's own time so the echo cannot bounce.Na•  INSERT INTO hidden_cars (reg, reason, make, model, name, hidden_at, active, reviewed, updated_at)
               VALUES (?,?,?,?,?,?,1,0,?)
               ON CONFLICT(reg) DO UPDATE SET
                 reason=excluded.reason, make=excluded.make, model=excluded.model,
                 name=excluded.name, hidden_at=excluded.hidden_at, active=1, reviewed=0,
                 updated_at=excluded.updated_at)rÏ   r   ry   r¤   r»   r   r   )r8   r¾  rV   rW   r!   r   r/   r   s           r   Úhide_carrž  œ  si   € ô �C‹.€CÞØÜ�4‹=€DðØ�‰ð3ð ˜$ t¬T«V°Z×5LÄ7Ã9ÐMô	
ð 	�‰Œà�
‰
�øˆ�
‰
�ús    A A1 Á1Bc                 óæ   • [        U 5      n [        U5      n UR                  SU=(       d
    [        5       U 45        UR	                  5         UR                  5         g ! UR                  5         f = f)Nz9UPDATE hidden_cars SET active=0, updated_at=? WHERE reg=?)rÏ   r   ry   r»   r   r   )r8   r   r/   r   s       r   Ú
unhide_carr   µ  sS   € Ü
�C‹.€CÜ�4‹=€DðØ�‰ÐPØ ×-¤G£I¨sÐ3ô	5à�‰Œà�
‰
�øˆ�
‰
�ús   ˜5A ÁA0c                 ó  • [        U5      n UR                  S5        [        5       nU  Hª  nUR                  S[        UR	                  S5      5      [        UR	                  S5      5      UR	                  S5      =(       d    SUR	                  S5      =(       d    SUR	                  S5      UR	                  S	5      UR	                  S
5      U45        M¬     UR                  5         UR                  5         g! UR                  5         f = f)zhReplace the DealerKit in stock records with this read (whole list,
a car that has left is gone from it).zDELETE FROM dealerkit_stockz½INSERT OR REPLACE INTO dealerkit_stock
                   (vehicle_id, reg, make, model, purchase_cost, retail_price, life_cycle_status, read_at)
                   VALUES (?,?,?,?,?,?,?,?)rÌ  r8   rV   rÊ   rW   Úpurchase_costÚretail_priceÚlife_cycle_statusN)r   ry   r»   rÌ   r  rÏ   r   r   )rã   r   r   r§   r„   s        r   Úsave_dealerkit_stockr¥  À  sÓ   € ô �4‹=€DðØ�‰Ð2Ô3Ü‹iˆÛˆAØ�L‰Lð/ô �Q—U‘U˜<Ó(Ó)¬9°Q·U±U¸5³\Ó+BÀAÇEÁEÈ&ÃM×DWÐUWÐYZ×Y^ÑY^Ð_fÓYg×YmÐkmØ—‘�Ó'¨¯©¨~Ó)>ÀÇÁÐFYÓ@ZÐ\_ðaö	bñ ð 	�‰Œà�
‰
�øˆ�
‰
�ús   �CC9 Ã9DrY  c                ó"  • [        U5      n [        R                  " U /UQ76  HB  nUR                  SU45      R	                  5       nU(       d  M-  US   s  UR                  5         $     UR                  5         g! UR                  5         f = f)a×  DealerKit's own vehicle id for a plate, from the stock records the
in stock read keeps (2026-09-12): a sold car was in stock once, so its
id is usually here, and the sale price read needs no list scan. These
rows carry DEALERKIT's plate, so alt_regs takes the car's other plates
(2026-09-20): a car whose seller kept a private plate is in here under
the plate underneath, not the one the purchase was recorded on. Tried
in the order given, so the first plate always wins.zPSELECT vehicle_id FROM dealerkit_stock WHERE reg=? ORDER BY read_at DESC LIMIT 1rÌ  N)r   r   rK  ry   r   r   )r8   r   Úalt_regsr   Úplater„   s         r   Údealerkit_stock_vehicle_idr©  Ó  s~   € ô �4‹=€DðÜ×&Ò& sÐ6¨XÕ6ˆEØ—‘ÐoØ#˜Xó'ß'/¡x£zð çˆqØ˜‘Ñ&ð 	�
‰
�ñ 7ð
 à�
‰
�øˆ�
‰
�ús   �A A< ÁA< Á(A< Á<Bc                 ó¨  • [        U5      n [        U 5      n UR                  SU 45      R                  5       nU(       a  US   UR	                  5         $ UR                  SXX#[
        R
                  R                  5       R                  SS945      nUR                  5         UR                  UR	                  5         $ ! UR	                  5         f = f)zOne car Dealer OS wants the deal price of. A car already waiting is
not queued twice (DealerKit sends every sale notice twice).z?SELECT id FROM sale_price_reads WHERE reg=? AND done_at IS NULLr1   zWINSERT INTO sale_price_reads (reg, api_id, name, sold_at, queued_at) VALUES (?,?,?,?,?)r¯   r°   )
r   rÏ   ry   r   r   r²   r§   r³   r   r¦  )r8   Úapi_idr!   Úsold_atr   r   r„   rb  s           r   Úqueue_sale_price_readr­  ç  s­   € ô �4‹=€Dð
Ü˜‹nˆØ�L‰LÐZÐ]`Ð\bÓc×lÑlÓnˆÞØ�T‘7ð 	�
‰
�ð �l‰lÐtØ¨¼×8IÑ8I×8MÑ8MÓ8O×8YÑ8YÐclÐ8YÐ8mÐnópˆà�‰ŒØ�}‰}à�
‰
�øˆ�
‰
�ús   �7B? ÁAB? Â?Cc                 óà   • [        U 5      n UR                  S5      R                  5        Vs/ s H  n[        U5      PM     snUR	                  5         $ s  snf ! UR	                  5         f = f)Nz@SELECT * FROM sale_price_reads WHERE done_at IS NULL ORDER BY idr  r"  s      r   Úpending_sale_price_readsr¯  ø  sU   € Ü�4‹=€DðØ!%§¡Ð.pÓ!q×!zÑ!zÔ!|Ó}Ò!|˜A”�Q–Ñ!|Ñ}à�
‰
�ùò ~øà�
‰
�úr  c                 ó  • [        U5      n UR                  S[        R                  R                  5       R	                  SS9[        U5      S S U 45        UR                  5         UR                  5         g ! UR                  5         f = f)Nz:UPDATE sale_price_reads SET done_at=?, result=? WHERE id=?r¯   r°   iô  )r   ry   r²   r§   r³   rÌ   r   r   )ra  rä  r   r   s       r   Úfinish_sale_price_readr±     sr   € Ü�4‹=€DðØ�‰ÐQÜ×'Ñ'×+Ñ+Ó-×7Ñ7ÀÐ7ÐKÌSÐQWË[ÐY]ÐZ]ÐM^Ð`fÐgô	ià�‰Œà�
‰
�øˆ�
‰
�ús   �AA8 Á8B
é   c                 ó8  • [        U5      nSn U =(       d    /  GHF  nU=(       d    0 n[        UR                  S5      =(       d2    UR                  S5      =(       d    UR                  S5      =(       d    S5      R                  5       n[        UR                  S5      =(       d    S5      R                  5       R	                  5       nU(       a  U(       a  U(       d  MÆ  UR                  SXxU[        UR                  S5      =(       d    S5      =(       d    SUR                  S5      =(       d    SUR                  S	5      =(       d    SU[        5       45        US
-  nGMI     UR                  5         UUR                  5         $ ! UR                  5         f = f)a.  The cars one shortlist.read action named, in the order Dealer OS
sent them (soonest closing first). A car already waiting keeps its
place and takes the newer ask; a car read earlier today is wanted
again, its old answer kept until the new one lands. Returns how many
rows are now waiting from this ask.r   r  r8   rÃ   rÊ   r-   a|  INSERT INTO deep_reads (car_key, platform, sale_date, reg, listing_url, name, asked_at, queued_at, wanted, attempts)
                   VALUES (?,?,?,?,?,?,?,?,1,0)
                   ON CONFLICT(car_key, platform, sale_date) DO UPDATE SET
                     reg=COALESCE(excluded.reg, deep_reads.reg),
                     listing_url=COALESCE(excluded.listing_url, deep_reads.listing_url),
                     name=COALESCE(excluded.name, deep_reads.name),
                     asked_at=MAX(COALESCE(excluded.asked_at, ''), COALESCE(deep_reads.asked_at, '')),
                     queued_at=excluded.queued_at, wanted=1, attempts=0Nr!   r   )
r   rÌ   r  r;  rÅ  ry   rÏ   r¤   r   r   )	rÆ   rÑ   Úasked_atr   r   r:   r�   r  r-   s	            r   Úqueue_deep_readsrµ    s,  € ô �4‹=€DØ	€AðØ—˜“ˆAØ—�RˆAÜ�a—e‘e˜E“l×P a§e¡e¨E£l×P°a·e±e¸MÓ6J×PÈbÓQ×WÑWÓYˆCÜ˜1Ÿ5™5 Ó,×2°Ó3×9Ñ9Ó;×AÑAÓCˆHÞžh®iÙØ�L‰LðKð  	¬9°Q·U±U¸5³\×5GÀRÓ+H×+PÈDØ—‘�}Ó%×-¨¨q¯u©u°V«}×/DÀÀhÔPTÓPVðXô
Yð �‰F‹Añ# ð$ 	�‰ŒØà�
‰
�øˆ�
‰
�ús   �E'F ÆFc                 óà   • [        U 5      n UR                  S5      R                  5        Vs/ s H  n[        U5      PM     snUR	                  5         $ s  snf ! UR	                  5         f = f)z¼Every car still waiting to be read, the order it was asked in,
leaving out one that has already been tried DEEP_READ_MAX_ATTEMPTS
times (deep_read_pass answers those with an error itself).z3SELECT * FROM deep_reads WHERE wanted=1 ORDER BY idr  r"  s      r   Úpending_deep_readsr·  .  sn   € ô �4‹=€DðØ!%§¡ØAó"CßCKÁ8Ã:ð"Nó Oò "N˜A”�Q–ñ "Nñ Oð 	�
‰
�ùòOøð 	�
‰
�úr  c                 ó  • [        U5      n UR                  SU 45        UR                  5         UR                  SU 45      R                  5       nU(       a  [	        US   5      OS UR                  5         $ ! UR                  5         f = f)zAOne more pass has started on this row. Returns the attempt count.z4UPDATE deep_reads SET attempts=attempts+1 WHERE id=?z*SELECT attempts FROM deep_reads WHERE id=?r_   r   )r   ry   r   r   r   r   )ra  r   r   r„   s       r   Ústart_deep_readr¹  :  sl   € ä�4‹=€DðØ�‰ÐKÈfÈYÔWØ�‰ŒØ�L‰LÐEÈÀyÓQ×ZÑZÓ\ˆÞ%&Œs�1�Z‘=Ô!¨AÐ-à�
‰
�øˆ�
‰
�ús   �AA9 Á9Bc                 ó<  • [        U5      n UR                  SU=(       d    0 R                  S5      =(       d
    [        5       [        R
                  " U=(       d    0 5      U 45        UR                  5         UR                  5         g! UR                  5         f = f)zgThe answer for one car, read or failed: kept whole as the push's
deep block, the row no longer waiting.zAUPDATE deep_reads SET wanted=0, read_at=?, deep_json=? WHERE id=?Úread_atN)r   ry   r  r¤   r>  rã  r   r   )ra  Údeepr   r   s       r   Úfinish_deep_readr½  F  sm   € ô �4‹=€DðØ�‰ÐXØ—z˜r×&Ñ& yÓ1×;´T³V¼T¿ZºZÈÏ
ÐPRÓ=SÐU[Ð\ô	^à�‰Œà�
‰
�øˆ�
‰
�ús   �A+B	 Â	Bc                 ó2  • [        U5      n 0 nUR                  SU 45      R                  5        H'  n [        R                  " US   5      X4S   US   4'   M)     UUR                  5         $ ! [
        [        4 a     MP  f = f! UR                  5         f = f)zñEvery answered deep read for a sale date, keyed (platform, car_key),
for the push to hang on each car record. A car still waiting with no
earlier answer is not here (its deep stays null, Dealer OS shows it
as reading from the ask it logged).z_SELECT platform, car_key, deep_json FROM deep_reads WHERE sale_date=? AND deep_json IS NOT NULLÚ	deep_jsonr-   Úcar_key)r   ry   r{   r>  r?  r@  rÙ   r   )rÑ   r   r   rò   r„   s        r   Údeep_reads_forrÁ  R  s˜   € ô
 �4‹=€Dð
ØˆØ—‘ÐØ(˜ló,ß,4©H«Jò7ˆAðÜ59·Z²ZÀÀ+ÁÓ5O��z‘] A i¡LÐ1Ó2ñ7ð à�
‰
�øô	 œzÐ*ó Úðûð 	�
‰
�ús.   �&B ´#A-ÁB Á-BÁ=B Â BÂB ÂBé   c                 óD  • [         R                  R                  5       [         R                  " U S9-
  R	                  5       n[        U5      n UR                  SU45      nUR                  5         UR                  UR                  5         $ ! UR                  5         f = f)z0Rows for sales more than keep_days ago, dropped.rÔ   z*DELETE FROM deep_reads WHERE sale_date < ?)
r²   r×   r�  rÛ   r³   r   ry   r   r_  r   )Ú	keep_daysr   rL  r   rb  s        r   Úprune_old_deep_readsrÅ  e  sq   € ä�m‰m×!Ñ!Ó#¤h×&8Ò&8¸iÑ&HÑH×SÑSÓU€FÜ�4‹=€DðØ�l‰lÐGÈ&ÈÓSˆØ�‰ŒØ�|‰|à�
‰
�øˆ�
‰
�ús   Á.B ÂBc                 óà   • [        U 5      n UR                  S5      R                  5        Vs/ s H  n[        U5      PM     snUR	                  5         $ s  snf ! UR	                  5         f = f)Nz*SELECT * FROM dealerkit_stock ORDER BY regr  r"  s      r   Údealerkit_stock_listrÇ  q  sU   € Ü�4‹=€DðØ!%§¡Ð.ZÓ![×!dÑ!dÔ!fÓgÒ!f˜A”�Q–Ñ!fÑgà�
‰
�ùò høà�
‰
�úr  c	                 ó¸  • [        U 5      n U (       d  g[        U5      n	 U	R                  SX=(       d    S[        R                  " [        U=(       d    / 5      5      U=(       d    SU=(       d    SU=(       d    SU=(       d    S[        5       U=(       d
    [        5       4	5        U	R                  5         U	R                  5         g! U	R                  5         f = f)aM  Release a car from the rules that held it back (Steven 2026-09-10, on
Dealer OS: "click a button to say un hold it back and add this to the
list and give a reason why, so it teaches bidbrain more"). rules is the
list of reasons the car was held on, kept beside his own reason so the
two can be read together when the rules are tuned.Naæ  INSERT INTO released_cars (reg, reason, rules, make, model, name, who, released_at, active, reviewed, updated_at)
               VALUES (?,?,?,?,?,?,?,?,1,0,?)
               ON CONFLICT(reg) DO UPDATE SET
                 reason=excluded.reason, rules=excluded.rules, make=excluded.make,
                 model=excluded.model, name=excluded.name, who=excluded.who,
                 released_at=excluded.released_at, active=1, reviewed=0,
                 updated_at=excluded.updated_atrÊ   )
rÏ   r   ry   r>  rã  rì   r¤   r»   r   r   )
r8   r¾  ÚrulesrV   rW   r!   rÁ  r   r/   r   s
             r   Úrelease_carrÊ  y  sš   € ô �C‹.€CÞØÜ�4‹=€DðØ�‰ð3ð —,˜B¤§
¢
¬4°·¸Ó+<Ó =¸t¿zÀrÈ5Ï;ÐTVØ�Z�R˜Ÿ ¤D£F¨J×,C¼'»)ðEô
	
ð 	�‰Œà�
‰
�øˆ�
‰
�ús    BC ÃCc                 óî   • [        U5      n UR                  S[        U 5      45      R                  5       nU(       a  US   =(       d    US   OS  UR	                  5         $ ! UR	                  5         f = f)Nz=SELECT updated_at, released_at FROM released_cars WHERE reg=?r/   Úreleased_at©r   ry   rÏ   r   r   ©r8   r   r   r„   s       r   Úreleased_updated_atrÏ  •  sa   € Ü�4‹=€DðØ�L‰LÐXÜ# C›.Ð*ó,ß,4©H«Jð 	
æ89��,‘×3 1 ]Ñ#3ø¸tÐCà�
‰
�øˆ�
‰
�úó   �AA" Á"A4c                 ót  • [        U 5      n UR                  S5      R                  5       n0 nU HM  n [        R                  " US   =(       d    S5      nUS   =(       d    SUS   =(       d    SUS   US.X4S	   '   MO     UUR                  5         $ ! [
         a    / n NKf = f! UR                  5         f = f)
zBreg to {reason, who, released_at, rules} for every active release.zMSELECT reg, reason, who, released_at, rules FROM released_cars WHERE active=1rÉ  r  r¾  rÊ   rÁ  rÌ  )r¾  rÁ  rÌ  rÉ  r8   )r   ry   r{   r>  r?  rÙ   r   )r   r   rã   rò   r„   rÉ  s         r   Úreleased_maprÒ  Ÿ  s¸   € ä�4‹=€DðØ�|‰|ð -ó .ß.6©h«jð 	àˆÛˆAðÜŸ
š
 1 W¡:×#5°Ó6�ð ()¨¡{×'8°bÀÀ5ÁÇÈRØ,-¨mÑ,<ÀuñNˆC�%‘‹Mñ ð à�
‰
�øô ó Ø’ðûð 	�
‰
�ús.   �&B% ´"BÁ,B% ÂB"ÂB% Â!B"Â"B% Â%B7c                 ó  • [        U5      n SU (       a  SOS-   S-   nUR                  U5      R                  5        Vs/ s H  n[        U5      PM     snUR	                  5         $ s  snf ! UR	                  5         f = f)z©Released cars with the reason given, for the assistant to read next to
the hide notes when the rules are tuned: a rule that keeps getting
overridden is a rule to soften.z`SELECT reg, reason, rules, make, model, name, who, released_at FROM released_cars WHERE active=1ú AND reviewed=0rÊ   z ORDER BY released_at DESCr  ©Úonly_newr   r   Úsqlr„   s        r   Úreleased_notesrØ  ²  st   € ô �4‹=€Dðð Þ8@Ñ#4ÀbñJà+ñ,ˆð "&§¡¨cÓ!2×!;Ñ!;Ô!=Ó>Ò!=˜A”�Q–Ñ!=Ñ>à�
‰
�ùò ?øà�
‰
�ús   �2A, ¿A'ÁA, Á'A, Á,A>c                 óä   • [        U 5      n UR                  S5      R                  5       nU Vs/ s H  n[        U5      PM     snUR	                  5         $ s  snf ! UR	                  5         f = f)z]Every active hide with its reason and timestamps, for the Dealer OS
echo and the hidden list.z|SELECT reg, reason, make, model, name, hidden_at, active, updated_at FROM hidden_cars WHERE active=1 ORDER BY hidden_at DESCr  ©r   r   rã   r„   s       r   Úhidden_listrÛ  À  sb   € ô �4‹=€DðØ�|‰|ðFóGçGOÁxÃzð 	ñ "&Ó&¢˜A”�Q–¡Ñ&à�
‰
�ùò 'øà�
‰
�úr  c                 óî   • [        U5      n UR                  S[        U 5      45      R                  5       nU(       a  US   =(       d    US   OS  UR	                  5         $ ! UR	                  5         f = f)Nz9SELECT updated_at, hidden_at FROM hidden_cars WHERE reg=?r/   Ú	hidden_atrÍ  rÎ  s       r   Úhidden_updated_atrÞ  Í  s`   € Ü�4‹=€DðØ�L‰LÐTÜ# C›.Ð*ó,ß,4©H«Jð 	
æ67��,‘×1 1 [¡>ø¸TÐAà�
‰
�øˆ�
‰
�úrÐ  c                 óÖ   • [        U 5      n UR                  S5      R                  5       nU Vs1 s H  o3S   iM	     snUR                  5         $ s  snf ! UR                  5         f = f)z*The set of registrations currently hidden.z*SELECT reg FROM hidden_cars WHERE active=1r8   ©r   ry   r{   r   rÚ  s       r   Úhidden_regsrá  ×  sU   € ä�4‹=€DðØ�|‰|ÐHÓI×RÑRÓTˆÙ"&Ó'¢$˜Q�%”¡$Ñ'à�
‰
�ùò (øà�
‰
�úó   �#A °A¾A ÁA ÁA(c                 óú  • [        U5      n/ n [        5       nU  H§  n[        UR                  S5      5      nU(       d  M&  UR	                  SU45      R                  5       nUR	                  SXeR                  SS5      UR                  SS5      UR                  SS5      U45        U(       a  M–  UR                  U5        M©     UR                  5         UR                  5         U$ ! UR                  5         f = f)zÜRecord cars bought on a platform whose sale was then cancelled or voided.
rows are purchase dicts (platform, reg, name, status). Keyed by normalised
reg, idempotent. Returns the regs newly added, so the run can log them.r8   z%SELECT 1 FROM walked_away WHERE reg=?a  INSERT INTO walked_away (reg, platform, name, status, noted_at)
                   VALUES (?, ?, ?, ?, ?)
                   ON CONFLICT(reg) DO UPDATE SET
                     platform=excluded.platform, name=excluded.name,
                     status=excluded.statusr-   rÊ   r!   r_  )	r   r¤   rÏ   r  ry   r   rí   r   r   )rã   r   r   r‡  r§   r„   r8   Úexistss           r   rZ  rZ  á  sÛ   € ô �4‹=€DØ
€CðÜ‹fˆÛˆAÜ˜AŸE™E %›LÓ)ˆCÞÙØ—\‘\Ø7¸#¸óAßAIÁÃð à�L‰Lð/ð
 —e‘e˜J¨Ó+¨Q¯U©U°6¸2Ó->ÀÇÁÀhÐPRÓ@SÐUXÐYô[÷ �6Ø—
‘
˜3–ñ ð 	�‰Œà�
‰
ŒØ€Jøð 	�
‰
�ús   �BC( Â1%C( Ã(C:c                 óÖ   • [        U 5      n UR                  S5      R                  5       nU Vs1 s H  o3S   iM	     snUR                  5         $ s  snf ! UR                  5         f = f)zORegistrations of cars bought then cancelled or voided, never to be shown
again.zSELECT reg FROM walked_awayr8   rà  rÚ  s       r   Úwalked_away_regsræ  þ  sW   € ô �4‹=€DðØ�|‰|Ð9Ó:×CÑCÓEˆÙ"&Ó'¢$˜Q�%”¡$Ñ'à�
‰
�ùò (øà�
‰
�úrâ  c                 óà   • [        U 5      n UR                  S5      R                  5        Vs/ s H  n[        U5      PM     snUR	                  5         $ s  snf ! UR	                  5         f = f)zFThe walked away cars with their detail, for audit and the CLI listing.zTSELECT reg, platform, name, status, noted_at FROM walked_away ORDER BY noted_at DESCr  r"  s      r   Úwalked_away_listrè  	  sg   € ä�4‹=€DðØ!%§¡ð%ó"&ç&.¡h£jð"1ó 2ò "1˜A”�Q–ñ "1ñ 2ð 	�
‰
�ùò	2øð 	�
‰
�úr  c                 óŠ  • [        U 5      n UR                  S5      R                  5       nUR                  5         / nU Hj  nUR	                  SSUS   US   =(       d    SUS   =(       d    SUS   US	   =(       d    S
R                  5       R                  5       S/ / / / / / / S.5        Ml     U$ ! UR                  5         f = f)aW  The cars whose sale fell through that are NOT in the purchases table.

walked_away and a cancelled purchase are the same real thing, a deal that
died, they just arrived by different routes: a cancelled purchase is one
we had already captured, walked_away also holds the ones read straight off
Motorway's own Cancelled list going back further than our purchases do.
So the purchases page's Cancelled view was showing 88 of 541 (Steven,
2026-08-30: "walked away cars are just cancelled cars with a different
reason, we should always be able to see them when we look at cancelled
cars unless filtered otherwise").

These are returned shaped like a purchase row so the page can show them in
the same table, but with id None and shell True: there is no purchase
record behind them, so nothing on such a row is editable and it opens no
drawer. Every money field is None rather than 0, the honest absence, so
they never move a total.

Deliberately NOT folded into list_purchases(): that feeds the DealerKit
push, the accounts email and the platform sync, none of which should ever
see a row with no purchase behind it.zãSELECT w.reg, w.platform, w.name, w.status, w.noted_at FROM walked_away w WHERE NOT EXISTS (SELECT 1 FROM purchases p                   WHERE REPLACE(UPPER(p.reg),' ','') = REPLACE(UPPER(w.reg),' ','')) ORDER BY w.noted_at DESCNTr8   r!   rÊ   r-   Únoted_atr_  Ú	Cancelled)r1   Úshellr8   r!   r-   re   rf   rg   rE   rD   rF   rX   rY   rZ   r­   )r   ry   r{   r   rí   r;  Ú
capitalize)r   r   rã   rò   r„   s        r   Úcancelled_shell_rowsrî    sÍ   € ô* �4‹=€DðØ�|‰|ð'ó(÷ )1©«
ð	 	ð 	�
‰
ŒØ
€CÛˆØ�
‰
Ø Ø�U‘8 Q v¡Y§_°"À!ÀJÁ-×BUÐSUØ˜j™Mð "# 8¡×!;°× BÑ BÓ D× OÑ OÓ QØ *Ø¸ÈØ °BÈrÐ\^ñ

ö 
	ñ ð €Jøð 	�
‰
�ús   �B0 Â0Cc                 ó0   • [        U 5      [        U 5      -  $ )z‹Every registration that must never appear on the daily page: the cars
Steven has hidden, plus the cars bought then cancelled on a platform.)rá  ræ  rY  s    r   Úsuppressed_regsrð  B  s   € ô �tÓÔ/°Ó5Ñ5Ð5r   c                 ó  • [        U5      n SnU (       a  US-  nUS-  nUR                  U5      R                  5        Vs/ s H  n[        U5      PM     snUR	                  5         $ s  snf ! UR	                  5         f = f)z~Active hidden cars that have a reason, for the assistant to mine into
rules. only_new limits to those not yet marked reviewed.zxSELECT reg, reason, make, model, name, hidden_at FROM hidden_cars WHERE active=1 AND reason IS NOT NULL AND reason != ''rÔ  z ORDER BY hidden_atr  rÕ  s        r   Úhidden_notesrò  H  sv   € ô �4‹=€DððHˆæØÐ$Ñ$ˆCØÐ$Ñ$ˆØ!%§¡¨cÓ!2×!;Ñ!;Ô!=Ó>Ò!=˜A”�Q–Ñ!=Ñ>à�
‰
�ùò ?øà�
‰
�ús   �4A. ÁA)ÁA. Á)A. Á.B c                 óÐ   • [        U5      n U  H  nUR                  S[        U5      45        M!     UR                  5         UR	                  5         g ! UR	                  5         f = f)Nz-UPDATE hidden_cars SET reviewed=1 WHERE reg=?)r   ry   rÏ   r   r   )r  r   r   r8   s       r   Úmark_notes_reviewedrô  W  sM   € Ü�4‹=€DðÛˆCØ�L‰LÐHÌ9ÐUXË>ÐJ[Ö\ñ à�‰Œà�
‰
�øˆ�
‰
�úó   �5A ÁA%c                 ó^  • U R                  S5      R                  5       nU(       a  SUS   =(       d    S;   a  gU R                  S5       Vs/ s H  o"S   PM	     nnUS   R                  SS5      nUR                  S	S
S5      R                  SS
S5      nU R                  S5        U R                  U5        SR                  S U 5       5      nU R                  SSR                  U5       SU S35        U R                  S5        U R                  S5        U R	                  5         gs  snf )aÖ  A star is one listing's, not one plate's (2026-09-14, Steven: a car
on Motorway and Carwow the same day "still shares its ... star between
the two cards. Fix this"). The bids table was unique on (reg,
sale_date), so the second listing's star overwrote the first; it is
unique on (reg, sale_date, platform) now, platform '' meaning any
listing of the plate (the Mac's own older stars). SQLite cannot change
a UNIQUE in place, so the table is rebuilt once, every row kept.z@SELECT sql FROM sqlite_master WHERE type='table' AND name='bids'z UNIQUE(reg, sale_date, platform)r×  rÊ   Nr$   r!   zUNIQUE(reg, sale_date)zCREATE TABLE bidszCREATE TABLE bids_newr   zCREATE TABLE "bids"zDROP TABLE IF EXISTS bids_newrW  c              3   ó4   #   • U  H  oS :X  a  SOUv •  M     g7f)r-   zCOALESCE(platform, '')Nr´   r±  s     r   r
  Ú(_bids_one_per_listing.<locals>.<genexpr>q  s   é € ÐWÒRVÈQ°z³/Ñ.ÀqÔHÒRVùs   ‚z INSERT OR IGNORE INTO bids_new (z	) SELECT z
 FROM bidszDROP TABLE bidsz#ALTER TABLE bids_new RENAME TO bids)ry   r   rº   rË   r   )r   r¬   r„   r…   r×  Úpickss         r   rz   rz   a  s  € ð �,‰,ÐYÓ
Z×
cÑ
cÓ
e€CÞÐ4¸¸U¹×9IÀrÓJØØ#Ÿ|™|Ð,EÔFÓGÒF˜!ˆfŒIÑF€DÐGØ
ˆe‰*×
Ñ
Ð5Ð7YÓ
Z€CØ
�+‰+Ð)Ð+BÀAÓ
F×
NÑ
NÐOdÐf}ð  @Aó  B€CØ‡L�LÐ0Ô1Ø‡L�L�ÔØ�I‰IÑWÑRVÓWÓW€EØ‡L�LÐ3°D·I±I¸d³OÐ3DÀIÈeÈWÐT^Ð_Ô`Ø‡L�LÐ"Ô#Ø‡L�LÐ6Ô7Ø‡K�K…Mùò Hs   ÁD*c                 óP   • U =(       d    SR                  5       R                  5       $ rÉ   )r;  rÅ  )r-   s    r   Ú	_plat_keyrû  x  s   € Ø�N˜×!Ñ!Ó#×)Ñ)Ó+Ð+r   c                 óx   • [        U5      n[        U5      =(       a    X0;   =(       d    U S[        U5       3U ;   $ )zˆWhether this listing is starred: a star on its plate for any listing
(platform ''), or one for its own platform. bidset is bids_for()'s.Ú|)rÏ   rÍ  rû  )Úbidsetr8   r-   r„   s       r   Ústarredrÿ  |  s9   € ô 	�#‹€AÜ�‹7×N˜™×M¨!¨¨A¬i¸Ó.AÐ-BÐ'CÀvÑ'MÐNr   c                 óV   • U  Vs1 s H  oR                  SS5      S   iM     sn$ s  snf )Nrý  r   r   )rÎ   )rþ  rH  s     r   Úbidset_regsr  ƒ  s'   € Ù(.Ó/ª 1�G‰G�C˜‹O˜AÔ©Ñ/Ð/ùÒ/s   …&c                 óä  • [        U 5      n U (       d  gU=(       d    0 n[        U	5      n U(       d”  UR                  S5      (       d~  UR                  SU
=(       d
    [	        5       X45      R
                  nU(       d&  UR                  SX45      R                  5       (       a"  UR                  5          UR                  5         gUR                  SXX4XVUUR                  S5      UR                  S5      UR                  S5      UR                  S	5      UR                  S
5      UR                  S5      UR                  S5      =(       d    S[        5       U(       a  SOSU
=(       d
    [	        5       45        U(       d;  UR                  S5      (       a%  UR                  SU
=(       d
    [	        5       X45        UR                  5         UR                  5         g! UR                  5         f = f)a]  Mark or unmark a car on Steven's bid shortlist for a sale date. details is
an optional dict of extra attributes (year, mileage, grade, fuel, body_type,
cap_clean, platform) captured so the app can learn Steven's taste over time.
updated_at (UTC ISO) is what the Dealer OS sync compares; an action
applied from Dealer OS passes the action's own time.Nr-   zOUPDATE bids SET active=0, updated_at=? WHERE reg=? AND sale_date=? AND active=1z.SELECT 1 FROM bids WHERE reg=? AND sale_date=?a   INSERT INTO bids (reg, sale_date, make, model, name, max_bid, reserve,
                 year, mileage, grade, fuel, body_type, cap_clean, platform, created_at, active,
                 updated_at)
               VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
               ON CONFLICT(reg, sale_date, platform) DO UPDATE SET
                 active=excluded.active, make=excluded.make, model=excluded.model,
                 name=excluded.name, max_bid=excluded.max_bid, reserve=excluded.reserve,
                 year=excluded.year, mileage=excluded.mileage, grade=excluded.grade,
                 fuel=excluded.fuel, body_type=excluded.body_type, cap_clean=excluded.cap_clean,
                 platform=excluded.platform, created_at=excluded.created_at,
                 updated_at=excluded.updated_atr%   r(   r)   r*   r,   r#   rÊ   r   r   z_UPDATE bids SET active=0, updated_at=? WHERE reg=? AND sale_date=? AND platform='' AND active=1)
rÏ   r   r  ry   r»   r_  r   r   r   r¤   )r8   rÑ   ÚonrV   rW   r!   Úmax_bidr"   Údetailsr   r/   r  r   r:   s                 r   Úset_bidr  ‡  sv  € ô �C‹.€CÞØØ��2€AÜ�4‹=€Dð"Þ˜!Ÿ%™% 
×+Ñ+ð —‘ÐnØ(×5¬G«I°sÐFóHßHPÉð æ�D—L‘LÐ!QÐTWÐScÓd×mÑm×oÑoØ—‘”Øð2 	�
‰
�ð1 	�‰ð
3ð ˜T¨$¸Ø�U‰U�6‹]˜AŸE™E )Ó,¨a¯e©e°G«n¸a¿e¹eÀF»mØ�U‰U�;Ó §¡ {Ó!3°Q·U±U¸:Ó5F×5LÈ"Ü‹Vž"‘Q ! Z×%<´7³9ð>ô	
ö" �a—e‘e˜J×'Ñ'ð �L‰LÐzØ$×1¬«	°3ÐBôDà�‰Œà�
‰
�øˆ�
‰
�ús   «B	G ÃDG ÇG/c                 óè   • [        U5      n UR                  SU 45      R                  5       nU Vs/ s H  n[        U5      PM     snUR	                  5         $ s  snf ! UR	                  5         f = f)z“Every bid row (on or off) for a sale date with its updated_at, for the
Dealer OS echo. An unstarred row is sent too, so an unstar reaches the page.z[SELECT reg, sale_date, platform, active, created_at, updated_at FROM bids WHERE sale_date=?r  ©rÑ   r   r   rã   r„   s        r   Úbids_with_updatedr	  ¹  sc   € ô �4‹=€DðØ�|‰|ØiØˆLóç"™(›*ð 	ñ "&Ó&¢˜A”�Q–¡Ñ&à�
‰
�ùò 'øà�
‰
�ús   �%A ²AÁA ÁA ÁA1c                 ó¤  • [        U5      n U(       dJ  UR                  S[        U 5      U45      R                  5       nU(       a  US   OS UR	                  5         $ UR                  S[        U 5      X=(       d    S45      R                  5       nU(       a  US   =(       d    US   OS UR	                  5         $ ! UR	                  5         f = f)z¤When this plate's star for the sale date last changed: the listing's
own row when platform is given (else the plate wide row), the newest
of any row when it is not.zWSELECT MAX(COALESCE(updated_at, created_at)) AS t FROM bids WHERE reg=? AND sale_date=?r›   NzzSELECT updated_at, created_at FROM bids WHERE reg=? AND sale_date=? AND platform IN (?, '') ORDER BY platform DESC LIMIT 1rÊ   r/   Ú
created_atrÍ  )r8   rÑ   r   r-   r   r„   s         r   Úbid_updated_atr  Æ  s´   € ô �4‹=€Dð
Þà—‘ÐvÜ'¨›n¨iÐ8ó:ß:B¹(»*ð æ�1�S’6 DÐ(ð
 	�
‰
�ð	 �L‰Lð ;Ü=FÀs»^ÈY×XfÐdfÐ<góißiqÑiqÓisð 	
æ78��,‘×2 1 \¡?ø¸dÐBà�
‰
�øˆ�
‰
�ús   �?B= ÁAB= Â=Cc           	      ó^  • [        U 5      n UR                  S5      R                  5       nUR                  5         SSKJn  U" 5       U" 5       U" 5       pen/ / / p˜nU Hì  n
U
S   =(       d    SR                  5       nU
S   =(       d    SR                  5       nU(       a  XK==   S-  ss'   U(       a  U(       a  X[ SU 3==   S-  ss'   U
S	   (       a  XjS	   ==   S-  ss'   U
S
   b  U
S
   OU
S   nU(       a  UR                  U5        U
S   (       a  UR                  U
S   5        U
S   (       d  MØ  U	R                  U
S   5        Mî     S n[        U5      UR                  5       UR                  5       [        U5      U" U5      U" U5      U" U	5      S.$ ! UR                  5         f = f)a  Aggregate the cars Steven has starred (active bids across all sale dates)
into a picture of his taste. Make and model are the signal he asked to learn;
price, platform, mileage and age come along for later. Surfaced quietly via
the --habits command, not yet on the cockpit.zeSELECT make, model, max_bid, reserve, year, mileage, platform
               FROM bids WHERE active=1r   )ÚCounterrV   rÊ   rW   r   r.   r-   r"   r  r(   r%   c                 ó^   • U (       d  g [        U 5      n U S   U S   U [        U 5      S-     S.$ )Nr   r   rs  )ÚminÚmaxÚmedian)r  rá   )r€  s    r   ÚbandÚ preference_profile.<locals>.bandù  s3   € ÞØÜ�d‹|ˆØ˜A‘w t¨B¡x¸4ÄÀDÃ	ÈQÁÑ;OÑPÐPr   )r  Úby_makeÚby_modelr�  r`  r(   r%   )r   ry   r{   r   Úcollectionsr  r;  rí   rá   Úmost_commonrø   )r   r   rã   r  r  r  r�  ÚpricesÚmileagesÚyearsr„   ÚmkÚmdr`  r  s                  r   Úpreference_profiler  Ø  s‰  € ô
 �4‹=€DðØ�|‰|ð+ó
÷ ‰(‹*ð 	ð
 	�
‰
ŒÝ#Ù%,£Y±³	¹7»9�{€GØ  " b�e€FÛˆØ�‰i�o˜2×$Ñ$Ó&ˆØ�‰j×˜B×%Ñ%Ó'ˆÞØ‹K˜1Ñ‹KÞ–"Ø�t˜1˜R˜D�\Ó" aÑ'Ó"ØˆZ�=Ø˜*™Ó&¨!Ñ+Ó&Ø ! )¡Ñ 8��)’¸aÀ	¹lˆÞØ�M‰M˜%Ô ØˆY�<Ø�O‰O˜A˜i™LÔ)ØˆV�9‰9Ø�L‰L˜˜6™Ö#ñ ò"Qô �T“Ø×&Ñ&Ó(Ø×(Ñ(Ó*Ü˜KÓ(Ù�f“Ù˜“>Ù�U“ñð øð7 	�
‰
�ús   �F ÆF,c           	      óH  • [        U5      n UR                  SU 45      R                  5       nU Vs1 s H>  oDS   =(       d    SR                  5       (       d  US   OUS    S[	        US   5       3iM@     snUR                  5         $ s  snf ! UR                  5         f = f)zˆThe starred listings for a sale date: a bare reg for a star on the
plate alone, "REG|platform" for a star on one listing. Ask starred().z=SELECT reg, platform FROM bids WHERE sale_date=? AND active=1r-   rÊ   r8   rý  )r   ry   r{   r;  rû  r   r  s        r   Úbids_forr   
  sŸ   € ô �4‹=€DðØ�|‰|ØKÈiÈ\ó
ç
‰(‹*ð 	ñ vzÓzÒuyÐpq :¡×!4°"× ;Ñ ;× =Ñ =��%’ÀaÈÁhÀZÈqÔQZÐ[\Ð]gÑ[hÓQiÐPjÐCkÒkÑuyÑzà�
‰
�ùò {øà�
‰
�ús   �%B ²AB
Á7B Â
B ÂB!c                 ó  • [        U5      n [        5       nUSS n[        U  Vs1 s H  oUR                  R                  iM     sn5      n[        S[        U5      S5       H?  nXgUS-    nUR                  SSR                  S[        U5      -  5       S3U/U-   5        MA     U  GH  nUR                  (       a  UR                  R                  OSn	UR                  (       a  UR                  R                  OSn
UR                  (       a  UR                  R                  OSnUR                  S	X5R                  R                  UR                  X©US
R                  UR                  5      S
R                  UR                  5      45        [!        X%R                  R                  [#        UR                  SS5      U5        GM      UR%                  5         UR'                  5         gs  snf ! UR'                  5         f = f)a  One row per car per day. Every save carries the whole current list
(run() writes after each platform finishes, carrying the earlier
platforms' cars forward), so the same car's earlier rows for today
are replaced, never stacked: before this (Steven 2026-09-02, Step 2 of
the review) each save INSERTed the full list again, 3 to 7 times a
day, and every count taken from this table was inflated by as much.
Blank regs (a listing with no plate) are replaced as a group for the
same reason, the batch is the complete current set of them.Nrg  r   i�  zDDELETE FROM assessments WHERE substr(run_at, 1, 10) = ? AND reg IN (rß   rà   r9   z¼INSERT INTO assessments
                   (run_at, reg, status, governing_source, governing_value,
                    max_bid, reasons, flags)
                   VALUES (?,?,?,?,?,?,?,?)z | rw   )r   r¤   rì   Úcarr8   Úrangerá   ry   rË   ÚpricingÚgoverning_valueÚgoverning_sourcer  r_  ÚreasonsÚflagsr4  rç   r   r   )Úassessmentsr   r   r§   r�  r2  r  r  ÚchunkÚgvÚgsÚmbs               r   Úsave_assessmentsr.    s‡  € ô �4‹=€DðÜ‹fˆØ�C�R�ˆÜ©Ó4ª 1—U‘U—Y”Y©Ñ4Ó5ˆÜ�qœ#˜d›) SÖ)ˆAØ˜1˜s™7�OˆEØ�L‰LðØ"Ÿx™x¨¬c°%«jÑ(8Ó9Ð:¸!ð=à�˜%‘ö!ñ *ô ˆAØ./¯i¯i�—‘×*Ò*¸TˆBØ/0¯y¯y�—‘×+Ò+¸dˆBØ&'§i§i�—‘×"Ò"°TˆBØ�L‰Lð/ð —e‘e—i‘i §¡¨2°2Ø—‘˜AŸI™IÓ&¨¯
©
°1·7±7Ó(;ð=ôô ˜t§U¡U§Y¡Y´¸¿¹¸}ÈdÓ0SÐUX×Yñ! ð" 	�‰Œà�
‰
�ùò5 5øð4 	�
‰
�ús   �G. ¥G)ÁFG. Ç)G. Ç.H c                 óŒ  • [        U 5      n UR                  S5      R                  5       nU Vs0 s H  o3S   [        R                  " US   5      _M!     snUR                  5         $ s  snf ! [        R                   a,  nS[        U5      ;   a  0 s SnAUR                  5         $ e SnAff = f! UR                  5         f = f)ay  Every buying rule override saved from the settings page, as a plain
dict. Empty when nothing has been saved yet, callers layer this over
pricing.DEFAULT_SETTINGS.

A database that has never been initialised at all (a fresh checkout
before the first real run, CI's own bare runner) reads the same as one
with nothing saved, rather than crashing whichever page render happened
to read settings first (caught for real 2026-08-26: CI failed on
render_purchases()'s own theme colour read, "no such table: settings",
the same failure Mark's v3.0.1 push had already hit, present since
theme colours started being read fresh at render time).zSELECT key, value FROM settingsr  rï  úno such tableN)	r   ry   r{   r>  r?  r   r   rî   rÌ   )r   r   rã   r„   Úes        r   r�  r�  A  s¤   € ô �4‹=€DðØ�|‰|Ð=Ó>×GÑGÓIˆÙ:>Ó?º$°Q�%‘œ$Ÿ*š* Q w¡ZÓ0Ò0¹$Ñ?ð 	�
‰
�ùò @øÜ×#Ñ#ó Øœc !›fÓ$ØŒIð 	�
‰
�ð 	ûðûð
 	�
‰
�úsF   �#A. °&A)ÁA. Á)A. Á.B.ÂB)ÂB.ÂB1 Â(B)Â)B.Â.B1 Â1Cc                 óF  • [        U5      n UR                  SU 45      R                  5       nU(       a  US   OS  UR                  5         $ ! [        R
                   a*  nS[        U5      ;   a   S nAUR                  5         g e S nAff = f! UR                  5         f = f)Nz+SELECT updated_at FROM settings WHERE key=?r/   r0  )r   ry   r   r   r   rî   rÌ   )r  r   r   r„   r1  s        r   Úsetting_updated_atr3  Y  sˆ   € Ü�4‹=€DðØ�L‰LÐFÈÈÓO×XÑXÓZˆÞ"#ˆq�Š¨Ð-ð 	�
‰
�øô ×#Ñ#ó Øœc !›fÓ$Ûð 	�
‰
�ð 	ûðûð
 	�
‰
�ús/   �.A ÁBÁ!BÁ0B ÂBÂBÂB ÂB c                 óD  • [        U 5      n UR                  S5       Vs0 s H  o"S   US   _M     snUR                  5         $ s  snf ! [        R                   a,  nS[        U5      ;   a  0 s SnAUR                  5         $ e SnAff = f! UR                  5         f = f)zB{key: updated_at} for every saved setting, for the Dealer OS push.z$SELECT key, updated_at FROM settingsr  r/   r0  N)r   ry   r   r   rî   rÌ   )r   r   r„   r1  s       r   Úsettings_updated_atr5  f  s�   € ä�4‹=€DðØ37·<±<Ð@fÔ3gÓhÒ3g¨a�%‘˜!˜L™/Ò)Ñ3gÑhð 	�
‰
�ùò iøÜ×#Ñ#ó Øœc !›fÓ$ØŒIð 	�
‰
�ð 	ûðûð
 	�
‰
�úsE   �A
  A²A
 ÁA
 Á
B
ÁBÁ.B
Á/B ÂBÂB
Â
B ÂBrµ  c                 ó®  • Ub<  [        U[        5      (       a  USS R                  5       (       d  [        SU< 35      e[	        U5      n U=(       d
    [        5       nU R                  5        H.  u  pVUR                  SU[        R                  " U5      U45        M0     UR                  5         UR                  5         g! UR                  5         f = f)a(  Save every key in values as a settings row, JSON encoded. Only touches
the keys given, other saved settings are left alone. updated_at (UTC
ISO) is what the Dealer OS sync compares; local saves stamp now.

updated_at must look like a timestamp. A caller that passes a database
path in that slot (a test, on 2026-09-02, while the argument order was
briefly different) wrote its throwaway values into the REAL settings
table with a temp file path as the stamp; refusing anything that is not
a timestamp makes that mistake fail loudly instead of silently.Né   z9set_settings updated_at must be a UTC ISO timestamp, got z£INSERT INTO settings (key, value, updated_at) VALUES (?,?,?)
                   ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at)rë   rÌ   ÚisdigitrÙ   r   r»   rÒ  ry   r>  rã  r   r   )rµ  r   r/   r   r§   r  rï  s          r   r’  r’  s  s°   € ð Ñ¤z°*¼c×'BÑ'BÀzÐRTÐSTÀ~×G]ÑG]×G_ÑG_ÜÐTÐU_ÑTbÐcÓdÐdÜ�4‹=€Dð
Ø×%œG›IˆØ Ÿ,™,ž.‰JˆCØ�L‰Lðjà”d—j’j Ó'¨Ð-öñ )ð 	�‰Œà�
‰
�øˆ�
‰
�ús   ÁA%C ÃCc                 óH   • [        U 5      R                  S5      =(       d    0 $ )aâ  The last known read outcome per platform site (motorway, carwow,
auction4cars, dealerauction, dealerway, lecapital), so a login that has died can
be flagged on the cockpit the moment it happens, not only noticed when
someone next runs and reads the console. Stored under the settings
table's own "site_health" key (reusing the existing generic key value
store, no new table needed, this is just the latest state per site,
never a history). Empty when nothing has ever been recorded.Úsite_healthrÍ  rY  s    r   Úget_site_healthr;  �  s   € ô ˜Ó×!Ñ! -Ó0×6°BÐ6r   Úsiter6  Úmessagec                 óh   • [        U5      n[        U5      U[        5       S.X@'   [        SU0U5        g)aö  Record whether the most recent attempt to read a platform succeeded,
called from every place that already tries to read one (Mark
2026-08-24: "add a notification when its required to relogin",
piggybacked on real runs rather than a separate poller, since Glass's
single session account has repeatedly shown that logging in just to
check is itself risky, see CLAUDE.md). A clean read overwrites a
previous failure, so the notice clears itself the moment the site
works again, no manual dismissal needed.)r6  r=  râ  r:  N)r;  rÍ  r¤   r’  )r<  r6  r=  r   Úhealths        r   Úrecord_site_healthr@  ˜  s1   € ô ˜TÓ"€FÜ˜r›(¨wÄdÃfÑM€F�LÜ�- Ð(¨$Õ/r   c                 óH   • [        U 5      R                  S5      =(       d    0 $ )a^  When each thing that can be RUN last actually ran, and how it went
(2026-08-31: "theres currently no way of seeing from the live auctions
the current statuses of the last time the runs were made"). One entry
per key: the five platforms, the on demand checks (glass,
dealerkit_stock, dealerkit_purchases, dealerkit_check, lecapital_funding,
compmatch, watchlist, under_offer, clickdealer), the two scheduled
syncs (purchases, lost_bids), plus "run" for the daily run as a whole,
each {"ok", "summary", "at"}.

Deliberately separate from site_health, which looks like it would do
but does not: that records whether a CONNECTION works, and is written
both by a real read during a run AND by a nightly auto login, so its
own checked_at can read fresh at 06:00 on a day no run happened at
all. This one only ever moves when something really ran, so an age
here answers "how old are the cars on my list", which is the question
that stops a stale car being bid on. Both are shown, and they can
honestly disagree: a connection can be fine while the read is a day
old, and the Run modal says so. Empty when nothing has run yet.Ú
run_statusrÍ  rY  s    r   Úget_run_statusrC  ¦  s   € ô& ˜Ó×!Ñ! ,Ó/×5°2Ð5r   r  c                 óh   • [        U5      n[        U5      U[        5       S.X@'   [        SU0U5        g)aO  Record how one run finished. Same read modify write shape as
record_site_health above, latest state per key, never a history, so a
later success clears an earlier failure with no manual dismissal.
summary is the one line the Run modal shows, and is normally the same
sentence already written to the progress bar, see daily_run._finish.)r6  r˜  ÚatrB  N)rC  rÍ  r¤   r’  )r  r6  r˜  r   r_  s        r   Úrecord_run_statusrF  ¼  s1   € ô ˜DÓ!€FÜ˜b›¨g¼T»VÑD€F�KÜ�, Ð'¨Õ.r   r8   rÃ   Úwatchedc           	      ó  • U (       a  U(       d  g[        U5      n UR                  S[        U 5      X=(       d    SU(       a  SOS[        5       45        UR	                  5         UR                  5         g! UR                  5         f = f)aî  Record the desired shortlist/watchlist state for one car on its own
platform (Mark 2026-08-27), called every time a star is toggled on the
cockpit for a Motorway or Carwow car. Upserts on (reg, platform), so a
quick star then unstar before the sync ever runs just leaves the LATEST
desired state queued, never two conflicting rows. synced_at and error
are always cleared on a fresh queue, so a car re starred after an
earlier successful sync is genuinely re synced, not skipped as already
done.Na*  INSERT INTO watchlist_sync (reg, platform, listing_url, watched, queued_at, synced_at, error) VALUES (?, ?, ?, ?, ?, NULL, NULL) ON CONFLICT(reg, platform) DO UPDATE SET listing_url=excluded.listing_url, watched=excluded.watched, queued_at=excluded.queued_at, synced_at=NULL, error=NULL, attempts=0rÊ   r   r   )r   ry   rÏ   r¤   r   r   )r8   r-   rÃ   rG  r   r   s         r   Úqueue_watchlist_syncrI  Ç  sh   € ö –hØÜ�4‹=€Dð
Ø�‰ðSô
 �s‹^˜X×'8°b¾w¹!ÈAÌtËvÐVô	Xð 	�‰Œà�
‰
�øˆ�
‰
�ús   œA	A6 Á6Bc                 óL  • [        U5      n U (       a'  UR                  SU [        45      R                  5       nO%UR                  S[        45      R                  5       nU Vs/ s H  n[	        U5      PM     snUR                  5         $ s  snf ! UR                  5         f = f)a  Every car still waiting to be synced to its own platform (synced_at
IS NULL), newest queued first. Includes a car whose last sync attempt
failed (error is set, synced_at stays NULL so it retries), until it has
failed WATCHLIST_MAX_ATTEMPTS times, after which it is left alone.znSELECT * FROM watchlist_sync WHERE platform = ? AND synced_at IS NULL AND attempts < ? ORDER BY queued_at DESCz]SELECT * FROM watchlist_sync WHERE synced_at IS NULL AND attempts < ? ORDER BY queued_at DESC)r   ry   ÚWATCHLIST_MAX_ATTEMPTSr{   rø   r   )r-   r   r   rã   r„   s        r   Úpending_watchlist_syncsrL  è  s•   € ô
 �4‹=€DðÞØ—<‘<ð;Ø=EÔG]Ð<^ó`ç`hÑ`hÓ`jñ ð —<‘<ð*Ü,BÐ+DóFçFNÁhÃjð ñ "&Ó&¢˜A”�Q–¡Ñ&à�
‰
�ùò 'øà�
‰
�ús   �AB Á$BÁ9B ÂB ÂB#ÚerrorÚgive_upc                 ó˜  • [        U5      n U(       a'  UR                  S[        5       [        U 5      U45        O[U(       a-  UR                  SU=(       d    S[        [        U 5      U45        O'UR                  SU=(       d    S[        U 5      U45        UR                  5         UR                  5         g! UR                  5         f = f)a…  Record the outcome of one sync attempt. A success clears the row's
own pending state (synced_at set, error cleared); a failure leaves
synced_at NULL, so the next pass retries it, with the real reason
recorded for whoever is looking. Silently does nothing if the row was
already re queued with a different desired state in the meantime (a
star toggled again mid pass), never overwrites a newer request with an
older attempt's own outcome.

give_up ends the retries there and then, by counting the attempt as
the last one allowed (2026-09-20). For a failure where the click
genuinely reached the platform, retrying is not a free second go: a
shortlist button is one switch, so a second click can undo what the
first one did. Steven found exactly that on Carwow, cars this
reported as failures were on his Carwow shortlist. A car starred
again in the app still re queues from scratch, attempts back to 0.zjUPDATE watchlist_sync SET synced_at = ?, error = NULL WHERE reg = ? AND platform = ? AND synced_at IS NULLzfUPDATE watchlist_sync SET error = ?, attempts = ? WHERE reg = ? AND platform = ? AND synced_at IS NULLrÊ   zqUPDATE watchlist_sync SET error = ?, attempts = attempts + 1 WHERE reg = ? AND platform = ? AND synced_at IS NULLN)r   ry   r¤   rÏ   rK  r   r   )r8   r-   r6  rM  r   rN  r   s          r   Úmark_watchlist_syncedrP  ü  s¨   € ô$ �4‹=€DðÞØ�L‰LðGä“œ 3›¨Ð2õ4ö Ø�L‰LðGà—˜"Ô4´iÀ³nÀhÐOõQð
 �L‰LðGà—˜"œi¨›n¨hÐ7ô9ð 	�‰Œà�
‰
�øˆ�
‰
�ús   �BB7 Â7C	c                 óÜ  • U =(       d    /  Vs1 s H  o"(       d  M  [        U5      iM     nn[        U5      n UR                  S5      R                  5       nU Vs/ s H!  n[        US   5      U;  d  M  US   US   4PM#     nnU H  u  pxUR                  SXx45        M     UR	                  5         [        U5      UR                  5         $ s  snf s  snf ! UR                  5         f = f)a-  Drop every still pending row for a car no longer on the list at all
(Steven 2026-09-02): once a car has sold or been withdrawn there is no
listing left to star, so its row could only ever fail, on every sync,
forever. Rows already synced are kept as the record they are. Returns
how many were dropped.z@SELECT reg, platform FROM watchlist_sync WHERE synced_at IS NULLr8   r-   z9DELETE FROM watchlist_sync WHERE reg = ? AND platform = ?)rÏ   r   ry   r{   r   rá   r   )	Úactive_regsr   r„   Úactiver   rã   r#  r8   r-   s	            r   Úprune_watchlist_syncrT  $  sÕ   € ð &1×%6°BÐ%6Ó=Ò%6˜q»1‹lŒi˜ŽlÑ%6€FÐ=Ü�4‹=€DðØ�|‰|Ð^Ó_×hÑhÓjˆÙ37Ó]²4¨a¼9ÀQÀuÁXÓ;NÐV\Ñ;\Ó)��5‘˜1˜Z™=Ó)±4ˆÐ]Û!‰MˆCØ�L‰LÐTÐWZÐVeÖfñ "à�‰ŒÜ�4‹yà�
‰
�ùò >ùò ^øð 	�
‰
�ús-   Ž
CœC¹#C ÁCÁ6CÂ:C ÃC ÃC+rã   c                 ó&  • [        U5      n UR                  SU45        [        5       nU  H­  nUR                  SU[        UR	                  S5      =(       d    S5      UR	                  S5      =(       d    SUR	                  S5      UR	                  S5      =(       d    SUR	                  S5      =(       d    SUR	                  S	5      =(       d    SU45        M¯     UR                  5         UR                  5         g
! UR                  5         f = f)a,  Replace the whole under_offer snapshot for one platform with what
was just read (Mark 2026-08-27), a live picture not a history: a car
accepted, lost, or no longer offered on simply is not in `rows` any
more and drops out here too, no separate cleanup needed. Other
platforms' own rows are untouched.z*DELETE FROM under_offer WHERE platform = ?zƒINSERT INTO under_offer (platform, reg, name, max_bid, noted_at, vehicle_ref, listing_url, read_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)r8   rÊ   r!   r  rê  rX  rÃ   N)r   ry   r¤   rÏ   r  r   r   )rã   r-   r   r   r§   r„   s         r   Úrecord_under_offerrV  7  sÏ   € ô �4‹=€DðØ�‰ÐAÀHÀ;ÔOÜ‹fˆÛˆAØ�L‰LðUàœ9 Q§U¡U¨5£\×%7°RÓ8¸!¿%¹%À»-×:MÈ2Ø—‘�yÓ! 1§5¡5¨Ó#4×#:¸¸A¿E¹EÀ-Ó<P×<VÐTVØ—‘�}Ó%×+¨¨Sð2ö3ñ ð 	�‰Œà�
‰
�øˆ�
‰
�ús   �C C> Ã>Dc                 óä   • [        U 5      n UR                  S5      R                  5       nU Vs/ s H  n[        U5      PM     snUR	                  5         $ s  snf ! UR	                  5         f = f)zVEvery car currently under offer, across every platform read so
far, newest read first.z8SELECT * FROM under_offer ORDER BY read_at DESC, id DESCr  rÚ  s       r   Úlist_under_offerrX  M  s]   € ô �4‹=€DðØ�|‰|ØFóHßHPÉË
ð 	á!%Ó&¢˜A”�Q–¡Ñ&à�
‰
�ùò 'øà�
‰
�úr  c                 ó~  • [        U 5      n UR                  SU(       a  SOS-   S-   5      R                  5       nU Vs/ s HL  nUS   US   [        R                  " US   5      [        US   5      US	   US
   US   [        US   5      S.PMN     snUR                  5         $ s  snf ! UR                  5         f = f)z¼Every saved view, starred ones first, newest first within each group.
Deleted views are tombstones (kept so the delete reaches Dealer OS on
the next echo), left out unless include_deleted.z`SELECT id, name, criteria, starred, created_at, remote_id, updated_at, deleted FROM saved_views rÊ   zWHERE deleted=0 z&ORDER BY starred DESC, created_at DESCr1   r!   Úcriteriarÿ  r  r0   r/   r2   )r1   r!   rZ  rÿ  r  r0   r/   r2   )r   ry   r{   r>  r?  rÍ  r   )r   Úinclude_deletedr   rã   r„   s        r   Úlist_saved_viewsr\  Y  sÎ   € ô �4‹=€DðØ�|‰|ð Þ)8¡2Ð>PñRà4ñ5ó
÷ ‰(‹*ð	 	ñ :>ó?ò :>°Að ˜‘w¨¨&©	¼t¿zºzÈ!ÈJÉ-Ó?XÜ   9¡Ó.¸aÀ¹oØ ™n¸A¸l¹OÜ   9¡Ó.ô0ñ :>ñ?ð
 	�
‰
�ùò?øð
 	�
‰
�ús   �2B* ¿AB%ÂB* Â%B* Â*B<c           	      ó�  • [        U5      n U=(       d    [        R                  " 5       R                  nU=(       d
    [	        5       nUR                  SU45      R                  5       n	U	(       a[  UR                  SU [        R                  " U5      U(       a  SOSX‰S   45        UR                  5         U	S   UR                  5         $ UR                  SU [        R                  " U5      U(       a  SOS[        5       Xx45      n
UR                  5         U
R                  UR                  5         $ ! UR                  5         f = f)a*  Save the current sidebar filter and sort state under a name Mark
chooses. criteria is a plain dict, stored JSON encoded. Returns the
view's id. remote_id is the identity Dealer OS and this Mac share (a
fresh uuid for a view made here); saving under an existing remote_id
updates that view in place.z,SELECT id FROM saved_views WHERE remote_id=?zXUPDATE saved_views SET name=?, criteria=?, starred=?, updated_at=?, deleted=0 WHERE id=?r   r   r1   ztINSERT INTO saved_views (name, criteria, starred, created_at, remote_id, updated_at, deleted) VALUES (?,?,?,?,?,?,0))r   r|   r}   r~   r»   ry   r   r>  rã  r   r   r¤   r¦  )r!   rZ  r   r0   rÿ  r/   r   ÚridÚnow_ur¬   rb  s              r   Ú	save_viewr`  l  sô   € ô �4‹=€DðØ×+œ4Ÿ:š:›<×+Ñ+ˆØ×'œg›iˆØ�l‰lÐIÈCÈ6ÓR×[Ñ[Ó]ˆÞØ�L‰LØjØ”t—z’z (Ó+¶'©Q¸qÀ%ÈTÉÐSôUð �K‰KŒMØ�t‘9ð 	�
‰
�ð �l‰lð%à”4—:’:˜hÓ'®g©¸1¼d»fÀcÐQó
ˆð
 	�‰ŒØ�}‰}à�
‰
�øˆ�
‰
�ús   �B-D3 ÃAD3 Ä3Ec                 óä   • [        U5      n UR                  SU(       a  SOSU=(       d
    [        5       U 45        UR                  5         UR	                  5         g! UR	                  5         f = f)znStar or unstar a saved view. Starred ones show as a quick one click
button next to the sort and view dropdown.z9UPDATE saved_views SET starred=?, updated_at=? WHERE id=?r   r   N©r   ry   r»   r   r   )Úview_idrÿ  r   r/   r   s        r   Ú	star_viewrd  ‰  sS   € ô �4‹=€DðØ�‰ÐPÞ"‘a¨¨:×+B¼»ÀGÐLô	Nà�‰Œà�
‰
�øˆ�
‰
�úó   �?A ÁA/c                 óä   • [        U5      n UR                  SU(       a  SOSU=(       d
    [        5       U 45        UR                  5         UR	                  5         g ! UR	                  5         f = f)Nz@UPDATE saved_views SET starred=?, updated_at=? WHERE remote_id=?r   r   rb  )r0   rÿ  r   r/   r   s        r   Ústar_view_remoterg  •  sQ   € Ü�4‹=€DðØ�‰ÐWÞ"‘a¨¨:×+B¼»ÀIÐNô	Pà�‰Œà�
‰
�øˆ�
‰
�úre  c                 óÐ   • [        U5      n UR                  SU=(       d
    [        5       U 45        UR                  5         UR	                  5         g ! UR	                  5         f = f)Nz@UPDATE saved_views SET deleted=1, updated_at=? WHERE remote_id=?rb  )r0   r   r/   r   s       r   Údelete_view_remoteri  Ÿ  sJ   € Ü�4‹=€DðØ�‰ÐWØ ×-¤G£I¨yÐ9ô	;à�‰Œà�
‰
�øˆ�
‰
�úrõ  c                 óÜ   • [        U5      n UR                  SU 45      R                  5       nU(       a  US   =(       d    US   OS  UR                  5         $ ! UR                  5         f = f)Nz@SELECT updated_at, created_at FROM saved_views WHERE remote_id=?r/   r  r!  )r0   r   r   r„   s       r   Úview_updated_atrk  ©  s[   € Ü�4‹=€DðØ�L‰LÐ[Ø#˜ó'ß'/¡x£zð 	
æ78��,‘×2 1 \¡?ø¸dÐBà�
‰
�øˆ�
‰
�ús   �:A ÁA+c                 óÐ   • [        U5      n UR                  SU=(       d
    [        5       U 45        UR                  5         UR	                  5         g! UR	                  5         f = f)zsA tombstone, not a real delete, so the deletion reaches Dealer OS on
the next echo; list_saved_views leaves it out.z9UPDATE saved_views SET deleted=1, updated_at=? WHERE id=?Nrb  )rc  r   r/   r   s       r   Údelete_viewrm  ³  sL   € ô �4‹=€DðØ�‰ÐPØ ×-¤G£I¨wÐ7ô	9à�‰Œà�
‰
�øˆ�
‰
�úrõ  é0   c                 ó¸  • [        U 5      n [        R                  R                  5       [        R                  " US9-
  R	                  SS9n[        U5      n UR                  SXX'45      R                  5       n	UR                  5         U	c  gS n
U
" U	S   5      U
" U5      :w  d  U
" U	S   5      U
" U5      :w  a  gS	U	S
   U	S   4$ ! UR                  5         f = f)as  A cached Glass's or Cazana value for this exact reg, mileage and
source, read within the last max_age_hours. Returns (hit, value, url):
hit is False when there is no row, it has aged out, or its stored
make/model does not match what is being asked for now (the plate may
have moved to a different car since it was cached, so a mismatch forces
a fresh, verified lookup rather than trusting a stale value for the
wrong car). url is the real Cazana result page the value was read
from (Mark 2026-08-25, "add a link on the card on the word Cazana to
deep link to lookup page"), None for Glass's or a genuinely missed
Cazana lookup.©Úhoursr¯   r°   z|SELECT value, url, make, model FROM valuation_cache
               WHERE reg=? AND mileage=? AND source=? AND valued_at >= ?)FNNc                 ób   • [        U =(       d    S5      R                  5       R                  5       $ rÉ   )rÌ   r;  rÅ  )r‹  s    r   rI  Ú&get_cached_valuation.<locals>.<lambda>×  s   € ”3�q—w˜B“<×%Ñ%Ó'×-Ñ-Ô/r   rV   rW   Trï  rx   )	rÏ   r²   r§   rÛ   r³   r   ry   r   r   )r8   r(   rÄ   rV   rW   Úmax_age_hoursr   rL  r   r¬   Ú_ns              r   Úget_cached_valuationrv  ¿  sÛ   € ô �C‹.€CÜ×Ñ×#Ñ#Ó%¬×(:Ò(:ÀÑ(OÑO×ZÑZÐdmÐZÐn€FÜ�4‹=€DðØ�l‰lðLà˜6Ð*ó
÷ ‰(‹*ð	 	ð 	�
‰
ŒØ
�{Ø Ù	/€BÙ	ˆ#ˆf‰+ƒ™"˜T›(Ó"¡b¨¨W©Ó&6¹"¸U»)Ó&CØ Ø��W‘˜s 5™zÐ)Ð)øð 	�
‰
�ús   Á"C ÃCc                 óÚ   • [        U 5      n [        U5      n UR                  SXX#XEU[        5       45        UR	                  5         UR                  5         g ! UR                  5         f = f)NaN  INSERT INTO valuation_cache (reg, mileage, source, make, model, value, url, valued_at)
               VALUES (?,?,?,?,?,?,?,?)
               ON CONFLICT(reg, mileage, source) DO UPDATE SET
                 make=excluded.make, model=excluded.model, value=excluded.value,
                 url=excluded.url, valued_at=excluded.valued_at)rÏ   r   ry   r¤   r   r   )	r8   r(   rÄ   rV   rW   rï  rx   r   r   s	            r   Úsave_cached_valuationrx  Ý  s[   € Ü
�C‹.€CÜ�4‹=€DðØ�‰ðCð
 ˜6¨°s¼D»FÐCô	
ð 	�‰Œà�
‰
�øˆ�
‰
�ús   ˜/A ÁA*c                 óB  • [         R                   R                  5       [         R                  " U S9-
  R                  SS9n[	        U5      n UR                  SU45      nUR                  5         UR                  UR                  5         $ ! UR                  5         f = f)zêDelete cached valuations older than max_age_hours, so the table does
not grow forever. get_cached_valuation already never returns a stale row
even if this has not run yet, this is just housekeeping. Returns the
number of rows removed.rp  r¯   r°   z/DELETE FROM valuation_cache WHERE valued_at < ?©	r²   r§   rÛ   r³   r   ry   r   r_  r   ©rt  r   rL  r   rb  s        r   Úprune_old_valuation_cacher|  î  s{   € ô
 ×Ñ×#Ñ#Ó%¬×(:Ò(:ÀÑ(OÑO×ZÑZÐdmÐZÐn€FÜ�4‹=€DðØ�l‰lÐLÈvÈiÓXˆØ�‰ŒØ�|‰|à�
‰
�øˆ�
‰
�úó   Á.B ÂBr7  c                 ób  • [         R                   R                  5       [         R                  " US9-
  R                  SS9n[	        U5      n UR                  SXU45      R                  5       nUR                  5         Uc  g[        R                  " US   5      $ ! UR                  5         f = f)a‹  Whatever a platform's own detail page read for this exact key within
the last max_age_hours (a plain dict of car field name to value, ready to
apply straight back onto a fresh car object). None on a miss, no row, or
the row has aged out. A short TTL by design (Mark's own choice, cache the
reserve too but keep it a few hours only, not the whole auction's life),
so a re seen car skips its own detail page visit only when it was
genuinely just read, never a stale figure days old. cache_key is a
normalised reg wherever the caller already has one, or a stable id
(Auction4Cars' own listing_url) when it does not, see the table's own
comment in SCHEMA.rp  r¯   r°   zaSELECT detail_json FROM listing_detail_cache WHERE cache_key=? AND platform=? AND fetched_at >= ?Nr<  )
r²   r§   rÛ   r³   r   ry   r   r   r>  r?  )r=  r-   rt  r   rL  r   r¬   s          r   Úget_cached_detailr     sž   € ô ×Ñ×#Ñ#Ó%¬×(:Ò(:ÀÑ(OÑO×ZÑZÐdmÐZÐn€FÜ�4‹=€DðØ�l‰lØoØ &Ð)ó
÷ ‰(‹*ð 	ð
 	�
‰
ŒØ
�{ØÜ�:Š:�c˜-Ñ(Ó)Ð)øð 	�
‰
�ús   Á"B ÂB.c                 óè   • [        U5      n UR                  SX[        R                  " U5      [	        5       45        UR                  5         UR                  5         g! UR                  5         f = f)zºSave whichever car fields a detail page read actually changed (an
empty dict is still worth saving, a car with a real detail page and
nothing new to say should not be re visited either).zÿINSERT INTO listing_detail_cache (cache_key, platform, detail_json, fetched_at)
               VALUES (?,?,?,?)
               ON CONFLICT(cache_key, platform) DO UPDATE SET
                 detail_json=excluded.detail_json, fetched_at=excluded.fetched_atN)r   ry   r>  rã  r¤   r   r   )r=  r-   Údetailr   r   s        r   Úsave_cached_detailr‚    sY   € ô �4‹=€Dð
Ø�‰ðUð ¤$§*¢*¨VÓ"4´d³fÐ=ô	
ð 	�‰Œà�
‰
�øˆ�
‰
�ús   �AA ÁA1iP  c                 óB  • [         R                   R                  5       [         R                  " U S9-
  R                  SS9n[	        U5      n UR                  SU45      nUR                  5         UR                  UR                  5         $ ! UR                  5         f = f)a  Sweep rows for cars long gone from every auction (a much longer cutoff
than the short TTL that governs actual reuse, this is just housekeeping
so the table does not grow forever, the same shape as
prune_old_valuation_cache). Returns the number of rows removed.rp  r¯   r°   z5DELETE FROM listing_detail_cache WHERE fetched_at < ?rz  r{  s        r   Úprune_old_listing_detail_cacher„  +  s}   € ô
 ×Ñ×#Ñ#Ó%¬×(:Ò(:ÀÑ(OÑO×ZÑZÐdmÐZÐn€FÜ�4‹=€DðØ�l‰lÐRÐU[ÐT]Ó^ˆØ�‰ŒØ�|‰|à�
‰
�øˆ�
‰
�úr}  )r   Nr   )×Ú__doc__r   r   r>  r²   r|   Ú	car_namesr   rÊ   r   r   rË   r   ÚabspathÚ__file__ÚDB_DIRÚDB_PATHr   rÌ   Ú
Connectionr   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�   rÍ  r4  r7  rA  rD  rL  rU  r›  r¡  r¤  r´  r³  r¸  rÈ  rÊ  rÏ  rÕ  rÜ  rà  ræ  rè  rñ  ró  rõ  r  r  r6  r-  r  r  r	  r  rÃ  r  r  r$  r'  r3  r,  r<  rÑ  rÂ  rA  rB  rH  rJ  rU  r]  rc  ri  rk  r|  rr  rv  rx  r  r—  r˜  r”  rš  r¡  r°  r¥  r‘  ro  r¾  rÊ  rÎ  rø  rú  rü  rþ  rQ  rž  r:  rJ  rM  rO  rT  rV  rZ  r]  r`  rc  re  rg  ri  rk  rm  rp  rs  r|  r‘  r“  r•  r—  rœ  rž  r   r¥  r©  r­  r¯  r±  ÚDEEP_READ_MAX_ATTEMPTSrµ  r·  r¹  r½  rÁ  rÅ  rÇ  rÊ  rÏ  rÒ  rØ  rÛ  rÞ  rá  rZ  ræ  rè  rî  rð  rò  rô  rz   rû  rÿ  r  r  r	  r  r  r   r.  r�  r3  r5  r’  r;  r@  rC  rF  rI  rK  rL  rP  rT  rV  rX  r\  r`  rd  rg  ri  rk  rm  rv  rx  r|  ÚDETAIL_CACHE_TTL_HOURSr  r‚  r„  r´   r   r   Ú<module>rŽ     s²  ðñ	ó Û 	Û Û Û å &Ý à	�‰�‰�b—g‘g—o‘o b§g¡g§o¡o°b·g±g·o±oÀhÓ6OÓ&PÓQÐSYÓ	Z€Ø
�'‰'�,‰,�v˜}Ó
-€ðE
€ðP  ñ �#ð  G×$6Ñ$6õ ð  ñ �#ð  Dõ õb'ðT#Ð õ
&bðRAˆcô Að1�ô 1ð $+ñ ˜Sð ¨tõ ð23�cô 3ð €ð ð °ð ¸ô õ [ð8 GNñ & Sð &°Sð &Àð &ÐRVõ &ðR &-°4ñ $˜sð $¸Eõ $ðN 59Àgñ ¨cð Àð ÐRVõ ð6 >@ÈWñ  Sð °sð Àsð ÐY]õ ð2 <>È7ñ * 3ð *°Sð *ÀSð *ÐW[õ *ðZ BIñ °ð ¸3ð ÈTõ ð  %,ñ ˜cð °õ ð( #*ñ ˜Cð ¨cõ ò&FÐ ð hÐ ð DÐ ð2 \Ð ò
&ð 
 sô 
ò¸Dõ ð 5<ñ 	¨cð 	Àõ 	ð˜Tô ð4¨ô ð,=˜Eô =õEð: %*°wñ H°ð HÀ4õ HðV %,ñ ˜cð °õ ð4 -4ñ  cð ¸õ ð 2Ð=RØ!9ÐEZØ#=ð
  6Ð>PØ/Ð>VñXÐ ð !3¸oÑNÐ ð ,3ñ  Sð °tõ ð" +-¸'ñ °Cð Àdõ ðB 29ñ 
¨3ð 
¸Tõ 
ð
?˜ô 
?ð &*°wÀTñ °ð Èdõ ò0ò*ð. 29ñ ¨3ð ¸Tõ ð" :Añ °3ð ÀTõ ð :<ÈÐY]ñ +À3ð +Ðbfõ +ð> :<Èñ +À3ð +ÐUYõ +ð0 <Cñ °Sð Àtõ ð: ØØØ,ØØØ0Ø$ØØØØð $ñ%Ð ð< ØØ ØØ"ØØ#Ø/ð ñÐ ð& Ð ð OÐ ð %3ÈÑYÐ ð €õVð.+ ô +ð 02¸À4Ø#¨$°BÀGñØ>AðØPTõð$ .5ñ  sð ¸õ ð %,ñ ˜cð °õ ð CJñ ¸Cð Èdõ ð6 AHñ ¸#ð ÈDõ ð0 79Àgñ fCÀð fCÐRVõ fCðR˜4ô ð6 13Àñ +¸#ð +ÈDõ +ð5˜%ô 5ð< aÐ ðD¨$ô Dõð6 18ñ ¨#ð ¸Dõ ð -/°BÀØ¨'ñ&:Ø$'ð&:Ø6:õ&:ðR ;=À"Ø ¨gñAØ(+ðAØ:>õAð6 -/¸Gñ .G°cð .GÈõ .Gðb (*°wÈjÐfjñ H°ð HÈð HÐ`cð HÐosõ Hð. .0¸Wñ °sð Èõ ð0 #*ñ "˜Cð "¨cõ "ð  'ñ ˜ð ¨3õ ð ,.¸7ñ H°Sð HÀtõ Hð8 .0¸Wñ !°sð !Èõ !ðP " y°fÑ=€Ø%¨vÑ6€ð 5<ñ ¨cð Àõ ð$ +2¸tñ  Cð ¸õ ð FPÐ]dñ ÐWZõ ðD %'°Gñ d¨cð dÀõ dòN1ð(  f¸Ñ>Ø¨¸TÑBð  w¸DÑAØ F¸Ñ=ðð& ¨&Ñ1Ø¨&Ñ1ð $¨uÑ5ðð *°UÀtÑLØ$°Ñ7Ø#¨wÑ7Ø"¨fÑ5Ø$°ÀÑFðð Øð à¨%Ñ0Ø'°7Ñ;Ø ¨GÑ4ðð 3¸eÑDØ.¸%Ñ@Ø8ÀGÑLðña5Ð ðp (/ñ  ð °4õ ò	Bòð &-ñ <˜sð <°õ <ò 	Ò)Ò+<Ò>OÚÒ0Ò2EÚÒ0Ò2EÚÒ0Ò2EÚÒ0Ò2EÚÒ5ðò 	Ò(Ò*;Ò=NÚÒ,Ò.AÚÒ0Ò2EÚÒ0Ò2EÚÒ0Ò2EÚÒ0Ò2EÚÒ2Ò4JÚÒ 6Ò8Pð	ñÐ ð, &ñ ˜ð ¨$õ ð" *1ñ 	 3õ 	ðJ�sô Jð7˜#ô 7ðBD˜ô DðN  'ñ H˜ð H¨4õ HòVð. %'°Gñ ¨cð Àõ ð. )0ñ  #ð °Cõ ð (/ñ  ð °4õ ð>  'ñ 	˜ð 	¨4õ 	ð "ñ �Cð  dõ ð4 +2ñ  Cð °dõ ð *1ñ  3ð °Tõ ð" <Cñ  ð °Sð Àtõ ð AHñ  ð ¸#ð ÈDõ ð '.ñ  ð °$õ ð4  'ñ '˜ð '¨4õ 'ðT *1ñ 
 3ð 
°Tõ 
ð *1ñ 	 3ð 	°Tõ 	ð +/¸TÈ$Ø#°ñ ð Ø+.ðØ=Aõð$ +/¸TÈ$Ø#°ñ ð Ø+.ðØ=Aõð ,3ñ " Sð "°sõ "ðJ (/ñ N ð N°3õ Nðb $+ñ ˜Sð ¨tõ ð* $ñ �cð ¨õ ð 6=ñ  Cð ¨sð Àõ ð,˜tð ¨ô ð"  "¨B°RÀWØñ¸sð Ø!%öð2 !(°Dñ ˜#ð ¸Tö ð ,3ñ  Sð °tö ð& <Có °Sö ð( '+°¸tÐQXñ È3ð Ð]`ö ð" *1ñ  3ð °Tö ð 8?ñ °ð À4ö ñ Ð ð 04Àñ ¸3ð ÈSö ðB $+ñ 	˜Sð 	¨tö 	ð )0ñ 	 #ð 	°Cö 	ð 07ñ 	¨ð 	¸4ö 	ð +2ñ  Cð °dö ñ& $&°7ñ 	¨Sð 	¸sö 	ð &-ñ ˜sð °ö ð  d°¸2ÀBÈBØ#°ñØðØ9=öð8 *1ñ  3ö ð %ñ �sð ¨ö ð& !¨gñ ¨ð ¸$ö ð $ñ 
�cð 
¨ö 
ð (/ñ  ö ð $ñ �cð ¨ö ð *1ñ  3ð °Tö ð: ")ñ ˜3ð ¨Sö ð ")ñ ˜3ð ¨Tö ð &-ñ +˜sð +°ö +ð\ !(ñ 6˜#ð 6¨Cö 6ð ¨Gñ  cð ¸ö ð +2ñ  Cð °dö óð.,˜3õ ,ðO dõ Oð0˜3õ 0ð  $¨"°B¸RØ $°À'Øñ/Ø<?ð/à $ö/ðd .5ñ 
 sð 
¸ö 
ð 07Àñ ¨ö ð$ $+ñ /˜Sð /¨tö /ðd %,ñ 
˜cð 
°ö 
ð /6ñ '¨ð '¸$ö 'ðT %ñ �sð ¨ö ð0 )0ñ 
 #ö 
ð %,ñ 
˜cð 
°ö 
ð ,3¸tò ˜ð  Sð Èö ð4 !(ñ 7˜#ð 7¨Dö 7ð <>È7ò 0˜Sñ 0 dñ 0°Sð 0ÀSð 0ÐW[ö 0ð  'ñ 6˜ð 6¨4ö 6ð, :<Èò /˜3ñ / Dð /°3ð /À3ð /ÐUYö /ð 6=ò˜cð ¨Sñ ¸sñ Ø#'ðØ/2ðØAEöñ< Ð ð -1¸gñ  cð ¸ð È$ö ð* )-¸'Ø+0ò%˜sð %¨cñ %°tñ %Ø"%ð%Ø47ñ%à$(ð%à59ö%ðP 3:ñ ¨Cð ¸cö ð& ?Fò ˜Tð ¨Sð ¸ð È$ö ð, ")ñ 	˜3ð 	¨Tö 	ð ")¸%ñ ˜3ð ÀDö ð& +2¸TÈ5Øñ Cð Ø"%öð:  $°ÀTñ 	¨3ð 	Èdö 	ð )-¸'Èdñ °Cð ÐW[ö ð /6À$ñ ¨ð È4ö ð ,3ñ  Sö ð &-¸ñ 	˜sð 	À$ö 	ñ KMÐZañ *ÐTWö *ð< IMÐZañ ÐTWð Ðfjö ñ" -/¸Gñ °cð Èö ñ Ð ð :PÐ]dñ *ÐWZö *ð2 AHñ ¸#ð ÈDö ñ$ 29Àgñ Àð ÐRU÷ r   