Skip to content

Add bulk product cards and weekly sales - #6

Merged
dvejsada merged 2 commits into
mainfrom
claude/week-sales-cards
Jun 28, 2026
Merged

dvejsada merged 2 commits into
mainfrom
claude/week-sales-cards

Conversation

@dvejsada

Copy link
Copy Markdown
Owner

Summary

Adds a bulk product fetch and a "deals of the week" feature to the client (API only — no MCP changes).

Changes

  • products.get_cards(product_ids, category_type="normal") — fetches basic data for many products in a single request via /api/v1/products/card (the bulk endpoint the library previously lacked). Results are returned in the requested order; IDs the API omits are skipped.
  • New ProductCard model — a trimmed, meaningful subset of the website's product card: id, name, brand, amount, unit, price (the sale price when on sale, otherwise the regular price), original_price, unit_price, currency, on_sale, sale_valid_till, in_stock. Display-only fields (image styling, slug, badges, tooltips, ratings, favourites, country flags) are dropped.
  • products.get_week_sales(page=0, size=30, sort="recommended") — fetches the "Akce týdne" deal IDs from /api/v1/categories/sales/components/week-sales, then enriches them through get_cards in one follow-up request.
  • Export ProductCard from the package.

Tests

Regression tests built from the real card payload (one regular product, one on sale) cover: card parsing, request-order preservation, the empty-input short-circuit, week-sales enrichment, and the no-deals case. Full suite: 142 passed; ruff/black/mypy clean.

Notes

  • Targets the unreleased 0.2.0 (folds in with the delivery-address fix). Happy to bump to 0.3.0 instead if you'd rather release the address fix on its own first.
  • No changes to the MCP server, per request. Exposing get_week_sales as an MCP tool is an easy follow-up if wanted.

🤖 Generated with Claude Code


Generated by Claude Code

Add products.get_cards(): fetch basic product data for many products in a
single request via /api/v1/products/card (the bulk endpoint the library
previously lacked). Results are mapped to a new typed ProductCard model —
a trimmed, meaningful subset (name, brand, amount, unit, current/original/
unit price, sale flag and validity, stock) with website display cruft
dropped — and returned in the requested order.

Add products.get_week_sales(): fetch the "Akce týdne" deal ids and enrich
them through get_cards in one follow-up request.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018KXnkpGcLfWU4p12TcZjfY
@dvejsada

Copy link
Copy Markdown
Owner Author

@claude review

@claude

claude Bot commented Jun 28, 2026 •

Copy link
Copy Markdown

Claude finished @dvejsada's task in 1m 51s —— View job


Code Review

  • Gather context and understand the request
  • Read repository files and analyze the diff
  • Post review feedback

Overall this is clean, well-structured code that follows the existing conventions well. A few things worth flagging before merge:

rohlik_api/models.py — ProductCard.from_api (lines 340–354)

in_stock has an inconsistent default. The field is declared in_stock: bool = True (defaults to "in stock"), but from_api unconditionally computes:

in_stock=stock.get("availabilityStatus") == "AVAILABLE",

When availabilityStatus is absent from the response, this evaluates to False — the opposite of the field default. A caller constructing ProductCard(...) by hand would get in_stock=True, while a caller using from_api on a response with no stock field gets in_stock=False. The field default should match the from_api behaviour, or the comparison should use an explicit fallback:

# Option A — treat unknown status as in-stock (matches field default)
in_stock=stock.get("availabilityStatus", "AVAILABLE") == "AVAILABLE",

# Option B — change the field default to False
in_stock: bool = False

Fix this →


rohlik_api/endpoints.py — product_cards (line 92)

category_type is a user-controlled string interpolated directly into the URL with no encoding:

return f"/api/v1/products/card?{params}&categoryType={category_type}"

Passing category_type="normal&injected=value" would silently append an extra query parameter. Other callers use only the default value, so this is low risk today, but it's worth encoding defensively:

from urllib.parse import quote
return f"/api/v1/products/card?{params}&categoryType={quote(category_type, safe='')}"

Note quote is already imported at the top of endpoints.py.

Fix this →


rohlik_api/services/products.py — get_week_sales (line 200)

product_ids is taken directly from the payload without a type check before being passed to get_cards:

product_ids = data.get("products") if isinstance(data, dict) else None
if not product_ids:
    return []
return await self.get_cards(product_ids)

If the API returns {"products": 0} or some other unexpected type, get_cards would receive a non-list and the for pid in product_ids in Endpoints.product_cards would produce unexpected results. A cheap guard keeps this safe:

if not isinstance(product_ids, list) or not product_ids:
    return []

Double _ensure_logged_in() call

get_week_sales calls _ensure_logged_in() itself, then delegates to get_cards which calls it again. Not a correctness issue (the method is idempotent), but it's a minor inefficiency. You could factor out a private _get_cards_no_auth_check or just accept the redundancy since it's one extra check.


Missing test coverage

The new tests cover the happy paths well. A few gaps worth considering:

  • HTTP error in get_cards (should return None)
  • Non-list API response in get_cards (also None)
  • get_week_sales returns None when the week-sales call fails
  • get_week_sales returns None when the enrichment get_cards call fails (IDs fetched successfully, card fetch fails)

Minor: _CARDS as a class attribute on the test class

This works, but pytest class-level data attributes that are lists or dicts can occasionally get mutated across tests (e.g. list(reversed(self._CARDS)) in the first test doesn't mutate, but it's worth being aware of). Since reversed() returns an iterator and list() creates a new list, this is safe here.


Summary: Two correctness-adjacent issues (in_stock default inconsistency, missing isinstance guard on product_ids) and one low-risk security hygiene issue (URL encoding). The core implementation and test structure are solid.

…duct_ids

- ProductCard.from_api: default to in-stock when availabilityStatus is
  absent, matching the field default (in_stock=True).
- Endpoints.product_cards: URL-encode category_type defensively.
- get_week_sales: guard that product_ids is a non-empty list before
  enriching.
- Add error-path tests: get_cards on HTTP error / non-list response, and
  get_week_sales when the sales call or the enrichment call fails.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018KXnkpGcLfWU4p12TcZjfY
@dvejsada
dvejsada merged commit 1c2f7c8 into main Jun 28, 2026
1 check passed
@dvejsada
dvejsada deleted the claude/week-sales-cards branch June 28, 2026 12:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants