Skip to content

Sync the cart when an ingredient replacement is saved - #446

Draft
adri wants to merge 1 commit into
masterfrom
cursor/ingredient-replacement-sync-12bd
Draft

adri wants to merge 1 commit into
masterfrom
cursor/ingredient-replacement-sync-12bd

Conversation

@adri

@adri adri commented Oct 10, 2026

Copy link
Copy Markdown
Owner

Naming a replacement did not order it. The swap only runs inside sync_supermarket, and set_ingredient_replacement only wrote replacement_ingredient_id.

What happened

Whey (#543, product 556846) and flatbread (#510) were put on the list with set_ingredient_quantity, which syncs. Both were then given a replacement. That second call stored the mapping and stopped. The cart kept the sold-out primary, substituted_for stayed null, and the replacement never appeared.

Manual quantities are included. substitute/2 runs on planned recipe quantities and on manual rows. Melk, pesto, and druiven did not swap because those primaries were still orderable. Flatbread later became orderable on its own, so there was nothing left to swap.

The swap is not reserved for order placement or for recipe-planned items. It runs on every sync: a quantity change, planning or unplanning a recipe, and the Quantum job at 00:00 UTC. While a paid order is in flight, sync freezes and writes nothing.

"Tijdelijk uitverkocht" is a live basket verdict (isOrderable: false). The stored product card stays IN_ASSORTMENT, so warning is nil. Two things could still hide that signal after the mapping was saved:

  • The basket is cached for five hours. A sync that decided from a copy taken while the product was still orderable would order the primary again.
  • A refused UpdateMyListBasket was ignored. The pre-GraphQL client retried; the GraphQL client returned the error and sync threw it away.

Fix

  • set_ingredient_replacement syncs immediately after a successful save, including when the mapping is cleared. A product the basket already cannot deliver is swapped then.
  • Every non-frozen sync drops the basket cache before it decides what to add.
  • A refused cart mutation is retried twice, with the cache dropped between attempts, and the last failure is logged at warning.

The sold-out primary stays in the cart. The replacement is added, and substituted_for is set on that row once it is actually in the basket. A product that sells out later still waits for the next sync.

Tests

mix check (format, credo --strict, compile --warnings-as-errors, 120 tests, sobelow).

  • A manual row whose product card is IN_ASSORTMENT, while the fixture basket says the same product cannot be delivered, is swapped by sync_supermarket/1 (the midnight path).
  • set_ingredient_replacement on that same setup orders the replacement and get_shopping_list reports substituted_for.
Open in Web Open in Cursor 

Naming a replacement only stored the foreign key. The swap runs inside
sync_supermarket, which quantity changes and the midnight job call, so a
manual item already on the list as "Tijdelijk uitverkocht" waited until
the next sync. That sync could also read a basket cached while the
product was still orderable, and a refused cart mutation was discarded.

Saving the mapping now syncs immediately. Every sync drops the basket
cache first, and a refused mutation is retried twice and then logged.

Co-authored-by: Adrian Philipp <adri@users.noreply.github.com>

This branch has not been deployed

No deployments
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