From 0b990e6cb3439f7160d71f6781d8773ca0dd5dbc Mon Sep 17 00:00:00 2001 From: vsem-azamat <60985938+vsem-azamat@users.noreply.github.com> Date: Sat, 17 Oct 2026 22:21:01 +0200 Subject: [PATCH] fix(checkout): index categories from the catalog items lookup The items lookup that Build runs for the basket's ids maps each item to its category through categories[].item_ids; the item records themselves carry no category field. That payload was only searched item by item, so the mapping was never used. When the item page came back empty and the search by the basket's line name (localized, e.g. English for a Czech venue) found nothing, checkout preview failed with "unable to resolve category_id". Index the lookup payloads like the item page, search and venue content payloads, so the category resolves without the extra search. --- .../checkoutpayload/checkoutpayload.go | 7 +++ .../checkoutpayload/checkoutpayload_test.go | 48 +++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/internal/service/checkoutpayload/checkoutpayload.go b/internal/service/checkoutpayload/checkoutpayload.go index 60db22d..5a523b4 100644 --- a/internal/service/checkoutpayload/checkoutpayload.go +++ b/internal/service/checkoutpayload/checkoutpayload.go @@ -180,6 +180,13 @@ func Build( warnings = append(warnings, fmt.Sprintf("unable to load catalog items for weighted pricing (slug=%s)", venueSlug)) } } + // The items lookup maps each item to its category through + // categories[].item_ids rather than a field on the item itself, so index it + // like the other category-bearing payloads. + mergeCheckoutCategoryIndexes(categoryIDsByItemID, buildCheckoutCategoryIDIndex(catalogItemsPayload)) + for _, payload := range fetchedCatalogPayloads { + mergeCheckoutCategoryIndexes(categoryIDsByItemID, buildCheckoutCategoryIDIndex(payload)) + } // Item search answers both open questions in one targeted request: the // response maps the item to its categories and carries the full catalog diff --git a/internal/service/checkoutpayload/checkoutpayload_test.go b/internal/service/checkoutpayload/checkoutpayload_test.go index 4559deb..c780b15 100644 --- a/internal/service/checkoutpayload/checkoutpayload_test.go +++ b/internal/service/checkoutpayload/checkoutpayload_test.go @@ -861,6 +861,54 @@ func TestBuildFetchesCatalogItemsMissingFromSuppliedCurrentCatalog(t *testing.T) } } +// The items lookup reports each item's category only through +// categories[].item_ids; the item records carry no category field. When the +// item page is empty and a search by the basket's (often translated) line name +// finds nothing, that mapping is the only source of the category. +func TestBuildResolvesCategoryFromCatalogItemsCategoryList(t *testing.T) { + const itemID = "000000000000000000000419" + basket := basketWithItem("CZK 125.80", map[string]any{ + "id": itemID, + "name": "Green Olives Stuffed with Blue Cheese Paste 300g", + "count": 2, + "price": 6290, + }) + basket["venue"].(map[string]any)["slug"] = "synthetic-hypermarket" + basket["venue"].(map[string]any)["currency"] = "CZK" + + searched := 0 + api := &fakeAPI{ + venueItemPageFn: func(_ context.Context, _, id string) (map[string]any, error) { + return map[string]any{"id": id, "sections": []any{}}, nil + }, + assortmentItemsFn: func(_ context.Context, _ string, itemIDs []string) (map[string]any, error) { + return map[string]any{ + "categories": []any{ + map[string]any{"id": "olives-category", "item_ids": []any{itemID}}, + }, + "items": []any{ + map[string]any{"id": itemID, "name": "Zelené olivy plněné pastou z modrého sýra 300g"}, + }, + }, nil + }, + searchFn: func(_ context.Context, _ string, _ string) (map[string]any, error) { + searched++ + return map[string]any{}, nil + }, + } + + payload, warnings, err := Build(context.Background(), api, nil, basket, domain.Location{}, "standard", 0, "") + if err != nil { + t.Fatalf("Build() error = %v (warnings: %v)", err, warnings) + } + if got := firstMenuItem(t, purchasePlan(t, payload))["category_id"]; got != "olives-category" { + t.Fatalf("category_id = %v, want olives-category", got) + } + if searched != 0 { + t.Fatalf("item search ran %d times, want 0: the items lookup already maps the category", searched) + } +} + // Grocery venues load categories lazily: the assortment lists categories with // empty item_ids, so the item-to-category mapping only exists in venue content. func TestBuildResolvesCategoryFromPagedVenueContent(t *testing.T) {