1.8.4
βοΈ Patch 1.8.4 β Keepa pipeline, wholesale targets, watchlist UI & repricer¶
π Summary¶
- Keepa Product Finder query β paste full explorer JSON in the watchlist UI; backend merges with defaults and paginates automatically (
page/perPagefrom Keepa are ignored). - Sync all ASINs β Max ASINs removed from the frontend; finder sync always runs through all pages (up to Keepaβs 10k cap).
- Keepa fetch pipeline β
re_fetchresets the queue before fetch; multiple batches per cron tick; broader row selection for pipeline steps (fixes stuck fetch withpending=0/fetched=0). - Analyze margin β optional Margin % (Nettospanne) next to Analyze by EAN (separate from cron margin); persisted in browser prefs.
- avg30 instead of avg90 β VK, rank, and EK use Keepa 30-day averages; column Avg VK (30d).
- Wholesale catalog targets β
are_targets_metfrom any in-stock wholesaler (not only Qogita); storedwholesale_best_*on analyze + Qogita cron refresh; Discord when targets newly met (with or withoutqid). - Archive watchlist rows β delete archives (soft ignore); Keepa sync/analyze/push skip archived rows; Restore brings them back without re-pushing to Qogita.
- Repricer inventory β Our stock / EK from FBA repricer (
fba_details+repricer_articles); click opens Multi Repricer on that SKU. - Watchlist UI clarity β Best wholesale column; status In Qogita catalog / Not in Qogita catalog (distinct from wholesale availability); Targets met chip for Keepa-only rows.
- GTIN β Qogita β GTIN links to Qogita catalog search; ASIN stays Amazon.de.
- Qogita push β processes all candidates in one run; watchlist sync once at end; default re-push analyzed items (
only_unpushed: false); session fix (no detached-instance 502 mid-batch). - Otto / base repricer β buy-price change forces channel recalc; Otto unpause only when min/max actually change; manual monitor by SKU skips stock/status filters.
β¨ Improvements¶
Keepa query selection (paste from explorer)¶
query_selection.pyβnormalize_query_selection()merges pasted JSON withDEFAULT_KEEPA_QUERY_SELECTION, stripspage/perPage.- Pipeline β
query_selectionstored in job state and resumed across token waits / restarts. - API β
GET /keepa/query-selection/default; sync / scan / pipeline start acceptquery_selection. - Frontend β multiline JSON editor, Reset to server default, saved in
localStorage.
Pagination is always driven by the backend (KEEPA_PER_PAGE = 50, page index 0β¦n).
Keepa fetch pipeline β re-fetch & throughput¶
reset_keepa_fetch_queue()β clearskeepa_product_fetched_at/keepa_scanned_aton all pipeline rows so a new fetch run actually processes ASINs again.- Fetch start β
re_fetch: trueby default onPOST /keepa/pipeline/fetch-products/start; frontend passes it when starting fetch. - Pipeline row selection β
_keepa_pipeline_rows()includes all non-archived rows with ASIN (source=keepaorASIN:stub GTINs). - Multi-batch ticks β one cron tick can run several fetch/sync batches while tokens allow (inner
whileloop inpipeline_runner). - Sync-asins β
page = 0init fix (non-pipeline sync no longer crashes).
Wholesale catalog targets (catalog_targets.py)¶
apply_catalog_targets_to_row()β persistswholesale_best_price,wholesale_best_stock,wholesale_best_seller,are_targets_met,inventory_of_offers_under_target_pricefromwholesale_shopping_list.are_targets_metβ lowest in-stock wholesale price β€ target EK (ortarget_price) and stock β₯target_quantity; works withoutqid.- Analyze β refreshes catalog targets even when Keepa metrics unchanged (
catalog_targets_unchangedshort-circuit). - Qogita cron β after sync: snapshot
are_targets_metβrefresh_all_watchlist_catalog_targets()β Discord for newly met items. in_qogita_catalogβ also refreshed during catalog-targets pass (fromqogita_preis/qogita_bestandin wholesale list).
Terminology (UI / status):
| Label | Meaning |
|---|---|
| Best wholesale | Cheapest in-stock offer from any supplier (Parfumtrend, Qogita, Jadro, β¦) |
| In Qogita catalog | GTIN has Qogita catalog data in wholesale_shopping_list β pushable to Qogita |
| Not in Qogita catalog | No Qogita price/stock in wholesale list (other wholesalers may still have stock) |
| Qogita watchlist | Live price/qty on Qogita after sync (qid set) |
| Keepa only | No qid β not on your Qogita watchlist yet |
Analyze β per-run target margin¶
- Toolbar Margin % (Nettospanne) used for manual Analyze by EAN runs.
- Target EK column header reflects the analyze margin (cron margin unchanged for scheduled runs).
avg30 pricing metrics¶
- Keepa product API requests 30 days of stats (
KEEPA_PRODUCT_STATS_DAYS = 30). - DB columns renamed:
avg30_new_price_cents,avg30_sales_rank,avg30_list_price_cents,avg30_seller_count. - EK / margin breakdown based on avg30 new price.
After deploy: run Fetch Products + Analyze by EAN once to refresh stored metrics.
Watchlist archive (soft delete)¶
archived_atonqogita_watchlist_items.- DELETE
/watchlist/{gtin}β removes from Qogita API when linked, then archives locally. - POST
/watchlist/{gtin}/restoreβ clearsarchived_at(does not auto-create on Qogita). - Archived rows excluded from list (default), Keepa stub import, analyze, push, and catalog sync.
- Status filter Archived (ignored); restore action in table.
Watchlist filters & status chips¶
- Targets met β uses wholesale
are_targets_met(no longer requiresqid). - Targets met (not on Qogita) β
qid IS NULLandare_targets_met. - Keepa-only rows: Targets met chip; Wholesale: {seller} when stock exists but not in Qogita catalog.
Repricer inventory column¶
repricer_inventory.pyβ join by ASIN onfba_details+repricer_articles.- API:
repricer_article_id,repricer_sku,repricer_stock,repricer_brutto_buy_price. - UI Our stock / EK; click β
/multi-repricerwith FBA tab + SKU (location.stateor?platform=fba&sku=).
Discord β targets met¶
- Embeds include target EK, catalog price, seller, stock, offers β€ target, on-Qogita flag.
- Fires after Qogita cron catalog-targets refresh when
are_targets_metflips false β true.
GTIN links¶
- GTIN β
https://www.qogita.com/categories/?query={gtin}(not forASIN:stubs).
Qogita push β full run, single sync¶
- No frontend batch loop /
QOGITA_PUSH_BATCH_SIZEcap. - Iterates all candidates;
upsert_from_api(..., commit=False)+ onecommit()per item (avoids SQLAlchemy detached session 502). - Post-push sync runs once after all pushes (not per batch).
- Default push includes rows already on Qogita so re-analyzed EK prices update.
Otto repricer (service_multi-repricer)¶
- Buy-price change β
sync_buy_pricesreportschangedSKUs; base repricer runsforce_recalc_channels_for_buy_price_change()(all channels,force_min_max). - Otto monitor cron β does not duplicate daily buy-price sync; unpause (
status=active,unpause_date=null) only when min/max actually change. - Manual monitor (
POST /otto/monitor/{sku}) β when SKU is passed, skips stock/status filters (force_monitoron product).
π Bug Fixes¶
Keepa fetch stopped after one batch¶
- Fetch queue not reset on re-run β
pending_fetch=0with nothing fetched; fixed viareset_keepa_fetch_queue+re_fetchon pipeline start. - Pipeline tick only ran one batch per cron interval; inner loop continues while tokens allow.
502 during Qogita push¶
- Instance is not bound to a Session β re-fetch row per GTIN; defer commit until after each successful push.
Keepa sync-asins page undefined¶
- Missing
page = 0init insync_query_asins(crash on non-pipeline sync).
Query selection validation¶
- Reject non-object JSON (e.g. arrays); empty object merges defaults only.
Duplicate updated_at on watchlist schema¶
- Removed duplicate column definition in
schemas/qogita.py.
β¨ Tests¶
test_keepa_query_selection.pyβ merge, strip pagination, reject invalid payloads.test_repricer_inventory.pyβ ASIN β repricer match.test_catalog_targets.pyβ wholesale best offer,are_targets_met, catalog refresh.test_keepa_pipeline.pyβ pipeline fetch / queue behaviour.test_recalculate_channels.py,test_cron_sync_flow.py,test_sync_buy_prices.pyβ buy-price change β force recalc, Otto unpause.- Updated
test_keepa_pricing.py,test_keepa_scanner_mocked.py,fixtures_keepa.pyfor avg30.
cd backend/service_omni
pytest app/tests/test_keepa_query_selection.py app/tests/test_repricer_inventory.py \
app/tests/test_catalog_targets.py app/tests/test_keepa_pipeline.py -v
cd backend/service_multi-repricer
pytest app/tests/test_recalculate_channels.py app/tests/test_cron_sync_flow.py -v
π Files changed¶
Backend (service_omni)
app/services/keepa/query_selection.py(new)app/services/keepa/constants.py,keepa_client.py,scanner.py,pipeline_runner.py,pipeline_state.py,pricing.pyapp/services/qogita/catalog_targets.py(new),repricer_inventory.py(new),watchlist_present.pyapp/services/price_alerts/watchlist_cron.py,discord_notifications.pyapp/crud/qogita.py,keepa_pipeline.py,qogita_catalog.pyapp/api/v1/endpoints/keepa.py,qogita.pyapp/models/keepa.py,qogita.pyapp/schemas/qogita.pyapp/tests/test_keepa_query_selection.py,test_repricer_inventory.py,test_catalog_targets.py,test_keepa_pipeline.py,test_keepa_pricing.py,test_keepa_scanner_mocked.py,fixtures_keepa.py
Backend (service_multi-repricer)
app/api/v1/endpoints/otto_articles.py,repricer_articles.pyapp/services/article_status.py,otto/otto_service.pyapp/models/otto_item.pyapp/tests/test_recalculate_channels.py,test_cron_sync_flow.py,test_sync_buy_prices.py,test_force_recalc_resolution.py
Frontend
src/pages/Statistics/Tabs/QogitaWatchlistTab.jsx,qogitaWatchlistPrefs.jssrc/pages/MultiRepricer/index.jsxsrc/services/api/omni/keepa.js,qogita.js
Postgres
docker/postgres/migrations/add_qogita_watchlist_archived.sqldocker/postgres/migrations/rename_keepa_avg90_to_avg30.sqldocker/postgres/migrations/add_watchlist_wholesale_catalog_columns.sqldocker/postgres/tables/qogita_watchlist_items.sql
π§ Database & deploy¶
\i docker/postgres/migrations/add_qogita_watchlist_archived.sql
\i docker/postgres/migrations/rename_keepa_avg90_to_avg30.sql
\i docker/postgres/migrations/add_watchlist_wholesale_catalog_columns.sql
Restart service-omni and service_multi-repricer. Rebuild frontend.
After deploy: run Sync ASINs β Fetch Products (with re_fetch) β Analyze by EAN, or wait for Qogita cron to populate wholesale_best_*.
Usage notes¶
Keepa finder query
- Copy selection JSON from Keepa Product Finder (including
page/perPageif present). - Paste into Keepa Product Finder query on the watchlist tab.
- 1. Sync ASINs β backend fetches all pages with your filters.
- 2. Fetch Products β re-queues and fetches when
re_fetchis on (default).
Analyze with custom margin
- Set Margin % next to 3. Analyze (by EAN) (e.g. 12).
- Run analyze β Target EK column shows that margin for display; stored EK uses the same run.
Wholesale vs Qogita catalog
- A row can show Parfumtrend under Best wholesale and Not in Qogita catalog at the same time β wholesale stock exists, but Qogitaβs assortment CSV has no price for that GTIN.
- Targets met can be true via a non-Qogita seller even when the item is Keepa only.
Archive vs delete on Qogita
| Action | Qogita API | Local DB | Keepa re-import |
|---|---|---|---|
| Delete | Removed if qid |
archived_at set |
Skipped |
| Restore | Not auto-added | archived_at null |
Allowed again |
Refresh after avg30 deploy
Run 2. Fetch Products then 3. Analyze by EAN so VK/EK reflect 30-day averages.
SQL β targets met, not on Qogita watchlist
SELECT gtin, name, calculated_ek_price, wholesale_best_price,
wholesale_best_seller, wholesale_best_stock, are_targets_met
FROM qogita_watchlist_items
WHERE archived_at IS NULL AND qid IS NULL AND are_targets_met = TRUE;