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) {