Skip to content

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 / perPage from 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_fetch resets the queue before fetch; multiple batches per cron tick; broader row selection for pipeline steps (fixes stuck fetch with pending=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_met from any in-stock wholesaler (not only Qogita); stored wholesale_best_* on analyze + Qogita cron refresh; Discord when targets newly met (with or without qid).
  • 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 with DEFAULT_KEEPA_QUERY_SELECTION, strips page / perPage.
  • Pipeline β€” query_selection stored in job state and resumed across token waits / restarts.
  • API β€” GET /keepa/query-selection/default; sync / scan / pipeline start accept query_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() β€” clears keepa_product_fetched_at / keepa_scanned_at on all pipeline rows so a new fetch run actually processes ASINs again.
  • Fetch start β€” re_fetch: true by default on POST /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=keepa or ASIN: stub GTINs).
  • Multi-batch ticks β€” one cron tick can run several fetch/sync batches while tokens allow (inner while loop in pipeline_runner).
  • Sync-asins β€” page = 0 init fix (non-pipeline sync no longer crashes).

Wholesale catalog targets (catalog_targets.py)

  • apply_catalog_targets_to_row() β€” persists wholesale_best_price, wholesale_best_stock, wholesale_best_seller, are_targets_met, inventory_of_offers_under_target_price from wholesale_shopping_list.
  • are_targets_met β€” lowest in-stock wholesale price ≀ target EK (or target_price) and stock β‰₯ target_quantity; works without qid.
  • Analyze β€” refreshes catalog targets even when Keepa metrics unchanged (catalog_targets_unchanged short-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 (from qogita_preis / qogita_bestand in 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_at on qogita_watchlist_items.
  • DELETE /watchlist/{gtin} β€” removes from Qogita API when linked, then archives locally.
  • POST /watchlist/{gtin}/restore β€” clears archived_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 requires qid).
  • Targets met (not on Qogita) β€” qid IS NULL and are_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 on fba_details + repricer_articles.
  • API: repricer_article_id, repricer_sku, repricer_stock, repricer_brutto_buy_price.
  • UI Our stock / EK; click β†’ /multi-repricer with FBA tab + SKU (location.state or ?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_met flips false β†’ true.
  • GTIN β†’ https://www.qogita.com/categories/?query={gtin} (not for ASIN: stubs).

Qogita push β€” full run, single sync

  • No frontend batch loop / QOGITA_PUSH_BATCH_SIZE cap.
  • Iterates all candidates; upsert_from_api(..., commit=False) + one commit() 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_prices reports changed SKUs; base repricer runs force_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_monitor on product).

πŸ› Bug Fixes

Keepa fetch stopped after one batch

  • Fetch queue not reset on re-run β†’ pending_fetch=0 with nothing fetched; fixed via reset_keepa_fetch_queue + re_fetch on 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 = 0 init in sync_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.py for 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.py
  • app/services/qogita/catalog_targets.py (new), repricer_inventory.py (new), watchlist_present.py
  • app/services/price_alerts/watchlist_cron.py, discord_notifications.py
  • app/crud/qogita.py, keepa_pipeline.py, qogita_catalog.py
  • app/api/v1/endpoints/keepa.py, qogita.py
  • app/models/keepa.py, qogita.py
  • app/schemas/qogita.py
  • app/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.py
  • app/services/article_status.py, otto/otto_service.py
  • app/models/otto_item.py
  • app/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.js
  • src/pages/MultiRepricer/index.jsx
  • src/services/api/omni/keepa.js, qogita.js

Postgres

  • docker/postgres/migrations/add_qogita_watchlist_archived.sql
  • docker/postgres/migrations/rename_keepa_avg90_to_avg30.sql
  • docker/postgres/migrations/add_watchlist_wholesale_catalog_columns.sql
  • docker/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

  1. Copy selection JSON from Keepa Product Finder (including page / perPage if present).
  2. Paste into Keepa Product Finder query on the watchlist tab.
  3. 1. Sync ASINs β€” backend fetches all pages with your filters.
  4. 2. Fetch Products β€” re-queues and fetches when re_fetch is on (default).

Analyze with custom margin

  1. Set Margin % next to 3. Analyze (by EAN) (e.g. 12).
  2. 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;