ai cooking
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Albertesons api scraping (#426)

* initial fun with bright data

* this is fun

* working

* trim it down

* good bye previous login

* categories

* zip unneeded

* we working lets wee when rese expires

* maybe have a staples provider

* okay simplif client some

* unsigned ints

* remove some unused

* put acmeresp in right place

* fix up staples some

* seems to be working but reese key will expoire

* fumpt

* use staple rows

* don't hard code sub in test

---------

Co-authored-by: paul miller <paul.miller>

authored by

Paul Miller
paul miller
and committed by
GitHub
ffbd1dd6 87d9a0af

+2204 -19
+98
cmd/albertsonsquery/main.go
··· 1 + package main 2 + 3 + import ( 4 + "context" 5 + "errors" 6 + "flag" 7 + "fmt" 8 + "io" 9 + "net/http" 10 + "os" 11 + "time" 12 + 13 + "careme/internal/albertsons/query" 14 + ) 15 + 16 + func main() { 17 + if err := run(context.Background(), os.Stdout, os.Args[1:]); err != nil { 18 + fmt.Fprintf(os.Stderr, "error: %v\n", err) 19 + os.Exit(1) 20 + } 21 + } 22 + 23 + func run(ctx context.Context, stdout io.Writer, args []string) error { 24 + return runWithHTTPClient(ctx, stdout, args, nil) 25 + } 26 + 27 + // exists just for UT 28 + func runWithHTTPClient(ctx context.Context, stdout io.Writer, args []string, httpClient *http.Client) error { 29 + fs := flag.NewFlagSet("albertsonsquery", flag.ContinueOnError) 30 + fs.SetOutput(io.Discard) 31 + 32 + var ( 33 + baseURL string 34 + storeID string 35 + subscriptionKey string 36 + reese84 string 37 + searchQuery string 38 + rows uint 39 + start uint 40 + timeoutSec int 41 + ) 42 + 43 + fs.StringVar(&baseURL, "base-url", query.DefaultSearchBaseURL, "Albertsons-family search base URL") 44 + fs.StringVar(&storeID, "store-id", "806", "store id to search against") 45 + fs.StringVar(&subscriptionKey, "subscription-key", envOrDefault("ALBERTSONS_SEARCH_SUBSCRIPTION_KEY", ""), "Albertsons pathway subscription key") 46 + fs.StringVar(&reese84, "reese84", envOrDefault("ALBERTSONS_SEARCH_REESE84", ""), "optional reese84 cookie value") 47 + fs.StringVar(&searchQuery, "query", "", "search query, default empty string") 48 + fs.UintVar(&rows, "rows", 60, "number of rows to request") 49 + fs.UintVar(&start, "start", 0, "pagination start offset") 50 + fs.IntVar(&timeoutSec, "timeout", 20, "HTTP timeout in seconds") 51 + 52 + if err := fs.Parse(args); err != nil { 53 + return err 54 + } 55 + if storeID == "" { 56 + return errors.New("store-id is required") 57 + } 58 + if subscriptionKey == "" { 59 + return errors.New("subscription-key is required") 60 + } 61 + 62 + // todo proxy through bright data ? timeout with context instead of http client? 63 + if httpClient == nil { 64 + httpClient = &http.Client{Timeout: time.Duration(timeoutSec) * time.Second} 65 + } 66 + client, err := query.NewSearchClient(query.SearchClientConfig{ 67 + BaseURL: baseURL, 68 + SubscriptionKey: subscriptionKey, 69 + Reese84: reese84, 70 + HTTPClient: httpClient, 71 + }) 72 + if err != nil { 73 + return fmt.Errorf("create search client: %w", err) 74 + } 75 + 76 + payload, err := client.Search(ctx, storeID, query.Category_Vegatables, query.SearchOptions{ 77 + Query: searchQuery, 78 + Start: start, 79 + Rows: rows, 80 + }) 81 + if err != nil { 82 + return fmt.Errorf("run search: %w", err) 83 + } 84 + 85 + for i, doc := range payload.Response.Docs { 86 + _, _ = fmt.Fprintf(stdout, "%d: %s (price: %.2f)\n", i+1, doc.Name, doc.Price) 87 + } 88 + _, err = fmt.Fprintf(stdout, "total products: %d\n", len(payload.Response.Docs)) 89 + 90 + return err 91 + } 92 + 93 + func envOrDefault(key, fallback string) string { 94 + if value := os.Getenv(key); value != "" { 95 + return value 96 + } 97 + return fallback 98 + }
+60
cmd/albertsonsquery/main_test.go
··· 1 + package main 2 + 3 + import ( 4 + "context" 5 + "io" 6 + "net/http" 7 + "strings" 8 + "testing" 9 + ) 10 + 11 + func TestRunOutputsReturnedDocCount(t *testing.T) { 12 + t.Parallel() 13 + 14 + httpClient := &http.Client{Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) { 15 + if r.URL.Path != "/abs/pub/xapi/wcax/pathway/search" { 16 + t.Fatalf("unexpected path: %s", r.URL.Path) 17 + } 18 + if got := r.URL.Query().Get("storeid"); got != "806" { 19 + t.Fatalf("unexpected storeid: %q", got) 20 + } 21 + if got := r.Header.Get("Ocp-Apim-Subscription-Key"); got != "test-key" { 22 + t.Fatalf("unexpected subscription key: %q", got) 23 + } 24 + 25 + return &http.Response{ 26 + StatusCode: http.StatusOK, 27 + Header: http.Header{ 28 + "Content-Type": []string{"application/json"}, 29 + }, 30 + Body: io.NopCloser(strings.NewReader(`{ 31 + "response":{"numFound":3,"disableTracking":false,"start":0,"miscInfo":{"attributionToken":"","query":"","sort":"","filter":"","nextPageToken":""},"isExactMatch":true,"docs":[{"id":"1","name":"Apples","price":1.99},{"id":"2","name":"Bananas","price":2.49},{"id":"3","name":"Carrots","price":3.99}]}, 32 + "offersData":{"departments":{},"upcs":{}}, 33 + "facet":{"ranges":[],"fields":[],"dynamic_facets":[]}, 34 + "appCode":"ok", 35 + "appMsg":"ok", 36 + "dynamic_filters":{} 37 + }`)), 38 + }, nil 39 + })} 40 + 41 + var stdout strings.Builder 42 + err := runWithHTTPClient(context.Background(), &stdout, []string{ 43 + "-base-url", "https://www.acmemarkets.com", 44 + "-store-id", "806", 45 + "-subscription-key", "test-key", 46 + }, httpClient) 47 + if err != nil { 48 + t.Fatalf("run returned error: %v", err) 49 + } 50 + 51 + if got := stdout.String(); got != "1: Apples (price: 1.99)\n2: Bananas (price: 2.49)\n3: Carrots (price: 3.99)\ntotal products: 3\n" { 52 + t.Fatalf("unexpected stdout: %q", got) 53 + } 54 + } 55 + 56 + type roundTripFunc func(*http.Request) (*http.Response, error) 57 + 58 + func (f roundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) { 59 + return f(r) 60 + }
+16 -10
internal/albertsons/locations.go
··· 3 3 import ( 4 4 "context" 5 5 "fmt" 6 + "log/slog" 6 7 "strings" 7 8 8 9 "careme/internal/cache" ··· 19 20 } 20 21 21 22 type LocationBackend struct { 22 - zipLookup centroidByZip 23 - spatial []locationtypes.Location 24 - hydrator *hydrator.LazyHydrator 23 + zipLookup centroidByZip 24 + spatial []locationtypes.Location 25 + hydrator *hydrator.LazyHydrator 26 + hasInventory bool 25 27 } 26 28 27 29 func NewLocationBackendFromConfig(ctx context.Context, cfg *config.Config, zipLookup centroidByZip) (*LocationBackend, error) { ··· 38 40 return nil, fmt.Errorf("create Albertsons list cache: %w", err) 39 41 } 40 42 41 - return newLocationBackend(ctx, listCache, zipLookup) 43 + slog.InfoContext(ctx, "ALBERTSONS invetory", "has", cfg.Albertsons.HasInventory()) 44 + 45 + return newLocationBackend(ctx, listCache, zipLookup, cfg.Albertsons.HasInventory()) 42 46 } 43 47 44 - func newLocationBackend(ctx context.Context, c cache.ListCache, zipLookup centroidByZip) (*LocationBackend, error) { 48 + func newLocationBackend(ctx context.Context, c cache.ListCache, zipLookup centroidByZip, inventory bool) (*LocationBackend, error) { 45 49 entries, err := storeindex.Load(ctx, c, LocationIndexCacheKey) 46 50 if err != nil { 47 51 return nil, fmt.Errorf("load albertsons locations index: %w", err) ··· 53 57 } 54 58 55 59 return &LocationBackend{ 56 - zipLookup: zipLookup, 57 - spatial: spatial, 58 - hydrator: hydrator.NewLazyHydrator(&loader{c}), 60 + zipLookup: zipLookup, 61 + spatial: spatial, 62 + hydrator: hydrator.NewLazyHydrator(&loader{c}), 63 + hasInventory: inventory, 59 64 }, nil 60 65 } 61 66 ··· 63 68 return IsID(locationID) 64 69 } 65 70 66 - func (*LocationBackend) HasInventory(locationID string) bool { 67 - return false 71 + func (l *LocationBackend) HasInventory(locationID string) bool { 72 + // do we want to make this dynamic 73 + return l.hasInventory 68 74 } 69 75 70 76 func (b *LocationBackend) GetLocationByID(ctx context.Context, locationID string) (*locationtypes.Location, error) {
+4 -4
internal/albertsons/locations_test.go
··· 25 25 t.Fatalf("RebuildLocationIndex returned error: %v", err) 26 26 } 27 27 28 - backend, err := newLocationBackend(context.Background(), cacheStore, zipLookup) 28 + backend, err := newLocationBackend(context.Background(), cacheStore, zipLookup, false) 29 29 if err != nil { 30 30 t.Fatalf("NewLocationBackend returned error: %v", err) 31 31 } ··· 86 86 t.Fatalf("RebuildLocationIndex returned error: %v", err) 87 87 } 88 88 89 - backend, err := newLocationBackend(context.Background(), cacheStore, zipLookup) 89 + backend, err := newLocationBackend(context.Background(), cacheStore, zipLookup, false) 90 90 if err != nil { 91 91 t.Fatalf("NewLocationBackend returned error: %v", err) 92 92 } ··· 123 123 t.Fatalf("RebuildLocationIndex returned error: %v", err) 124 124 } 125 125 126 - backend, err := newLocationBackend(context.Background(), cacheStore, zipLookup) 126 + backend, err := newLocationBackend(context.Background(), cacheStore, zipLookup, false) 127 127 if err != nil { 128 128 t.Fatalf("NewLocationBackend returned error: %v", err) 129 129 } ··· 145 145 146 146 cacheStore := cache.NewInMemoryCache() 147 147 148 - _, err := newLocationBackend(context.Background(), cacheStore, staticZIPLookup{}) 148 + _, err := newLocationBackend(context.Background(), cacheStore, staticZIPLookup{}, false) 149 149 if err == nil { 150 150 t.Fatal("expected NewLocationBackend to return an error") 151 151 }
+1
internal/albertsons/query/acmeresp.json
··· 1 + {"response":{"numFound":194,"disableTracking":false,"start":0,"miscInfo":{"attributionToken":"tQHwtAoLCOXQls4GEK3biUEQARokNjlkNzY5NjYtMDAwMC0yMjRlLWEwNjMtZDQzYTJjZDJlZmI3KiQ2YjQ3ZjU0OS1iOGRhLTQ4YjUtODQ4Ni04Yzc1MGVjMWM3NTMyLPSZhCKOvp0V1LKdFcLwnhX2mYQitreMLajlqi2Q97Iwn9a3LZuVpDGc1rctOh53ZWItYWNtZW1hcmtldHMtc2VydmluZy1jb25maWdIAVgBYAFoAXoCcGg","query":"","sort":"","filter":"(inventory(806, price) >0) AND (inventory(806,attributes.custom_type_2) = 1) AND (inventory(806, attributes.instore): ANY(\"1\", \"2\")) AND (categories: ANY(\"Meat & Seafood > Fish & Shellfish\"))","nextPageToken":"wNiZWZyQ2YyE2M0QWLzYDMh1SZ0IjMtADMwATL1YTO2cDZ5YDJaQU20DOEG4sp5WOCLIBMzIgC"},"isExactMatch":true,"docs":[{"status":"active","name":"waterfront BISTRO Atlantic Salmon Fillet Skin On Vacuum Sealed Pack - 2 Lb","pid":"970115396","upc":"0023249700000","id":"970115396","storeId":"806","featured":false,"isDYOCake":false,"inventoryAvailable":"1","pastPurchased":false,"restrictedValue":"0","salesRank":99999,"agreementId":0,"featuredProductId":0,"imageUrl":"https://images.albertsons-media.com/is/image/ABS/970115396","price":17.98,"promoDescription":"Safeway Club Price: $8.99 lb SAVE up to: $3.0 lb","promoText":" $8.99 lb SAVE up to: $3.0 lb","promoType":"P","promoEndDate":"2026-03-26T23:59:00","basePrice":23.98,"basePricePer":11.99,"pricePer":8.99,"displayType":"2","aisleId":"1_19_3_1","aisleName":"Fish & Shellfish|1_19_3","departmentName":"Meat & Seafood","shelfName":"Fish","shelfNameWithId":"Fish|1_19_3_1","aisleLocation":"Meat & Seafood","snapEligible":true,"unitOfMeasure":"LB","sellByWeight":"P","averageWeight":["2.000"],"unitQuantity":"LB","displayUnitQuantityText":"ea","displayEstimateText":"approx","previousPurchaseQty":0,"maxPurchaseQty":20,"minWeight":"2.000","maxWeight":"2.000","isHhcProduct":false,"prop65WarningTypeCD":"","prop65WarningText":"","prop65WarningIconRequired":false,"isArProduct":true,"isMtoProduct":false,"isCustomizable":false,"inStoreShoppingElig":false,"preparationTime":"0","isMarketplaceItem":"N","algoType":"","triggerQuantity":0,"idOfAisle":"1_19_3","idOfShelf":"1_19_3_1","idOfDepartment":"1_19","warnings":[{"foodIndicator":"","warnMsgTxt":"","warningSourceNm":""}],"requiresReturn":false,"channelEligibility":{"pickUp":true,"delivery":true,"inStore":true,"shipping":false},"channelInventory":{"delivery":"1","pickup":"1","instore":"1","shipping":"0"},"productReview":{"avgRating":"5.0","reviewCount":"3","isReviewWriteEligible":"true","isReviewDisplayEligible":"true","isForOnetimeReview":"true","reviewTemplateType":"default"},"itemSizeQty":"1","itemPackageQty":"1","itemRetailSect":"330","badges":[],"labels":[{"labelName":"SNAP"}]},{"status":"active","name":"Fresh Atlantic Salmon Color Added Portion 1 Count - Each","pid":"960137751","upc":"0023564700000","id":"960137751","storeId":"806","featured":false,"isDYOCake":false,"inventoryAvailable":"1","pastPurchased":false,"restrictedValue":"0","salesRank":1385,"agreementId":0,"featuredProductId":0,"imageUrl":"https://images.albertsons-media.com/is/image/ABS/960137751","price":5.49,"promoDescription":"Safeway Club Price: $5.49 ea SAVE up to: $0.5 ea","promoText":" $5.49 ea SAVE up to: $0.5 ea","promoType":"P","promoEndDate":"2026-03-26T23:59:00","basePrice":5.99,"basePricePer":19.17,"pricePer":17.57,"displayType":"1","aisleId":"1_19_3_1","aisleName":"Fish & Shellfish|1_19_3","departmentName":"Meat & Seafood","shelfName":"Fish","shelfNameWithId":"Fish|1_19_3_1","aisleLocation":"Seafood","snapEligible":true,"unitOfMeasure":"EA","sellByWeight":"I","averageWeight":["0.320"],"unitQuantity":"POUND","displayUnitQuantityText":"ea","displayEstimateText":null,"previousPurchaseQty":0,"maxPurchaseQty":20,"minWeight":"0.320","maxWeight":"0.320","isHhcProduct":false,"prop65WarningTypeCD":"","prop65WarningText":"","prop65WarningIconRequired":false,"isArProduct":true,"isMtoProduct":false,"isCustomizable":false,"inStoreShoppingElig":false,"preparationTime":"0","isMarketplaceItem":"N","algoType":"","triggerQuantity":0,"idOfAisle":"1_19_3","idOfShelf":"1_19_3_1","idOfDepartment":"1_19","warnings":[{"foodIndicator":"","warnMsgTxt":"","warningSourceNm":""}],"requiresReturn":false,"channelEligibility":{"pickUp":true,"delivery":true,"inStore":true,"shipping":false},"channelInventory":{"delivery":"1","pickup":"1","instore":"1","shipping":"0"},"productReview":{"avgRating":"3.7","reviewCount":"29","isReviewWriteEligible":"true","isReviewDisplayEligible":"true","isForOnetimeReview":"true","reviewTemplateType":"default"},"itemSizeQty":"1","itemPackageQty":"1","itemRetailSect":"331","badges":[],"labels":[{"labelName":"SNAP"}]},{"status":"active","name":"waterfront BISTRO Boneless Skin On Wild Alaskan Pink Salmon Fillets - 16 Oz","pid":"960190258","upc":"0002113012545","id":"960190258","storeId":"806","featured":false,"isDYOCake":false,"inventoryAvailable":"1","pastPurchased":false,"restrictedValue":"0","salesRank":396,"agreementId":0,"featuredProductId":0,"imageUrl":"https://images.albertsons-media.com/is/image/ABS/960190258","price":6.99,"promoDescription":"Safeway Club Price: $6.99 SAVE up to: $1.0","promoText":" $6.99 SAVE up to: $1.0","promoType":"P","promoEndDate":"2026-04-09T23:59:00","basePrice":7.99,"basePricePer":7.99,"pricePer":6.99,"displayType":"-1","aisleId":"1_19_3_1","aisleName":"Fish & Shellfish|1_19_3","departmentName":"Meat & Seafood","shelfName":"Fish","shelfNameWithId":"Fish|1_19_3_1","aisleLocation":"Meat & Seafood","shelfXcoordinateNbr":"0","shelfYcoordinateNbr":"25.5","slotXcoordinateNbr":"7.53","slotYcoordinateNbr":"0","fixtureXcoordinateNbr":"3562.46","fixtureYcoordinateNbr":"5995.07","aislePositionTxt":"LEFT","shelfDimensionDpth":"22","snapEligible":true,"unitOfMeasure":"OZ","sellByWeight":"I","averageWeight":["0.000"],"unitQuantity":"POUND","displayUnitQuantityText":"ea","displayEstimateText":null,"previousPurchaseQty":0,"maxPurchaseQty":20,"minWeight":"0.000","maxWeight":"0.000","isHhcProduct":false,"prop65WarningTypeCD":"","prop65WarningText":"","prop65WarningIconRequired":false,"isArProduct":true,"isMtoProduct":false,"isCustomizable":false,"inStoreShoppingElig":false,"preparationTime":"0","isMarketplaceItem":"N","algoType":"","triggerQuantity":0,"idOfAisle":"1_19_3","idOfShelf":"1_19_3_1","idOfDepartment":"1_19","warnings":[{"foodIndicator":"","warnMsgTxt":"","warningSourceNm":""}],"requiresReturn":false,"channelEligibility":{"pickUp":true,"delivery":true,"inStore":true,"shipping":false},"channelInventory":{"delivery":"1","pickup":"1","instore":"1","shipping":"0"},"productReview":{"avgRating":"4.4","reviewCount":"447","isReviewWriteEligible":"true","isReviewDisplayEligible":"true","isForOnetimeReview":"true","reviewTemplateType":"default"},"itemSizeQty":"16.00","itemPackageQty":"1","itemRetailSect":"330","badges":[],"labels":[{"labelName":"SNAP"}]},{"status":"active","name":"Fresh Farmed Atlantic Salmon Fillet Color Added - 1.5 lb","pid":"186190041","upc":"0023011600000","id":"186190041","storeId":"806","featured":false,"isDYOCake":false,"inventoryAvailable":"1","pastPurchased":false,"restrictedValue":"0","salesRank":281,"agreementId":0,"featuredProductId":0,"imageUrl":"https://images.albertsons-media.com/is/image/ABS/186190041","price":13.49,"promoDescription":"Safeway Club Price: $8.99 lb SAVE up to: $3.0 lb","promoText":" $8.99 lb SAVE up to: $3.0 lb","promoType":"P","promoEndDate":"2026-03-26T23:59:00","basePrice":17.99,"basePricePer":11.99,"pricePer":8.99,"displayType":"2","aisleId":"1_19_3_1","aisleName":"Fish & Shellfish|1_19_3","departmentName":"Meat & Seafood","shelfName":"Fish","shelfNameWithId":"Fish|1_19_3_1","aisleLocation":"Seafood","snapEligible":true,"unitOfMeasure":"LB","sellByWeight":"P","averageWeight":["1.500"],"unitQuantity":"LB","displayUnitQuantityText":"ea","displayEstimateText":"approx","previousPurchaseQty":0,"maxPurchaseQty":20,"minWeight":"1.500","maxWeight":"1.500","isHhcProduct":false,"prop65WarningTypeCD":"","prop65WarningText":"","prop65WarningIconRequired":false,"isArProduct":true,"isMtoProduct":false,"isCustomizable":false,"inStoreShoppingElig":false,"preparationTime":"0","isMarketplaceItem":"N","algoType":"","triggerQuantity":0,"idOfAisle":"1_19_3","idOfShelf":"1_19_3_1","idOfDepartment":"1_19","warnings":[{"foodIndicator":"","warnMsgTxt":"","warningSourceNm":""}],"requiresReturn":false,"channelEligibility":{"pickUp":true,"delivery":true,"inStore":true,"shipping":false},"channelInventory":{"delivery":"1","pickup":"1","instore":"1","shipping":"0"},"productReview":{"avgRating":"4.3","reviewCount":"164","isReviewWriteEligible":"true","isReviewDisplayEligible":"true","isForOnetimeReview":"true","reviewTemplateType":"default"},"itemSizeQty":"1","itemPackageQty":"1","bestSellerRank":1,"itemRetailSect":"331","badges":[{"badgeName":"Bestseller","color":"PRIMARY","isStrikethrough":false,"isBoldText":false,"icon":""}],"labels":[{"labelName":"SNAP"}]},{"status":"active","name":"waterfront BISTRO Shrimp Cooked Deveined With Cocktail Sauce - 10 Oz","pid":"960310330","upc":"0002113012699","id":"960310330","storeId":"806","featured":false,"isDYOCake":false,"inventoryAvailable":"1","pastPurchased":false,"restrictedValue":"0","salesRank":4372,"agreementId":0,"featuredProductId":0,"imageUrl":"https://images.albertsons-media.com/is/image/ABS/960310330","price":5.00,"promoEndDate":null,"basePrice":5.00,"basePricePer":8.00,"pricePer":8.00,"displayType":"-1","aisleId":"1_19_3_4","aisleName":"Fish & Shellfish|1_19_3","departmentName":"Meat & Seafood","shelfName":"Shrimp & Prawns","shelfNameWithId":"Shrimp & Prawns|1_19_3_4","aisleLocation":"Meat & Seafood","shelfXcoordinateNbr":"96","shelfYcoordinateNbr":"3","slotXcoordinateNbr":"67.65","slotYcoordinateNbr":"0","fixtureXcoordinateNbr":"3961.06","fixtureYcoordinateNbr":"5415.76","aislePositionTxt":"PERIMETER","shelfDimensionDpth":"12","snapEligible":true,"unitOfMeasure":"OZ","sellByWeight":"I","averageWeight":["0.000"],"unitQuantity":"POUND","displayUnitQuantityText":"ea","displayEstimateText":null,"previousPurchaseQty":0,"maxPurchaseQty":20,"minWeight":"0.000","maxWeight":"0.000","isHhcProduct":false,"prop65WarningTypeCD":"","prop65WarningText":"","prop65WarningIconRequired":false,"isArProduct":true,"isMtoProduct":false,"isCustomizable":false,"inStoreShoppingElig":false,"preparationTime":"0","isMarketplaceItem":"N","algoType":"","triggerQuantity":0,"idOfAisle":"1_19_3","idOfShelf":"1_19_3_4","idOfDepartment":"1_19","warnings":[{"foodIndicator":"","warnMsgTxt":"","warningSourceNm":""}],"requiresReturn":false,"channelEligibility":{"pickUp":true,"delivery":true,"inStore":true,"shipping":false},"channelInventory":{"delivery":"1","pickup":"1","instore":"1","shipping":"0"},"productReview":{"avgRating":"4.4","reviewCount":"56","isReviewWriteEligible":"true","isReviewDisplayEligible":"true","isForOnetimeReview":"true","reviewTemplateType":"default"},"itemSizeQty":"10.00","itemPackageQty":"1","itemRetailSect":"330","badges":[],"labels":[{"labelName":"SNAP"}]},{"status":"active","name":"waterfront BISTRO Boneless Skinless Wild Alaskan Cod Fillets - 16 Oz","pid":"960227381","upc":"0002113012547","id":"960227381","storeId":"806","featured":false,"isDYOCake":false,"inventoryAvailable":"1","pastPurchased":false,"restrictedValue":"0","salesRank":640,"agreementId":0,"featuredProductId":0,"imageUrl":"https://images.albertsons-media.com/is/image/ABS/960227381","price":6.99,"promoDescription":"Safeway Club Price: $6.99 SAVE up to: $1.0","promoText":" $6.99 SAVE up to: $1.0","promoType":"P","promoEndDate":"2026-04-09T23:59:00","basePrice":7.99,"basePricePer":7.99,"pricePer":6.99,"displayType":"-1","aisleId":"1_19_3_1","aisleName":"Fish & Shellfish|1_19_3","departmentName":"Meat & Seafood","shelfName":"Fish","shelfNameWithId":"Fish|1_19_3_1","aisleLocation":"Meat & Seafood","shelfXcoordinateNbr":"0","shelfYcoordinateNbr":"25.5","slotXcoordinateNbr":"24.31","slotYcoordinateNbr":"0","fixtureXcoordinateNbr":"3404.98","fixtureYcoordinateNbr":"5995.07","aislePositionTxt":"LEFT","shelfDimensionDpth":"22","snapEligible":true,"unitOfMeasure":"OZ","sellByWeight":"I","averageWeight":["0.000"],"unitQuantity":"POUND","displayUnitQuantityText":"ea","displayEstimateText":null,"previousPurchaseQty":0,"maxPurchaseQty":20,"minWeight":"0.000","maxWeight":"0.000","isHhcProduct":false,"prop65WarningTypeCD":"","prop65WarningText":"","prop65WarningIconRequired":false,"isArProduct":true,"isMtoProduct":false,"isCustomizable":false,"inStoreShoppingElig":false,"preparationTime":"0","isMarketplaceItem":"N","algoType":"","triggerQuantity":0,"idOfAisle":"1_19_3","idOfShelf":"1_19_3_1","idOfDepartment":"1_19","warnings":[{"foodIndicator":"","warnMsgTxt":"","warningSourceNm":""}],"requiresReturn":false,"channelEligibility":{"pickUp":true,"delivery":true,"inStore":true,"shipping":false},"channelInventory":{"delivery":"1","pickup":"1","instore":"1","shipping":"0"},"productReview":{"avgRating":"4.4","reviewCount":"379","isReviewWriteEligible":"true","isReviewDisplayEligible":"true","isForOnetimeReview":"true","reviewTemplateType":"default"},"itemSizeQty":"16.00","itemPackageQty":"1","itemRetailSect":"330","badges":[],"labels":[{"labelName":"SNAP"}]},{"status":"active","name":"waterfront BISTRO Frozen Raw EZ Peel Shell Tail On Large Shrimp 26-30 Count - 2 Lb","pid":"960330099","upc":"0064728331152","id":"960330099","storeId":"806","featured":false,"isDYOCake":false,"inventoryAvailable":"1","pastPurchased":false,"restrictedValue":"0","salesRank":1682,"agreementId":0,"featuredProductId":0,"imageUrl":"https://images.albertsons-media.com/is/image/ABS/960330099","price":18.98,"promoEndDate":null,"basePrice":18.98,"basePricePer":9.49,"pricePer":9.49,"displayType":"2","aisleId":"1_19_3_4","aisleName":"Fish & Shellfish|1_19_3","departmentName":"Meat & Seafood","shelfName":"Shrimp & Prawns","shelfNameWithId":"Shrimp & Prawns|1_19_3_4","aisleLocation":"Meat & Seafood","snapEligible":true,"unitOfMeasure":"OZ","sellByWeight":"I","averageWeight":["2.000"],"unitQuantity":"POUND","displayUnitQuantityText":null,"displayEstimateText":null,"previousPurchaseQty":0,"maxPurchaseQty":20,"minWeight":"2.000","maxWeight":"2.000","isHhcProduct":false,"prop65WarningTypeCD":"","prop65WarningText":"","prop65WarningIconRequired":false,"isArProduct":true,"isMtoProduct":false,"isCustomizable":false,"inStoreShoppingElig":false,"preparationTime":"0","isMarketplaceItem":"N","algoType":"","triggerQuantity":0,"idOfAisle":"1_19_3","idOfShelf":"1_19_3_4","idOfDepartment":"1_19","warnings":[{"foodIndicator":"","warnMsgTxt":"","warningSourceNm":""}],"requiresReturn":false,"channelEligibility":{"pickUp":true,"delivery":true,"inStore":true,"shipping":false},"channelInventory":{"delivery":"1","pickup":"1","instore":"1","shipping":"0"},"productReview":{"avgRating":"4.8","reviewCount":"1900","isReviewWriteEligible":"true","isReviewDisplayEligible":"true","isForOnetimeReview":"true","reviewTemplateType":"default"},"itemSizeQty":"32","itemPackageQty":"1","itemRetailSect":"330","badges":[],"labels":[{"labelName":"SNAP"}]},{"status":"active","name":"Waterfront Bistro Scallops Peruvian 20 - 30 Ct - 16 OZ","pid":"970095877","upc":"0002113016602","id":"970095877","storeId":"806","featured":false,"isDYOCake":false,"inventoryAvailable":"1","pastPurchased":false,"restrictedValue":"0","salesRank":99999,"agreementId":0,"featuredProductId":0,"imageUrl":"https://images.albertsons-media.com/is/image/ABS/970095877","price":15.99,"promoEndDate":null,"basePrice":15.99,"basePricePer":15.99,"pricePer":15.99,"displayType":"-1","aisleId":"1_19_3_3","aisleName":"Fish & Shellfish|1_19_3","departmentName":"Meat & Seafood","shelfName":"Oyster, Clams & Scallops","shelfNameWithId":"Oyster, Clams & Scallops|1_19_3_3","aisleLocation":"Meat & Seafood","shelfXcoordinateNbr":"0","shelfYcoordinateNbr":"39.5","slotXcoordinateNbr":"57.15","slotYcoordinateNbr":"0","fixtureXcoordinateNbr":"3483.72","fixtureYcoordinateNbr":"5995.07","aislePositionTxt":"LEFT","shelfDimensionDpth":"22","snapEligible":true,"unitOfMeasure":"OZ","sellByWeight":"I","averageWeight":["0.000"],"unitQuantity":"POUND","displayUnitQuantityText":"ea","displayEstimateText":null,"previousPurchaseQty":0,"maxPurchaseQty":20,"minWeight":"0.000","maxWeight":"0.000","isHhcProduct":false,"prop65WarningTypeCD":"","prop65WarningText":"","prop65WarningIconRequired":false,"isArProduct":true,"isMtoProduct":false,"isCustomizable":false,"inStoreShoppingElig":false,"preparationTime":"0","isMarketplaceItem":"N","algoType":"","triggerQuantity":0,"idOfAisle":"1_19_3","idOfShelf":"1_19_3_3","idOfDepartment":"1_19","warnings":[{"foodIndicator":"","warnMsgTxt":"","warningSourceNm":""}],"requiresReturn":false,"channelEligibility":{"pickUp":true,"delivery":true,"inStore":true,"shipping":false},"channelInventory":{"delivery":"1","pickup":"1","instore":"1","shipping":"0"},"productReview":{"avgRating":"4.2","reviewCount":"54","isReviewWriteEligible":"true","isReviewDisplayEligible":"true","isForOnetimeReview":"true","reviewTemplateType":"default"},"itemSizeQty":"16","itemPackageQty":"1","itemRetailSect":"330","badges":[],"labels":[{"labelName":"SNAP"}]},{"status":"active","name":"Red Swimming Jumbo Crab Meat Lump - 16 Oz","pid":"970127065","upc":"0085000909732","id":"970127065","storeId":"806","featured":false,"isDYOCake":false,"inventoryAvailable":"1","pastPurchased":false,"restrictedValue":"0","salesRank":99999,"agreementId":0,"featuredProductId":0,"imageUrl":"https://images.albertsons-media.com/is/image/ABS/970127065","price":12.99,"promoDescription":"Safeway Club Price: $12.99 SAVE up to: $7.0","promoText":" $12.99 SAVE up to: $7.0","promoType":"P","promoEndDate":"2026-03-31T23:59:00","basePrice":19.99,"basePricePer":19.99,"pricePer":12.99,"displayType":"-1","aisleId":"1_19_3_2","aisleName":"Fish & Shellfish|1_19_3","departmentName":"Meat & Seafood","shelfName":"Lobster & Crab","shelfNameWithId":"Lobster & Crab|1_19_3_2","aisleLocation":"Meat & Seafood","snapEligible":true,"unitOfMeasure":"OZ","sellByWeight":"I","averageWeight":["0.000"],"unitQuantity":"POUND","displayUnitQuantityText":"ea","displayEstimateText":null,"previousPurchaseQty":0,"maxPurchaseQty":20,"minWeight":"0.000","maxWeight":"0.000","isHhcProduct":false,"prop65WarningTypeCD":"","prop65WarningText":"","prop65WarningIconRequired":false,"isArProduct":true,"isMtoProduct":false,"isCustomizable":false,"inStoreShoppingElig":false,"preparationTime":"0","isMarketplaceItem":"N","algoType":"","triggerQuantity":0,"idOfAisle":"1_19_3","idOfShelf":"1_19_3_2","idOfDepartment":"1_19","warnings":[{"foodIndicator":"","warnMsgTxt":"","warningSourceNm":""}],"requiresReturn":false,"channelEligibility":{"pickUp":true,"delivery":true,"inStore":true,"shipping":false},"channelInventory":{"delivery":"1","pickup":"1","instore":"1","shipping":"0"},"productReview":{"avgRating":"5.0","reviewCount":"1","isReviewWriteEligible":"true","isReviewDisplayEligible":"true","isForOnetimeReview":"true","reviewTemplateType":"default"},"itemSizeQty":"16","itemPackageQty":"1","itemRetailSect":"330","badges":[],"labels":[{"labelName":"SNAP"}]},{"status":"active","name":"Baltimore Crab Company Crab Meat Lump - 16 Oz","pid":"960120412","upc":"0085000909760","id":"960120412","storeId":"806","featured":false,"isDYOCake":false,"inventoryAvailable":"1","pastPurchased":false,"restrictedValue":"0","salesRank":8540,"agreementId":0,"featuredProductId":0,"imageUrl":"https://images.albertsons-media.com/is/image/ABS/960120412","price":12.99,"promoDescription":"Safeway Club Price: $12.99 SAVE up to: $7.0","promoText":" $12.99 SAVE up to: $7.0","promoType":"P","promoEndDate":"2026-03-31T23:59:00","basePrice":19.99,"basePricePer":19.99,"pricePer":12.99,"displayType":"-1","aisleId":"1_19_3_2","aisleName":"Fish & Shellfish|1_19_3","departmentName":"Meat & Seafood","shelfName":"Lobster & Crab","shelfNameWithId":"Lobster & Crab|1_19_3_2","aisleLocation":"Meat & Seafood","snapEligible":true,"unitOfMeasure":"OZ","sellByWeight":"I","averageWeight":["0.000"],"unitQuantity":"POUND","displayUnitQuantityText":"ea","displayEstimateText":null,"previousPurchaseQty":0,"maxPurchaseQty":20,"minWeight":"0.000","maxWeight":"0.000","isHhcProduct":false,"prop65WarningTypeCD":"","prop65WarningText":"","prop65WarningIconRequired":false,"isArProduct":true,"isMtoProduct":false,"isCustomizable":false,"inStoreShoppingElig":false,"preparationTime":"0","isMarketplaceItem":"N","algoType":"","triggerQuantity":0,"idOfAisle":"1_19_3","idOfShelf":"1_19_3_2","idOfDepartment":"1_19","warnings":[{"foodIndicator":"","warnMsgTxt":"","warningSourceNm":""}],"requiresReturn":false,"channelEligibility":{"pickUp":true,"delivery":true,"inStore":true,"shipping":false},"channelInventory":{"delivery":"1","pickup":"1","instore":"1","shipping":"0"},"productReview":{"avgRating":"4.1","reviewCount":"20","isReviewWriteEligible":"true","isReviewDisplayEligible":"true","isForOnetimeReview":"true","reviewTemplateType":"default"},"itemSizeQty":"16","itemPackageQty":"1","itemRetailSect":"330","badges":[],"labels":[{"labelName":"SNAP"}]},{"status":"active","name":"Key West Pinks Shrimp Extra Large P and D - 12 Oz","pid":"970586868","upc":"0001558500588","id":"970586868","storeId":"806","featured":false,"isDYOCake":false,"inventoryAvailable":"1","pastPurchased":false,"restrictedValue":"0","salesRank":99999,"agreementId":0,"featuredProductId":0,"imageUrl":"https://images.albertsons-media.com/is/image/ABS/970586868","price":13.69,"promoEndDate":null,"basePrice":13.69,"basePricePer":18.25,"pricePer":18.25,"displayType":"-1","aisleId":"1_19_3_4","aisleName":"Fish & Shellfish|1_19_3","departmentName":"Meat & Seafood","shelfName":"Shrimp & Prawns","shelfNameWithId":"Shrimp & Prawns|1_19_3_4","aisleLocation":"Meat & Seafood","shelfXcoordinateNbr":"0","shelfYcoordinateNbr":"15","slotXcoordinateNbr":"0","slotYcoordinateNbr":"0","fixtureXcoordinateNbr":"3717.22","fixtureYcoordinateNbr":"5415.76","aislePositionTxt":"PERIMETER","shelfDimensionDpth":"12","snapEligible":true,"unitOfMeasure":"OZ","sellByWeight":"I","averageWeight":["0.000"],"unitQuantity":"POUND","displayUnitQuantityText":"ea","displayEstimateText":null,"previousPurchaseQty":0,"maxPurchaseQty":20,"minWeight":"0.000","maxWeight":"0.000","isHhcProduct":false,"prop65WarningTypeCD":"","prop65WarningText":"","prop65WarningIconRequired":false,"isArProduct":true,"isMtoProduct":false,"isCustomizable":false,"inStoreShoppingElig":false,"preparationTime":"0","isMarketplaceItem":"N","algoType":"","triggerQuantity":0,"idOfAisle":"1_19_3","idOfShelf":"1_19_3_4","idOfDepartment":"1_19","warnings":[{"foodIndicator":"","warnMsgTxt":"","warningSourceNm":""}],"requiresReturn":false,"channelEligibility":{"pickUp":true,"delivery":true,"inStore":true,"shipping":false},"channelInventory":{"delivery":"1","pickup":"1","instore":"1","shipping":"0"},"productReview":{"avgRating":"0.0","reviewCount":"0","isReviewWriteEligible":"true","isReviewDisplayEligible":"true","isForOnetimeReview":"true","reviewTemplateType":"default"},"itemSizeQty":"12","itemPackageQty":"1","itemRetailSect":"330","badges":[],"labels":[{"labelName":"SNAP"}]},{"status":"active","name":"Service Case Fresh Flounder Fillet - 1 lb","pid":"960322929","upc":"0023005100000","id":"960322929","storeId":"806","featured":false,"isDYOCake":false,"inventoryAvailable":"1","pastPurchased":false,"restrictedValue":"0","salesRank":17801,"agreementId":0,"featuredProductId":0,"imageUrl":"https://images.albertsons-media.com/is/image/ABS/960322929","price":11.99,"promoEndDate":null,"basePrice":11.99,"basePricePer":11.99,"pricePer":11.99,"displayType":"2","aisleId":"1_19_3_1","aisleName":"Fish & Shellfish|1_19_3","departmentName":"Meat & Seafood","shelfName":"Fish","shelfNameWithId":"Fish|1_19_3_1","aisleLocation":"Seafood","snapEligible":true,"unitOfMeasure":"LB","sellByWeight":"P","averageWeight":["1.000"],"unitQuantity":"LB","displayUnitQuantityText":"ea","displayEstimateText":"approx","previousPurchaseQty":0,"maxPurchaseQty":20,"minWeight":"1.000","maxWeight":"1.000","isHhcProduct":false,"prop65WarningTypeCD":"","prop65WarningText":"","prop65WarningIconRequired":false,"isArProduct":true,"isMtoProduct":false,"isCustomizable":false,"inStoreShoppingElig":false,"preparationTime":"0","isMarketplaceItem":"N","algoType":"","triggerQuantity":0,"idOfAisle":"1_19_3","idOfShelf":"1_19_3_1","idOfDepartment":"1_19","warnings":[{"foodIndicator":"","warnMsgTxt":"","warningSourceNm":""}],"requiresReturn":false,"channelEligibility":{"pickUp":true,"delivery":true,"inStore":true,"shipping":false},"channelInventory":{"delivery":"1","pickup":"1","instore":"1","shipping":"0"},"productReview":{"avgRating":"4.5","reviewCount":"2","isReviewWriteEligible":"true","isReviewDisplayEligible":"true","isForOnetimeReview":"true","reviewTemplateType":"default"},"itemSizeQty":"1.00","itemPackageQty":"1","itemRetailSect":"331","badges":[],"labels":[{"labelName":"SNAP"}]},{"status":"active","name":"Surimi Snow Legs W/cocktail Sauce 15oz - 15 OZ","pid":"971310258","upc":"0073114960156","id":"971310258","storeId":"806","featured":false,"isDYOCake":false,"inventoryAvailable":"1","pastPurchased":false,"restrictedValue":"0","salesRank":99999,"agreementId":0,"featuredProductId":0,"imageUrl":"https://images.albertsons-media.com/is/image/ABS/971310258","price":10.00,"promoEndDate":null,"basePrice":10.00,"basePricePer":10.67,"pricePer":10.67,"displayType":"-1","aisleId":"1_19_3_2","aisleName":"Fish & Shellfish|1_19_3","departmentName":"Meat & Seafood","shelfName":"Lobster & Crab","shelfNameWithId":"Lobster & Crab|1_19_3_2","aisleLocation":"Deli","snapEligible":true,"unitOfMeasure":"OZ","sellByWeight":"I","averageWeight":["0.000"],"unitQuantity":"POUND","displayUnitQuantityText":"ea","displayEstimateText":null,"previousPurchaseQty":0,"maxPurchaseQty":20,"minWeight":"0.000","maxWeight":"0.000","isHhcProduct":false,"prop65WarningTypeCD":"","prop65WarningText":"","prop65WarningIconRequired":false,"isArProduct":false,"isMtoProduct":false,"isCustomizable":false,"inStoreShoppingElig":false,"preparationTime":"0","isMarketplaceItem":"N","algoType":"","triggerQuantity":0,"idOfAisle":"1_19_3","idOfShelf":"1_19_3_2","idOfDepartment":"1_19","warnings":[{"foodIndicator":"","warnMsgTxt":"","warningSourceNm":""}],"requiresReturn":false,"channelEligibility":{"pickUp":true,"delivery":true,"inStore":true,"shipping":false},"channelInventory":{"delivery":"1","pickup":"1","instore":"1","shipping":"0"},"productReview":{"avgRating":"0.0","reviewCount":"0","isReviewWriteEligible":"true","isReviewDisplayEligible":"true","isForOnetimeReview":"true","reviewTemplateType":"default"},"itemSizeQty":"15","itemPackageQty":"1","itemRetailSect":"330","badges":[],"labels":[{"labelName":"SNAP"}]},{"status":"active","name":"PanaPesca Seafood Mix R&S Skin Pack - 10.6 Oz","pid":"960323232","upc":"0061458362409","id":"960323232","storeId":"806","featured":false,"isDYOCake":false,"inventoryAvailable":"1","pastPurchased":false,"restrictedValue":"0","salesRank":33212,"agreementId":0,"featuredProductId":0,"imageUrl":"https://images.albertsons-media.com/is/image/ABS/960323232","price":4.49,"promoDescription":"Safeway Club Price: $4.49 SAVE up to: $1.0","promoText":" $4.49 SAVE up to: $1.0","promoType":"P","promoEndDate":"2026-04-09T23:59:00","basePrice":5.49,"basePricePer":8.29,"pricePer":6.78,"displayType":"-1","aisleId":"1_19_3_3","aisleName":"Fish & Shellfish|1_19_3","departmentName":"Meat & Seafood","shelfName":"Oyster, Clams & Scallops","shelfNameWithId":"Oyster, Clams & Scallops|1_19_3_3","aisleLocation":"Meat & Seafood","shelfXcoordinateNbr":"0","shelfYcoordinateNbr":"11.5","slotXcoordinateNbr":"66.7","slotYcoordinateNbr":"0","fixtureXcoordinateNbr":"3562.46","fixtureYcoordinateNbr":"5995.07","aislePositionTxt":"LEFT","shelfDimensionDpth":"22","snapEligible":true,"unitOfMeasure":"OZ","sellByWeight":"I","averageWeight":["0.000"],"unitQuantity":"POUND","displayUnitQuantityText":"ea","displayEstimateText":null,"previousPurchaseQty":0,"maxPurchaseQty":20,"minWeight":"0.000","maxWeight":"0.000","isHhcProduct":false,"prop65WarningTypeCD":"","prop65WarningText":"","prop65WarningIconRequired":false,"isArProduct":true,"isMtoProduct":false,"isCustomizable":false,"inStoreShoppingElig":false,"preparationTime":"0","isMarketplaceItem":"N","algoType":"","triggerQuantity":0,"idOfAisle":"1_19_3","idOfShelf":"1_19_3_3","idOfDepartment":"1_19","warnings":[{"foodIndicator":"","warnMsgTxt":"","warningSourceNm":""}],"requiresReturn":false,"channelEligibility":{"pickUp":true,"delivery":true,"inStore":true,"shipping":false},"channelInventory":{"delivery":"1","pickup":"1","instore":"1","shipping":"0"},"productReview":{"avgRating":"5.0","reviewCount":"2","isReviewWriteEligible":"true","isReviewDisplayEligible":"true","isForOnetimeReview":"true","reviewTemplateType":"default"},"itemSizeQty":"10.6","itemPackageQty":"1","itemRetailSect":"330","badges":[],"labels":[{"labelName":"SNAP"}]},{"status":"active","name":"waterfront BISTRO Raw EZ Peel Shell Tail On Jumbo Shrimp 21-25 Ct - 32 Oz","pid":"960000640","upc":"0064728331636","id":"960000640","storeId":"806","featured":false,"isDYOCake":false,"inventoryAvailable":"1","pastPurchased":false,"restrictedValue":"0","salesRank":798,"agreementId":0,"featuredProductId":0,"imageUrl":"https://images.albertsons-media.com/is/image/ABS/960000640","price":13.98,"promoDescription":"Safeway Club Price: $13.98 SAVE up to: $7.01","promoText":" $13.98 SAVE up to: $7.01","promoType":"P","promoEndDate":"2026-03-26T23:59:00","basePrice":20.99,"basePricePer":10.50,"pricePer":6.99,"displayType":"-1","aisleId":"1_19_3_4","aisleName":"Fish & Shellfish|1_19_3","departmentName":"Meat & Seafood","shelfName":"Shrimp & Prawns","shelfNameWithId":"Shrimp & Prawns|1_19_3_4","aisleLocation":"Meat & Seafood","snapEligible":true,"unitOfMeasure":"LB","sellByWeight":"I","averageWeight":["0.000"],"unitQuantity":"LB","displayUnitQuantityText":"ea","displayEstimateText":null,"previousPurchaseQty":0,"maxPurchaseQty":20,"minWeight":"0.000","maxWeight":"0.000","isHhcProduct":false,"prop65WarningTypeCD":"","prop65WarningText":"","prop65WarningIconRequired":false,"isArProduct":true,"isMtoProduct":false,"isCustomizable":false,"inStoreShoppingElig":false,"preparationTime":"0","isMarketplaceItem":"N","algoType":"","triggerQuantity":0,"idOfAisle":"1_19_3","idOfShelf":"1_19_3_4","idOfDepartment":"1_19","warnings":[{"foodIndicator":"","warnMsgTxt":"","warningSourceNm":""}],"requiresReturn":false,"channelEligibility":{"pickUp":true,"delivery":true,"inStore":true,"shipping":false},"channelInventory":{"delivery":"1","pickup":"1","instore":"1","shipping":"0"},"productReview":{"avgRating":"4.8","reviewCount":"1019","isReviewWriteEligible":"true","isReviewDisplayEligible":"true","isForOnetimeReview":"true","reviewTemplateType":"default"},"itemSizeQty":"2","itemPackageQty":"1","itemRetailSect":"330","badges":[],"labels":[{"labelName":"SNAP"}]},{"status":"active","name":"Service Case Snow Crab Leg Cluster - 2 Lb","pid":"970031179","upc":"0023636500000","id":"970031179","storeId":"806","featured":false,"isDYOCake":false,"inventoryAvailable":"1","pastPurchased":false,"restrictedValue":"0","salesRank":99999,"agreementId":0,"featuredProductId":0,"imageUrl":"https://images.albertsons-media.com/is/image/ABS/970031179","price":29.98,"promoEndDate":null,"basePrice":29.98,"basePricePer":14.99,"pricePer":14.99,"displayType":"2","aisleId":"1_19_3_2","aisleName":"Fish & Shellfish|1_19_3","departmentName":"Meat & Seafood","shelfName":"Lobster & Crab","shelfNameWithId":"Lobster & Crab|1_19_3_2","aisleLocation":"Seafood","snapEligible":true,"unitOfMeasure":"LB","sellByWeight":"P","averageWeight":["2.000"],"unitQuantity":"LB","displayUnitQuantityText":"ea","displayEstimateText":"approx","previousPurchaseQty":0,"maxPurchaseQty":20,"minWeight":"2.000","maxWeight":"2.000","isHhcProduct":false,"prop65WarningTypeCD":"","prop65WarningText":"","prop65WarningIconRequired":false,"isArProduct":true,"isMtoProduct":false,"isCustomizable":false,"inStoreShoppingElig":false,"preparationTime":"0","isMarketplaceItem":"N","algoType":"","triggerQuantity":0,"idOfAisle":"1_19_3","idOfShelf":"1_19_3_2","idOfDepartment":"1_19","warnings":[{"foodIndicator":"","warnMsgTxt":"","warningSourceNm":""}],"requiresReturn":false,"channelEligibility":{"pickUp":true,"delivery":true,"inStore":true,"shipping":false},"channelInventory":{"delivery":"1","pickup":"1","instore":"1","shipping":"0"},"productReview":{"avgRating":"4.4","reviewCount":"8","isReviewWriteEligible":"true","isReviewDisplayEligible":"true","isForOnetimeReview":"true","reviewTemplateType":"default"},"itemSizeQty":"1.00","itemPackageQty":"1","bestSellerRank":1,"itemRetailSect":"331","badges":[{"badgeName":"Bestseller","color":"PRIMARY","isStrikethrough":false,"isBoldText":false,"icon":""}],"labels":[{"labelName":"SNAP"}]},{"status":"active","name":"Dungeness Crab Cluster Cooked Frozen 1 Count - 0.50 Lb (subject to availability)","pid":"960290429","upc":"0023632300000","id":"960290429","storeId":"806","featured":false,"isDYOCake":false,"inventoryAvailable":"1","pastPurchased":false,"restrictedValue":"0","salesRank":18855,"agreementId":0,"featuredProductId":0,"imageUrl":"https://images.albertsons-media.com/is/image/ABS/960290429","price":11.50,"promoEndDate":null,"basePrice":11.50,"basePricePer":22.99,"pricePer":22.99,"displayType":"1","aisleId":"1_19_3_2","aisleName":"Fish & Shellfish|1_19_3","departmentName":"Meat & Seafood","shelfName":"Lobster & Crab","shelfNameWithId":"Lobster & Crab|1_19_3_2","aisleLocation":"Seafood","snapEligible":true,"unitOfMeasure":"LB","sellByWeight":"P","averageWeight":["0.500"],"unitQuantity":"LB","displayUnitQuantityText":"ea","displayEstimateText":"approx","previousPurchaseQty":0,"maxPurchaseQty":20,"minWeight":"0.500","maxWeight":"0.500","isHhcProduct":false,"prop65WarningTypeCD":"","prop65WarningText":"","prop65WarningIconRequired":false,"isArProduct":true,"isMtoProduct":false,"isCustomizable":false,"inStoreShoppingElig":false,"preparationTime":"0","isMarketplaceItem":"N","algoType":"","triggerQuantity":0,"idOfAisle":"1_19_3","idOfShelf":"1_19_3_2","idOfDepartment":"1_19","warnings":[{"foodIndicator":"","warnMsgTxt":"","warningSourceNm":""}],"requiresReturn":false,"channelEligibility":{"pickUp":true,"delivery":true,"inStore":true,"shipping":false},"channelInventory":{"delivery":"1","pickup":"1","instore":"1","shipping":"0"},"productReview":{"avgRating":"3.0","reviewCount":"2","isReviewWriteEligible":"true","isReviewDisplayEligible":"true","isForOnetimeReview":"true","reviewTemplateType":"default"},"itemSizeQty":"1.00","itemPackageQty":"1","itemRetailSect":"331","badges":[],"labels":[{"labelName":"SNAP"}]},{"status":"active","name":"Previously Frozen Cooked Jumbo Snow Crab 10 Up Clusters - 3 lb","pid":"970031218","upc":"0023774900000","id":"970031218","storeId":"806","featured":false,"isDYOCake":false,"inventoryAvailable":"1","pastPurchased":false,"restrictedValue":"0","salesRank":99999,"agreementId":0,"featuredProductId":0,"imageUrl":"https://images.albertsons-media.com/is/image/ABS/970031218","price":65.97,"promoEndDate":null,"basePrice":65.97,"basePricePer":21.99,"pricePer":21.99,"displayType":"2","aisleId":"1_19_3_2","aisleName":"Fish & Shellfish|1_19_3","departmentName":"Meat & Seafood","shelfName":"Lobster & Crab","shelfNameWithId":"Lobster & Crab|1_19_3_2","aisleLocation":"Seafood","snapEligible":true,"unitOfMeasure":"LB","sellByWeight":"P","averageWeight":["3.000"],"unitQuantity":"LB","displayUnitQuantityText":"ea","displayEstimateText":"approx","previousPurchaseQty":0,"maxPurchaseQty":20,"minWeight":"3.000","maxWeight":"3.000","isHhcProduct":false,"prop65WarningTypeCD":"","prop65WarningText":"","prop65WarningIconRequired":false,"isArProduct":true,"isMtoProduct":false,"isCustomizable":false,"inStoreShoppingElig":false,"preparationTime":"0","isMarketplaceItem":"N","algoType":"","triggerQuantity":0,"idOfAisle":"1_19_3","idOfShelf":"1_19_3_2","idOfDepartment":"1_19","warnings":[{"foodIndicator":"","warnMsgTxt":"","warningSourceNm":""}],"requiresReturn":false,"channelEligibility":{"pickUp":true,"delivery":true,"inStore":true,"shipping":false},"channelInventory":{"delivery":"1","pickup":"1","instore":"1","shipping":"0"},"productReview":{"avgRating":"4.8","reviewCount":"4","isReviewWriteEligible":"true","isReviewDisplayEligible":"true","isForOnetimeReview":"true","reviewTemplateType":"default"},"itemSizeQty":"1.00","itemPackageQty":"1","itemRetailSect":"331","badges":[],"labels":[{"labelName":"SNAP"}]},{"status":"active","name":"Shrimp Raw 61-70 Ct Peeled & Deveined Tail-off - 2 LB","pid":"970033939","upc":"0087605900338","id":"970033939","storeId":"806","featured":false,"isDYOCake":false,"inventoryAvailable":"1","pastPurchased":false,"restrictedValue":"0","salesRank":99999,"agreementId":0,"featuredProductId":0,"imageUrl":"https://images.albertsons-media.com/is/image/ABS/970033939","price":16.98,"promoEndDate":null,"basePrice":16.98,"basePricePer":8.49,"pricePer":8.49,"displayType":"-1","aisleId":"1_19_3_4","aisleName":"Fish & Shellfish|1_19_3","departmentName":"Meat & Seafood","shelfName":"Shrimp & Prawns","shelfNameWithId":"Shrimp & Prawns|1_19_3_4","aisleLocation":"Meat & Seafood","shelfXcoordinateNbr":"0","shelfYcoordinateNbr":"3","slotXcoordinateNbr":"70.2","slotYcoordinateNbr":"0","fixtureXcoordinateNbr":"3656.26","fixtureYcoordinateNbr":"5415.76","aislePositionTxt":"PERIMETER","shelfDimensionDpth":"12","snapEligible":true,"unitOfMeasure":"LB","sellByWeight":"I","averageWeight":["0.000"],"unitQuantity":"LB","displayUnitQuantityText":"ea","displayEstimateText":null,"previousPurchaseQty":0,"maxPurchaseQty":20,"minWeight":"0.000","maxWeight":"0.000","isHhcProduct":false,"prop65WarningTypeCD":"","prop65WarningText":"","prop65WarningIconRequired":false,"isArProduct":true,"isMtoProduct":false,"isCustomizable":false,"inStoreShoppingElig":false,"preparationTime":"0","isMarketplaceItem":"N","algoType":"","triggerQuantity":0,"idOfAisle":"1_19_3","idOfShelf":"1_19_3_4","idOfDepartment":"1_19","warnings":[{"foodIndicator":"","warnMsgTxt":"","warningSourceNm":""}],"requiresReturn":false,"channelEligibility":{"pickUp":true,"delivery":true,"inStore":true,"shipping":false},"channelInventory":{"delivery":"1","pickup":"1","instore":"1","shipping":"0"},"productReview":{"avgRating":"5.0","reviewCount":"1","isReviewWriteEligible":"true","isReviewDisplayEligible":"true","isForOnetimeReview":"true","reviewTemplateType":"default"},"itemSizeQty":"2","itemPackageQty":"1","itemRetailSect":"330","badges":[],"labels":[{"labelName":"SNAP"}]},{"status":"active","name":"Icy Ocean Wild Caught Raw Frozen Yellowfin Tuna Medallions - 1 LB","pid":"971078570","upc":"0081001117724","id":"971078570","storeId":"806","featured":false,"isDYOCake":false,"inventoryAvailable":"1","pastPurchased":false,"restrictedValue":"0","salesRank":99999,"agreementId":0,"featuredProductId":0,"imageUrl":"https://images.albertsons-media.com/is/image/ABS/971078570","price":6.99,"promoEndDate":null,"basePrice":6.99,"basePricePer":6.99,"pricePer":6.99,"displayType":"-1","aisleId":"1_19_3_1","aisleName":"Fish & Shellfish|1_19_3","departmentName":"Meat & Seafood","shelfName":"Fish","shelfNameWithId":"Fish|1_19_3_1","aisleLocation":"Meat & Seafood","shelfXcoordinateNbr":"0","shelfYcoordinateNbr":"1.5","slotXcoordinateNbr":"63.04","slotYcoordinateNbr":"0","fixtureXcoordinateNbr":"3562.46","fixtureYcoordinateNbr":"5995.07","aislePositionTxt":"LEFT","shelfDimensionDpth":"26","snapEligible":false,"unitOfMeasure":"OZ","sellByWeight":"I","averageWeight":["0.000"],"unitQuantity":"POUND","displayUnitQuantityText":"ea","displayEstimateText":null,"previousPurchaseQty":0,"maxPurchaseQty":20,"minWeight":"0.000","maxWeight":"0.000","isHhcProduct":false,"prop65WarningTypeCD":"","prop65WarningText":"","prop65WarningIconRequired":false,"isArProduct":true,"isMtoProduct":false,"isCustomizable":false,"inStoreShoppingElig":false,"preparationTime":"0","isMarketplaceItem":"N","algoType":"","triggerQuantity":0,"idOfAisle":"1_19_3","idOfShelf":"1_19_3_1","idOfDepartment":"1_19","warnings":[{"foodIndicator":"","warnMsgTxt":"","warningSourceNm":""}],"requiresReturn":false,"channelEligibility":{"pickUp":true,"delivery":true,"inStore":true,"shipping":false},"channelInventory":{"delivery":"1","pickup":"1","instore":"1","shipping":"0"},"productReview":{"avgRating":"0.0","reviewCount":"0","isReviewWriteEligible":"true","isReviewDisplayEligible":"true","isForOnetimeReview":"true","reviewTemplateType":"default"},"itemSizeQty":"16","itemPackageQty":"1","itemRetailSect":"330","badges":[],"labels":[]},{"status":"active","name":"Service Case Fresh Pacific Cod Fillet - 1 lb","pid":"960221441","upc":"0023116700000","id":"960221441","storeId":"806","featured":false,"isDYOCake":false,"inventoryAvailable":"1","pastPurchased":false,"restrictedValue":"0","salesRank":21219,"agreementId":0,"featuredProductId":0,"imageUrl":"https://images.albertsons-media.com/is/image/ABS/960221441","price":11.99,"promoEndDate":null,"basePrice":11.99,"basePricePer":11.99,"pricePer":11.99,"displayType":"2","aisleId":"1_19_3_1","aisleName":"Fish & Shellfish|1_19_3","departmentName":"Meat & Seafood","shelfName":"Fish","shelfNameWithId":"Fish|1_19_3_1","aisleLocation":"Seafood","snapEligible":true,"unitOfMeasure":"LB","sellByWeight":"P","averageWeight":["1.000"],"unitQuantity":"LB","displayUnitQuantityText":"ea","displayEstimateText":"approx","previousPurchaseQty":0,"maxPurchaseQty":20,"minWeight":"1.000","maxWeight":"1.000","isHhcProduct":false,"prop65WarningTypeCD":"","prop65WarningText":"","prop65WarningIconRequired":false,"isArProduct":true,"isMtoProduct":false,"isCustomizable":false,"inStoreShoppingElig":false,"preparationTime":"0","isMarketplaceItem":"N","algoType":"","triggerQuantity":0,"idOfAisle":"1_19_3","idOfShelf":"1_19_3_1","idOfDepartment":"1_19","warnings":[{"foodIndicator":"","warnMsgTxt":"","warningSourceNm":""}],"requiresReturn":false,"channelEligibility":{"pickUp":true,"delivery":true,"inStore":true,"shipping":false},"channelInventory":{"delivery":"1","pickup":"1","instore":"1","shipping":"0"},"productReview":{"avgRating":"5.0","reviewCount":"2","isReviewWriteEligible":"true","isReviewDisplayEligible":"true","isForOnetimeReview":"true","reviewTemplateType":"default"},"itemSizeQty":"1.00","itemPackageQty":"1","itemRetailSect":"331","badges":[],"labels":[{"labelName":"SNAP"}]},{"status":"active","name":"Ocean Crown Red Swimming Crab Meat - LB","pid":"970124177","upc":"0693608630033","id":"970124177","storeId":"806","featured":false,"isDYOCake":false,"inventoryAvailable":"0","pastPurchased":false,"restrictedValue":"0","salesRank":99999,"agreementId":0,"featuredProductId":0,"imageUrl":"https://images.albertsons-media.com/is/image/ABS/970124177","price":12.99,"promoDescription":"Safeway Club Price: $12.99 SAVE up to: $7.0","promoText":" $12.99 SAVE up to: $7.0","promoType":"P","promoEndDate":"2026-03-31T23:59:00","basePrice":19.99,"basePricePer":19.99,"pricePer":12.99,"displayType":"-1","aisleId":"1_19_3_2","aisleName":"Fish & Shellfish|1_19_3","departmentName":"Meat & Seafood","shelfName":"Lobster & Crab","shelfNameWithId":"Lobster & Crab|1_19_3_2","aisleLocation":"Meat & Seafood","snapEligible":true,"unitOfMeasure":"OZ","sellByWeight":"I","averageWeight":["0.000"],"unitQuantity":"POUND","displayUnitQuantityText":"ea","displayEstimateText":null,"previousPurchaseQty":0,"maxPurchaseQty":20,"minWeight":"0.000","maxWeight":"0.000","isHhcProduct":false,"prop65WarningTypeCD":"","prop65WarningText":"","prop65WarningIconRequired":false,"isArProduct":true,"isMtoProduct":false,"isCustomizable":false,"inStoreShoppingElig":false,"preparationTime":"0","isMarketplaceItem":"N","algoType":"","triggerQuantity":0,"idOfAisle":"1_19_3","idOfShelf":"1_19_3_2","idOfDepartment":"1_19","warnings":[{"foodIndicator":"","warnMsgTxt":"","warningSourceNm":""}],"requiresReturn":false,"channelEligibility":{"pickUp":true,"delivery":true,"inStore":true,"shipping":false},"channelInventory":{"delivery":"0","pickup":"0","instore":"0","shipping":"0"},"productReview":{"avgRating":"3.0","reviewCount":"1","isReviewWriteEligible":"true","isReviewDisplayEligible":"true","isForOnetimeReview":"true","reviewTemplateType":"default"},"itemSizeQty":"16.00","itemPackageQty":"1","itemRetailSect":"330","badges":[],"labels":[{"labelName":"SNAP"}]},{"status":"active","name":"Blue Sea Salmon Fillets - 2 LB","pid":"970036953","upc":"0087605900726","id":"970036953","storeId":"806","featured":false,"isDYOCake":false,"inventoryAvailable":"1","pastPurchased":false,"restrictedValue":"0","salesRank":99999,"agreementId":0,"featuredProductId":0,"imageUrl":"https://images.albertsons-media.com/is/image/ABS/970036953","price":11.99,"promoDescription":"Safeway Club Price: $11.99 25.0% Off - SAVE up to $4.0","promoText":" $11.99 25.0% Off - SAVE up to $4.0","promoType":"P","promoEndDate":"2026-04-16T23:59:00","basePrice":15.99,"basePricePer":8.00,"pricePer":6.00,"displayType":"-1","aisleId":"1_19_3_1","aisleName":"Fish & Shellfish|1_19_3","departmentName":"Meat & Seafood","shelfName":"Fish","shelfNameWithId":"Fish|1_19_3_1","aisleLocation":"Meat & Seafood","shelfXcoordinateNbr":"0","shelfYcoordinateNbr":"62.5","slotXcoordinateNbr":"21.36","slotYcoordinateNbr":"0","fixtureXcoordinateNbr":"3404.98","fixtureYcoordinateNbr":"5995.07","aislePositionTxt":"LEFT","shelfDimensionDpth":"24","snapEligible":true,"unitOfMeasure":"LB","sellByWeight":"I","averageWeight":["0.000"],"unitQuantity":"LB","displayUnitQuantityText":"ea","displayEstimateText":null,"previousPurchaseQty":0,"maxPurchaseQty":20,"minWeight":"0.000","maxWeight":"0.000","isHhcProduct":false,"prop65WarningTypeCD":"","prop65WarningText":"","prop65WarningIconRequired":false,"isArProduct":true,"isMtoProduct":false,"isCustomizable":false,"inStoreShoppingElig":false,"preparationTime":"0","isMarketplaceItem":"N","algoType":"","triggerQuantity":0,"idOfAisle":"1_19_3","idOfShelf":"1_19_3_1","idOfDepartment":"1_19","warnings":[{"foodIndicator":"","warnMsgTxt":"","warningSourceNm":""}],"requiresReturn":false,"channelEligibility":{"pickUp":true,"delivery":true,"inStore":true,"shipping":false},"channelInventory":{"delivery":"1","pickup":"1","instore":"1","shipping":"0"},"productReview":{"avgRating":"2.0","reviewCount":"4","isReviewWriteEligible":"true","isReviewDisplayEligible":"true","isForOnetimeReview":"true","reviewTemplateType":"default"},"itemSizeQty":"2","itemPackageQty":"1","itemRetailSect":"330","badges":[],"labels":[{"labelName":"SNAP"}]},{"status":"active","name":"Fresh Baked Jumbo Crab Cake - Net Weight 3.5 oz ea","pid":"970045083","upc":"0023138800000","id":"970045083","storeId":"806","featured":false,"isDYOCake":false,"inventoryAvailable":"1","pastPurchased":false,"restrictedValue":"0","salesRank":99999,"agreementId":0,"featuredProductId":0,"imageUrl":"https://images.albertsons-media.com/is/image/ABS/970045083","price":7.99,"promoDescription":"Safeway Club Price: $7.99 ea SAVE up to: $0.5 ea","promoText":" $7.99 ea SAVE up to: $0.5 ea","promoType":"P","promoEndDate":"2026-03-26T23:59:00","basePrice":8.49,"basePricePer":8.49,"pricePer":7.99,"displayType":"-1","aisleId":"1_19_3_2","aisleName":"Fish & Shellfish|1_19_3","departmentName":"Meat & Seafood","shelfName":"Lobster & Crab","shelfNameWithId":"Lobster & Crab|1_19_3_2","aisleLocation":"Seafood","snapEligible":true,"unitOfMeasure":"EA","sellByWeight":"I","averageWeight":["0.000"],"unitQuantity":"EACH","displayUnitQuantityText":"ea","displayEstimateText":null,"previousPurchaseQty":0,"maxPurchaseQty":20,"minWeight":"0.000","maxWeight":"0.000","isHhcProduct":false,"prop65WarningTypeCD":"","prop65WarningText":"","prop65WarningIconRequired":false,"isArProduct":true,"isMtoProduct":false,"isCustomizable":false,"inStoreShoppingElig":false,"preparationTime":"0","isMarketplaceItem":"N","algoType":"","triggerQuantity":0,"idOfAisle":"1_19_3","idOfShelf":"1_19_3_2","idOfDepartment":"1_19","warnings":[{"foodIndicator":"","warnMsgTxt":"","warningSourceNm":""}],"requiresReturn":false,"channelEligibility":{"pickUp":true,"delivery":true,"inStore":true,"shipping":false},"channelInventory":{"delivery":"1","pickup":"1","instore":"1","shipping":"0"},"productReview":{"avgRating":"0.0","reviewCount":"0","isReviewWriteEligible":"true","isReviewDisplayEligible":"true","isForOnetimeReview":"true","reviewTemplateType":"default"},"itemSizeQty":"1.00","itemPackageQty":"1","itemRetailSect":"331","badges":[],"labels":[{"labelName":"SNAP"}]},{"status":"active","name":"Tilapia Fillet Fresh - 1 Lb","pid":"186350058","upc":"0023016900000","id":"186350058","storeId":"806","featured":false,"isDYOCake":false,"inventoryAvailable":"1","pastPurchased":false,"restrictedValue":"0","salesRank":7007,"agreementId":0,"featuredProductId":0,"imageUrl":"https://images.albertsons-media.com/is/image/ABS/186350058","price":7.99,"promoDescription":"Safeway Club Price: $7.99 lb SAVE up to: $2.0 lb","promoText":" $7.99 lb SAVE up to: $2.0 lb","promoType":"P","promoEndDate":"2026-03-26T23:59:00","basePrice":9.99,"basePricePer":9.99,"pricePer":7.99,"displayType":"2","aisleId":"1_19_3_1","aisleName":"Fish & Shellfish|1_19_3","departmentName":"Meat & Seafood","shelfName":"Fish","shelfNameWithId":"Fish|1_19_3_1","aisleLocation":"Seafood","snapEligible":true,"unitOfMeasure":"LB","sellByWeight":"P","averageWeight":["1.000"],"unitQuantity":"LB","displayUnitQuantityText":"ea","displayEstimateText":"approx","previousPurchaseQty":0,"maxPurchaseQty":20,"minWeight":"1.000","maxWeight":"1.000","isHhcProduct":false,"prop65WarningTypeCD":"","prop65WarningText":"","prop65WarningIconRequired":false,"isArProduct":true,"isMtoProduct":false,"isCustomizable":false,"inStoreShoppingElig":false,"preparationTime":"0","isMarketplaceItem":"N","algoType":"","triggerQuantity":0,"idOfAisle":"1_19_3","idOfShelf":"1_19_3_1","idOfDepartment":"1_19","warnings":[{"foodIndicator":"","warnMsgTxt":"","warningSourceNm":""}],"requiresReturn":false,"channelEligibility":{"pickUp":true,"delivery":true,"inStore":true,"shipping":false},"channelInventory":{"delivery":"1","pickup":"1","instore":"1","shipping":"0"},"productReview":{"avgRating":"3.8","reviewCount":"11","isReviewWriteEligible":"true","isReviewDisplayEligible":"true","isForOnetimeReview":"true","reviewTemplateType":"default"},"itemSizeQty":"1.00","itemPackageQty":"1","itemRetailSect":"331","badges":[],"labels":[{"labelName":"SNAP"}]},{"status":"active","name":"Phillips Lump Crab Meat - 8 OZ","pid":"970000836","upc":"0007005725117","id":"970000836","storeId":"806","featured":false,"isDYOCake":false,"inventoryAvailable":"1","pastPurchased":false,"restrictedValue":"0","salesRank":99999,"agreementId":0,"featuredProductId":0,"imageUrl":"https://images.albertsons-media.com/is/image/ABS/970000836","price":17.99,"promoEndDate":null,"basePrice":17.99,"basePricePer":35.98,"pricePer":35.98,"displayType":"-1","aisleId":"1_19_3_2","aisleName":"Fish & Shellfish|1_19_3","departmentName":"Meat & Seafood","shelfName":"Lobster & Crab","shelfNameWithId":"Lobster & Crab|1_19_3_2","aisleLocation":"Meat & Seafood","snapEligible":true,"unitOfMeasure":"OZ","sellByWeight":"I","averageWeight":["0.000"],"unitQuantity":"POUND","displayUnitQuantityText":"ea","displayEstimateText":null,"previousPurchaseQty":0,"maxPurchaseQty":20,"minWeight":"0.000","maxWeight":"0.000","isHhcProduct":false,"prop65WarningTypeCD":"","prop65WarningText":"","prop65WarningIconRequired":false,"isArProduct":true,"isMtoProduct":false,"isCustomizable":false,"inStoreShoppingElig":false,"preparationTime":"0","isMarketplaceItem":"N","algoType":"","triggerQuantity":0,"idOfAisle":"1_19_3","idOfShelf":"1_19_3_2","idOfDepartment":"1_19","warnings":[{"foodIndicator":"","warnMsgTxt":"","warningSourceNm":""}],"requiresReturn":false,"channelEligibility":{"pickUp":true,"delivery":true,"inStore":true,"shipping":false},"channelInventory":{"delivery":"1","pickup":"1","instore":"1","shipping":"0"},"productReview":{"avgRating":"3.7","reviewCount":"11","isReviewWriteEligible":"true","isReviewDisplayEligible":"true","isForOnetimeReview":"true","reviewTemplateType":"default"},"itemSizeQty":"8.00","itemPackageQty":"1","itemRetailSect":"330","badges":[],"labels":[{"labelName":"SNAP"}]},{"status":"active","name":"Catfish Nuggets Previously Frozen - 1 Lb","pid":"960026823","upc":"0023001300000","id":"960026823","storeId":"806","featured":false,"isDYOCake":false,"inventoryAvailable":"1","pastPurchased":false,"restrictedValue":"0","salesRank":20328,"agreementId":0,"featuredProductId":0,"imageUrl":"https://images.albertsons-media.com/is/image/ABS/960026823","price":3.49,"promoEndDate":null,"basePrice":3.49,"basePricePer":3.49,"pricePer":3.49,"displayType":"2","aisleId":"1_19_3_1","aisleName":"Fish & Shellfish|1_19_3","departmentName":"Meat & Seafood","shelfName":"Fish","shelfNameWithId":"Fish|1_19_3_1","aisleLocation":"Seafood","snapEligible":true,"unitOfMeasure":"LB","sellByWeight":"P","averageWeight":["1.000"],"unitQuantity":"LB","displayUnitQuantityText":"ea","displayEstimateText":"approx","previousPurchaseQty":0,"maxPurchaseQty":20,"minWeight":"1.000","maxWeight":"1.000","isHhcProduct":false,"prop65WarningTypeCD":"","prop65WarningText":"","prop65WarningIconRequired":false,"isArProduct":true,"isMtoProduct":false,"isCustomizable":false,"inStoreShoppingElig":false,"preparationTime":"0","isMarketplaceItem":"N","algoType":"","triggerQuantity":0,"idOfAisle":"1_19_3","idOfShelf":"1_19_3_1","idOfDepartment":"1_19","warnings":[{"foodIndicator":"","warnMsgTxt":"","warningSourceNm":""}],"requiresReturn":false,"channelEligibility":{"pickUp":true,"delivery":true,"inStore":true,"shipping":false},"channelInventory":{"delivery":"1","pickup":"1","instore":"1","shipping":"0"},"productReview":{"avgRating":"5.0","reviewCount":"1","isReviewWriteEligible":"true","isReviewDisplayEligible":"true","isForOnetimeReview":"true","reviewTemplateType":"default"},"itemSizeQty":"1.00","itemPackageQty":"1","itemRetailSect":"331","badges":[],"labels":[{"labelName":"SNAP"}]},{"status":"active","name":"Shrimp Steamed 21-25 Count - 1 Lb","pid":"970047745","upc":"0023827300000","id":"970047745","storeId":"806","featured":false,"isDYOCake":false,"inventoryAvailable":"1","pastPurchased":false,"restrictedValue":"0","salesRank":99999,"agreementId":0,"featuredProductId":0,"imageUrl":"https://images.albertsons-media.com/is/image/ABS/970047745","price":8.99,"promoDescription":"Safeway Club Price: $8.99 lb SAVE up to: $5.0 lb","promoText":" $8.99 lb SAVE up to: $5.0 lb","promoType":"P","promoEndDate":"2026-03-26T23:59:00","basePrice":13.99,"basePricePer":13.99,"pricePer":8.99,"displayType":"2","aisleId":"1_19_3_4","aisleName":"Fish & Shellfish|1_19_3","departmentName":"Meat & Seafood","shelfName":"Shrimp & Prawns","shelfNameWithId":"Shrimp & Prawns|1_19_3_4","aisleLocation":"Meat & Seafood","snapEligible":true,"unitOfMeasure":"LB","sellByWeight":"P","averageWeight":["1.000"],"unitQuantity":"LB","displayUnitQuantityText":"ea","displayEstimateText":"approx","previousPurchaseQty":0,"maxPurchaseQty":20,"minWeight":"1.000","maxWeight":"1.000","isHhcProduct":false,"prop65WarningTypeCD":"","prop65WarningText":"","prop65WarningIconRequired":false,"isArProduct":true,"isMtoProduct":false,"isCustomizable":false,"inStoreShoppingElig":false,"preparationTime":"0","isMarketplaceItem":"N","algoType":"","triggerQuantity":0,"idOfAisle":"1_19_3","idOfShelf":"1_19_3_4","idOfDepartment":"1_19","warnings":[{"foodIndicator":"","warnMsgTxt":"","warningSourceNm":""}],"requiresReturn":false,"channelEligibility":{"pickUp":true,"delivery":true,"inStore":true,"shipping":false},"channelInventory":{"delivery":"1","pickup":"1","instore":"1","shipping":"0"},"productReview":{"avgRating":"3.3","reviewCount":"3","isReviewWriteEligible":"true","isReviewDisplayEligible":"true","isForOnetimeReview":"true","reviewTemplateType":"default"},"itemSizeQty":"1.00","itemPackageQty":"1","itemRetailSect":"331","badges":[],"labels":[{"labelName":"SNAP"}]},{"status":"active","name":"Signature SELECT Pacific Cod Fillet Raw Previously Frozen Multi Meal Deal - 1 Lb","pid":"970027909","upc":"0023140200000","id":"970027909","storeId":"806","featured":false,"isDYOCake":false,"inventoryAvailable":"1","pastPurchased":false,"restrictedValue":"0","salesRank":99999,"agreementId":0,"featuredProductId":0,"imageUrl":"https://images.albertsons-media.com/is/image/ABS/970027909","price":6.99,"promoEndDate":null,"basePrice":6.99,"basePricePer":6.99,"pricePer":6.99,"displayType":"2","aisleId":"1_19_3_1","aisleName":"Fish & Shellfish|1_19_3","departmentName":"Meat & Seafood","shelfName":"Fish","shelfNameWithId":"Fish|1_19_3_1","aisleLocation":"Meat & Seafood","snapEligible":true,"unitOfMeasure":"LB","sellByWeight":"P","averageWeight":["1.000"],"unitQuantity":"LB","displayUnitQuantityText":"ea","displayEstimateText":"approx","previousPurchaseQty":0,"maxPurchaseQty":20,"minWeight":"1.000","maxWeight":"1.000","isHhcProduct":false,"prop65WarningTypeCD":"","prop65WarningText":"","prop65WarningIconRequired":false,"isArProduct":true,"isMtoProduct":false,"isCustomizable":false,"inStoreShoppingElig":false,"preparationTime":"0","isMarketplaceItem":"N","algoType":"","triggerQuantity":0,"idOfAisle":"1_19_3","idOfShelf":"1_19_3_1","idOfDepartment":"1_19","warnings":[{"foodIndicator":"","warnMsgTxt":"","warningSourceNm":""}],"requiresReturn":false,"channelEligibility":{"pickUp":false,"delivery":false,"inStore":true,"shipping":false},"channelInventory":{"delivery":"1","pickup":"1","instore":"1","shipping":"0"},"productReview":{"avgRating":"0.0","reviewCount":"0","isReviewWriteEligible":"true","isReviewDisplayEligible":"true","isForOnetimeReview":"true","reviewTemplateType":"default"},"itemSizeQty":"1.00","itemPackageQty":"1","itemRetailSect":"331","badges":[],"labels":[{"labelName":"SNAP"}]},{"status":"active","name":"Fresh Littleneck Clams 50 ct - ea","pid":"960024465","upc":"0023019400000","id":"960024465","storeId":"806","featured":false,"isDYOCake":false,"inventoryAvailable":"1","pastPurchased":false,"restrictedValue":"0","salesRank":28965,"agreementId":0,"featuredProductId":0,"imageUrl":"https://images.albertsons-media.com/is/image/ABS/960024465","price":22.49,"promoEndDate":null,"basePrice":22.49,"basePricePer":11.25,"pricePer":11.25,"displayType":"-1","aisleId":"1_19_3_3","aisleName":"Fish & Shellfish|1_19_3","departmentName":"Meat & Seafood","shelfName":"Oyster, Clams & Scallops","shelfNameWithId":"Oyster, Clams & Scallops|1_19_3_3","aisleLocation":"Seafood","snapEligible":true,"unitOfMeasure":"EA","sellByWeight":"I","averageWeight":["0.000"],"unitQuantity":"POUND","displayUnitQuantityText":"ea","displayEstimateText":null,"previousPurchaseQty":0,"maxPurchaseQty":20,"minWeight":"0.000","maxWeight":"0.000","isHhcProduct":false,"prop65WarningTypeCD":"","prop65WarningText":"","prop65WarningIconRequired":false,"isArProduct":true,"isMtoProduct":false,"isCustomizable":false,"inStoreShoppingElig":false,"preparationTime":"0","isMarketplaceItem":"N","algoType":"","triggerQuantity":0,"idOfAisle":"1_19_3","idOfShelf":"1_19_3_3","idOfDepartment":"1_19","warnings":[{"foodIndicator":"","warnMsgTxt":"","warningSourceNm":""}],"requiresReturn":false,"channelEligibility":{"pickUp":true,"delivery":true,"inStore":true,"shipping":false},"channelInventory":{"delivery":"1","pickup":"1","instore":"1","shipping":"0"},"productReview":{"avgRating":"1.0","reviewCount":"1","isReviewWriteEligible":"true","isReviewDisplayEligible":"true","isForOnetimeReview":"true","reviewTemplateType":"default"},"itemSizeQty":"1","itemPackageQty":"1","itemRetailSect":"331","badges":[],"labels":[{"labelName":"SNAP"}]}]},"offersData":{"departments":{"330":{"offers":{"62753237":{"deleted":false,"deliveryChannel":"EC","ecomDescription":"Schedule & Save 5%","endDate":"1893524400000","extlOfferId":"62753237-ND","isClippable":false,"isDisplayable":true,"maxPurchaseQty":9999,"minPurchaseQty":1,"offerId":"62753237","offerPgm":"SC","offerProgramType":"ITEM_DISCOUNT","offerProtoType":"ITEM_DISCOUNT","offerSubPgm":"02","offerTs":"1770224308678","price":0.0,"purchaseInd":"L","startDate":"1746640800000"},"88132609":{"deleted":false,"deliveryChannel":"EC","ecomDescription":"Schedule & Save 5%","endDate":"1893524400000","extlOfferId":"88132609-ND","isClippable":false,"isDisplayable":true,"maxPurchaseQty":9999,"minPurchaseQty":1,"offerId":"88132609","offerPgm":"SC","offerProgramType":"ITEM_DISCOUNT","offerProtoType":"ITEM_DISCOUNT","offerSubPgm":"02","offerTs":"1770224287189","price":0.0,"purchaseInd":"L","startDate":"1746640800000"}}},"331":{"offers":{"62753237":{"deleted":false,"deliveryChannel":"EC","ecomDescription":"Schedule & Save 5%","endDate":"1893524400000","extlOfferId":"62753237-ND","isClippable":false,"isDisplayable":true,"maxPurchaseQty":9999,"minPurchaseQty":1,"offerId":"62753237","offerPgm":"SC","offerProgramType":"ITEM_DISCOUNT","offerProtoType":"ITEM_DISCOUNT","offerSubPgm":"02","offerTs":"1770224308678","price":0.0,"purchaseInd":"L","startDate":"1746640800000"},"88132609":{"deleted":false,"deliveryChannel":"EC","ecomDescription":"Schedule & Save 5%","endDate":"1893524400000","extlOfferId":"88132609-ND","isClippable":false,"isDisplayable":true,"maxPurchaseQty":9999,"minPurchaseQty":1,"offerId":"88132609","offerPgm":"SC","offerProgramType":"ITEM_DISCOUNT","offerProtoType":"ITEM_DISCOUNT","offerSubPgm":"02","offerTs":"1770224287189","price":0.0,"purchaseInd":"L","startDate":"1746640800000"}}}},"upcs":{"0002113012547":{"weeklyAdBadge":false},"0002113016602":{"weeklyAdBadge":false},"0002113012545":{"weeklyAdBadge":false},"0023116700000":{"weeklyAdBadge":false},"0061458362409":{"weeklyAdBadge":false},"0023774900000":{"weeklyAdBadge":false},"0023249700000":{"weeklyAdBadge":false},"0023005100000":{"weeklyAdBadge":false},"0085000909760":{"weeklyAdBadge":false},"0073114960156":{"weeklyAdBadge":false},"0023564700000":{"weeklyAdBadge":false},"0693608630033":{"weeklyAdBadge":false},"0064728331152":{"weeklyAdBadge":false},"0007005725117":{"weeklyAdBadge":false},"0023632300000":{"weeklyAdBadge":false},"0023001300000":{"weeklyAdBadge":false},"0023016900000":{"weeklyAdBadge":false},"0023827300000":{"weeklyAdBadge":false},"0023011600000":{"weeklyAdBadge":true},"0087605900338":{"weeklyAdBadge":false},"0081001117724":{"weeklyAdBadge":false},"0023138800000":{"weeklyAdBadge":false},"0064728331636":{"weeklyAdBadge":true},"0023019400000":{"weeklyAdBadge":false},"0023636500000":{"weeklyAdBadge":false},"0002113012699":{"weeklyAdBadge":false},"0085000909732":{"weeklyAdBadge":false},"0023140200000":{"weeklyAdBadge":false,"offers":{"76426233":{"deleted":false,"deliveryChannel":"IS","ecomDescription":"$4.99 each. When you buy 4. Limit 4.","endDate":"1798743600000","extlOfferId":"76426233-ND","isClippable":false,"isDisplayable":true,"maxPurchaseQty":9999,"minPurchaseQty":4,"offerId":"76426233","offerPgm":"SC","offerProgramType":"MUST_BUY","offerProtoType":"MUST_BUY","offerSubPgm":"02","offerTs":"1766429239256","price":0.0,"purchaseInd":"L","startDate":"1767294000000"}}},"0001558500588":{"weeklyAdBadge":false},"0087605900726":{"weeklyAdBadge":false}}},"facet":{"ranges":[{"key":"Price","value":[{"count":0,"start":"*","end":"0"},{"count":21,"start":"0","end":"5"},{"count":64,"start":"5","end":"10"},{"count":60,"start":"10","end":"15"},{"count":41,"start":"15","end":"25"},{"count":11,"start":"25","end":"100"}]}],"fields":[{"key":"brand","value":[{"count":"50","name":"Seafood Market"},{"count":"26","name":"waterfront BISTRO"},{"count":"7","name":"OPEN NATURE"},{"count":"7","name":"WATERFRONT BISTRO"},{"count":"5","name":"Acme"},{"count":"4","name":"Blue Sea"},{"count":"4","name":"Phillips"},{"count":"4","name":"Sea Cuisine"},{"count":"3","name":"TransOcean"},{"count":"2","name":"Annasea"},{"count":"2","name":"Blue Hill Bay"},{"count":"2","name":"Dockside Classics"},{"count":"2","name":"Honey Smoked Fish Co."},{"count":"2","name":"Icy Ocean"},{"count":"2","name":"Royal Asia"},{"count":"2","name":"Signature Banner"},{"count":"2","name":"Vita Classic"},{"count":"1","name":"Anglesea"},{"count":"1","name":"Bass"},{"count":"1","name":"Chef's Net"},{"count":"1","name":"Cox's Shrimp Co."},{"count":"1","name":"Creation Gardens"},{"count":"1","name":"Echo Falls"},{"count":"1","name":"Fishermans Net"},{"count":"1","name":"Gorton's"},{"count":"1","name":"Key West Pink"},{"count":"1","name":"MeTompkin"},{"count":"1","name":"Natural Blue"},{"count":"1","name":"Next Wave Seafood"},{"count":"1","name":"North Coast"},{"count":"1","name":"O ORGANICS"},{"count":"1","name":"Ocean"},{"count":"1","name":"PanaPesca"},{"count":"1","name":"Phillys Premium"},{"count":"1","name":"Sail"},{"count":"1","name":"Sea Mazz"},{"count":"1","name":"Short Cuts"},{"count":"1","name":"Signature SELECT"},{"count":"1","name":"Water"}]},{"key":"departmentName","value":[{"count":"194","name":"Meat & Seafood"}]},{"key":"diet","value":[{"count":"12","name":"Pescatarian"}]},{"key":"dietaryRestriction","value":[{"count":"20","name":"Gluten Free"}]},{"key":"nutritionContent","value":[{"count":"10","name":"Carb-Conscious"},{"count":"10","name":"No Added Sugars"}]},{"key":"offerType","value":[{"count":"136","name":"N"},{"count":"58","name":"Y"}]},{"key":"condition","value":[{"count":"32","name":"Previously Frozen"},{"count":"18","name":"Fresh"},{"count":"12","name":"Frozen"}]},{"key":"departmentName","value":[{"count":"194","name":"Meat & Seafood"}]},{"key":"fish_cut","value":[{"count":"37","name":"Fillet"},{"count":"6","name":"Portion"},{"count":"3","name":"Steak"},{"count":"1","name":"Lon"},{"count":"1","name":"Medallion"},{"count":"1","name":"Nuggets"},{"count":"1","name":"Whole"}]},{"key":"flavor","value":[{"count":"3","name":"Plain Flavor"},{"count":"2","name":"Honey Flavor"},{"count":"1","name":"Cajun Flavor"},{"count":"1","name":"Dill Flavor"},{"count":"1","name":"Pepper Flavor"}]},{"key":"form","value":[{"count":"27","name":"Peeled"},{"count":"19","name":"Cooked"},{"count":"6","name":"Deveined"},{"count":"4","name":"Raw"},{"count":"4","name":"Tail On"},{"count":"4","name":"Whole"},{"count":"2","name":"Shell & Tail On"},{"count":"1","name":"Breaded"},{"count":"1","name":"Shell On"},{"count":"1","name":"Shell-On"},{"count":"1","name":"Steamed"},{"count":"1","name":"Tail Off"}]},{"key":"is_snap_eligible","value":[{"count":"177","name":"SNAP eligible"}]},{"key":"preparation","value":[{"count":"23","name":"Raw"},{"count":"10","name":"Steamed"},{"count":"9","name":"Cooked"},{"count":"9","name":"Smoked"},{"count":"9","name":"Unseasoned"},{"count":"4","name":"Seasoned"},{"count":"3","name":"Marinated"},{"count":"1","name":"Beer Battered"},{"count":"1","name":"Brined"},{"count":"1","name":"Deveined"},{"count":"1","name":"Grilling"},{"count":"1","name":"Plain"},{"count":"1","name":"Steaming"}]},{"key":"raising_method","value":[{"count":"43","name":"Wild-Caught"},{"count":"14","name":"Farm-Raised"},{"count":"2","name":"Wild-caught"}]},{"key":"shelfName","value":[{"count":"64","name":"Fish"},{"count":"62","name":"Shrimp & Prawns"},{"count":"42","name":"Lobster & Crab"},{"count":"14","name":"Oyster, Clams & Scallops"},{"count":"12","name":"Smoked & Cured Fish"}]},{"key":"shellfish_part","value":[{"count":"6","name":"Claw & Leg"},{"count":"5","name":"Shellfish Extracts"},{"count":"3","name":"Cluster"},{"count":"3","name":"Tail"},{"count":"2","name":"Whole"}]},{"key":"species","value":[{"count":"38","name":"Shrimp"},{"count":"22","name":"Salmon"},{"count":"8","name":"Tilapia"},{"count":"6","name":"Cod"},{"count":"5","name":"Catfish"},{"count":"5","name":"Scallop"},{"count":"4","name":"Herring"},{"count":"4","name":"Other Species"},{"count":"4","name":"Tuna"},{"count":"3","name":"Clam"},{"count":"3","name":"Haddock"},{"count":"3","name":"Mussel"},{"count":"3","name":"Swordfish"},{"count":"2","name":"Flounder"},{"count":"1","name":"Bass"},{"count":"1","name":"Halibut"},{"count":"1","name":"Pollock"},{"count":"1","name":"Rockfish"},{"count":"1","name":"Trout"}]}],"dynamic_facets":[{"facetId":"offerType","displayText":"Offer Type","filters":[{"count":"136","name":"N"},{"count":"58","name":"Y"}]},{"facetId":"departmentName","displayText":"Department","filters":[{"count":"194","name":"Meat & Seafood"}]},{"facetId":"species","displayText":"Species","filters":[{"count":"38","name":"Shrimp"},{"count":"22","name":"Salmon"},{"count":"8","name":"Tilapia"},{"count":"6","name":"Cod"},{"count":"5","name":"Catfish"},{"count":"5","name":"Scallop"},{"count":"4","name":"Herring"},{"count":"4","name":"Other Species"},{"count":"4","name":"Tuna"},{"count":"3","name":"Clam"},{"count":"3","name":"Haddock"},{"count":"3","name":"Mussel"},{"count":"3","name":"Swordfish"},{"count":"2","name":"Flounder"},{"count":"1","name":"Bass"},{"count":"1","name":"Halibut"},{"count":"1","name":"Pollock"},{"count":"1","name":"Rockfish"},{"count":"1","name":"Trout"}]},{"facetId":"brand","displayText":"Brand","filters":[{"count":"50","name":"Seafood Market"},{"count":"26","name":"waterfront BISTRO"},{"count":"7","name":"OPEN NATURE"},{"count":"7","name":"WATERFRONT BISTRO"},{"count":"5","name":"Acme"},{"count":"4","name":"Blue Sea"},{"count":"4","name":"Phillips"},{"count":"4","name":"Sea Cuisine"},{"count":"3","name":"TransOcean"},{"count":"2","name":"Annasea"},{"count":"2","name":"Blue Hill Bay"},{"count":"2","name":"Dockside Classics"},{"count":"2","name":"Honey Smoked Fish Co."},{"count":"2","name":"Icy Ocean"},{"count":"2","name":"Royal Asia"},{"count":"2","name":"Signature Banner"},{"count":"2","name":"Vita Classic"},{"count":"1","name":"Anglesea"},{"count":"1","name":"Bass"},{"count":"1","name":"Chef's Net"},{"count":"1","name":"Cox's Shrimp Co."},{"count":"1","name":"Creation Gardens"},{"count":"1","name":"Echo Falls"},{"count":"1","name":"Fishermans Net"},{"count":"1","name":"Gorton's"},{"count":"1","name":"Key West Pink"},{"count":"1","name":"MeTompkin"},{"count":"1","name":"Natural Blue"},{"count":"1","name":"Next Wave Seafood"},{"count":"1","name":"North Coast"},{"count":"1","name":"O ORGANICS"},{"count":"1","name":"Ocean"},{"count":"1","name":"PanaPesca"},{"count":"1","name":"Phillys Premium"},{"count":"1","name":"Sail"},{"count":"1","name":"Sea Mazz"},{"count":"1","name":"Short Cuts"},{"count":"1","name":"Signature SELECT"},{"count":"1","name":"Water"}]},{"facetId":"condition","displayText":"Condition","filters":[{"count":"32","name":"Previously Frozen"},{"count":"18","name":"Fresh"},{"count":"12","name":"Frozen"}]},{"facetId":"form","displayText":"Form","filters":[{"count":"27","name":"Peeled"},{"count":"19","name":"Cooked"},{"count":"6","name":"Deveined"},{"count":"4","name":"Raw"},{"count":"4","name":"Tail On"},{"count":"4","name":"Whole"},{"count":"2","name":"Shell & Tail On"},{"count":"1","name":"Breaded"},{"count":"1","name":"Shell On"},{"count":"1","name":"Shell-On"},{"count":"1","name":"Steamed"},{"count":"1","name":"Tail Off"}]},{"facetId":"shellfish_part","displayText":"Shellfish Part","filters":[{"count":"6","name":"Claw & Leg"},{"count":"5","name":"Shellfish Extracts"},{"count":"3","name":"Cluster"},{"count":"3","name":"Tail"},{"count":"2","name":"Whole"}]},{"facetId":"raising_method","displayText":"Raising Method","filters":[{"count":"43","name":"Wild-Caught"},{"count":"14","name":"Farm-Raised"},{"count":"2","name":"Wild-caught"}]},{"facetId":"fish_cut","displayText":"Fish Cut","filters":[{"count":"37","name":"Fillet"},{"count":"6","name":"Portion"},{"count":"3","name":"Steak"},{"count":"1","name":"Lon"},{"count":"1","name":"Medallion"},{"count":"1","name":"Nuggets"},{"count":"1","name":"Whole"}]},{"facetId":"preparation","displayText":"Preparation","filters":[{"count":"23","name":"Raw"},{"count":"10","name":"Steamed"},{"count":"9","name":"Cooked"},{"count":"9","name":"Smoked"},{"count":"9","name":"Unseasoned"},{"count":"4","name":"Seasoned"},{"count":"3","name":"Marinated"},{"count":"1","name":"Beer Battered"},{"count":"1","name":"Brined"},{"count":"1","name":"Deveined"},{"count":"1","name":"Grilling"},{"count":"1","name":"Plain"},{"count":"1","name":"Steaming"}]},{"facetId":"dietaryRestriction","displayText":"Dietary Restriction","filters":[{"count":"20","name":"Gluten Free"}]},{"facetId":"nutritionContent","displayText":"Nutrition Content","filters":[{"count":"10","name":"Carb-Conscious"},{"count":"10","name":"No Added Sugars"}]},{"facetId":"flavor","displayText":"Flavor","filters":[{"count":"3","name":"Plain Flavor"},{"count":"2","name":"Honey Flavor"},{"count":"1","name":"Cajun Flavor"},{"count":"1","name":"Dill Flavor"},{"count":"1","name":"Pepper Flavor"}]},{"facetId":"diet","displayText":"Diet","filters":[{"count":"12","name":"Pescatarian"}]},{"facetId":"is_snap_eligible","displayText":"SNAP","filters":[{"count":"177","name":"SNAP eligible"}]}]},"appCode":"[GR200, P-GKE200][A-CT: 200][PA-CT: 200]","appMsg":"[GR : SUCCESS., P-GKE: Success.][A-CT: Success.][PA-CT: Success.]","dynamic_filters":{}}
+139
internal/albertsons/query/client.go
··· 1 + package query 2 + 3 + import ( 4 + "context" 5 + "encoding/json" 6 + "errors" 7 + "fmt" 8 + "io" 9 + "net/http" 10 + "net/url" 11 + "strings" 12 + "time" 13 + ) 14 + 15 + const ( 16 + Category_Vegatables = "GR-C-categ-8c62c848" 17 + Category_Fruit = "GR-C-categ-a8eea474" 18 + Category_Seafood = "GR-C-Categ-6090cd27" 19 + Category_Meat = "GR-MeatF-fffc8662" 20 + Category_Wine = "GR-S-Searc-db592d50" 21 + ) 22 + 23 + func StapleCategories() []string { 24 + return []string{ 25 + Category_Vegatables, 26 + Category_Fruit, 27 + Category_Seafood, 28 + Category_Meat, 29 + } 30 + } 31 + 32 + const ( 33 + DefaultSearchBaseURL = "https://www.safeway.com" 34 + defaultSearchPath = "/abs/pub/xapi/wcax/pathway/search" 35 + defaultSearchRows = 60 // how high can we go. Shoudl we paginate just to 36 + defaultSearchChannel = "instore" 37 + defaultSearchUser = "G" 38 + ) 39 + 40 + type SearchClient struct { 41 + baseURL string 42 + subscriptionKey string 43 + reese84 string 44 + httpClient *http.Client 45 + } 46 + 47 + type SearchClientConfig struct { 48 + BaseURL string 49 + SubscriptionKey string 50 + Reese84 string 51 + HTTPClient *http.Client 52 + } 53 + 54 + type SearchOptions struct { 55 + Query string 56 + Start uint 57 + Rows uint 58 + Sort string 59 + } 60 + 61 + func NewSearchClient(cfg SearchClientConfig) (*SearchClient, error) { 62 + subscriptionKey := strings.TrimSpace(cfg.SubscriptionKey) 63 + if subscriptionKey == "" { 64 + return nil, errors.New("subscription key is required") 65 + } 66 + 67 + baseURL := strings.TrimSpace(cfg.BaseURL) 68 + if baseURL == "" { 69 + baseURL = DefaultSearchBaseURL 70 + } 71 + baseURL = strings.TrimRight(baseURL, "/") 72 + 73 + httpClient := cfg.HTTPClient 74 + if httpClient == nil { 75 + httpClient = &http.Client{Timeout: 20 * time.Second} 76 + } 77 + 78 + return &SearchClient{ 79 + baseURL: baseURL, 80 + subscriptionKey: subscriptionKey, 81 + reese84: strings.TrimSpace(cfg.Reese84), 82 + httpClient: httpClient, 83 + }, nil 84 + } 85 + 86 + func (c *SearchClient) Search(ctx context.Context, storeID, category string, opts SearchOptions) (*PathwaySearchPayload, error) { 87 + storeID = strings.TrimSpace(storeID) 88 + if storeID == "" { 89 + return nil, errors.New("store id is required") 90 + } 91 + 92 + endpoint, err := url.Parse(c.baseURL + defaultSearchPath) 93 + if err != nil { 94 + return nil, fmt.Errorf("parse search URL: %w", err) 95 + } 96 + if opts.Rows == 0 { 97 + opts.Rows = defaultSearchRows 98 + } 99 + 100 + query := endpoint.Query() 101 + query.Set("url", c.baseURL) 102 + query.Set("q", strings.TrimSpace(opts.Query)) 103 + query.Set("rows", fmt.Sprintf("%d", opts.Rows)) 104 + query.Set("start", fmt.Sprintf("%d", opts.Start)) 105 + query.Set("channel", defaultSearchChannel) 106 + query.Set("storeid", storeID) 107 + query.Set("sort", strings.TrimSpace(opts.Sort)) 108 + query.Set("widget-id", category) 109 + endpoint.RawQuery = query.Encode() 110 + 111 + req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil) 112 + if err != nil { 113 + return nil, fmt.Errorf("build request: %w", err) 114 + } 115 + req.Header.Set("Accept", "application/json, text/plain, */*") 116 + req.Header.Set("Accept-Language", "en-US,en;q=0.9") 117 + req.Header.Set("ocp-apim-subscription-key", c.subscriptionKey) 118 + 119 + req.AddCookie(&http.Cookie{Name: "reese84", Value: c.reese84}) 120 + 121 + resp, err := c.httpClient.Do(req) 122 + if err != nil { 123 + return nil, fmt.Errorf("request %q: %w", endpoint.String(), err) 124 + } 125 + defer func() { 126 + _ = resp.Body.Close() 127 + }() 128 + 129 + if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices { 130 + errbody, _ := io.ReadAll(resp.Body) 131 + return nil, fmt.Errorf("search request failed: status %d: %s", resp.StatusCode, strings.TrimSpace(string(errbody))) 132 + } 133 + 134 + var payload PathwaySearchPayload 135 + if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil { 136 + return nil, fmt.Errorf("decode json response: %w", err) 137 + } 138 + return &payload, nil 139 + }
+131
internal/albertsons/query/client_test.go
··· 1 + package query 2 + 3 + import ( 4 + "context" 5 + "io" 6 + "net/http" 7 + "net/url" 8 + "strings" 9 + "testing" 10 + ) 11 + 12 + func TestNewSearchClientRequiresSubscriptionKey(t *testing.T) { 13 + t.Parallel() 14 + 15 + _, err := NewSearchClient(SearchClientConfig{}) 16 + if err == nil || !strings.Contains(err.Error(), "subscription key is required") { 17 + t.Fatalf("unexpected error: %v", err) 18 + } 19 + } 20 + 21 + func TestSearchBuildsExpectedRequest(t *testing.T) { 22 + t.Parallel() 23 + 24 + var capturedReq *http.Request 25 + client, err := NewSearchClient(SearchClientConfig{ 26 + BaseURL: "https://www.acmemarkets.com", 27 + SubscriptionKey: "test-subscription-key", 28 + Reese84: "reese-cookie", 29 + HTTPClient: &http.Client{ 30 + Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) { 31 + capturedReq = r 32 + return &http.Response{ 33 + StatusCode: http.StatusOK, 34 + Header: http.Header{ 35 + "Content-Type": []string{"application/json"}, 36 + }, 37 + // this is going to fail 38 + Body: io.NopCloser(strings.NewReader(`{"response":{"numFound":3,"disableTracking":false,"start":0,"miscInfo":{"attributionToken":"","query":"","sort":"","filter":"","nextPageToken":""},"isExactMatch":true,"docs":[{"id":"1","name":"Apples","price":1.99},{"id":"2","name":"Bananas","price":2.49},{"id":"3","name":"Carrots","price":3.99}]}}`)), 39 + }, nil 40 + }), 41 + }, 42 + }) 43 + if err != nil { 44 + t.Fatalf("NewSearchClient returned error: %v", err) 45 + } 46 + 47 + payload, err := client.Search(context.Background(), "806", Category_Vegatables, SearchOptions{}) 48 + if err != nil { 49 + t.Fatalf("Search returned error: %v", err) 50 + } 51 + 52 + if capturedReq == nil { 53 + t.Fatal("expected request to be captured") 54 + } 55 + if capturedReq.URL.Path != defaultSearchPath { 56 + t.Fatalf("unexpected path: %s", capturedReq.URL.Path) 57 + } 58 + if payload.Response.NumFound != 3 { 59 + t.Fatalf("expected 3 docs") 60 + } 61 + 62 + query := capturedReq.URL.Query() 63 + assertQueryValue(t, query, "url", "https://www.acmemarkets.com") 64 + assertQueryValue(t, query, "q", "") 65 + assertQueryValue(t, query, "rows", "60") 66 + assertQueryValue(t, query, "start", "0") 67 + assertQueryValue(t, query, "channel", "instore") 68 + assertQueryValue(t, query, "storeid", "806") 69 + assertQueryValue(t, query, "sort", "") 70 + assertQueryValue(t, query, "widget-id", Category_Vegatables) 71 + 72 + if got := capturedReq.Header.Get("Accept"); got != "application/json, text/plain, */*" { 73 + t.Fatalf("unexpected accept header: %q", got) 74 + } 75 + if got := capturedReq.Header.Get("Accept-Language"); got != "en-US,en;q=0.9" { 76 + t.Fatalf("unexpected accept-language header: %q", got) 77 + } 78 + if got := capturedReq.Header.Get("Ocp-Apim-Subscription-Key"); got != "test-subscription-key" { 79 + t.Fatalf("unexpected subscription header: %q", got) 80 + } 81 + 82 + reese84Cookie, err := capturedReq.Cookie("reese84") 83 + if err != nil { 84 + t.Fatalf("expected reese84 cookie: %v", err) 85 + } 86 + if reese84Cookie.Value != "reese-cookie" { 87 + t.Fatalf("unexpected reese84 cookie: %q", reese84Cookie.Value) 88 + } 89 + } 90 + 91 + func TestSearchInfersSafewayBannerByDefault(t *testing.T) { 92 + t.Parallel() 93 + 94 + var capturedReq *http.Request 95 + client, err := NewSearchClient(SearchClientConfig{ 96 + SubscriptionKey: "test-subscription-key", 97 + HTTPClient: &http.Client{ 98 + Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) { 99 + capturedReq = r 100 + return &http.Response{ 101 + StatusCode: http.StatusOK, 102 + Body: io.NopCloser(strings.NewReader(`{}`)), 103 + }, nil 104 + }), 105 + }, 106 + }) 107 + if err != nil { 108 + t.Fatalf("NewSearchClient returned error: %v", err) 109 + } 110 + 111 + if _, err := client.Search(context.Background(), "1444", Category_Vegatables, SearchOptions{}); err != nil { 112 + t.Fatalf("Search returned error: %v", err) 113 + } 114 + 115 + if got := capturedReq.URL.Query().Get("url"); got != DefaultSearchBaseURL { 116 + t.Fatalf("unexpected url query value: %q", got) 117 + } 118 + } 119 + 120 + type roundTripFunc func(*http.Request) (*http.Response, error) 121 + 122 + func (f roundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) { 123 + return f(r) 124 + } 125 + 126 + func assertQueryValue(t *testing.T, values url.Values, key, want string) { 127 + t.Helper() 128 + if got := values.Get(key); got != want { 129 + t.Fatalf("unexpected %s: got %q want %q", key, got, want) 130 + } 131 + }
+144
internal/albertsons/query/types.go
··· 1 + package query 2 + 3 + // PathwaySearchPayload matches the Albertsons/Safeway pathway search response. 4 + type PathwaySearchPayload struct { 5 + Response PathwaySearchResponse `json:"response"` 6 + } 7 + 8 + type PathwayDynamicFilter struct{} 9 + 10 + type PathwaySearchResponse struct { 11 + NumFound int `json:"numFound"` 12 + DisableTracking bool `json:"disableTracking"` 13 + Start int `json:"start"` 14 + MiscInfo PathwaySearchMiscInfo `json:"miscInfo"` 15 + IsExactMatch bool `json:"isExactMatch"` 16 + Docs []PathwaySearchProduct `json:"docs"` 17 + } 18 + 19 + type PathwaySearchMiscInfo struct { 20 + AttributionToken string `json:"attributionToken"` 21 + Query string `json:"query"` 22 + Sort string `json:"sort"` 23 + Filter string `json:"filter"` 24 + NextPageToken string `json:"nextPageToken"` 25 + } 26 + 27 + type PathwaySearchProduct struct { 28 + Status string `json:"status"` 29 + Name string `json:"name"` 30 + PID string `json:"pid"` 31 + UPC string `json:"upc"` 32 + ID string `json:"id"` 33 + StoreID string `json:"storeId"` 34 + Featured bool `json:"featured"` 35 + IsDYOCake bool `json:"isDYOCake"` 36 + InventoryAvailable string `json:"inventoryAvailable"` 37 + PastPurchased bool `json:"pastPurchased"` 38 + RestrictedValue string `json:"restrictedValue"` 39 + SalesRank int `json:"salesRank"` 40 + AgreementID int `json:"agreementId"` 41 + FeaturedProductID int `json:"featuredProductId"` 42 + ImageURL string `json:"imageUrl"` 43 + Price float64 `json:"price"` 44 + PromoDescription string `json:"promoDescription"` 45 + PromoText string `json:"promoText"` 46 + PromoType string `json:"promoType"` 47 + PromoEndDate *string `json:"promoEndDate"` 48 + BasePrice float64 `json:"basePrice"` 49 + BasePricePer float64 `json:"basePricePer"` 50 + PricePer float64 `json:"pricePer"` 51 + DisplayType string `json:"displayType"` 52 + AisleID string `json:"aisleId"` 53 + AisleName string `json:"aisleName"` 54 + DepartmentName string `json:"departmentName"` 55 + ShelfName string `json:"shelfName"` 56 + ShelfNameWithID string `json:"shelfNameWithId"` 57 + AisleLocation string `json:"aisleLocation"` 58 + ShelfXCoordinateNbr *string `json:"shelfXcoordinateNbr"` 59 + ShelfYCoordinateNbr *string `json:"shelfYcoordinateNbr"` 60 + SlotXCoordinateNbr *string `json:"slotXcoordinateNbr"` 61 + SlotYCoordinateNbr *string `json:"slotYcoordinateNbr"` 62 + FixtureXCoordinateNbr *string `json:"fixtureXcoordinateNbr"` 63 + FixtureYCoordinateNbr *string `json:"fixtureYcoordinateNbr"` 64 + AislePositionTxt *string `json:"aislePositionTxt"` 65 + ShelfDimensionDpth *string `json:"shelfDimensionDpth"` 66 + SnapEligible bool `json:"snapEligible"` 67 + UnitOfMeasure string `json:"unitOfMeasure"` 68 + SellByWeight string `json:"sellByWeight"` 69 + AverageWeight []string `json:"averageWeight"` 70 + UnitQuantity string `json:"unitQuantity"` 71 + DisplayUnitQuantityText *string `json:"displayUnitQuantityText"` 72 + DisplayEstimateText *string `json:"displayEstimateText"` 73 + PreviousPurchaseQty int `json:"previousPurchaseQty"` 74 + MaxPurchaseQty int `json:"maxPurchaseQty"` 75 + MinWeight string `json:"minWeight"` 76 + MaxWeight string `json:"maxWeight"` 77 + IsHhcProduct bool `json:"isHhcProduct"` 78 + Prop65WarningTypeCD string `json:"prop65WarningTypeCD"` 79 + Prop65WarningText string `json:"prop65WarningText"` 80 + Prop65WarningIconRequired bool `json:"prop65WarningIconRequired"` 81 + IsArProduct bool `json:"isArProduct"` 82 + IsMtoProduct bool `json:"isMtoProduct"` 83 + IsCustomizable bool `json:"isCustomizable"` 84 + InStoreShoppingElig bool `json:"inStoreShoppingElig"` 85 + PreparationTime string `json:"preparationTime"` 86 + IsMarketplaceItem string `json:"isMarketplaceItem"` 87 + AlgoType string `json:"algoType"` 88 + TriggerQuantity int `json:"triggerQuantity"` 89 + IDOfAisle string `json:"idOfAisle"` 90 + IDOfShelf string `json:"idOfShelf"` 91 + IDOfDepartment string `json:"idOfDepartment"` 92 + Warnings []PathwaySearchWarning `json:"warnings"` 93 + RequiresReturn bool `json:"requiresReturn"` 94 + ChannelEligibility PathwayChannelEligibility `json:"channelEligibility"` 95 + ChannelInventory PathwayChannelInventory `json:"channelInventory"` 96 + ProductReview PathwayProductReview `json:"productReview"` 97 + ItemSizeQty string `json:"itemSizeQty"` 98 + ItemPackageQty string `json:"itemPackageQty"` 99 + BestSellerRank *int `json:"bestSellerRank"` 100 + ItemRetailSect string `json:"itemRetailSect"` 101 + Badges []PathwaySearchBadge `json:"badges"` 102 + Labels []PathwaySearchLabel `json:"labels"` 103 + } 104 + 105 + type PathwaySearchWarning struct { 106 + FoodIndicator string `json:"foodIndicator"` 107 + WarnMsgTxt string `json:"warnMsgTxt"` 108 + WarningSourceNm string `json:"warningSourceNm"` 109 + } 110 + 111 + type PathwayChannelEligibility struct { 112 + PickUp bool `json:"pickUp"` 113 + Delivery bool `json:"delivery"` 114 + InStore bool `json:"inStore"` 115 + Shipping bool `json:"shipping"` 116 + } 117 + 118 + type PathwayChannelInventory struct { 119 + Delivery string `json:"delivery"` 120 + Pickup string `json:"pickup"` 121 + Instore string `json:"instore"` 122 + Shipping string `json:"shipping"` 123 + } 124 + 125 + type PathwayProductReview struct { 126 + AvgRating string `json:"avgRating"` 127 + ReviewCount string `json:"reviewCount"` 128 + IsReviewWriteEligible string `json:"isReviewWriteEligible"` 129 + IsReviewDisplayEligible string `json:"isReviewDisplayEligible"` 130 + IsForOnetimeReview string `json:"isForOnetimeReview"` 131 + ReviewTemplateType string `json:"reviewTemplateType"` 132 + } 133 + 134 + type PathwaySearchBadge struct { 135 + BadgeName string `json:"badgeName"` 136 + Color string `json:"color"` 137 + IsStrikethrough bool `json:"isStrikethrough"` 138 + IsBoldText bool `json:"isBoldText"` 139 + Icon string `json:"icon"` 140 + } 141 + 142 + type PathwaySearchLabel struct { 143 + LabelName string `json:"labelName"` 144 + }
+34
internal/albertsons/query/types_test.go
··· 1 + package query 2 + 3 + import ( 4 + "encoding/json" 5 + "os" 6 + "testing" 7 + ) 8 + 9 + func TestPathwaySearchPayloadUnmarshalFixture(t *testing.T) { 10 + t.Parallel() 11 + 12 + raw, err := os.ReadFile("acmeresp.json") 13 + if err != nil { 14 + t.Fatalf("ReadFile(acmeresp.json) returned error: %v", err) 15 + } 16 + 17 + var payload PathwaySearchPayload 18 + if err := json.Unmarshal(raw, &payload); err != nil { 19 + t.Fatalf("Unmarshal returned error: %v", err) 20 + } 21 + 22 + if payload.Response.NumFound != 194 { 23 + t.Fatalf("unexpected numFound: %d", payload.Response.NumFound) 24 + } 25 + if len(payload.Response.Docs) == 0 { 26 + t.Fatal("expected docs to be populated") 27 + } 28 + if payload.Response.Docs[0].StoreID != "806" { 29 + t.Fatalf("unexpected first doc storeId: %q", payload.Response.Docs[0].StoreID) 30 + } 31 + if payload.Response.Docs[0].ChannelEligibility.Delivery != true { 32 + t.Fatalf("expected first doc delivery eligibility to be true") 33 + } 34 + }
+192
internal/albertsons/staples.go
··· 1 + package albertsons 2 + 3 + import ( 4 + "context" 5 + "encoding/json" 6 + "fmt" 7 + "log/slog" 8 + "strings" 9 + 10 + "careme/internal/albertsons/query" 11 + "careme/internal/config" 12 + "careme/internal/kroger" 13 + "careme/internal/parallelism" 14 + 15 + "github.com/samber/lo" 16 + ) 17 + 18 + var defaultStaplesSignature = lo.Must(json.Marshal(query.StapleCategories())) 19 + 20 + type searchClient interface { 21 + Search(ctx context.Context, storeID, category string, opts query.SearchOptions) (*query.PathwaySearchPayload, error) 22 + } 23 + 24 + type searchClientFactory func(baseURL string) (searchClient, error) 25 + 26 + type identityProvider struct{} 27 + 28 + type StaplesProvider struct { 29 + identityProvider 30 + newClient searchClientFactory 31 + } 32 + 33 + func NewIdentityProvider() identityProvider { 34 + return identityProvider{} 35 + } 36 + 37 + func NewStaplesProvider(cfg config.AlbertsonsConfig) StaplesProvider { 38 + return newStaplesProviderWithFactory(func(baseURL string) (searchClient, error) { 39 + querycfg := query.SearchClientConfig{ 40 + SubscriptionKey: cfg.SearchSubscriptionKey, 41 + Reese84: cfg.SearchReese84, 42 + BaseURL: baseURL, 43 + } 44 + return query.NewSearchClient(querycfg) 45 + }) 46 + } 47 + 48 + // only used for testing 49 + func newStaplesProviderWithFactory(factory searchClientFactory) StaplesProvider { 50 + return StaplesProvider{ 51 + newClient: factory, 52 + } 53 + } 54 + 55 + func (p identityProvider) Signature() string { 56 + return string(defaultStaplesSignature) 57 + } 58 + 59 + func (p identityProvider) IsID(locationID string) bool { 60 + return IsID(locationID) 61 + } 62 + 63 + var stapleRows = map[string]uint{ 64 + query.Category_Vegatables: 150, // do we need way more of this? 65 + query.Category_Fruit: 100, 66 + query.Category_Meat: 100, 67 + query.Category_Seafood: 60, 68 + } 69 + 70 + func (p StaplesProvider) FetchStaples(ctx context.Context, locationID string) ([]kroger.Ingredient, error) { 71 + client, storeID, err := p.clientForLocation(locationID) 72 + if err != nil { 73 + return nil, err 74 + } 75 + 76 + return parallelism.Flatten(query.StapleCategories(), func(category string) ([]kroger.Ingredient, error) { 77 + payload, err := client.Search(ctx, storeID, category, query.SearchOptions{ 78 + // how many rows? different per category? Should we paginate 79 + Rows: stapleRows[category], 80 + }) 81 + if err != nil { 82 + return nil, err 83 + } 84 + 85 + ingredients := lo.Map(payload.Response.Docs, func(product query.PathwaySearchProduct, _ int) kroger.Ingredient { 86 + return productToIngredient(product) 87 + }) 88 + slog.InfoContext(ctx, "found albertsons staples for category", "count", len(ingredients), "category", category, "location", locationID) 89 + return ingredients, nil 90 + }) 91 + } 92 + 93 + // since this is mostly used by wine it isn't actuallyt they helpful. 94 + func (p StaplesProvider) GetIngredients(ctx context.Context, locationID string, searchTerm string, skip int) ([]kroger.Ingredient, error) { 95 + client, storeID, err := p.clientForLocation(locationID) 96 + if err != nil { 97 + return nil, err 98 + } 99 + 100 + // should we just resturn all instead of search term? how many is this? 101 + payload, err := client.Search(ctx, storeID, query.Category_Wine, query.SearchOptions{ 102 + Query: searchTerm, Rows: 100, 103 + }) 104 + if err != nil { 105 + return nil, err 106 + } 107 + 108 + ingredients := lo.Map(payload.Response.Docs, func(product query.PathwaySearchProduct, _ int) kroger.Ingredient { 109 + return productToIngredient(product) 110 + }) 111 + if skip >= len(ingredients) { 112 + return []kroger.Ingredient{}, nil 113 + } 114 + return ingredients[skip:], nil 115 + } 116 + 117 + // clientForLocation takes a prefixed store id and looks up chaing base url and returnes unprefixed id. 118 + func (p StaplesProvider) clientForLocation(locationID string) (searchClient, string, error) { 119 + baseURL, storeID, ok := searchBaseURLAndStoreID(locationID) 120 + if !ok { 121 + return nil, "", fmt.Errorf("invalid albertsons location id %q", locationID) 122 + } 123 + 124 + client, err := p.newClient(baseURL) 125 + if err != nil { 126 + return nil, "", err 127 + } 128 + return client, storeID, nil 129 + } 130 + 131 + func searchBaseURLAndStoreID(locationID string) (string, string, bool) { 132 + locationID = strings.TrimSpace(locationID) 133 + for _, chain := range defaultChains { 134 + storeID := strings.TrimPrefix(locationID, chain.IDPrefix) 135 + if storeID == "" || storeID == locationID { 136 + continue 137 + } 138 + // should we append local elsewhere instead of trimming here? 139 + host := strings.TrimPrefix(chain.Domain, "local.") 140 + return "https://www." + host, storeID, true 141 + } 142 + return "", "", false 143 + } 144 + 145 + func productToIngredient(product query.PathwaySearchProduct) kroger.Ingredient { 146 + productID := stringPtr(strings.TrimSpace(product.ID)) 147 + description := stringPtr(strings.TrimSpace(product.Name)) 148 + size := sizeText(product) 149 + regularPrice := float32Ptr(product.BasePrice) 150 + salePrice := float32Ptr(product.Price) 151 + categories := lo.Compact([]string{product.DepartmentName, product.ShelfName}) 152 + 153 + var categoryPtr *[]string 154 + if len(categories) > 0 { 155 + categoryPtr = &categories 156 + } 157 + 158 + return kroger.Ingredient{ 159 + ProductId: productID, 160 + Description: description, 161 + Size: size, 162 + PriceRegular: regularPrice, 163 + PriceSale: salePrice, 164 + Categories: categoryPtr, 165 + } 166 + } 167 + 168 + // this is a bit squirely shouldn't we take one ratehr than joiing both? 169 + func sizeText(product query.PathwaySearchProduct) *string { 170 + sizeParts := lo.Compact([]string{product.ItemSizeQty, product.UnitOfMeasure}) 171 + if len(sizeParts) == 0 { 172 + return nil 173 + } 174 + size := strings.Join(sizeParts, " ") 175 + return &size 176 + } 177 + 178 + func stringPtr(value string) *string { 179 + value = strings.TrimSpace(value) 180 + if value == "" { 181 + return nil 182 + } 183 + return &value 184 + } 185 + 186 + func float32Ptr(value float64) *float32 { 187 + if value <= 0 { 188 + return nil 189 + } 190 + out := float32(value) 191 + return &out 192 + }
+167
internal/albertsons/staples_test.go
··· 1 + package albertsons 2 + 3 + import ( 4 + "context" 5 + "encoding/json" 6 + "slices" 7 + "sync" 8 + "testing" 9 + 10 + "careme/internal/albertsons/query" 11 + ) 12 + 13 + func TestIdentityProviderSignature_UsesStapleCategories(t *testing.T) { 14 + t.Parallel() 15 + 16 + got := NewIdentityProvider().Signature() 17 + want, err := json.Marshal(query.StapleCategories()) 18 + if err != nil { 19 + t.Fatalf("marshal staple categories: %v", err) 20 + } 21 + if got != string(want) { 22 + t.Fatalf("unexpected signature: got %q want %q", got, want) 23 + } 24 + } 25 + 26 + type stubSearchClient struct { 27 + results map[string]query.PathwaySearchPayload 28 + mu sync.Mutex 29 + calls []string 30 + } 31 + 32 + func (s *stubSearchClient) Search(_ context.Context, storeID, category string, opts query.SearchOptions) (*query.PathwaySearchPayload, error) { 33 + s.mu.Lock() 34 + s.calls = append(s.calls, storeID+":"+category+":"+opts.Query) 35 + s.mu.Unlock() 36 + 37 + payload := s.results[category] 38 + return &payload, nil 39 + } 40 + 41 + func (s *stubSearchClient) hasCall(want string) bool { 42 + s.mu.Lock() 43 + defer s.mu.Unlock() 44 + return slices.Contains(s.calls, want) 45 + } 46 + 47 + func (s *stubSearchClient) callCount() int { 48 + s.mu.Lock() 49 + defer s.mu.Unlock() 50 + return len(s.calls) 51 + } 52 + 53 + func TestStaplesProvider_MapsProductsToIngredients(t *testing.T) { 54 + t.Parallel() 55 + 56 + var requestedBaseURL string 57 + client := &stubSearchClient{ 58 + results: map[string]query.PathwaySearchPayload{ 59 + query.Category_Vegatables: { 60 + Response: query.PathwaySearchResponse{ 61 + Docs: []query.PathwaySearchProduct{{ 62 + ID: "veg-1", 63 + Name: "Broccoli Crown", 64 + Price: 2.99, 65 + BasePrice: 3.49, 66 + ItemSizeQty: "1", 67 + UnitOfMeasure: "EA", 68 + DepartmentName: "Produce", 69 + ShelfName: "Vegetables", 70 + }}, 71 + }, 72 + }, 73 + }, 74 + } 75 + provider := newStaplesProviderWithFactory(func(baseURL string) (searchClient, error) { 76 + requestedBaseURL = baseURL 77 + return client, nil 78 + }) 79 + 80 + got, err := provider.FetchStaples(t.Context(), "safeway_1142") 81 + if err != nil { 82 + t.Fatalf("FetchStaples returned error: %v", err) 83 + } 84 + if requestedBaseURL != "https://www.safeway.com" { 85 + t.Fatalf("unexpected base URL: %q", requestedBaseURL) 86 + } 87 + if got, want := client.callCount(), len(query.StapleCategories()); got != want { 88 + t.Fatalf("expected %d category calls, got %d", want, got) 89 + } 90 + if len(got) != 1 { 91 + t.Fatalf("expected 1 mapped ingredient, got %d", len(got)) 92 + } 93 + 94 + first := got[0] 95 + if first.ProductId == nil || *first.ProductId != "veg-1" { 96 + t.Fatalf("unexpected product id: %+v", first.ProductId) 97 + } 98 + if first.Description == nil || *first.Description != "Broccoli Crown" { 99 + t.Fatalf("unexpected description: %+v", first.Description) 100 + } 101 + if first.Size == nil || *first.Size != "1 EA" { 102 + t.Fatalf("unexpected size: %+v", first.Size) 103 + } 104 + if first.PriceRegular == nil || *first.PriceRegular != float32(3.49) { 105 + t.Fatalf("unexpected regular price: %+v", first.PriceRegular) 106 + } 107 + if first.PriceSale == nil || *first.PriceSale != float32(2.99) { 108 + t.Fatalf("unexpected sale price: %+v", first.PriceSale) 109 + } 110 + if first.Categories == nil || !slices.Equal(*first.Categories, []string{"Produce", "Vegetables"}) { 111 + t.Fatalf("unexpected categories: %+v", first.Categories) 112 + } 113 + } 114 + 115 + func TestStaplesProvider_InvalidLocationID(t *testing.T) { 116 + t.Parallel() 117 + 118 + provider := newStaplesProviderWithFactory(func(baseURL string) (searchClient, error) { 119 + t.Fatalf("unexpected client creation for base URL %q", baseURL) 120 + return nil, nil 121 + }) 122 + 123 + _, err := provider.FetchStaples(t.Context(), "1142") 124 + if err == nil { 125 + t.Fatal("expected invalid location error") 126 + } 127 + if got, want := err.Error(), `invalid albertsons location id "1142"`; got != want { 128 + t.Fatalf("unexpected error: got %q want %q", got, want) 129 + } 130 + } 131 + 132 + func TestStaplesProvider_GetIngredients_UsesSearchTermAndSkip(t *testing.T) { 133 + t.Parallel() 134 + 135 + client := &stubSearchClient{ 136 + results: map[string]query.PathwaySearchPayload{ 137 + query.Category_Wine: { 138 + Response: query.PathwaySearchResponse{ 139 + Docs: []query.PathwaySearchProduct{ 140 + {ID: "veg-1", Name: "Pinot Tomatoes", Price: 1.99}, 141 + {ID: "veg-2", Name: "Rose Radishes", Price: 2.49}, 142 + }, 143 + }, 144 + }, 145 + }, 146 + } 147 + provider := newStaplesProviderWithFactory(func(baseURL string) (searchClient, error) { 148 + if baseURL != "https://www.acmemarkets.com" { 149 + t.Fatalf("unexpected base URL: %q", baseURL) 150 + } 151 + return client, nil 152 + }) 153 + 154 + got, err := provider.GetIngredients(t.Context(), "acmemarkets_806", "pinot", 1) 155 + if err != nil { 156 + t.Fatalf("GetIngredients returned error: %v", err) 157 + } 158 + if !client.hasCall("806:" + query.Category_Wine + ":pinot") { 159 + t.Fatalf("missing expected search call") 160 + } 161 + if len(got) != 1 { 162 + t.Fatalf("expected 1 ingredient after skip, got %d", len(got)) 163 + } 164 + if got[0].Description == nil || *got[0].Description != "Rose Radishes" { 165 + t.Fatalf("unexpected description: %+v", got[0].Description) 166 + } 167 + }
+112
internal/brightdata/acmexapi.curl
··· 1 + 2 + 3 + 4 + 5 + Rencoded 6 + 7 + curl 'https://www.acmemarkets.com/abs/pub/xapi/wcax/pathway/search?url=https://www.acmemarkets.com&q=&rows=30&start=0&channel=instore&storeid=806&sort=&widget-id=GR-C-Categ-6090cd27&dvid=web-4.1search&visitorId=6b47f549-b8da-48b5-8486-8c750ec1c753&uuid=null' \ 8 + -H 'accept: application/json, text/plain, */*' \ 9 + -H 'accept-language: en-US,en;q=0.9' \ 10 + -b 'ACI_S_abs_previouslogin=%7B%0A%20%20%22info%22%3A%20%7B%0A%20%20%20%20%22COMMON%22%3A%20%7B%0A%20%20%20%20%20%20%22houseId%22%3A%20%22729022287633%22%2C%0A%20%20%20%20%20%20%22clubCard%22%3A%20%2249549812729%22%2C%0A%20%20%20%20%20%20%22userType%22%3A%20%22G%22%2C%0A%20%20%20%20%20%20%22storeId%22%3A%20%22806%22%2C%0A%20%20%20%20%20%20%22zipcode%22%3A%20%2219711%22%2C%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D; reese84=3:Um6Sa3mRcAnh+3ITa9FjwA==:PmU0Vm8qdx4HH8zDCIXBS3qCcFxSw0pY2SvDbCINWhSqme0yNw/jBBLVEa8ZYcDZ35mMrlxvZyjBsuKHLF1GdEE5VET/NCWzebW8eyhMI7qrOCaGWEc8RFxsjSROtEYAdsUpZj8lTY9SiDcSxbsCz9b0Z03aRwTVR/UHih/kmsw5HxB2W/FLacqtFFrzAK2lTCQbqEaVci9CfgYlSJSEzLR+Kvi7TEO+L/jhATbMhgMmffKsrNI8XKFdFt2pESVZz2mX2D3q8mY36ZK4RUMKxqyCqnZV7//5P7pT1Ja8dYyI32LkD0G+i1OviFQ2/GuwPjFokXHBA9XX92dpkTnXcGeLsWwE6Q116T9iMoPW5VPjmlNI+hbN/jZCgj8HXHmZM1Hpm0qrpJH9h3tpmvV6yUozf/m4DkebSy4A2nFICkTAMiDxamOXwdACn8eli+BR6Ild0P9ZKPv1RiJkYZdN3V+T++ro3CuMI0oDHzWu4WzQ2ucRz5NiOKxp58+fgowg6zxMPbSwgqLrnPZ758YZTnoEfr/mYGUfGK+AnW7+aQ5vMeGT1sJMBYCUcjV8F6I0n5M2/0rSjp2uri5Bxa96+Q==:kpAJfuxaqq0kDZ/9EtrMUKMOxr/gYX9srCuoThNrh6M=' \ 11 + -H 'ocp-apim-subscription-key: e914eec9448c4d5eb672debf5011cf8f' 12 + 13 + ACI_S_abs_previouslogin is just url encoded this 14 + 15 + { 16 + "info": { 17 + "COMMON": { 18 + "houseId": "729022287633", 19 + "clubCard": "49549812729", 20 + "userType": "G", 21 + "storeId": "806", 22 + "zipcode": "19711", 23 + } 24 + } 25 + } 26 + 27 + reese84 is a cookie that is set by the server and is used for authentication. It is a base64 encoded string that contains the user's session information. The value of the cookie is: 28 + 29 + 30 + 31 + 32 + Last working 33 + 34 + 35 + curl 'https://www.acmemarkets.com/abs/pub/xapi/wcax/pathway/search?url=https://www.acmemarkets.com&q=&rows=30&start=0&channel=instore&storeid=806&sort=&widget-id=GR-C-Categ-6090cd27&dvid=web-4.1search&visitorId=6b47f549-b8da-48b5-8486-8c750ec1c753&uuid=null&pgm=abs&includeOffer=true&banner=acmemarkets&facet=false' \ 36 + -H 'accept: application/json, text/plain, */*' \ 37 + -H 'accept-language: en-US,en;q=0.9' \ 38 + -b 'ACI_S_abs_previouslogin=%7B%22info%22%3A%7B%22COMMON%22%3A%7B%22houseId%22%3A%22729022287633%22%2C%22clubCard%22%3A%2249549812729%22%2C%22userType%22%3A%22G%22%2C%22storeId%22%3A%22806%22%2C%22zipcode%22%3A%2219711%22%2C%22banner%22%3A%22acmemarkets%22%2C%22preference%22%3A%22J4U%22%2C%22isClosed%22%3A%22false%22%2C%22isCrossBannerMFCUser%22%3A%22false%22%2C%22Selection%22%3A%22user%22%2C%22isARenrolled%22%3A%22false%22%2C%22userData%22%3A%7B%7D%2C%22grsSessionId%22%3A%22063024fb-35d7-4da1-b6ee-49398ee1eb93%22%2C%22siteType%22%3A%22C%22%2C%22customerType%22%3A%22%22%2C%22resolvedBy%22%3A%22%22%2C%22multiAccounts%22%3A%22false%22%2C%22emailVerified%22%3A%22true%22%2C%22phoneVerified%22%3A%22false%22%2C%22emailPrompt180%22%3A%22false%22%2C%22phonePrompt180%22%3A%22false%22%2C%22deviceId%22%3A%22%22%7D%2C%22J4U%22%3A%7B%22userType%22%3A%22R%22%2C%22storeId%22%3A%22806%22%2C%22zipcode%22%3A%2219711%22%2C%22address%22%3A%22100%2BSuburban%2BDr%2C%2BNewark%2C%2BDE%2B19711%22%2C%22preferredBanner%22%3A%22acmemarkets%22%2C%22userData%22%3A%7B%22guestStoreId%22%3A%222633%22%2C%22guestZipCode%22%3A%2206840%22%2C%22defaultStore%22%3Afalse%7D%7D%2C%22SHOP%22%3A%7B%22userType%22%3A%22R%22%2C%22storeId%22%3A%22806%22%2C%22zipcode%22%3A%2219711%22%2C%22address%22%3A%22100%2BSuburban%2BDr%2C%2BNewark%2C%2BDE%2B19711%22%2C%22userData%22%3A%7B%22guestStoreId%22%3A%22806%22%2C%22guestZipCode%22%3A%2219711%22%2C%22signInCalled%22%3Afalse%2C%22driveUpAndGoIsEnabled%22%3A%22false%22%2C%22unattendedDeliveryIsEnabled%22%3A%22false%22%2C%22expireTime%22%3A%221774470178000%22%7D%7D%7D%7D; reese84=3:Um6Sa3mRcAnh+3ITa9FjwA==:PmU0Vm8qdx4HH8zDCIXBS3qCcFxSw0pY2SvDbCINWhSqme0yNw/jBBLVEa8ZYcDZ35mMrlxvZyjBsuKHLF1GdEE5VET/NCWzebW8eyhMI7qrOCaGWEc8RFxsjSROtEYAdsUpZj8lTY9SiDcSxbsCz9b0Z03aRwTVR/UHih/kmsw5HxB2W/FLacqtFFrzAK2lTCQbqEaVci9CfgYlSJSEzLR+Kvi7TEO+L/jhATbMhgMmffKsrNI8XKFdFt2pESVZz2mX2D3q8mY36ZK4RUMKxqyCqnZV7//5P7pT1Ja8dYyI32LkD0G+i1OviFQ2/GuwPjFokXHBA9XX92dpkTnXcGeLsWwE6Q116T9iMoPW5VPjmlNI+hbN/jZCgj8HXHmZM1Hpm0qrpJH9h3tpmvV6yUozf/m4DkebSy4A2nFICkTAMiDxamOXwdACn8eli+BR6Ild0P9ZKPv1RiJkYZdN3V+T++ro3CuMI0oDHzWu4WzQ2ucRz5NiOKxp58+fgowg6zxMPbSwgqLrnPZ758YZTnoEfr/mYGUfGK+AnW7+aQ5vMeGT1sJMBYCUcjV8F6I0n5M2/0rSjp2uri5Bxa96+Q==:kpAJfuxaqq0kDZ/9EtrMUKMOxr/gYX9srCuoThNrh6M=' \ 39 + -H 'ocp-apim-subscription-key: e914eec9448c4d5eb672debf5011cf8f' 40 + 41 + 42 + 43 + curl 'https://www.acmemarkets.com/abs/pub/xapi/wcax/pathway/search?url=https://www.acmemarkets.com&q=&rows=30&start=0&channel=instore&storeid=806&sort=&widget-id=GR-C-Categ-6090cd27&dvid=web-4.1search&visitorId=6b47f549-b8da-48b5-8486-8c750ec1c753&uuid=null&pgm=abs&includeOffer=true&banner=acmemarkets&facet=false' \ 44 + -H 'accept: application/json, text/plain, */*' \ 45 + -H 'accept-language: en-US,en;q=0.9' \ 46 + -b 'ACI_S_abs_previouslogin=%7B%22info%22%3A%7B%22COMMON%22%3A%7B%22houseId%22%3A%22729022287633%22%2C%22clubCard%22%3A%2249549812729%22%2C%22userType%22%3A%22G%22%2C%22storeId%22%3A%22806%22%2C%22zipcode%22%3A%2219711%22%2C%22banner%22%3A%22acmemarkets%22%2C%22preference%22%3A%22J4U%22%2C%22isClosed%22%3A%22false%22%2C%22isCrossBannerMFCUser%22%3A%22false%22%2C%22Selection%22%3A%22user%22%2C%22isARenrolled%22%3A%22false%22%2C%22userData%22%3A%7B%7D%2C%22grsSessionId%22%3A%22063024fb-35d7-4da1-b6ee-49398ee1eb93%22%2C%22siteType%22%3A%22C%22%2C%22customerType%22%3A%22%22%2C%22resolvedBy%22%3A%22%22%2C%22multiAccounts%22%3A%22false%22%2C%22emailVerified%22%3A%22true%22%2C%22phoneVerified%22%3A%22false%22%2C%22emailPrompt180%22%3A%22false%22%2C%22phonePrompt180%22%3A%22false%22%2C%22deviceId%22%3A%22%22%7D%2C%22J4U%22%3A%7B%22userType%22%3A%22R%22%2C%22storeId%22%3A%22806%22%2C%22zipcode%22%3A%2219711%22%2C%22address%22%3A%22100%2BSuburban%2BDr%2C%2BNewark%2C%2BDE%2B19711%22%2C%22preferredBanner%22%3A%22acmemarkets%22%2C%22userData%22%3A%7B%22guestStoreId%22%3A%222633%22%2C%22guestZipCode%22%3A%2206840%22%2C%22defaultStore%22%3Afalse%7D%7D%2C%22SHOP%22%3A%7B%22userType%22%3A%22R%22%2C%22storeId%22%3A%22806%22%2C%22zipcode%22%3A%2219711%22%2C%22address%22%3A%22100%2BSuburban%2BDr%2C%2BNewark%2C%2BDE%2B19711%22%2C%22userData%22%3A%7B%22guestStoreId%22%3A%22806%22%2C%22guestZipCode%22%3A%2219711%22%2C%22signInCalled%22%3Afalse%2C%22driveUpAndGoIsEnabled%22%3A%22false%22%2C%22unattendedDeliveryIsEnabled%22%3A%22false%22%2C%22expireTime%22%3A%221774470178000%22%7D%7D%7D%7D; reese84=3:Um6Sa3mRcAnh+3ITa9FjwA==:PmU0Vm8qdx4HH8zDCIXBS3qCcFxSw0pY2SvDbCINWhSqme0yNw/jBBLVEa8ZYcDZ35mMrlxvZyjBsuKHLF1GdEE5VET/NCWzebW8eyhMI7qrOCaGWEc8RFxsjSROtEYAdsUpZj8lTY9SiDcSxbsCz9b0Z03aRwTVR/UHih/kmsw5HxB2W/FLacqtFFrzAK2lTCQbqEaVci9CfgYlSJSEzLR+Kvi7TEO+L/jhATbMhgMmffKsrNI8XKFdFt2pESVZz2mX2D3q8mY36ZK4RUMKxqyCqnZV7//5P7pT1Ja8dYyI32LkD0G+i1OviFQ2/GuwPjFokXHBA9XX92dpkTnXcGeLsWwE6Q116T9iMoPW5VPjmlNI+hbN/jZCgj8HXHmZM1Hpm0qrpJH9h3tpmvV6yUozf/m4DkebSy4A2nFICkTAMiDxamOXwdACn8eli+BR6Ild0P9ZKPv1RiJkYZdN3V+T++ro3CuMI0oDHzWu4WzQ2ucRz5NiOKxp58+fgowg6zxMPbSwgqLrnPZ758YZTnoEfr/mYGUfGK+AnW7+aQ5vMeGT1sJMBYCUcjV8F6I0n5M2/0rSjp2uri5Bxa96+Q==:kpAJfuxaqq0kDZ/9EtrMUKMOxr/gYX9srCuoThNrh6M=' \ 47 + -H 'ocp-apim-subscription-key: e914eec9448c4d5eb672debf5011cf8f' 48 + 49 + 50 + 51 + curl 'https://www.acmemarkets.com/abs/pub/xapi/wcax/pathway/search?url=https://www.acmemarkets.com&q=&rows=30&start=0&channel=instore&storeid=806&sort=&widget-id=GR-C-Categ-6090cd27&dvid=web-4.1search&visitorId=6b47f549-b8da-48b5-8486-8c750ec1c753&uuid=null&pgm=abs&includeOffer=true&banner=acmemarkets&facet=false' \ 52 + -H 'accept: application/json, text/plain, */*' \ 53 + -H 'accept-language: en-US,en;q=0.9' \ 54 + -b 'ACI_S_abs_previouslogin=%7B%22info%22%3A%7B%22COMMON%22%3A%7B%22houseId%22%3A%22729022287633%22%2C%22clubCard%22%3A%2249549812729%22%2C%22userType%22%3A%22G%22%2C%22storeId%22%3A%22806%22%2C%22zipcode%22%3A%2219711%22%2C%22banner%22%3A%22acmemarkets%22%2C%22preference%22%3A%22J4U%22%2C%22isClosed%22%3A%22false%22%2C%22isCrossBannerMFCUser%22%3A%22false%22%2C%22Selection%22%3A%22user%22%2C%22isARenrolled%22%3A%22false%22%2C%22userData%22%3A%7B%7D%2C%22grsSessionId%22%3A%22063024fb-35d7-4da1-b6ee-49398ee1eb93%22%2C%22siteType%22%3A%22C%22%2C%22customerType%22%3A%22%22%2C%22resolvedBy%22%3A%22%22%2C%22multiAccounts%22%3A%22false%22%2C%22emailVerified%22%3A%22true%22%2C%22phoneVerified%22%3A%22false%22%2C%22emailPrompt180%22%3A%22false%22%2C%22phonePrompt180%22%3A%22false%22%2C%22deviceId%22%3A%22%22%7D%2C%22J4U%22%3A%7B%22userType%22%3A%22R%22%2C%22storeId%22%3A%22806%22%2C%22zipcode%22%3A%2219711%22%2C%22address%22%3A%22100%2BSuburban%2BDr%2C%2BNewark%2C%2BDE%2B19711%22%2C%22preferredBanner%22%3A%22acmemarkets%22%2C%22userData%22%3A%7B%22guestStoreId%22%3A%222633%22%2C%22guestZipCode%22%3A%2206840%22%2C%22defaultStore%22%3Afalse%7D%7D%2C%22SHOP%22%3A%7B%22userType%22%3A%22R%22%2C%22storeId%22%3A%22806%22%2C%22zipcode%22%3A%2219711%22%2C%22address%22%3A%22100%2BSuburban%2BDr%2C%2BNewark%2C%2BDE%2B19711%22%2C%22userData%22%3A%7B%22guestStoreId%22%3A%22806%22%2C%22guestZipCode%22%3A%2219711%22%2C%22signInCalled%22%3Afalse%2C%22driveUpAndGoIsEnabled%22%3A%22false%22%2C%22unattendedDeliveryIsEnabled%22%3A%22false%22%2C%22expireTime%22%3A%221774470178000%22%7D%7D%7D%7D; reese84=3:Um6Sa3mRcAnh+3ITa9FjwA==:PmU0Vm8qdx4HH8zDCIXBS3qCcFxSw0pY2SvDbCINWhSqme0yNw/jBBLVEa8ZYcDZ35mMrlxvZyjBsuKHLF1GdEE5VET/NCWzebW8eyhMI7qrOCaGWEc8RFxsjSROtEYAdsUpZj8lTY9SiDcSxbsCz9b0Z03aRwTVR/UHih/kmsw5HxB2W/FLacqtFFrzAK2lTCQbqEaVci9CfgYlSJSEzLR+Kvi7TEO+L/jhATbMhgMmffKsrNI8XKFdFt2pESVZz2mX2D3q8mY36ZK4RUMKxqyCqnZV7//5P7pT1Ja8dYyI32LkD0G+i1OviFQ2/GuwPjFokXHBA9XX92dpkTnXcGeLsWwE6Q116T9iMoPW5VPjmlNI+hbN/jZCgj8HXHmZM1Hpm0qrpJH9h3tpmvV6yUozf/m4DkebSy4A2nFICkTAMiDxamOXwdACn8eli+BR6Ild0P9ZKPv1RiJkYZdN3V+T++ro3CuMI0oDHzWu4WzQ2ucRz5NiOKxp58+fgowg6zxMPbSwgqLrnPZ758YZTnoEfr/mYGUfGK+AnW7+aQ5vMeGT1sJMBYCUcjV8F6I0n5M2/0rSjp2uri5Bxa96+Q==:kpAJfuxaqq0kDZ/9EtrMUKMOxr/gYX9srCuoThNrh6M=; AMCV_A7BF3BC75245ADF20A490D4D%40AdobeOrg=179643557%7CMCIDTS%7C20538%7CMCMID%7C11176332726178312564444395020237210543%7CMCAAMLH-1775148149%7C9%7CMCAAMB-1775148149%7C6G1ynYcLPuiQxYZrsz_pkqfLG9yMXBpb2zX5dvJdYQJzPXImdj0y%7CMCOPTOUT-1774550549s%7CNONE%7CvVersion%7C5.5.0%7CMCCIDH%7C1464169025; SWY_SHARED_SESSION_INFO=%7B%22info%22%3A%7B%22COMMON%22%3A%7B%22Selection%22%3A%22user%22%2C%22preference%22%3A%22J4U%22%2C%22userType%22%3A%22G%22%2C%22zipcode%22%3A%2219711%22%2C%22banner%22%3A%22acmemarkets%22%2C%22siteType%22%3A%22C%22%2C%22customerType%22%3A%22%22%2C%22resolvedBy%22%3A%22%22%7D%2C%22J4U%22%3A%7B%22zipcode%22%3A%2219711%22%2C%22storeId%22%3A%22806%22%7D%2C%22SHOP%22%3A%7B%22zipcode%22%3A%2219711%22%2C%22storeId%22%3A%22806%22%7D%7D%7D; SWY_SYND_USER_INFO=%7B%22storeAddress%22%3A%22%22%2C%22storeZip%22%3A%2219711%22%2C%22storeId%22%3A%22806%22%2C%22preference%22%3A%22J4U%22%7D; mbox=PC#03b8319f82634490994e9117016a6f12.35_0#1837788151|session#873f096bd37f42a3ac8d53bcffa7a971#1774545211; OptanonConsent=isGpcEnabled=0&datestamp=Thu+Mar+26+2026+09%3A42%3A30+GMT-0700+(Pacific+Daylight+Time)&version=202409.1.0&browserGpcFlag=0&isIABGlobal=false&hosts=&consentId=ad8ee733-21b7-4a88-9c98-833650d44dbe&interactionCount=1&isAnonUser=1&landingPath=NotLandingPage&groups=C0001%3A1%2CC0002%3A1%2CC0004%3A1%2CC0003%3A1&AwaitingReconsent=false' \ 55 + -H 'ocp-apim-subscription-key: e914eec9448c4d5eb672debf5011cf8f' 56 + 57 + 58 + 59 + curl 'https://www.acmemarkets.com/abs/pub/xapi/wcax/pathway/search?url=https://www.acmemarkets.com&q=&rows=30&start=0&channel=instore&storeid=806&sort=&widget-id=GR-C-Categ-6090cd27&dvid=web-4.1search&visitorId=6b47f549-b8da-48b5-8486-8c750ec1c753&uuid=null&pgm=abs&includeOffer=true&banner=acmemarkets&facet=false' \ 60 + -H 'accept: application/json, text/plain, */*' \ 61 + -H 'accept-language: en-US,en;q=0.9' \ 62 + -b 'ACI_S_abs_previouslogin=%7B%22info%22%3A%7B%22COMMON%22%3A%7B%22houseId%22%3A%22729022287633%22%2C%22clubCard%22%3A%2249549812729%22%2C%22userType%22%3A%22G%22%2C%22storeId%22%3A%22806%22%2C%22zipcode%22%3A%2219711%22%2C%22banner%22%3A%22acmemarkets%22%2C%22preference%22%3A%22J4U%22%2C%22isClosed%22%3A%22false%22%2C%22isCrossBannerMFCUser%22%3A%22false%22%2C%22Selection%22%3A%22user%22%2C%22isARenrolled%22%3A%22false%22%2C%22userData%22%3A%7B%7D%2C%22grsSessionId%22%3A%22063024fb-35d7-4da1-b6ee-49398ee1eb93%22%2C%22siteType%22%3A%22C%22%2C%22customerType%22%3A%22%22%2C%22resolvedBy%22%3A%22%22%2C%22multiAccounts%22%3A%22false%22%2C%22emailVerified%22%3A%22true%22%2C%22phoneVerified%22%3A%22false%22%2C%22emailPrompt180%22%3A%22false%22%2C%22phonePrompt180%22%3A%22false%22%2C%22deviceId%22%3A%22%22%7D%2C%22J4U%22%3A%7B%22userType%22%3A%22R%22%2C%22storeId%22%3A%22806%22%2C%22zipcode%22%3A%2219711%22%2C%22address%22%3A%22100%2BSuburban%2BDr%2C%2BNewark%2C%2BDE%2B19711%22%2C%22preferredBanner%22%3A%22acmemarkets%22%2C%22userData%22%3A%7B%22guestStoreId%22%3A%222633%22%2C%22guestZipCode%22%3A%2206840%22%2C%22defaultStore%22%3Afalse%7D%7D%2C%22SHOP%22%3A%7B%22userType%22%3A%22R%22%2C%22storeId%22%3A%22806%22%2C%22zipcode%22%3A%2219711%22%2C%22address%22%3A%22100%2BSuburban%2BDr%2C%2BNewark%2C%2BDE%2B19711%22%2C%22userData%22%3A%7B%22guestStoreId%22%3A%22806%22%2C%22guestZipCode%22%3A%2219711%22%2C%22signInCalled%22%3Afalse%2C%22driveUpAndGoIsEnabled%22%3A%22false%22%2C%22unattendedDeliveryIsEnabled%22%3A%22false%22%2C%22expireTime%22%3A%221774470178000%22%7D%7D%7D%7D; __gpi=UID=000013497e5d0cce:T=1773168126:RT=1774480896:S=ALNI_MZTEodi_t-OHCr4WH4t6AaQ9p-Mhw; __eoi=ID=4ac3989bb54cc211:T=1773168126:RT=1774480896:S=AA-Afja0nnjCotgTkMb1VWPM2gGd; signifyd_sessionId=19aHa79e-ee22-P208-b0f0-b8fb-4bcc076_1f86; _gcl_au=1.1.2103349931.1773168127.1395106364.1774481114.1774481114; ACI_S_ECommRedirectURL=https%3A%2F%2Fwww.acmemarkets.com%2Fmeal-plans-recipes%2Fbundles%2Fsalmon-bowl; _uetvid=d735eb501cb011f1a8fd8f2794cb7c5a; s_nr30=1774481418916-New; incap_ses_362_2083770=ay7GVnhIl2xde15RQRUGBapexWkAAAAAblRs+QtXwJKLpkm9FEoVkg==; s_sq=sfsafewayprod1%3D%2526c.%2526a.%2526activitymap.%2526page%253Dacmemarkets%25253Aloyalty%25253Aaisle-vs%25253Ameat-seafood%25253Aseafood-favorites%2526link%253DLoad%252520more%2526region%253Dsearch-grid_0%2526pageIDType%253D1%2526.activitymap%2526.a%2526.c%2526pid%253Dacmemarkets%25253Aloyalty%25253Aaisle-vs%25253Ameat-seafood%25253Aseafood-favorites%2526pidt%253D1%2526oid%253DLoad%252520more%2526oidt%253D3%2526ot%253DSUBMIT; nlbi_2083770_2147483392=y9iKSBGewFqLN8QJaKN5ZQAAAAApNyfTKepnWS5DnliBkr6x; reese84=3:Um6Sa3mRcAnh+3ITa9FjwA==:PmU0Vm8qdx4HH8zDCIXBS3qCcFxSw0pY2SvDbCINWhSqme0yNw/jBBLVEa8ZYcDZ35mMrlxvZyjBsuKHLF1GdEE5VET/NCWzebW8eyhMI7qrOCaGWEc8RFxsjSROtEYAdsUpZj8lTY9SiDcSxbsCz9b0Z03aRwTVR/UHih/kmsw5HxB2W/FLacqtFFrzAK2lTCQbqEaVci9CfgYlSJSEzLR+Kvi7TEO+L/jhATbMhgMmffKsrNI8XKFdFt2pESVZz2mX2D3q8mY36ZK4RUMKxqyCqnZV7//5P7pT1Ja8dYyI32LkD0G+i1OviFQ2/GuwPjFokXHBA9XX92dpkTnXcGeLsWwE6Q116T9iMoPW5VPjmlNI+hbN/jZCgj8HXHmZM1Hpm0qrpJH9h3tpmvV6yUozf/m4DkebSy4A2nFICkTAMiDxamOXwdACn8eli+BR6Ild0P9ZKPv1RiJkYZdN3V+T++ro3CuMI0oDHzWu4WzQ2ucRz5NiOKxp58+fgowg6zxMPbSwgqLrnPZ758YZTnoEfr/mYGUfGK+AnW7+aQ5vMeGT1sJMBYCUcjV8F6I0n5M2/0rSjp2uri5Bxa96+Q==:kpAJfuxaqq0kDZ/9EtrMUKMOxr/gYX9srCuoThNrh6M=; _ga_KC35M14HLD=GS2.1.s1774543349$o2$g0$t1774543349$j60$l0$h0; AMCV_A7BF3BC75245ADF20A490D4D%40AdobeOrg=179643557%7CMCIDTS%7C20538%7CMCMID%7C11176332726178312564444395020237210543%7CMCAAMLH-1775148149%7C9%7CMCAAMB-1775148149%7C6G1ynYcLPuiQxYZrsz_pkqfLG9yMXBpb2zX5dvJdYQJzPXImdj0y%7CMCOPTOUT-1774550549s%7CNONE%7CvVersion%7C5.5.0%7CMCCIDH%7C1464169025; SWY_SHARED_SESSION_INFO=%7B%22info%22%3A%7B%22COMMON%22%3A%7B%22Selection%22%3A%22user%22%2C%22preference%22%3A%22J4U%22%2C%22userType%22%3A%22G%22%2C%22zipcode%22%3A%2219711%22%2C%22banner%22%3A%22acmemarkets%22%2C%22siteType%22%3A%22C%22%2C%22customerType%22%3A%22%22%2C%22resolvedBy%22%3A%22%22%7D%2C%22J4U%22%3A%7B%22zipcode%22%3A%2219711%22%2C%22storeId%22%3A%22806%22%7D%2C%22SHOP%22%3A%7B%22zipcode%22%3A%2219711%22%2C%22storeId%22%3A%22806%22%7D%7D%7D; SWY_SYND_USER_INFO=%7B%22storeAddress%22%3A%22%22%2C%22storeZip%22%3A%2219711%22%2C%22storeId%22%3A%22806%22%2C%22preference%22%3A%22J4U%22%7D; mbox=PC#03b8319f82634490994e9117016a6f12.35_0#1837788151|session#873f096bd37f42a3ac8d53bcffa7a971#1774545211; OptanonConsent=isGpcEnabled=0&datestamp=Thu+Mar+26+2026+09%3A42%3A30+GMT-0700+(Pacific+Daylight+Time)&version=202409.1.0&browserGpcFlag=0&isIABGlobal=false&hosts=&consentId=ad8ee733-21b7-4a88-9c98-833650d44dbe&interactionCount=1&isAnonUser=1&landingPath=NotLandingPage&groups=C0001%3A1%2CC0002%3A1%2CC0004%3A1%2CC0003%3A1&AwaitingReconsent=false' \ 63 + -H 'ocp-apim-subscription-key: e914eec9448c4d5eb672debf5011cf8f' 64 + 65 + 66 + curl 'https://www.acmemarkets.com/abs/pub/xapi/wcax/pathway/search?url=https://www.acmemarkets.com&q=&rows=30&start=0&channel=instore&storeid=806&sort=&widget-id=GR-C-Categ-6090cd27&dvid=web-4.1search&visitorId=6b47f549-b8da-48b5-8486-8c750ec1c753&uuid=null&pgm=abs&includeOffer=true&banner=acmemarkets&facet=false' \ 67 + -H 'accept: application/json, text/plain, */*' \ 68 + -H 'accept-language: en-US,en;q=0.9' \ 69 + -b 'abs_gsession=%7B%22info%22%3A%7B%22COMMON%22%3A%7B%22Selection%22%3A%22user%22%2C%22preference%22%3A%22J4U%22%2C%22userType%22%3A%22G%22%2C%22zipcode%22%3A%2219711%22%2C%22banner%22%3A%22acmemarkets%22%2C%22siteType%22%3A%22C%22%2C%22customerType%22%3A%22%22%2C%22resolvedBy%22%3A%22%22%7D%2C%22J4U%22%3A%7B%22zipcode%22%3A%2219711%22%2C%22storeId%22%3A%22806%22%7D%2C%22SHOP%22%3A%7B%22zipcode%22%3A%2219711%22%2C%22storeId%22%3A%22806%22%7D%7D%7D; ACI_S_abs_previouslogin=%7B%22info%22%3A%7B%22COMMON%22%3A%7B%22houseId%22%3A%22729022287633%22%2C%22clubCard%22%3A%2249549812729%22%2C%22userType%22%3A%22G%22%2C%22storeId%22%3A%22806%22%2C%22zipcode%22%3A%2219711%22%2C%22banner%22%3A%22acmemarkets%22%2C%22preference%22%3A%22J4U%22%2C%22isClosed%22%3A%22false%22%2C%22isCrossBannerMFCUser%22%3A%22false%22%2C%22Selection%22%3A%22user%22%2C%22isARenrolled%22%3A%22false%22%2C%22userData%22%3A%7B%7D%2C%22grsSessionId%22%3A%22063024fb-35d7-4da1-b6ee-49398ee1eb93%22%2C%22siteType%22%3A%22C%22%2C%22customerType%22%3A%22%22%2C%22resolvedBy%22%3A%22%22%2C%22multiAccounts%22%3A%22false%22%2C%22emailVerified%22%3A%22true%22%2C%22phoneVerified%22%3A%22false%22%2C%22emailPrompt180%22%3A%22false%22%2C%22phonePrompt180%22%3A%22false%22%2C%22deviceId%22%3A%22%22%7D%2C%22J4U%22%3A%7B%22userType%22%3A%22R%22%2C%22storeId%22%3A%22806%22%2C%22zipcode%22%3A%2219711%22%2C%22address%22%3A%22100%2BSuburban%2BDr%2C%2BNewark%2C%2BDE%2B19711%22%2C%22preferredBanner%22%3A%22acmemarkets%22%2C%22userData%22%3A%7B%22guestStoreId%22%3A%222633%22%2C%22guestZipCode%22%3A%2206840%22%2C%22defaultStore%22%3Afalse%7D%7D%2C%22SHOP%22%3A%7B%22userType%22%3A%22R%22%2C%22storeId%22%3A%22806%22%2C%22zipcode%22%3A%2219711%22%2C%22address%22%3A%22100%2BSuburban%2BDr%2C%2BNewark%2C%2BDE%2B19711%22%2C%22userData%22%3A%7B%22guestStoreId%22%3A%22806%22%2C%22guestZipCode%22%3A%2219711%22%2C%22signInCalled%22%3Afalse%2C%22driveUpAndGoIsEnabled%22%3A%22false%22%2C%22unattendedDeliveryIsEnabled%22%3A%22false%22%2C%22expireTime%22%3A%221774470178000%22%7D%7D%7D%7D; __gads=ID=b14266123a5dffb5:T=1773168126:RT=1774480896:S=ALNI_Mbv6PS2oQlrtLUEeqks0-g1OrRtZQ; __gpi=UID=000013497e5d0cce:T=1773168126:RT=1774480896:S=ALNI_MZTEodi_t-OHCr4WH4t6AaQ9p-Mhw; __eoi=ID=4ac3989bb54cc211:T=1773168126:RT=1774480896:S=AA-Afja0nnjCotgTkMb1VWPM2gGd; signifyd_sessionId=19aHa79e-ee22-P208-b0f0-b8fb-4bcc076_1f86; _gcl_au=1.1.2103349931.1773168127.1395106364.1774481114.1774481114; ACI_S_ECommRedirectURL=https%3A%2F%2Fwww.acmemarkets.com%2Fmeal-plans-recipes%2Fbundles%2Fsalmon-bowl; _uetvid=d735eb501cb011f1a8fd8f2794cb7c5a; s_nr30=1774481418916-New; incap_ses_362_2083770=ay7GVnhIl2xde15RQRUGBapexWkAAAAAblRs+QtXwJKLpkm9FEoVkg==; s_sq=sfsafewayprod1%3D%2526c.%2526a.%2526activitymap.%2526page%253Dacmemarkets%25253Aloyalty%25253Aaisle-vs%25253Ameat-seafood%25253Aseafood-favorites%2526link%253DLoad%252520more%2526region%253Dsearch-grid_0%2526pageIDType%253D1%2526.activitymap%2526.a%2526.c%2526pid%253Dacmemarkets%25253Aloyalty%25253Aaisle-vs%25253Ameat-seafood%25253Aseafood-favorites%2526pidt%253D1%2526oid%253DLoad%252520more%2526oidt%253D3%2526ot%253DSUBMIT; nlbi_2083770_2147483392=y9iKSBGewFqLN8QJaKN5ZQAAAAApNyfTKepnWS5DnliBkr6x; reese84=3:Um6Sa3mRcAnh+3ITa9FjwA==:PmU0Vm8qdx4HH8zDCIXBS3qCcFxSw0pY2SvDbCINWhSqme0yNw/jBBLVEa8ZYcDZ35mMrlxvZyjBsuKHLF1GdEE5VET/NCWzebW8eyhMI7qrOCaGWEc8RFxsjSROtEYAdsUpZj8lTY9SiDcSxbsCz9b0Z03aRwTVR/UHih/kmsw5HxB2W/FLacqtFFrzAK2lTCQbqEaVci9CfgYlSJSEzLR+Kvi7TEO+L/jhATbMhgMmffKsrNI8XKFdFt2pESVZz2mX2D3q8mY36ZK4RUMKxqyCqnZV7//5P7pT1Ja8dYyI32LkD0G+i1OviFQ2/GuwPjFokXHBA9XX92dpkTnXcGeLsWwE6Q116T9iMoPW5VPjmlNI+hbN/jZCgj8HXHmZM1Hpm0qrpJH9h3tpmvV6yUozf/m4DkebSy4A2nFICkTAMiDxamOXwdACn8eli+BR6Ild0P9ZKPv1RiJkYZdN3V+T++ro3CuMI0oDHzWu4WzQ2ucRz5NiOKxp58+fgowg6zxMPbSwgqLrnPZ758YZTnoEfr/mYGUfGK+AnW7+aQ5vMeGT1sJMBYCUcjV8F6I0n5M2/0rSjp2uri5Bxa96+Q==:kpAJfuxaqq0kDZ/9EtrMUKMOxr/gYX9srCuoThNrh6M=; _ga_KC35M14HLD=GS2.1.s1774543349$o2$g0$t1774543349$j60$l0$h0; AMCV_A7BF3BC75245ADF20A490D4D%40AdobeOrg=179643557%7CMCIDTS%7C20538%7CMCMID%7C11176332726178312564444395020237210543%7CMCAAMLH-1775148149%7C9%7CMCAAMB-1775148149%7C6G1ynYcLPuiQxYZrsz_pkqfLG9yMXBpb2zX5dvJdYQJzPXImdj0y%7CMCOPTOUT-1774550549s%7CNONE%7CvVersion%7C5.5.0%7CMCCIDH%7C1464169025; SWY_SHARED_SESSION_INFO=%7B%22info%22%3A%7B%22COMMON%22%3A%7B%22Selection%22%3A%22user%22%2C%22preference%22%3A%22J4U%22%2C%22userType%22%3A%22G%22%2C%22zipcode%22%3A%2219711%22%2C%22banner%22%3A%22acmemarkets%22%2C%22siteType%22%3A%22C%22%2C%22customerType%22%3A%22%22%2C%22resolvedBy%22%3A%22%22%7D%2C%22J4U%22%3A%7B%22zipcode%22%3A%2219711%22%2C%22storeId%22%3A%22806%22%7D%2C%22SHOP%22%3A%7B%22zipcode%22%3A%2219711%22%2C%22storeId%22%3A%22806%22%7D%7D%7D; SWY_SYND_USER_INFO=%7B%22storeAddress%22%3A%22%22%2C%22storeZip%22%3A%2219711%22%2C%22storeId%22%3A%22806%22%2C%22preference%22%3A%22J4U%22%7D; mbox=PC#03b8319f82634490994e9117016a6f12.35_0#1837788151|session#873f096bd37f42a3ac8d53bcffa7a971#1774545211; OptanonConsent=isGpcEnabled=0&datestamp=Thu+Mar+26+2026+09%3A42%3A30+GMT-0700+(Pacific+Daylight+Time)&version=202409.1.0&browserGpcFlag=0&isIABGlobal=false&hosts=&consentId=ad8ee733-21b7-4a88-9c98-833650d44dbe&interactionCount=1&isAnonUser=1&landingPath=NotLandingPage&groups=C0001%3A1%2CC0002%3A1%2CC0004%3A1%2CC0003%3A1&AwaitingReconsent=false' \ 70 + -H 'ocp-apim-subscription-key: e914eec9448c4d5eb672debf5011cf8f' 71 + 72 + 73 + 74 + 75 + curl 'https://www.acmemarkets.com/abs/pub/xapi/wcax/pathway/search?url=https://www.acmemarkets.com&q=&rows=30&start=0&channel=instore&storeid=806&sort=&widget-id=GR-C-Categ-6090cd27&dvid=web-4.1search&visitorId=6b47f549-b8da-48b5-8486-8c750ec1c753&uuid=null&pgm=abs&includeOffer=true&banner=acmemarkets&facet=false' \ 76 + -H 'accept: application/json, text/plain, */*' \ 77 + -H 'accept-language: en-US,en;q=0.9' \ 78 + -b 'JSESSIONID=node01rxmiasw55sif1jysb15t32zj7948572.node0; visid_incap_2083770=K/cZdXpDRvK/rQXq4rM2DPplsGkAAAAAQkIPAAAAAACAUEXDAUusQBLT44imoibKTHDVrlw+wJDM; SAFEWAY_MODAL_LINK=; _ga_862QXSJ5RF=GS2.2.s1774475776$o3$g0$t1774475776$j60$l0$h0; _ga_PH9HKJ88MQ=GS2.2.s1774475776$o3$g0$t1774475776$j60$l0$h0; abs_gsession=%7B%22info%22%3A%7B%22COMMON%22%3A%7B%22Selection%22%3A%22user%22%2C%22preference%22%3A%22J4U%22%2C%22userType%22%3A%22G%22%2C%22zipcode%22%3A%2219711%22%2C%22banner%22%3A%22acmemarkets%22%2C%22siteType%22%3A%22C%22%2C%22customerType%22%3A%22%22%2C%22resolvedBy%22%3A%22%22%7D%2C%22J4U%22%3A%7B%22zipcode%22%3A%2219711%22%2C%22storeId%22%3A%22806%22%7D%2C%22SHOP%22%3A%7B%22zipcode%22%3A%2219711%22%2C%22storeId%22%3A%22806%22%7D%7D%7D; ACI_S_abs_previouslogin=%7B%22info%22%3A%7B%22COMMON%22%3A%7B%22houseId%22%3A%22729022287633%22%2C%22clubCard%22%3A%2249549812729%22%2C%22userType%22%3A%22G%22%2C%22storeId%22%3A%22806%22%2C%22zipcode%22%3A%2219711%22%2C%22banner%22%3A%22acmemarkets%22%2C%22preference%22%3A%22J4U%22%2C%22isClosed%22%3A%22false%22%2C%22isCrossBannerMFCUser%22%3A%22false%22%2C%22Selection%22%3A%22user%22%2C%22isARenrolled%22%3A%22false%22%2C%22userData%22%3A%7B%7D%2C%22grsSessionId%22%3A%22063024fb-35d7-4da1-b6ee-49398ee1eb93%22%2C%22siteType%22%3A%22C%22%2C%22customerType%22%3A%22%22%2C%22resolvedBy%22%3A%22%22%2C%22multiAccounts%22%3A%22false%22%2C%22emailVerified%22%3A%22true%22%2C%22phoneVerified%22%3A%22false%22%2C%22emailPrompt180%22%3A%22false%22%2C%22phonePrompt180%22%3A%22false%22%2C%22deviceId%22%3A%22%22%7D%2C%22J4U%22%3A%7B%22userType%22%3A%22R%22%2C%22storeId%22%3A%22806%22%2C%22zipcode%22%3A%2219711%22%2C%22address%22%3A%22100%2BSuburban%2BDr%2C%2BNewark%2C%2BDE%2B19711%22%2C%22preferredBanner%22%3A%22acmemarkets%22%2C%22userData%22%3A%7B%22guestStoreId%22%3A%222633%22%2C%22guestZipCode%22%3A%2206840%22%2C%22defaultStore%22%3Afalse%7D%7D%2C%22SHOP%22%3A%7B%22userType%22%3A%22R%22%2C%22storeId%22%3A%22806%22%2C%22zipcode%22%3A%2219711%22%2C%22address%22%3A%22100%2BSuburban%2BDr%2C%2BNewark%2C%2BDE%2B19711%22%2C%22userData%22%3A%7B%22guestStoreId%22%3A%22806%22%2C%22guestZipCode%22%3A%2219711%22%2C%22signInCalled%22%3Afalse%2C%22driveUpAndGoIsEnabled%22%3A%22false%22%2C%22unattendedDeliveryIsEnabled%22%3A%22false%22%2C%22expireTime%22%3A%221774470178000%22%7D%7D%7D%7D; __gads=ID=b14266123a5dffb5:T=1773168126:RT=1774480896:S=ALNI_Mbv6PS2oQlrtLUEeqks0-g1OrRtZQ; __gpi=UID=000013497e5d0cce:T=1773168126:RT=1774480896:S=ALNI_MZTEodi_t-OHCr4WH4t6AaQ9p-Mhw; __eoi=ID=4ac3989bb54cc211:T=1773168126:RT=1774480896:S=AA-Afja0nnjCotgTkMb1VWPM2gGd; signifyd_sessionId=19aHa79e-ee22-P208-b0f0-b8fb-4bcc076_1f86; _gcl_au=1.1.2103349931.1773168127.1395106364.1774481114.1774481114; ACI_S_ECommRedirectURL=https%3A%2F%2Fwww.acmemarkets.com%2Fmeal-plans-recipes%2Fbundles%2Fsalmon-bowl; _uetvid=d735eb501cb011f1a8fd8f2794cb7c5a; s_nr30=1774481418916-New; incap_ses_362_2083770=ay7GVnhIl2xde15RQRUGBapexWkAAAAAblRs+QtXwJKLpkm9FEoVkg==; s_sq=sfsafewayprod1%3D%2526c.%2526a.%2526activitymap.%2526page%253Dacmemarkets%25253Aloyalty%25253Aaisle-vs%25253Ameat-seafood%25253Aseafood-favorites%2526link%253DLoad%252520more%2526region%253Dsearch-grid_0%2526pageIDType%253D1%2526.activitymap%2526.a%2526.c%2526pid%253Dacmemarkets%25253Aloyalty%25253Aaisle-vs%25253Ameat-seafood%25253Aseafood-favorites%2526pidt%253D1%2526oid%253DLoad%252520more%2526oidt%253D3%2526ot%253DSUBMIT; nlbi_2083770_2147483392=y9iKSBGewFqLN8QJaKN5ZQAAAAApNyfTKepnWS5DnliBkr6x; reese84=3:Um6Sa3mRcAnh+3ITa9FjwA==:PmU0Vm8qdx4HH8zDCIXBS3qCcFxSw0pY2SvDbCINWhSqme0yNw/jBBLVEa8ZYcDZ35mMrlxvZyjBsuKHLF1GdEE5VET/NCWzebW8eyhMI7qrOCaGWEc8RFxsjSROtEYAdsUpZj8lTY9SiDcSxbsCz9b0Z03aRwTVR/UHih/kmsw5HxB2W/FLacqtFFrzAK2lTCQbqEaVci9CfgYlSJSEzLR+Kvi7TEO+L/jhATbMhgMmffKsrNI8XKFdFt2pESVZz2mX2D3q8mY36ZK4RUMKxqyCqnZV7//5P7pT1Ja8dYyI32LkD0G+i1OviFQ2/GuwPjFokXHBA9XX92dpkTnXcGeLsWwE6Q116T9iMoPW5VPjmlNI+hbN/jZCgj8HXHmZM1Hpm0qrpJH9h3tpmvV6yUozf/m4DkebSy4A2nFICkTAMiDxamOXwdACn8eli+BR6Ild0P9ZKPv1RiJkYZdN3V+T++ro3CuMI0oDHzWu4WzQ2ucRz5NiOKxp58+fgowg6zxMPbSwgqLrnPZ758YZTnoEfr/mYGUfGK+AnW7+aQ5vMeGT1sJMBYCUcjV8F6I0n5M2/0rSjp2uri5Bxa96+Q==:kpAJfuxaqq0kDZ/9EtrMUKMOxr/gYX9srCuoThNrh6M=; _ga_KC35M14HLD=GS2.1.s1774543349$o2$g0$t1774543349$j60$l0$h0; AMCV_A7BF3BC75245ADF20A490D4D%40AdobeOrg=179643557%7CMCIDTS%7C20538%7CMCMID%7C11176332726178312564444395020237210543%7CMCAAMLH-1775148149%7C9%7CMCAAMB-1775148149%7C6G1ynYcLPuiQxYZrsz_pkqfLG9yMXBpb2zX5dvJdYQJzPXImdj0y%7CMCOPTOUT-1774550549s%7CNONE%7CvVersion%7C5.5.0%7CMCCIDH%7C1464169025; SWY_SHARED_SESSION_INFO=%7B%22info%22%3A%7B%22COMMON%22%3A%7B%22Selection%22%3A%22user%22%2C%22preference%22%3A%22J4U%22%2C%22userType%22%3A%22G%22%2C%22zipcode%22%3A%2219711%22%2C%22banner%22%3A%22acmemarkets%22%2C%22siteType%22%3A%22C%22%2C%22customerType%22%3A%22%22%2C%22resolvedBy%22%3A%22%22%7D%2C%22J4U%22%3A%7B%22zipcode%22%3A%2219711%22%2C%22storeId%22%3A%22806%22%7D%2C%22SHOP%22%3A%7B%22zipcode%22%3A%2219711%22%2C%22storeId%22%3A%22806%22%7D%7D%7D; SWY_SYND_USER_INFO=%7B%22storeAddress%22%3A%22%22%2C%22storeZip%22%3A%2219711%22%2C%22storeId%22%3A%22806%22%2C%22preference%22%3A%22J4U%22%7D; mbox=PC#03b8319f82634490994e9117016a6f12.35_0#1837788151|session#873f096bd37f42a3ac8d53bcffa7a971#1774545211; OptanonConsent=isGpcEnabled=0&datestamp=Thu+Mar+26+2026+09%3A42%3A30+GMT-0700+(Pacific+Daylight+Time)&version=202409.1.0&browserGpcFlag=0&isIABGlobal=false&hosts=&consentId=ad8ee733-21b7-4a88-9c98-833650d44dbe&interactionCount=1&isAnonUser=1&landingPath=NotLandingPage&groups=C0001%3A1%2CC0002%3A1%2CC0004%3A1%2CC0003%3A1&AwaitingReconsent=false' \ 79 + -H 'ocp-apim-subscription-key: e914eec9448c4d5eb672debf5011cf8f' 80 + 81 + 82 + 83 + 84 + curl 'https://www.acmemarkets.com/abs/pub/xapi/wcax/pathway/search?url=https://www.acmemarkets.com&q=&rows=30&start=0&channel=instore&storeid=806&sort=&widget-id=GR-C-Categ-6090cd27&dvid=web-4.1search&visitorId=6b47f549-b8da-48b5-8486-8c750ec1c753&uuid=null&pgm=abs&includeOffer=true&banner=acmemarkets&facet=false' \ 85 + -H 'accept: application/json, text/plain, */*' \ 86 + -H 'accept-language: en-US,en;q=0.9' \ 87 + -b 'AMCVS_A7BF3BC75245ADF20A490D4D%40AdobeOrg=1; ACI_S_ECommBanner=acmemarkets; ACI_S_ECommSignInCount=0; at_check=true; nlbi_2083770=EdQXPm5EWEU5GYXsaKN5ZQAAAAB8ymloJOMURv6MdOpYLVb4; incap_ses_397_2083770=BbMTaeIMqTa9uC/Qim2CBTulwWkAAAAAcd8J0nW048yTn+Z7+cW2ug==; ACI_S_ECommReInit=net|slot; JSESSIONID=node01rxmiasw55sif1jysb15t32zj7948572.node0; visid_incap_2083770=K/cZdXpDRvK/rQXq4rM2DPplsGkAAAAAQkIPAAAAAACAUEXDAUusQBLT44imoibKTHDVrlw+wJDM; SAFEWAY_MODAL_LINK=; _ga_862QXSJ5RF=GS2.2.s1774475776$o3$g0$t1774475776$j60$l0$h0; _ga_PH9HKJ88MQ=GS2.2.s1774475776$o3$g0$t1774475776$j60$l0$h0; abs_gsession=%7B%22info%22%3A%7B%22COMMON%22%3A%7B%22Selection%22%3A%22user%22%2C%22preference%22%3A%22J4U%22%2C%22userType%22%3A%22G%22%2C%22zipcode%22%3A%2219711%22%2C%22banner%22%3A%22acmemarkets%22%2C%22siteType%22%3A%22C%22%2C%22customerType%22%3A%22%22%2C%22resolvedBy%22%3A%22%22%7D%2C%22J4U%22%3A%7B%22zipcode%22%3A%2219711%22%2C%22storeId%22%3A%22806%22%7D%2C%22SHOP%22%3A%7B%22zipcode%22%3A%2219711%22%2C%22storeId%22%3A%22806%22%7D%7D%7D; ACI_S_abs_previouslogin=%7B%22info%22%3A%7B%22COMMON%22%3A%7B%22houseId%22%3A%22729022287633%22%2C%22clubCard%22%3A%2249549812729%22%2C%22userType%22%3A%22G%22%2C%22storeId%22%3A%22806%22%2C%22zipcode%22%3A%2219711%22%2C%22banner%22%3A%22acmemarkets%22%2C%22preference%22%3A%22J4U%22%2C%22isClosed%22%3A%22false%22%2C%22isCrossBannerMFCUser%22%3A%22false%22%2C%22Selection%22%3A%22user%22%2C%22isARenrolled%22%3A%22false%22%2C%22userData%22%3A%7B%7D%2C%22grsSessionId%22%3A%22063024fb-35d7-4da1-b6ee-49398ee1eb93%22%2C%22siteType%22%3A%22C%22%2C%22customerType%22%3A%22%22%2C%22resolvedBy%22%3A%22%22%2C%22multiAccounts%22%3A%22false%22%2C%22emailVerified%22%3A%22true%22%2C%22phoneVerified%22%3A%22false%22%2C%22emailPrompt180%22%3A%22false%22%2C%22phonePrompt180%22%3A%22false%22%2C%22deviceId%22%3A%22%22%7D%2C%22J4U%22%3A%7B%22userType%22%3A%22R%22%2C%22storeId%22%3A%22806%22%2C%22zipcode%22%3A%2219711%22%2C%22address%22%3A%22100%2BSuburban%2BDr%2C%2BNewark%2C%2BDE%2B19711%22%2C%22preferredBanner%22%3A%22acmemarkets%22%2C%22userData%22%3A%7B%22guestStoreId%22%3A%222633%22%2C%22guestZipCode%22%3A%2206840%22%2C%22defaultStore%22%3Afalse%7D%7D%2C%22SHOP%22%3A%7B%22userType%22%3A%22R%22%2C%22storeId%22%3A%22806%22%2C%22zipcode%22%3A%2219711%22%2C%22address%22%3A%22100%2BSuburban%2BDr%2C%2BNewark%2C%2BDE%2B19711%22%2C%22userData%22%3A%7B%22guestStoreId%22%3A%22806%22%2C%22guestZipCode%22%3A%2219711%22%2C%22signInCalled%22%3Afalse%2C%22driveUpAndGoIsEnabled%22%3A%22false%22%2C%22unattendedDeliveryIsEnabled%22%3A%22false%22%2C%22expireTime%22%3A%221774470178000%22%7D%7D%7D%7D; __gads=ID=b14266123a5dffb5:T=1773168126:RT=1774480896:S=ALNI_Mbv6PS2oQlrtLUEeqks0-g1OrRtZQ; __gpi=UID=000013497e5d0cce:T=1773168126:RT=1774480896:S=ALNI_MZTEodi_t-OHCr4WH4t6AaQ9p-Mhw; __eoi=ID=4ac3989bb54cc211:T=1773168126:RT=1774480896:S=AA-Afja0nnjCotgTkMb1VWPM2gGd; signifyd_sessionId=19aHa79e-ee22-P208-b0f0-b8fb-4bcc076_1f86; _gcl_au=1.1.2103349931.1773168127.1395106364.1774481114.1774481114; ACI_S_ECommRedirectURL=https%3A%2F%2Fwww.acmemarkets.com%2Fmeal-plans-recipes%2Fbundles%2Fsalmon-bowl; _uetvid=d735eb501cb011f1a8fd8f2794cb7c5a; s_nr30=1774481418916-New; incap_ses_362_2083770=ay7GVnhIl2xde15RQRUGBapexWkAAAAAblRs+QtXwJKLpkm9FEoVkg==; s_sq=sfsafewayprod1%3D%2526c.%2526a.%2526activitymap.%2526page%253Dacmemarkets%25253Aloyalty%25253Aaisle-vs%25253Ameat-seafood%25253Aseafood-favorites%2526link%253DLoad%252520more%2526region%253Dsearch-grid_0%2526pageIDType%253D1%2526.activitymap%2526.a%2526.c%2526pid%253Dacmemarkets%25253Aloyalty%25253Aaisle-vs%25253Ameat-seafood%25253Aseafood-favorites%2526pidt%253D1%2526oid%253DLoad%252520more%2526oidt%253D3%2526ot%253DSUBMIT; nlbi_2083770_2147483392=y9iKSBGewFqLN8QJaKN5ZQAAAAApNyfTKepnWS5DnliBkr6x; reese84=3:Um6Sa3mRcAnh+3ITa9FjwA==:PmU0Vm8qdx4HH8zDCIXBS3qCcFxSw0pY2SvDbCINWhSqme0yNw/jBBLVEa8ZYcDZ35mMrlxvZyjBsuKHLF1GdEE5VET/NCWzebW8eyhMI7qrOCaGWEc8RFxsjSROtEYAdsUpZj8lTY9SiDcSxbsCz9b0Z03aRwTVR/UHih/kmsw5HxB2W/FLacqtFFrzAK2lTCQbqEaVci9CfgYlSJSEzLR+Kvi7TEO+L/jhATbMhgMmffKsrNI8XKFdFt2pESVZz2mX2D3q8mY36ZK4RUMKxqyCqnZV7//5P7pT1Ja8dYyI32LkD0G+i1OviFQ2/GuwPjFokXHBA9XX92dpkTnXcGeLsWwE6Q116T9iMoPW5VPjmlNI+hbN/jZCgj8HXHmZM1Hpm0qrpJH9h3tpmvV6yUozf/m4DkebSy4A2nFICkTAMiDxamOXwdACn8eli+BR6Ild0P9ZKPv1RiJkYZdN3V+T++ro3CuMI0oDHzWu4WzQ2ucRz5NiOKxp58+fgowg6zxMPbSwgqLrnPZ758YZTnoEfr/mYGUfGK+AnW7+aQ5vMeGT1sJMBYCUcjV8F6I0n5M2/0rSjp2uri5Bxa96+Q==:kpAJfuxaqq0kDZ/9EtrMUKMOxr/gYX9srCuoThNrh6M=; _ga_KC35M14HLD=GS2.1.s1774543349$o2$g0$t1774543349$j60$l0$h0; AMCV_A7BF3BC75245ADF20A490D4D%40AdobeOrg=179643557%7CMCIDTS%7C20538%7CMCMID%7C11176332726178312564444395020237210543%7CMCAAMLH-1775148149%7C9%7CMCAAMB-1775148149%7C6G1ynYcLPuiQxYZrsz_pkqfLG9yMXBpb2zX5dvJdYQJzPXImdj0y%7CMCOPTOUT-1774550549s%7CNONE%7CvVersion%7C5.5.0%7CMCCIDH%7C1464169025; SWY_SHARED_SESSION_INFO=%7B%22info%22%3A%7B%22COMMON%22%3A%7B%22Selection%22%3A%22user%22%2C%22preference%22%3A%22J4U%22%2C%22userType%22%3A%22G%22%2C%22zipcode%22%3A%2219711%22%2C%22banner%22%3A%22acmemarkets%22%2C%22siteType%22%3A%22C%22%2C%22customerType%22%3A%22%22%2C%22resolvedBy%22%3A%22%22%7D%2C%22J4U%22%3A%7B%22zipcode%22%3A%2219711%22%2C%22storeId%22%3A%22806%22%7D%2C%22SHOP%22%3A%7B%22zipcode%22%3A%2219711%22%2C%22storeId%22%3A%22806%22%7D%7D%7D; SWY_SYND_USER_INFO=%7B%22storeAddress%22%3A%22%22%2C%22storeZip%22%3A%2219711%22%2C%22storeId%22%3A%22806%22%2C%22preference%22%3A%22J4U%22%7D; mbox=PC#03b8319f82634490994e9117016a6f12.35_0#1837788151|session#873f096bd37f42a3ac8d53bcffa7a971#1774545211; OptanonConsent=isGpcEnabled=0&datestamp=Thu+Mar+26+2026+09%3A42%3A30+GMT-0700+(Pacific+Daylight+Time)&version=202409.1.0&browserGpcFlag=0&isIABGlobal=false&hosts=&consentId=ad8ee733-21b7-4a88-9c98-833650d44dbe&interactionCount=1&isAnonUser=1&landingPath=NotLandingPage&groups=C0001%3A1%2CC0002%3A1%2CC0004%3A1%2CC0003%3A1&AwaitingReconsent=false' \ 88 + -H 'ocp-apim-subscription-key: e914eec9448c4d5eb672debf5011cf8f' 89 + 90 + 91 + 92 + curl 'https://www.acmemarkets.com/abs/pub/xapi/wcax/pathway/search?url=https://www.acmemarkets.com&q=&rows=30&start=0&channel=instore&storeid=806&sort=&widget-id=GR-C-Categ-6090cd27&dvid=web-4.1search&visitorId=6b47f549-b8da-48b5-8486-8c750ec1c753&uuid=null&pgm=abs&includeOffer=true&banner=acmemarkets&facet=false' \ 93 + -H 'accept: application/json, text/plain, */*' \ 94 + -H 'accept-language: en-US,en;q=0.9' \ 95 + -b 'absVisitorId=6b47f549-b8da-48b5-8486-8c750ec1c753; akacd_PR-bg-www-prod-acmemarkets=3951498377~rv=59~id=c9438d2c2e61873bdcd74214268c738c; AMCVS_A7BF3BC75245ADF20A490D4D%40AdobeOrg=1; ACI_S_ECommBanner=acmemarkets; ACI_S_ECommSignInCount=0; at_check=true; nlbi_2083770=EdQXPm5EWEU5GYXsaKN5ZQAAAAB8ymloJOMURv6MdOpYLVb4; incap_ses_397_2083770=BbMTaeIMqTa9uC/Qim2CBTulwWkAAAAAcd8J0nW048yTn+Z7+cW2ug==; ACI_S_ECommReInit=net|slot; JSESSIONID=node01rxmiasw55sif1jysb15t32zj7948572.node0; visid_incap_2083770=K/cZdXpDRvK/rQXq4rM2DPplsGkAAAAAQkIPAAAAAACAUEXDAUusQBLT44imoibKTHDVrlw+wJDM; SAFEWAY_MODAL_LINK=; _ga_862QXSJ5RF=GS2.2.s1774475776$o3$g0$t1774475776$j60$l0$h0; _ga_PH9HKJ88MQ=GS2.2.s1774475776$o3$g0$t1774475776$j60$l0$h0; abs_gsession=%7B%22info%22%3A%7B%22COMMON%22%3A%7B%22Selection%22%3A%22user%22%2C%22preference%22%3A%22J4U%22%2C%22userType%22%3A%22G%22%2C%22zipcode%22%3A%2219711%22%2C%22banner%22%3A%22acmemarkets%22%2C%22siteType%22%3A%22C%22%2C%22customerType%22%3A%22%22%2C%22resolvedBy%22%3A%22%22%7D%2C%22J4U%22%3A%7B%22zipcode%22%3A%2219711%22%2C%22storeId%22%3A%22806%22%7D%2C%22SHOP%22%3A%7B%22zipcode%22%3A%2219711%22%2C%22storeId%22%3A%22806%22%7D%7D%7D; ACI_S_abs_previouslogin=%7B%22info%22%3A%7B%22COMMON%22%3A%7B%22houseId%22%3A%22729022287633%22%2C%22clubCard%22%3A%2249549812729%22%2C%22userType%22%3A%22G%22%2C%22storeId%22%3A%22806%22%2C%22zipcode%22%3A%2219711%22%2C%22banner%22%3A%22acmemarkets%22%2C%22preference%22%3A%22J4U%22%2C%22isClosed%22%3A%22false%22%2C%22isCrossBannerMFCUser%22%3A%22false%22%2C%22Selection%22%3A%22user%22%2C%22isARenrolled%22%3A%22false%22%2C%22userData%22%3A%7B%7D%2C%22grsSessionId%22%3A%22063024fb-35d7-4da1-b6ee-49398ee1eb93%22%2C%22siteType%22%3A%22C%22%2C%22customerType%22%3A%22%22%2C%22resolvedBy%22%3A%22%22%2C%22multiAccounts%22%3A%22false%22%2C%22emailVerified%22%3A%22true%22%2C%22phoneVerified%22%3A%22false%22%2C%22emailPrompt180%22%3A%22false%22%2C%22phonePrompt180%22%3A%22false%22%2C%22deviceId%22%3A%22%22%7D%2C%22J4U%22%3A%7B%22userType%22%3A%22R%22%2C%22storeId%22%3A%22806%22%2C%22zipcode%22%3A%2219711%22%2C%22address%22%3A%22100%2BSuburban%2BDr%2C%2BNewark%2C%2BDE%2B19711%22%2C%22preferredBanner%22%3A%22acmemarkets%22%2C%22userData%22%3A%7B%22guestStoreId%22%3A%222633%22%2C%22guestZipCode%22%3A%2206840%22%2C%22defaultStore%22%3Afalse%7D%7D%2C%22SHOP%22%3A%7B%22userType%22%3A%22R%22%2C%22storeId%22%3A%22806%22%2C%22zipcode%22%3A%2219711%22%2C%22address%22%3A%22100%2BSuburban%2BDr%2C%2BNewark%2C%2BDE%2B19711%22%2C%22userData%22%3A%7B%22guestStoreId%22%3A%22806%22%2C%22guestZipCode%22%3A%2219711%22%2C%22signInCalled%22%3Afalse%2C%22driveUpAndGoIsEnabled%22%3A%22false%22%2C%22unattendedDeliveryIsEnabled%22%3A%22false%22%2C%22expireTime%22%3A%221774470178000%22%7D%7D%7D%7D; __gads=ID=b14266123a5dffb5:T=1773168126:RT=1774480896:S=ALNI_Mbv6PS2oQlrtLUEeqks0-g1OrRtZQ; __gpi=UID=000013497e5d0cce:T=1773168126:RT=1774480896:S=ALNI_MZTEodi_t-OHCr4WH4t6AaQ9p-Mhw; __eoi=ID=4ac3989bb54cc211:T=1773168126:RT=1774480896:S=AA-Afja0nnjCotgTkMb1VWPM2gGd; signifyd_sessionId=19aHa79e-ee22-P208-b0f0-b8fb-4bcc076_1f86; _gcl_au=1.1.2103349931.1773168127.1395106364.1774481114.1774481114; ACI_S_ECommRedirectURL=https%3A%2F%2Fwww.acmemarkets.com%2Fmeal-plans-recipes%2Fbundles%2Fsalmon-bowl; _uetvid=d735eb501cb011f1a8fd8f2794cb7c5a; s_nr30=1774481418916-New; incap_ses_362_2083770=ay7GVnhIl2xde15RQRUGBapexWkAAAAAblRs+QtXwJKLpkm9FEoVkg==; s_sq=sfsafewayprod1%3D%2526c.%2526a.%2526activitymap.%2526page%253Dacmemarkets%25253Aloyalty%25253Aaisle-vs%25253Ameat-seafood%25253Aseafood-favorites%2526link%253DLoad%252520more%2526region%253Dsearch-grid_0%2526pageIDType%253D1%2526.activitymap%2526.a%2526.c%2526pid%253Dacmemarkets%25253Aloyalty%25253Aaisle-vs%25253Ameat-seafood%25253Aseafood-favorites%2526pidt%253D1%2526oid%253DLoad%252520more%2526oidt%253D3%2526ot%253DSUBMIT; nlbi_2083770_2147483392=y9iKSBGewFqLN8QJaKN5ZQAAAAApNyfTKepnWS5DnliBkr6x; reese84=3:Um6Sa3mRcAnh+3ITa9FjwA==:PmU0Vm8qdx4HH8zDCIXBS3qCcFxSw0pY2SvDbCINWhSqme0yNw/jBBLVEa8ZYcDZ35mMrlxvZyjBsuKHLF1GdEE5VET/NCWzebW8eyhMI7qrOCaGWEc8RFxsjSROtEYAdsUpZj8lTY9SiDcSxbsCz9b0Z03aRwTVR/UHih/kmsw5HxB2W/FLacqtFFrzAK2lTCQbqEaVci9CfgYlSJSEzLR+Kvi7TEO+L/jhATbMhgMmffKsrNI8XKFdFt2pESVZz2mX2D3q8mY36ZK4RUMKxqyCqnZV7//5P7pT1Ja8dYyI32LkD0G+i1OviFQ2/GuwPjFokXHBA9XX92dpkTnXcGeLsWwE6Q116T9iMoPW5VPjmlNI+hbN/jZCgj8HXHmZM1Hpm0qrpJH9h3tpmvV6yUozf/m4DkebSy4A2nFICkTAMiDxamOXwdACn8eli+BR6Ild0P9ZKPv1RiJkYZdN3V+T++ro3CuMI0oDHzWu4WzQ2ucRz5NiOKxp58+fgowg6zxMPbSwgqLrnPZ758YZTnoEfr/mYGUfGK+AnW7+aQ5vMeGT1sJMBYCUcjV8F6I0n5M2/0rSjp2uri5Bxa96+Q==:kpAJfuxaqq0kDZ/9EtrMUKMOxr/gYX9srCuoThNrh6M=; _ga_KC35M14HLD=GS2.1.s1774543349$o2$g0$t1774543349$j60$l0$h0; AMCV_A7BF3BC75245ADF20A490D4D%40AdobeOrg=179643557%7CMCIDTS%7C20538%7CMCMID%7C11176332726178312564444395020237210543%7CMCAAMLH-1775148149%7C9%7CMCAAMB-1775148149%7C6G1ynYcLPuiQxYZrsz_pkqfLG9yMXBpb2zX5dvJdYQJzPXImdj0y%7CMCOPTOUT-1774550549s%7CNONE%7CvVersion%7C5.5.0%7CMCCIDH%7C1464169025; SWY_SHARED_SESSION_INFO=%7B%22info%22%3A%7B%22COMMON%22%3A%7B%22Selection%22%3A%22user%22%2C%22preference%22%3A%22J4U%22%2C%22userType%22%3A%22G%22%2C%22zipcode%22%3A%2219711%22%2C%22banner%22%3A%22acmemarkets%22%2C%22siteType%22%3A%22C%22%2C%22customerType%22%3A%22%22%2C%22resolvedBy%22%3A%22%22%7D%2C%22J4U%22%3A%7B%22zipcode%22%3A%2219711%22%2C%22storeId%22%3A%22806%22%7D%2C%22SHOP%22%3A%7B%22zipcode%22%3A%2219711%22%2C%22storeId%22%3A%22806%22%7D%7D%7D; SWY_SYND_USER_INFO=%7B%22storeAddress%22%3A%22%22%2C%22storeZip%22%3A%2219711%22%2C%22storeId%22%3A%22806%22%2C%22preference%22%3A%22J4U%22%7D; mbox=PC#03b8319f82634490994e9117016a6f12.35_0#1837788151|session#873f096bd37f42a3ac8d53bcffa7a971#1774545211; OptanonConsent=isGpcEnabled=0&datestamp=Thu+Mar+26+2026+09%3A42%3A30+GMT-0700+(Pacific+Daylight+Time)&version=202409.1.0&browserGpcFlag=0&isIABGlobal=false&hosts=&consentId=ad8ee733-21b7-4a88-9c98-833650d44dbe&interactionCount=1&isAnonUser=1&landingPath=NotLandingPage&groups=C0001%3A1%2CC0002%3A1%2CC0004%3A1%2CC0003%3A1&AwaitingReconsent=false' \ 96 + -H 'ocp-apim-subscription-key: e914eec9448c4d5eb672debf5011cf8f' 97 + 98 + 99 + 100 + curl 'https://www.acmemarkets.com/abs/pub/xapi/wcax/pathway/search?url=https://www.acmemarkets.com&q=&rows=30&start=0&channel=instore&storeid=806&sort=&widget-id=GR-C-Categ-6090cd27&dvid=web-4.1search&visitorId=6b47f549-b8da-48b5-8486-8c750ec1c753&uuid=null&pgm=abs&includeOffer=true&banner=acmemarkets&facet=false' \ 101 + -H 'accept: application/json, text/plain, */*' \ 102 + -H 'accept-language: en-US,en;q=0.9' \ 103 + -b '_pin_unauth=dWlkPU56ZGpNR1psWmprdE1XTTFOaTAwTjJRNUxUazJZVFF0WXpsaE1UUm1ZbUZqTkRjNQ; absVisitorId=6b47f549-b8da-48b5-8486-8c750ec1c753; akacd_PR-bg-www-prod-acmemarkets=3951498377~rv=59~id=c9438d2c2e61873bdcd74214268c738c; AMCVS_A7BF3BC75245ADF20A490D4D%40AdobeOrg=1; ACI_S_ECommBanner=acmemarkets; ACI_S_ECommSignInCount=0; at_check=true; nlbi_2083770=EdQXPm5EWEU5GYXsaKN5ZQAAAAB8ymloJOMURv6MdOpYLVb4; incap_ses_397_2083770=BbMTaeIMqTa9uC/Qim2CBTulwWkAAAAAcd8J0nW048yTn+Z7+cW2ug==; ACI_S_ECommReInit=net|slot; JSESSIONID=node01rxmiasw55sif1jysb15t32zj7948572.node0; visid_incap_2083770=K/cZdXpDRvK/rQXq4rM2DPplsGkAAAAAQkIPAAAAAACAUEXDAUusQBLT44imoibKTHDVrlw+wJDM; SAFEWAY_MODAL_LINK=; _ga_862QXSJ5RF=GS2.2.s1774475776$o3$g0$t1774475776$j60$l0$h0; _ga_PH9HKJ88MQ=GS2.2.s1774475776$o3$g0$t1774475776$j60$l0$h0; abs_gsession=%7B%22info%22%3A%7B%22COMMON%22%3A%7B%22Selection%22%3A%22user%22%2C%22preference%22%3A%22J4U%22%2C%22userType%22%3A%22G%22%2C%22zipcode%22%3A%2219711%22%2C%22banner%22%3A%22acmemarkets%22%2C%22siteType%22%3A%22C%22%2C%22customerType%22%3A%22%22%2C%22resolvedBy%22%3A%22%22%7D%2C%22J4U%22%3A%7B%22zipcode%22%3A%2219711%22%2C%22storeId%22%3A%22806%22%7D%2C%22SHOP%22%3A%7B%22zipcode%22%3A%2219711%22%2C%22storeId%22%3A%22806%22%7D%7D%7D; ACI_S_abs_previouslogin=%7B%22info%22%3A%7B%22COMMON%22%3A%7B%22houseId%22%3A%22729022287633%22%2C%22clubCard%22%3A%2249549812729%22%2C%22userType%22%3A%22G%22%2C%22storeId%22%3A%22806%22%2C%22zipcode%22%3A%2219711%22%2C%22banner%22%3A%22acmemarkets%22%2C%22preference%22%3A%22J4U%22%2C%22isClosed%22%3A%22false%22%2C%22isCrossBannerMFCUser%22%3A%22false%22%2C%22Selection%22%3A%22user%22%2C%22isARenrolled%22%3A%22false%22%2C%22userData%22%3A%7B%7D%2C%22grsSessionId%22%3A%22063024fb-35d7-4da1-b6ee-49398ee1eb93%22%2C%22siteType%22%3A%22C%22%2C%22customerType%22%3A%22%22%2C%22resolvedBy%22%3A%22%22%2C%22multiAccounts%22%3A%22false%22%2C%22emailVerified%22%3A%22true%22%2C%22phoneVerified%22%3A%22false%22%2C%22emailPrompt180%22%3A%22false%22%2C%22phonePrompt180%22%3A%22false%22%2C%22deviceId%22%3A%22%22%7D%2C%22J4U%22%3A%7B%22userType%22%3A%22R%22%2C%22storeId%22%3A%22806%22%2C%22zipcode%22%3A%2219711%22%2C%22address%22%3A%22100%2BSuburban%2BDr%2C%2BNewark%2C%2BDE%2B19711%22%2C%22preferredBanner%22%3A%22acmemarkets%22%2C%22userData%22%3A%7B%22guestStoreId%22%3A%222633%22%2C%22guestZipCode%22%3A%2206840%22%2C%22defaultStore%22%3Afalse%7D%7D%2C%22SHOP%22%3A%7B%22userType%22%3A%22R%22%2C%22storeId%22%3A%22806%22%2C%22zipcode%22%3A%2219711%22%2C%22address%22%3A%22100%2BSuburban%2BDr%2C%2BNewark%2C%2BDE%2B19711%22%2C%22userData%22%3A%7B%22guestStoreId%22%3A%22806%22%2C%22guestZipCode%22%3A%2219711%22%2C%22signInCalled%22%3Afalse%2C%22driveUpAndGoIsEnabled%22%3A%22false%22%2C%22unattendedDeliveryIsEnabled%22%3A%22false%22%2C%22expireTime%22%3A%221774470178000%22%7D%7D%7D%7D; __gads=ID=b14266123a5dffb5:T=1773168126:RT=1774480896:S=ALNI_Mbv6PS2oQlrtLUEeqks0-g1OrRtZQ; __gpi=UID=000013497e5d0cce:T=1773168126:RT=1774480896:S=ALNI_MZTEodi_t-OHCr4WH4t6AaQ9p-Mhw; __eoi=ID=4ac3989bb54cc211:T=1773168126:RT=1774480896:S=AA-Afja0nnjCotgTkMb1VWPM2gGd; signifyd_sessionId=19aHa79e-ee22-P208-b0f0-b8fb-4bcc076_1f86; _gcl_au=1.1.2103349931.1773168127.1395106364.1774481114.1774481114; ACI_S_ECommRedirectURL=https%3A%2F%2Fwww.acmemarkets.com%2Fmeal-plans-recipes%2Fbundles%2Fsalmon-bowl; _uetvid=d735eb501cb011f1a8fd8f2794cb7c5a; s_nr30=1774481418916-New; incap_ses_362_2083770=ay7GVnhIl2xde15RQRUGBapexWkAAAAAblRs+QtXwJKLpkm9FEoVkg==; s_sq=sfsafewayprod1%3D%2526c.%2526a.%2526activitymap.%2526page%253Dacmemarkets%25253Aloyalty%25253Aaisle-vs%25253Ameat-seafood%25253Aseafood-favorites%2526link%253DLoad%252520more%2526region%253Dsearch-grid_0%2526pageIDType%253D1%2526.activitymap%2526.a%2526.c%2526pid%253Dacmemarkets%25253Aloyalty%25253Aaisle-vs%25253Ameat-seafood%25253Aseafood-favorites%2526pidt%253D1%2526oid%253DLoad%252520more%2526oidt%253D3%2526ot%253DSUBMIT; nlbi_2083770_2147483392=y9iKSBGewFqLN8QJaKN5ZQAAAAApNyfTKepnWS5DnliBkr6x; reese84=3:Um6Sa3mRcAnh+3ITa9FjwA==:PmU0Vm8qdx4HH8zDCIXBS3qCcFxSw0pY2SvDbCINWhSqme0yNw/jBBLVEa8ZYcDZ35mMrlxvZyjBsuKHLF1GdEE5VET/NCWzebW8eyhMI7qrOCaGWEc8RFxsjSROtEYAdsUpZj8lTY9SiDcSxbsCz9b0Z03aRwTVR/UHih/kmsw5HxB2W/FLacqtFFrzAK2lTCQbqEaVci9CfgYlSJSEzLR+Kvi7TEO+L/jhATbMhgMmffKsrNI8XKFdFt2pESVZz2mX2D3q8mY36ZK4RUMKxqyCqnZV7//5P7pT1Ja8dYyI32LkD0G+i1OviFQ2/GuwPjFokXHBA9XX92dpkTnXcGeLsWwE6Q116T9iMoPW5VPjmlNI+hbN/jZCgj8HXHmZM1Hpm0qrpJH9h3tpmvV6yUozf/m4DkebSy4A2nFICkTAMiDxamOXwdACn8eli+BR6Ild0P9ZKPv1RiJkYZdN3V+T++ro3CuMI0oDHzWu4WzQ2ucRz5NiOKxp58+fgowg6zxMPbSwgqLrnPZ758YZTnoEfr/mYGUfGK+AnW7+aQ5vMeGT1sJMBYCUcjV8F6I0n5M2/0rSjp2uri5Bxa96+Q==:kpAJfuxaqq0kDZ/9EtrMUKMOxr/gYX9srCuoThNrh6M=; _ga_KC35M14HLD=GS2.1.s1774543349$o2$g0$t1774543349$j60$l0$h0; AMCV_A7BF3BC75245ADF20A490D4D%40AdobeOrg=179643557%7CMCIDTS%7C20538%7CMCMID%7C11176332726178312564444395020237210543%7CMCAAMLH-1775148149%7C9%7CMCAAMB-1775148149%7C6G1ynYcLPuiQxYZrsz_pkqfLG9yMXBpb2zX5dvJdYQJzPXImdj0y%7CMCOPTOUT-1774550549s%7CNONE%7CvVersion%7C5.5.0%7CMCCIDH%7C1464169025; SWY_SHARED_SESSION_INFO=%7B%22info%22%3A%7B%22COMMON%22%3A%7B%22Selection%22%3A%22user%22%2C%22preference%22%3A%22J4U%22%2C%22userType%22%3A%22G%22%2C%22zipcode%22%3A%2219711%22%2C%22banner%22%3A%22acmemarkets%22%2C%22siteType%22%3A%22C%22%2C%22customerType%22%3A%22%22%2C%22resolvedBy%22%3A%22%22%7D%2C%22J4U%22%3A%7B%22zipcode%22%3A%2219711%22%2C%22storeId%22%3A%22806%22%7D%2C%22SHOP%22%3A%7B%22zipcode%22%3A%2219711%22%2C%22storeId%22%3A%22806%22%7D%7D%7D; SWY_SYND_USER_INFO=%7B%22storeAddress%22%3A%22%22%2C%22storeZip%22%3A%2219711%22%2C%22storeId%22%3A%22806%22%2C%22preference%22%3A%22J4U%22%7D; mbox=PC#03b8319f82634490994e9117016a6f12.35_0#1837788151|session#873f096bd37f42a3ac8d53bcffa7a971#1774545211; OptanonConsent=isGpcEnabled=0&datestamp=Thu+Mar+26+2026+09%3A42%3A30+GMT-0700+(Pacific+Daylight+Time)&version=202409.1.0&browserGpcFlag=0&isIABGlobal=false&hosts=&consentId=ad8ee733-21b7-4a88-9c98-833650d44dbe&interactionCount=1&isAnonUser=1&landingPath=NotLandingPage&groups=C0001%3A1%2CC0002%3A1%2CC0004%3A1%2CC0003%3A1&AwaitingReconsent=false' \ 104 + -H 'ocp-apim-subscription-key: e914eec9448c4d5eb672debf5011cf8f' 105 + 106 + 107 + 108 + curl 'https://www.acmemarkets.com/abs/pub/xapi/wcax/pathway/search?request-id=4151774543350575240&url=https://www.acmemarkets.com&search-uid=&q=&rows=30&start=0&channel=instore&storeid=806&sort=&widget-id=GR-C-Categ-6090cd27&dvid=web-4.1search&visitorId=6b47f549-b8da-48b5-8486-8c750ec1c753&uuid=null&pgm=abs&includeOffer=true&banner=acmemarkets&facet=false' \ 109 + -H 'accept: application/json, text/plain, */*' \ 110 + -H 'accept-language: en-US,en;q=0.9' \ 111 + -b '__pdst=35d960ac30cd40d792cbb592147d3bea; _fbp=fb.1.1773168127330.765868103270359932; _pin_unauth=dWlkPU56ZGpNR1psWmprdE1XTTFOaTAwTjJRNUxUazJZVFF0WXpsaE1UUm1ZbUZqTkRjNQ; absVisitorId=6b47f549-b8da-48b5-8486-8c750ec1c753; akacd_PR-bg-www-prod-acmemarkets=3951498377~rv=59~id=c9438d2c2e61873bdcd74214268c738c; AMCVS_A7BF3BC75245ADF20A490D4D%40AdobeOrg=1; ACI_S_ECommBanner=acmemarkets; ACI_S_ECommSignInCount=0; at_check=true; nlbi_2083770=EdQXPm5EWEU5GYXsaKN5ZQAAAAB8ymloJOMURv6MdOpYLVb4; incap_ses_397_2083770=BbMTaeIMqTa9uC/Qim2CBTulwWkAAAAAcd8J0nW048yTn+Z7+cW2ug==; ACI_S_ECommReInit=net|slot; JSESSIONID=node01rxmiasw55sif1jysb15t32zj7948572.node0; visid_incap_2083770=K/cZdXpDRvK/rQXq4rM2DPplsGkAAAAAQkIPAAAAAACAUEXDAUusQBLT44imoibKTHDVrlw+wJDM; SAFEWAY_MODAL_LINK=; _ga_862QXSJ5RF=GS2.2.s1774475776$o3$g0$t1774475776$j60$l0$h0; _ga_PH9HKJ88MQ=GS2.2.s1774475776$o3$g0$t1774475776$j60$l0$h0; abs_gsession=%7B%22info%22%3A%7B%22COMMON%22%3A%7B%22Selection%22%3A%22user%22%2C%22preference%22%3A%22J4U%22%2C%22userType%22%3A%22G%22%2C%22zipcode%22%3A%2219711%22%2C%22banner%22%3A%22acmemarkets%22%2C%22siteType%22%3A%22C%22%2C%22customerType%22%3A%22%22%2C%22resolvedBy%22%3A%22%22%7D%2C%22J4U%22%3A%7B%22zipcode%22%3A%2219711%22%2C%22storeId%22%3A%22806%22%7D%2C%22SHOP%22%3A%7B%22zipcode%22%3A%2219711%22%2C%22storeId%22%3A%22806%22%7D%7D%7D; ACI_S_abs_previouslogin=%7B%22info%22%3A%7B%22COMMON%22%3A%7B%22houseId%22%3A%22729022287633%22%2C%22clubCard%22%3A%2249549812729%22%2C%22userType%22%3A%22G%22%2C%22storeId%22%3A%22806%22%2C%22zipcode%22%3A%2219711%22%2C%22banner%22%3A%22acmemarkets%22%2C%22preference%22%3A%22J4U%22%2C%22isClosed%22%3A%22false%22%2C%22isCrossBannerMFCUser%22%3A%22false%22%2C%22Selection%22%3A%22user%22%2C%22isARenrolled%22%3A%22false%22%2C%22userData%22%3A%7B%7D%2C%22grsSessionId%22%3A%22063024fb-35d7-4da1-b6ee-49398ee1eb93%22%2C%22siteType%22%3A%22C%22%2C%22customerType%22%3A%22%22%2C%22resolvedBy%22%3A%22%22%2C%22multiAccounts%22%3A%22false%22%2C%22emailVerified%22%3A%22true%22%2C%22phoneVerified%22%3A%22false%22%2C%22emailPrompt180%22%3A%22false%22%2C%22phonePrompt180%22%3A%22false%22%2C%22deviceId%22%3A%22%22%7D%2C%22J4U%22%3A%7B%22userType%22%3A%22R%22%2C%22storeId%22%3A%22806%22%2C%22zipcode%22%3A%2219711%22%2C%22address%22%3A%22100%2BSuburban%2BDr%2C%2BNewark%2C%2BDE%2B19711%22%2C%22preferredBanner%22%3A%22acmemarkets%22%2C%22userData%22%3A%7B%22guestStoreId%22%3A%222633%22%2C%22guestZipCode%22%3A%2206840%22%2C%22defaultStore%22%3Afalse%7D%7D%2C%22SHOP%22%3A%7B%22userType%22%3A%22R%22%2C%22storeId%22%3A%22806%22%2C%22zipcode%22%3A%2219711%22%2C%22address%22%3A%22100%2BSuburban%2BDr%2C%2BNewark%2C%2BDE%2B19711%22%2C%22userData%22%3A%7B%22guestStoreId%22%3A%22806%22%2C%22guestZipCode%22%3A%2219711%22%2C%22signInCalled%22%3Afalse%2C%22driveUpAndGoIsEnabled%22%3A%22false%22%2C%22unattendedDeliveryIsEnabled%22%3A%22false%22%2C%22expireTime%22%3A%221774470178000%22%7D%7D%7D%7D; __gads=ID=b14266123a5dffb5:T=1773168126:RT=1774480896:S=ALNI_Mbv6PS2oQlrtLUEeqks0-g1OrRtZQ; __gpi=UID=000013497e5d0cce:T=1773168126:RT=1774480896:S=ALNI_MZTEodi_t-OHCr4WH4t6AaQ9p-Mhw; __eoi=ID=4ac3989bb54cc211:T=1773168126:RT=1774480896:S=AA-Afja0nnjCotgTkMb1VWPM2gGd; signifyd_sessionId=19aHa79e-ee22-P208-b0f0-b8fb-4bcc076_1f86; _gcl_au=1.1.2103349931.1773168127.1395106364.1774481114.1774481114; ACI_S_ECommRedirectURL=https%3A%2F%2Fwww.acmemarkets.com%2Fmeal-plans-recipes%2Fbundles%2Fsalmon-bowl; _uetvid=d735eb501cb011f1a8fd8f2794cb7c5a; s_nr30=1774481418916-New; incap_ses_362_2083770=ay7GVnhIl2xde15RQRUGBapexWkAAAAAblRs+QtXwJKLpkm9FEoVkg==; s_sq=sfsafewayprod1%3D%2526c.%2526a.%2526activitymap.%2526page%253Dacmemarkets%25253Aloyalty%25253Aaisle-vs%25253Ameat-seafood%25253Aseafood-favorites%2526link%253DLoad%252520more%2526region%253Dsearch-grid_0%2526pageIDType%253D1%2526.activitymap%2526.a%2526.c%2526pid%253Dacmemarkets%25253Aloyalty%25253Aaisle-vs%25253Ameat-seafood%25253Aseafood-favorites%2526pidt%253D1%2526oid%253DLoad%252520more%2526oidt%253D3%2526ot%253DSUBMIT; nlbi_2083770_2147483392=y9iKSBGewFqLN8QJaKN5ZQAAAAApNyfTKepnWS5DnliBkr6x; reese84=3:Um6Sa3mRcAnh+3ITa9FjwA==:PmU0Vm8qdx4HH8zDCIXBS3qCcFxSw0pY2SvDbCINWhSqme0yNw/jBBLVEa8ZYcDZ35mMrlxvZyjBsuKHLF1GdEE5VET/NCWzebW8eyhMI7qrOCaGWEc8RFxsjSROtEYAdsUpZj8lTY9SiDcSxbsCz9b0Z03aRwTVR/UHih/kmsw5HxB2W/FLacqtFFrzAK2lTCQbqEaVci9CfgYlSJSEzLR+Kvi7TEO+L/jhATbMhgMmffKsrNI8XKFdFt2pESVZz2mX2D3q8mY36ZK4RUMKxqyCqnZV7//5P7pT1Ja8dYyI32LkD0G+i1OviFQ2/GuwPjFokXHBA9XX92dpkTnXcGeLsWwE6Q116T9iMoPW5VPjmlNI+hbN/jZCgj8HXHmZM1Hpm0qrpJH9h3tpmvV6yUozf/m4DkebSy4A2nFICkTAMiDxamOXwdACn8eli+BR6Ild0P9ZKPv1RiJkYZdN3V+T++ro3CuMI0oDHzWu4WzQ2ucRz5NiOKxp58+fgowg6zxMPbSwgqLrnPZ758YZTnoEfr/mYGUfGK+AnW7+aQ5vMeGT1sJMBYCUcjV8F6I0n5M2/0rSjp2uri5Bxa96+Q==:kpAJfuxaqq0kDZ/9EtrMUKMOxr/gYX9srCuoThNrh6M=; _ga_KC35M14HLD=GS2.1.s1774543349$o2$g0$t1774543349$j60$l0$h0; AMCV_A7BF3BC75245ADF20A490D4D%40AdobeOrg=179643557%7CMCIDTS%7C20538%7CMCMID%7C11176332726178312564444395020237210543%7CMCAAMLH-1775148149%7C9%7CMCAAMB-1775148149%7C6G1ynYcLPuiQxYZrsz_pkqfLG9yMXBpb2zX5dvJdYQJzPXImdj0y%7CMCOPTOUT-1774550549s%7CNONE%7CvVersion%7C5.5.0%7CMCCIDH%7C1464169025; SWY_SHARED_SESSION_INFO=%7B%22info%22%3A%7B%22COMMON%22%3A%7B%22Selection%22%3A%22user%22%2C%22preference%22%3A%22J4U%22%2C%22userType%22%3A%22G%22%2C%22zipcode%22%3A%2219711%22%2C%22banner%22%3A%22acmemarkets%22%2C%22siteType%22%3A%22C%22%2C%22customerType%22%3A%22%22%2C%22resolvedBy%22%3A%22%22%7D%2C%22J4U%22%3A%7B%22zipcode%22%3A%2219711%22%2C%22storeId%22%3A%22806%22%7D%2C%22SHOP%22%3A%7B%22zipcode%22%3A%2219711%22%2C%22storeId%22%3A%22806%22%7D%7D%7D; SWY_SYND_USER_INFO=%7B%22storeAddress%22%3A%22%22%2C%22storeZip%22%3A%2219711%22%2C%22storeId%22%3A%22806%22%2C%22preference%22%3A%22J4U%22%7D; mbox=PC#03b8319f82634490994e9117016a6f12.35_0#1837788151|session#873f096bd37f42a3ac8d53bcffa7a971#1774545211; OptanonConsent=isGpcEnabled=0&datestamp=Thu+Mar+26+2026+09%3A42%3A30+GMT-0700+(Pacific+Daylight+Time)&version=202409.1.0&browserGpcFlag=0&isIABGlobal=false&hosts=&consentId=ad8ee733-21b7-4a88-9c98-833650d44dbe&interactionCount=1&isAnonUser=1&landingPath=NotLandingPage&groups=C0001%3A1%2CC0002%3A1%2CC0004%3A1%2CC0003%3A1&AwaitingReconsent=false' \ 112 + -H 'ocp-apim-subscription-key: e914eec9448c4d5eb672debf5011cf8f'
+619
internal/brightdata/client.go
··· 1 + package brightdata 2 + 3 + import ( 4 + "bytes" 5 + "context" 6 + "encoding/json" 7 + "errors" 8 + "fmt" 9 + "io" 10 + "net/http" 11 + "net/url" 12 + "strconv" 13 + "strings" 14 + "time" 15 + ) 16 + 17 + const ( 18 + DefaultBaseURL = "https://api.brightdata.com" 19 + defaultPollInterval = 2 * time.Second 20 + ) 21 + 22 + type Format string 23 + 24 + const ( 25 + FormatJSON Format = "json" 26 + FormatNDJSON Format = "ndjson" 27 + FormatCSV Format = "csv" 28 + ) 29 + 30 + type SnapshotStatus string 31 + 32 + const ( 33 + SnapshotStatusStarting SnapshotStatus = "starting" 34 + SnapshotStatusRunning SnapshotStatus = "running" 35 + SnapshotStatusBuilding SnapshotStatus = "building" 36 + SnapshotStatusReady SnapshotStatus = "ready" 37 + SnapshotStatusFailed SnapshotStatus = "failed" 38 + ) 39 + 40 + // Client calls the Bright Data scraper API. 41 + type Client struct { 42 + baseURL string 43 + apiKey string 44 + httpClient *http.Client 45 + } 46 + 47 + // APIError captures a non-success Bright Data response. 48 + type APIError struct { 49 + StatusCode int 50 + Body string 51 + } 52 + 53 + func (e *APIError) Error() string { 54 + body := strings.TrimSpace(e.Body) 55 + if body == "" { 56 + return fmt.Sprintf("bright data request failed: status %d", e.StatusCode) 57 + } 58 + return fmt.Sprintf("bright data request failed: status %d: %s", e.StatusCode, body) 59 + } 60 + 61 + type ScrapeOptions struct { 62 + Notify bool 63 + IncludeErrors bool 64 + Format Format 65 + CustomOutputFields string 66 + } 67 + 68 + type TriggerOptions struct { 69 + Notify bool 70 + IncludeErrors bool 71 + Format Format 72 + CustomOutputFields string 73 + } 74 + 75 + type DownloadOptions struct { 76 + Format Format 77 + } 78 + 79 + type ScrapeResponse struct { 80 + StatusCode int 81 + ContentType string 82 + Header http.Header 83 + Body []byte 84 + SnapshotID string 85 + Message string 86 + } 87 + 88 + func (r *ScrapeResponse) DecodeJSON(dest any) error { 89 + if len(r.Body) == 0 { 90 + return errors.New("response body is empty") 91 + } 92 + if err := json.Unmarshal(r.Body, dest); err != nil { 93 + return fmt.Errorf("decode json response: %w", err) 94 + } 95 + return nil 96 + } 97 + 98 + type TriggerResponse struct { 99 + SnapshotID string `json:"snapshot_id"` 100 + } 101 + 102 + type ProgressResponse struct { 103 + SnapshotID string `json:"snapshot_id"` 104 + DatasetID string `json:"dataset_id"` 105 + Status SnapshotStatus `json:"status"` 106 + ErrorMessage string `json:"error_message,omitempty"` 107 + } 108 + 109 + func (r *ProgressResponse) Ready() bool { 110 + return r != nil && r.Status == SnapshotStatusReady 111 + } 112 + 113 + func (r *ProgressResponse) Failed() bool { 114 + return r != nil && r.Status == SnapshotStatusFailed 115 + } 116 + 117 + type DownloadResponse struct { 118 + StatusCode int 119 + ContentType string 120 + Header http.Header 121 + Body []byte 122 + Ready bool 123 + Status SnapshotStatus 124 + Message string 125 + } 126 + 127 + func (r *DownloadResponse) DecodeJSON(dest any) error { 128 + if len(r.Body) == 0 { 129 + return errors.New("response body is empty") 130 + } 131 + if err := json.Unmarshal(r.Body, dest); err != nil { 132 + return fmt.Errorf("decode json response: %w", err) 133 + } 134 + return nil 135 + } 136 + 137 + type DeliveryExtension string 138 + 139 + const ( 140 + DeliveryExtensionJSON DeliveryExtension = "json" 141 + DeliveryExtensionNDJSON DeliveryExtension = "ndjson" 142 + DeliveryExtensionCSV DeliveryExtension = "csv" 143 + ) 144 + 145 + type AzureDeliveryOptions struct { 146 + Container string 147 + Account string 148 + Key string 149 + SASToken string 150 + Directory string 151 + FilenameTemplate string 152 + Extension DeliveryExtension 153 + Compress bool 154 + BatchSize int 155 + } 156 + 157 + type DeliveryResponse struct { 158 + DeliveryID string `json:"delivery_id"` 159 + } 160 + 161 + type DeliveryStatus struct { 162 + DeliveryID string `json:"delivery_id"` 163 + Status string `json:"status"` 164 + Message string `json:"message,omitempty"` 165 + } 166 + 167 + type asyncResponse struct { 168 + SnapshotID string `json:"snapshot_id"` 169 + Status SnapshotStatus `json:"status,omitempty"` 170 + Message string `json:"message,omitempty"` 171 + } 172 + 173 + type scrapePayload struct { 174 + Input any `json:"input"` 175 + CustomOutputFields string `json:"custom_output_fields,omitempty"` 176 + } 177 + 178 + type deliverSnapshotPayload struct { 179 + Deliver azureDeliveryPayload `json:"deliver"` 180 + Compress bool `json:"compress"` 181 + BatchSize int `json:"batch_size,omitempty"` 182 + } 183 + 184 + type azureDeliveryPayload struct { 185 + Type string `json:"type"` 186 + Filename azureFilenamePayload `json:"filename"` 187 + Container string `json:"container"` 188 + Credentials azureCredentialsPayload `json:"credentials"` 189 + Directory string `json:"directory,omitempty"` 190 + } 191 + 192 + type azureFilenamePayload struct { 193 + Template string `json:"template"` 194 + Extension string `json:"extension"` 195 + } 196 + 197 + type azureCredentialsPayload struct { 198 + Account string `json:"account"` 199 + Key string `json:"key,omitempty"` 200 + SASToken string `json:"sas_token,omitempty"` 201 + } 202 + 203 + // NewClient creates a Bright Data client with the default API base URL. 204 + func NewClient(apiKey string, httpClient *http.Client) (*Client, error) { 205 + return NewClientWithBaseURL(DefaultBaseURL, apiKey, httpClient) 206 + } 207 + 208 + // NewClientWithBaseURL creates a Bright Data client for the provided base URL. 209 + func NewClientWithBaseURL(baseURL, apiKey string, httpClient *http.Client) (*Client, error) { 210 + baseURL = strings.TrimSpace(baseURL) 211 + if baseURL == "" { 212 + baseURL = DefaultBaseURL 213 + } 214 + apiKey = strings.TrimSpace(apiKey) 215 + if apiKey == "" { 216 + return nil, errors.New("api key is required") 217 + } 218 + if httpClient == nil { 219 + httpClient = &http.Client{Timeout: 30 * time.Second} 220 + } 221 + 222 + return &Client{ 223 + baseURL: strings.TrimRight(baseURL, "/"), 224 + apiKey: apiKey, 225 + httpClient: httpClient, 226 + }, nil 227 + } 228 + 229 + // Scrape sends a synchronous scrape request. When Bright Data cannot finish 230 + // within the sync timeout, it returns a snapshot ID that can be polled later. 231 + func (c *Client) Scrape(ctx context.Context, datasetID string, input any, opts ScrapeOptions) (*ScrapeResponse, error) { 232 + if err := validateDatasetAndInput(datasetID, input); err != nil { 233 + return nil, err 234 + } 235 + 236 + req, err := c.newJSONRequest(ctx, http.MethodPost, "/datasets/v3/scrape", queryForRequest(datasetID, opts.Notify, opts.IncludeErrors, opts.Format, opts.CustomOutputFields), scrapePayload{ 237 + Input: input, 238 + CustomOutputFields: strings.TrimSpace(opts.CustomOutputFields), 239 + }) 240 + if err != nil { 241 + return nil, err 242 + } 243 + 244 + resp, err := c.httpClient.Do(req) 245 + if err != nil { 246 + return nil, fmt.Errorf("scrape request: %w", err) 247 + } 248 + defer func() { 249 + _ = resp.Body.Close() 250 + }() 251 + 252 + body, err := io.ReadAll(resp.Body) 253 + if err != nil { 254 + return nil, fmt.Errorf("read scrape response: %w", err) 255 + } 256 + 257 + if resp.StatusCode == http.StatusAccepted { 258 + var async asyncResponse 259 + if err := json.Unmarshal(body, &async); err != nil { 260 + return nil, fmt.Errorf("decode async scrape response: %w", err) 261 + } 262 + return &ScrapeResponse{ 263 + StatusCode: resp.StatusCode, 264 + ContentType: resp.Header.Get("Content-Type"), 265 + Header: resp.Header.Clone(), 266 + Body: body, 267 + SnapshotID: async.SnapshotID, 268 + Message: async.Message, 269 + }, nil 270 + } 271 + 272 + if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices { 273 + return nil, &APIError{StatusCode: resp.StatusCode, Body: string(body)} 274 + } 275 + 276 + return &ScrapeResponse{ 277 + StatusCode: resp.StatusCode, 278 + ContentType: resp.Header.Get("Content-Type"), 279 + Header: resp.Header.Clone(), 280 + Body: body, 281 + }, nil 282 + } 283 + 284 + // Trigger starts an asynchronous scrape and returns the snapshot ID. 285 + func (c *Client) Trigger(ctx context.Context, datasetID string, input any, opts TriggerOptions) (*TriggerResponse, error) { 286 + if err := validateDatasetAndInput(datasetID, input); err != nil { 287 + return nil, err 288 + } 289 + 290 + req, err := c.newJSONRequest(ctx, http.MethodPost, "/datasets/v3/trigger", queryForRequest(datasetID, opts.Notify, opts.IncludeErrors, opts.Format, opts.CustomOutputFields), input) 291 + if err != nil { 292 + return nil, err 293 + } 294 + 295 + resp, err := c.httpClient.Do(req) 296 + if err != nil { 297 + return nil, fmt.Errorf("trigger request: %w", err) 298 + } 299 + defer func() { 300 + _ = resp.Body.Close() 301 + }() 302 + 303 + body, err := io.ReadAll(resp.Body) 304 + if err != nil { 305 + return nil, fmt.Errorf("read trigger response: %w", err) 306 + } 307 + if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices { 308 + return nil, &APIError{StatusCode: resp.StatusCode, Body: string(body)} 309 + } 310 + 311 + var out TriggerResponse 312 + if err := json.Unmarshal(body, &out); err != nil { 313 + return nil, fmt.Errorf("decode trigger response: %w", err) 314 + } 315 + if strings.TrimSpace(out.SnapshotID) == "" { 316 + return nil, errors.New("trigger response missing snapshot_id") 317 + } 318 + return &out, nil 319 + } 320 + 321 + // Progress fetches the status of an asynchronous snapshot. 322 + func (c *Client) Progress(ctx context.Context, snapshotID string) (*ProgressResponse, error) { 323 + snapshotID = strings.TrimSpace(snapshotID) 324 + if snapshotID == "" { 325 + return nil, errors.New("snapshot ID is required") 326 + } 327 + 328 + req, err := c.newJSONRequest(ctx, http.MethodGet, "/datasets/v3/progress/"+url.PathEscape(snapshotID), nil, nil) 329 + if err != nil { 330 + return nil, err 331 + } 332 + 333 + resp, err := c.httpClient.Do(req) 334 + if err != nil { 335 + return nil, fmt.Errorf("progress request: %w", err) 336 + } 337 + defer func() { 338 + _ = resp.Body.Close() 339 + }() 340 + 341 + body, err := io.ReadAll(resp.Body) 342 + if err != nil { 343 + return nil, fmt.Errorf("read progress response: %w", err) 344 + } 345 + if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices { 346 + return nil, &APIError{StatusCode: resp.StatusCode, Body: string(body)} 347 + } 348 + 349 + var out ProgressResponse 350 + if err := json.Unmarshal(body, &out); err != nil { 351 + return nil, fmt.Errorf("decode progress response: %w", err) 352 + } 353 + return &out, nil 354 + } 355 + 356 + // WaitForSnapshot polls progress until the snapshot is ready or failed. 357 + func (c *Client) WaitForSnapshot(ctx context.Context, snapshotID string, pollInterval time.Duration) (*ProgressResponse, error) { 358 + if pollInterval <= 0 { 359 + pollInterval = defaultPollInterval 360 + } 361 + 362 + for { 363 + progress, err := c.Progress(ctx, snapshotID) 364 + if err != nil { 365 + return nil, err 366 + } 367 + if progress.Ready() { 368 + return progress, nil 369 + } 370 + if progress.Failed() { 371 + if progress.ErrorMessage == "" { 372 + return progress, fmt.Errorf("snapshot %s failed", snapshotID) 373 + } 374 + return progress, fmt.Errorf("snapshot %s failed: %s", snapshotID, progress.ErrorMessage) 375 + } 376 + 377 + timer := time.NewTimer(pollInterval) 378 + select { 379 + case <-ctx.Done(): 380 + timer.Stop() 381 + return nil, ctx.Err() 382 + case <-timer.C: 383 + } 384 + } 385 + } 386 + 387 + // DownloadSnapshot retrieves a completed snapshot body. If the snapshot is not 388 + // ready yet, Bright Data responds with HTTP 202 and a status message. 389 + func (c *Client) DownloadSnapshot(ctx context.Context, snapshotID string, opts DownloadOptions) (*DownloadResponse, error) { 390 + snapshotID = strings.TrimSpace(snapshotID) 391 + if snapshotID == "" { 392 + return nil, errors.New("snapshot ID is required") 393 + } 394 + 395 + query := url.Values{} 396 + if opts.Format != "" { 397 + query.Set("format", string(opts.Format)) 398 + } 399 + 400 + req, err := c.newJSONRequest(ctx, http.MethodGet, "/datasets/v3/snapshot/"+url.PathEscape(snapshotID), query, nil) 401 + if err != nil { 402 + return nil, err 403 + } 404 + 405 + resp, err := c.httpClient.Do(req) 406 + if err != nil { 407 + return nil, fmt.Errorf("download snapshot request: %w", err) 408 + } 409 + defer func() { 410 + _ = resp.Body.Close() 411 + }() 412 + 413 + body, err := io.ReadAll(resp.Body) 414 + if err != nil { 415 + return nil, fmt.Errorf("read snapshot response: %w", err) 416 + } 417 + 418 + if resp.StatusCode == http.StatusAccepted { 419 + var pending asyncResponse 420 + if err := json.Unmarshal(body, &pending); err != nil { 421 + return nil, fmt.Errorf("decode pending snapshot response: %w", err) 422 + } 423 + return &DownloadResponse{ 424 + StatusCode: resp.StatusCode, 425 + ContentType: resp.Header.Get("Content-Type"), 426 + Header: resp.Header.Clone(), 427 + Ready: false, 428 + Status: pending.Status, 429 + Message: pending.Message, 430 + }, nil 431 + } 432 + 433 + if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices { 434 + return nil, &APIError{StatusCode: resp.StatusCode, Body: string(body)} 435 + } 436 + 437 + return &DownloadResponse{ 438 + StatusCode: resp.StatusCode, 439 + ContentType: resp.Header.Get("Content-Type"), 440 + Header: resp.Header.Clone(), 441 + Body: body, 442 + Ready: true, 443 + }, nil 444 + } 445 + 446 + // WaitAndDownload waits for a snapshot and then downloads the results. 447 + func (c *Client) WaitAndDownload(ctx context.Context, snapshotID string, pollInterval time.Duration, opts DownloadOptions) (*DownloadResponse, error) { 448 + if _, err := c.WaitForSnapshot(ctx, snapshotID, pollInterval); err != nil { 449 + return nil, err 450 + } 451 + return c.DownloadSnapshot(ctx, snapshotID, opts) 452 + } 453 + 454 + // DeliverToAzure asks Bright Data to deliver a completed snapshot directly to 455 + // Azure Blob Storage. 456 + func (c *Client) DeliverToAzure(ctx context.Context, snapshotID string, opts AzureDeliveryOptions) (*DeliveryResponse, error) { 457 + snapshotID = strings.TrimSpace(snapshotID) 458 + if snapshotID == "" { 459 + return nil, errors.New("snapshot ID is required") 460 + } 461 + if err := opts.validate(); err != nil { 462 + return nil, err 463 + } 464 + 465 + filenameTemplate := strings.TrimSpace(opts.FilenameTemplate) 466 + if filenameTemplate == "" { 467 + filenameTemplate = snapshotID 468 + } 469 + extension := opts.Extension 470 + if extension == "" { 471 + extension = DeliveryExtensionJSON 472 + } 473 + 474 + payload := deliverSnapshotPayload{ 475 + Deliver: azureDeliveryPayload{ 476 + Type: "azure", 477 + Filename: azureFilenamePayload{Template: filenameTemplate, Extension: string(extension)}, 478 + Container: opts.Container, 479 + Credentials: azureCredentialsPayload{ 480 + Account: opts.Account, 481 + Key: opts.Key, 482 + SASToken: opts.SASToken, 483 + }, 484 + Directory: strings.TrimSpace(opts.Directory), 485 + }, 486 + Compress: opts.Compress, 487 + BatchSize: opts.BatchSize, 488 + } 489 + 490 + req, err := c.newJSONRequest(ctx, http.MethodPost, "/datasets/v3/deliver/"+url.PathEscape(snapshotID), nil, payload) 491 + if err != nil { 492 + return nil, err 493 + } 494 + 495 + resp, err := c.httpClient.Do(req) 496 + if err != nil { 497 + return nil, fmt.Errorf("deliver snapshot request: %w", err) 498 + } 499 + defer func() { 500 + _ = resp.Body.Close() 501 + }() 502 + 503 + body, err := io.ReadAll(resp.Body) 504 + if err != nil { 505 + return nil, fmt.Errorf("read deliver response: %w", err) 506 + } 507 + if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices { 508 + return nil, &APIError{StatusCode: resp.StatusCode, Body: string(body)} 509 + } 510 + 511 + var out DeliveryResponse 512 + if err := json.Unmarshal(body, &out); err != nil { 513 + return nil, fmt.Errorf("decode deliver response: %w", err) 514 + } 515 + if strings.TrimSpace(out.DeliveryID) == "" { 516 + return nil, errors.New("deliver response missing delivery_id") 517 + } 518 + return &out, nil 519 + } 520 + 521 + // DeliveryStatus fetches the current state of a snapshot delivery request. 522 + func (c *Client) DeliveryStatus(ctx context.Context, deliveryID string) (*DeliveryStatus, error) { 523 + deliveryID = strings.TrimSpace(deliveryID) 524 + if deliveryID == "" { 525 + return nil, errors.New("delivery ID is required") 526 + } 527 + 528 + req, err := c.newJSONRequest(ctx, http.MethodGet, "/datasets/v3/delivery/"+url.PathEscape(deliveryID), nil, nil) 529 + if err != nil { 530 + return nil, err 531 + } 532 + 533 + resp, err := c.httpClient.Do(req) 534 + if err != nil { 535 + return nil, fmt.Errorf("delivery status request: %w", err) 536 + } 537 + defer func() { 538 + _ = resp.Body.Close() 539 + }() 540 + 541 + body, err := io.ReadAll(resp.Body) 542 + if err != nil { 543 + return nil, fmt.Errorf("read delivery status response: %w", err) 544 + } 545 + if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices { 546 + return nil, &APIError{StatusCode: resp.StatusCode, Body: string(body)} 547 + } 548 + 549 + var out DeliveryStatus 550 + if err := json.Unmarshal(body, &out); err != nil { 551 + return nil, fmt.Errorf("decode delivery status response: %w", err) 552 + } 553 + return &out, nil 554 + } 555 + 556 + func (o AzureDeliveryOptions) validate() error { 557 + if strings.TrimSpace(o.Container) == "" { 558 + return errors.New("container is required") 559 + } 560 + if strings.TrimSpace(o.Account) == "" { 561 + return errors.New("storage account is required") 562 + } 563 + if strings.TrimSpace(o.Key) == "" && strings.TrimSpace(o.SASToken) == "" { 564 + return errors.New("storage key or sas token is required") 565 + } 566 + return nil 567 + } 568 + 569 + func validateDatasetAndInput(datasetID string, input any) error { 570 + if strings.TrimSpace(datasetID) == "" { 571 + return errors.New("dataset ID is required") 572 + } 573 + if input == nil { 574 + return errors.New("input is required") 575 + } 576 + return nil 577 + } 578 + 579 + func queryForRequest(datasetID string, notify, includeErrors bool, format Format, customOutputFields string) url.Values { 580 + query := url.Values{} 581 + query.Set("dataset_id", datasetID) 582 + query.Set("notify", strconv.FormatBool(notify)) 583 + query.Set("include_errors", strconv.FormatBool(includeErrors)) 584 + if format != "" { 585 + query.Set("format", string(format)) 586 + } 587 + if strings.TrimSpace(customOutputFields) != "" { 588 + query.Set("custom_output_fields", strings.TrimSpace(customOutputFields)) 589 + } 590 + return query 591 + } 592 + 593 + func (c *Client) newJSONRequest(ctx context.Context, method, path string, query url.Values, payload any) (*http.Request, error) { 594 + endpoint := c.baseURL + path 595 + if len(query) > 0 { 596 + endpoint += "?" + query.Encode() 597 + } 598 + 599 + var body io.Reader 600 + if payload != nil { 601 + encoded, err := json.Marshal(payload) 602 + if err != nil { 603 + return nil, fmt.Errorf("marshal request: %w", err) 604 + } 605 + body = bytes.NewReader(encoded) 606 + } 607 + 608 + req, err := http.NewRequestWithContext(ctx, method, endpoint, body) 609 + if err != nil { 610 + return nil, fmt.Errorf("build request: %w", err) 611 + } 612 + 613 + req.Header.Set("Authorization", "Bearer "+c.apiKey) 614 + req.Header.Set("Accept", "application/json") 615 + if payload != nil { 616 + req.Header.Set("Content-Type", "application/json") 617 + } 618 + return req, nil 619 + }
+284
internal/brightdata/client_test.go
··· 1 + package brightdata 2 + 3 + import ( 4 + "context" 5 + "encoding/json" 6 + "io" 7 + "net/http" 8 + "strings" 9 + "testing" 10 + "time" 11 + ) 12 + 13 + func TestNewClient_RequiresAPIKey(t *testing.T) { 14 + t.Parallel() 15 + 16 + _, err := NewClient("", nil) 17 + if err == nil || !strings.Contains(err.Error(), "api key is required") { 18 + t.Fatalf("unexpected error: %v", err) 19 + } 20 + } 21 + 22 + func TestScrape_BuildsRequestAndReturnsRawBody(t *testing.T) { 23 + t.Parallel() 24 + 25 + type walmartInput struct { 26 + URL string `json:"url"` 27 + ZipCode string `json:"zip_code"` 28 + StoreID string `json:"store_id"` 29 + MaxItems int `json:"max_items,omitempty"` 30 + } 31 + 32 + var capturedReq *http.Request 33 + var capturedBody scrapePayload 34 + client := newTestClient(t, func(r *http.Request) (*http.Response, error) { 35 + capturedReq = r 36 + if err := json.NewDecoder(r.Body).Decode(&capturedBody); err != nil { 37 + t.Fatalf("decode request body: %v", err) 38 + } 39 + return jsonHTTPResponse(http.StatusOK, `[{"name":"Red Bull"},{"name":"OLIPOP"}]`), nil 40 + }) 41 + 42 + resp, err := client.Scrape(context.Background(), "gd_m693oc1r1gebnayxq", []walmartInput{ 43 + { 44 + URL: "https://www.walmart.com/ip/5332753715", 45 + ZipCode: "33177", 46 + StoreID: "6397", 47 + MaxItems: 4, 48 + }, 49 + }, ScrapeOptions{ 50 + Notify: false, 51 + IncludeErrors: true, 52 + Format: FormatJSON, 53 + }) 54 + if err != nil { 55 + t.Fatalf("scrape: %v", err) 56 + } 57 + 58 + if capturedReq == nil { 59 + t.Fatal("expected request to be captured") 60 + } 61 + if capturedReq.URL.Path != "/datasets/v3/scrape" { 62 + t.Fatalf("unexpected path: %s", capturedReq.URL.Path) 63 + } 64 + if got := capturedReq.URL.Query().Get("dataset_id"); got != "gd_m693oc1r1gebnayxq" { 65 + t.Fatalf("unexpected dataset_id: %q", got) 66 + } 67 + if got := capturedReq.URL.Query().Get("notify"); got != "false" { 68 + t.Fatalf("unexpected notify value: %q", got) 69 + } 70 + if got := capturedReq.URL.Query().Get("include_errors"); got != "true" { 71 + t.Fatalf("unexpected include_errors value: %q", got) 72 + } 73 + if got := capturedReq.Header.Get("Authorization"); got != "Bearer test-token" { 74 + t.Fatalf("unexpected authorization header: %q", got) 75 + } 76 + 77 + inputs, ok := capturedBody.Input.([]any) 78 + if !ok || len(inputs) != 1 { 79 + t.Fatalf("unexpected wrapped input payload: %#v", capturedBody.Input) 80 + } 81 + first, ok := inputs[0].(map[string]any) 82 + if !ok { 83 + t.Fatalf("unexpected first input type: %T", inputs[0]) 84 + } 85 + if got := first["zip_code"]; got != "33177" { 86 + t.Fatalf("unexpected zip_code: %#v", got) 87 + } 88 + if got := string(resp.Body); got != `[{"name":"Red Bull"},{"name":"OLIPOP"}]` { 89 + t.Fatalf("unexpected body: %s", resp.Body) 90 + } 91 + } 92 + 93 + func TestScrape_ReturnsSnapshotIDOnAccepted(t *testing.T) { 94 + t.Parallel() 95 + 96 + client := newTestClient(t, func(r *http.Request) (*http.Response, error) { 97 + return jsonHTTPResponse(http.StatusAccepted, `{"snapshot_id":"s_123","message":"still running"}`), nil 98 + }) 99 + 100 + resp, err := client.Scrape(context.Background(), "gd_test", []map[string]string{{"url": "https://example.com"}}, ScrapeOptions{}) 101 + if err != nil { 102 + t.Fatalf("scrape: %v", err) 103 + } 104 + if resp.SnapshotID != "s_123" { 105 + t.Fatalf("unexpected snapshot id: %q", resp.SnapshotID) 106 + } 107 + if resp.Message != "still running" { 108 + t.Fatalf("unexpected message: %q", resp.Message) 109 + } 110 + } 111 + 112 + func TestTrigger_BuildsRawInputArray(t *testing.T) { 113 + t.Parallel() 114 + 115 + var capturedReq *http.Request 116 + var capturedBody []map[string]any 117 + client := newTestClient(t, func(r *http.Request) (*http.Response, error) { 118 + capturedReq = r 119 + if err := json.NewDecoder(r.Body).Decode(&capturedBody); err != nil { 120 + t.Fatalf("decode body: %v", err) 121 + } 122 + return jsonHTTPResponse(http.StatusOK, `{"snapshot_id":"s_async"}`), nil 123 + }) 124 + 125 + resp, err := client.Trigger(context.Background(), "gd_async", []map[string]string{ 126 + {"url": "https://www.walmart.com/ip/5332753715", "zip_code": "33177", "store_id": "6397"}, 127 + }, TriggerOptions{ 128 + IncludeErrors: true, 129 + Format: FormatNDJSON, 130 + }) 131 + if err != nil { 132 + t.Fatalf("trigger: %v", err) 133 + } 134 + 135 + if capturedReq == nil { 136 + t.Fatal("expected request to be captured") 137 + } 138 + if capturedReq.URL.Path != "/datasets/v3/trigger" { 139 + t.Fatalf("unexpected path: %s", capturedReq.URL.Path) 140 + } 141 + if got := capturedReq.URL.Query().Get("format"); got != "ndjson" { 142 + t.Fatalf("unexpected format: %q", got) 143 + } 144 + if len(capturedBody) != 1 || capturedBody[0]["store_id"] != "6397" { 145 + t.Fatalf("unexpected trigger body: %#v", capturedBody) 146 + } 147 + if resp.SnapshotID != "s_async" { 148 + t.Fatalf("unexpected snapshot id: %q", resp.SnapshotID) 149 + } 150 + } 151 + 152 + func TestWaitAndDownload_PollsThenReturnsBody(t *testing.T) { 153 + t.Parallel() 154 + 155 + progressCalls := 0 156 + client := newTestClient(t, func(r *http.Request) (*http.Response, error) { 157 + switch r.URL.Path { 158 + case "/datasets/v3/progress/s_ready": 159 + progressCalls++ 160 + if progressCalls == 1 { 161 + return jsonHTTPResponse(http.StatusOK, `{"snapshot_id":"s_ready","dataset_id":"gd_test","status":"running"}`), nil 162 + } 163 + return jsonHTTPResponse(http.StatusOK, `{"snapshot_id":"s_ready","dataset_id":"gd_test","status":"ready"}`), nil 164 + case "/datasets/v3/snapshot/s_ready": 165 + return jsonHTTPResponse(http.StatusOK, `[{"sku":"123"}]`), nil 166 + default: 167 + return jsonHTTPResponse(http.StatusNotFound, `{"error":"not found"}`), nil 168 + } 169 + }) 170 + 171 + resp, err := client.WaitAndDownload(context.Background(), "s_ready", time.Millisecond, DownloadOptions{Format: FormatJSON}) 172 + if err != nil { 173 + t.Fatalf("wait and download: %v", err) 174 + } 175 + 176 + if !resp.Ready { 177 + t.Fatal("expected ready response") 178 + } 179 + if progressCalls < 2 { 180 + t.Fatalf("expected at least two progress calls, got %d", progressCalls) 181 + } 182 + if got := string(resp.Body); got != `[{"sku":"123"}]` { 183 + t.Fatalf("unexpected body: %s", resp.Body) 184 + } 185 + } 186 + 187 + func TestDownloadSnapshot_NotReady(t *testing.T) { 188 + t.Parallel() 189 + 190 + client := newTestClient(t, func(r *http.Request) (*http.Response, error) { 191 + return jsonHTTPResponse(http.StatusAccepted, `{"status":"building","message":"Snapshot is building, try again in 10s"}`), nil 192 + }) 193 + 194 + resp, err := client.DownloadSnapshot(context.Background(), "s_building", DownloadOptions{}) 195 + if err != nil { 196 + t.Fatalf("download snapshot: %v", err) 197 + } 198 + if resp.Ready { 199 + t.Fatal("expected not-ready response") 200 + } 201 + if resp.Status != SnapshotStatusBuilding { 202 + t.Fatalf("unexpected status: %q", resp.Status) 203 + } 204 + } 205 + 206 + func TestDeliverToAzure_BuildsDeliveryRequest(t *testing.T) { 207 + t.Parallel() 208 + 209 + var capturedReq *http.Request 210 + var capturedBody map[string]any 211 + client := newTestClient(t, func(r *http.Request) (*http.Response, error) { 212 + capturedReq = r 213 + if err := json.NewDecoder(r.Body).Decode(&capturedBody); err != nil { 214 + t.Fatalf("decode body: %v", err) 215 + } 216 + return jsonHTTPResponse(http.StatusOK, `{"delivery_id":"d_123"}`), nil 217 + }) 218 + 219 + resp, err := client.DeliverToAzure(context.Background(), "s_ready", AzureDeliveryOptions{ 220 + Container: "brightdata-results", 221 + Account: "acct", 222 + Key: "base64-key", 223 + Directory: "walmart/2026-03-25", 224 + FilenameTemplate: "snapshot_{{id}}", 225 + Extension: DeliveryExtensionJSON, 226 + Compress: true, 227 + BatchSize: 1000, 228 + }) 229 + if err != nil { 230 + t.Fatalf("deliver to azure: %v", err) 231 + } 232 + 233 + if capturedReq == nil { 234 + t.Fatal("expected request to be captured") 235 + } 236 + if capturedReq.URL.Path != "/datasets/v3/deliver/s_ready" { 237 + t.Fatalf("unexpected path: %s", capturedReq.URL.Path) 238 + } 239 + deliver, ok := capturedBody["deliver"].(map[string]any) 240 + if !ok { 241 + t.Fatalf("unexpected deliver payload: %#v", capturedBody) 242 + } 243 + if got := deliver["type"]; got != "azure" { 244 + t.Fatalf("unexpected delivery type: %#v", got) 245 + } 246 + if got := deliver["container"]; got != "brightdata-results" { 247 + t.Fatalf("unexpected container: %#v", got) 248 + } 249 + creds, ok := deliver["credentials"].(map[string]any) 250 + if !ok || creds["account"] != "acct" || creds["key"] != "base64-key" { 251 + t.Fatalf("unexpected credentials: %#v", deliver["credentials"]) 252 + } 253 + if resp.DeliveryID != "d_123" { 254 + t.Fatalf("unexpected delivery id: %q", resp.DeliveryID) 255 + } 256 + } 257 + 258 + type roundTripFunc func(*http.Request) (*http.Response, error) 259 + 260 + func (f roundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) { 261 + return f(r) 262 + } 263 + 264 + func newTestClient(t *testing.T, fn roundTripFunc) *Client { 265 + t.Helper() 266 + 267 + client, err := NewClientWithBaseURL("https://api.example.test", "test-token", &http.Client{ 268 + Transport: fn, 269 + }) 270 + if err != nil { 271 + t.Fatalf("new client: %v", err) 272 + } 273 + return client 274 + } 275 + 276 + func jsonHTTPResponse(status int, body string) *http.Response { 277 + return &http.Response{ 278 + StatusCode: status, 279 + Header: http.Header{ 280 + "Content-Type": []string{"application/json"}, 281 + }, 282 + Body: io.NopCloser(strings.NewReader(body)), 283 + } 284 + }
+27
internal/brightdata/interaction.js
··· 1 + navigate(input.url); 2 + 3 + // Wait for the main search grid to load 4 + const grid_selector = '.pc-grid'; 5 + wait(grid_selector, {timeout: 60000}); 6 + 7 + // Wait for product items to appear 8 + const product_selector = 'product-item-al-v2'; 9 + wait(product_selector, {timeout: 60000}); 10 + 11 + // Use load_more to handle infinite scroll 12 + // The container is the pc-grid element 13 + load_more(grid_selector, {timeout: 60000}); 14 + 15 + // Parse the page to get product URLs 16 + const {product_urls} = parse(); 17 + 18 + console.log(`Found ${product_urls.length} products`); 19 + 20 + if (!product_urls || product_urls.length === 0) { 21 + throw new Error('No products found on the page'); 22 + } 23 + 24 + // Collect URLs using next_stage 25 + for (let url of product_urls) { 26 + next_stage({url}); 27 + }
+37
internal/brightdata/parse.js
··· 1 + // Extract product URLs from the page 2 + const base_url = location.href; 3 + 4 + // Acme Markets uses product-item-al-v2 elements with links inside 5 + // The links have the pattern /shop/product-details.{id}.html 6 + const product_links = $('a[href*="/shop/product-details"]').toArray(); 7 + 8 + console.log(`Found ${product_links.length} product links`); 9 + 10 + // Use a Set to avoid duplicates 11 + const product_url_set = new Set(); 12 + 13 + product_links.forEach(link => { 14 + const href = $(link).attr('href'); 15 + 16 + // Validate the href 17 + if (href && 18 + !href.includes('javascript:') && 19 + !href.startsWith('#') && 20 + href.includes('/shop/product-details')) { 21 + 22 + // Convert relative URLs to absolute 23 + try { 24 + const absolute_url = new URL(href, base_url).href; 25 + product_url_set.add(absolute_url); 26 + } catch (e) { 27 + console.log(`Invalid URL: ${href}`); 28 + } 29 + } 30 + }); 31 + 32 + // Convert Set to Array 33 + const product_urls = Array.from(product_url_set); 34 + 35 + console.log(`Parser found ${product_urls.length} unique product URLs`); 36 + 37 + return {product_urls};
+2
internal/brightdata/product_interaction.js
··· 1 + navigate(input.url); 2 + collect(parse());
+85
internal/brightdata/product_parse.js
··· 1 + // Extract product name 2 + let product_name = $('[data-isproduct-tracking-disabled]').text_sane(); 3 + 4 + // Extract brand from product name (first word if it contains "Organic" or similar) 5 + let brand = null; 6 + if (product_name) { 7 + let words = product_name.split(' '); 8 + if (words[0] && (words[0].toLowerCase() === 'organic' || /^[A-Z]/.test(words[0]))) { 9 + brand = words[0]; 10 + } 11 + } 12 + 13 + // Extract currency symbol from price text 14 + let price_element = $('.product-comp-v1__price__text span').eq(1); 15 + let price_text = price_element.text_sane(); 16 + let currency_symbol = null; 17 + let currency_code = 'USD'; // default 18 + 19 + if (price_text) { 20 + // Extract currency symbol (any non-digit, non-decimal character at start) 21 + let symbol_match = price_text.match(/^([^\d.,]+)/); 22 + if (symbol_match) { 23 + currency_symbol = symbol_match[1].trim(); 24 + // Map common symbols to currency codes 25 + const currency_map = { 26 + '$': 'USD', 27 + '€': 'EUR', 28 + '£': 'GBP', 29 + '¥': 'JPY', 30 + '₹': 'INR', 31 + 'C$': 'CAD', 32 + 'A$': 'AUD' 33 + }; 34 + currency_code = currency_map[currency_symbol] || 'USD'; 35 + } 36 + } 37 + 38 + // Extract price - remove currency symbol and parse 39 + let price = null; 40 + if (price_text) { 41 + let price_value = parseFloat(price_text.replace(/[^\d.]/g, '')); 42 + if (!isNaN(price_value)) { 43 + price = new Money(price_value, currency_code); 44 + } 45 + } 46 + 47 + // Extract price per unit 48 + let price_per_unit_element = $('.color-neutral-80.body-text-xxs span').eq(1); 49 + let price_per_unit_text = price_per_unit_element.text_sane(); 50 + let price_per_unit = null; 51 + if (price_per_unit_text) { 52 + // Extract numeric value from text like "($7.99 / Lb)" 53 + let match = price_per_unit_text.match(/([\d.]+)/); 54 + if (match) { 55 + let price_per_unit_value = parseFloat(match[1]); 56 + if (!isNaN(price_per_unit_value)) { 57 + price_per_unit = new Money(price_per_unit_value, currency_code); 58 + } 59 + } 60 + } 61 + 62 + // Extract quantity from product name 63 + let quantity = null; 64 + if (product_name) { 65 + let match = product_name.match(/(\d+\s*(?:Lb|Oz|oz|lb|kg|g|ml|L))/i); 66 + if (match) { 67 + quantity = match[1]; 68 + } 69 + } 70 + 71 + // Extract delivery availability - use eq(0) to get only the first matching element 72 + let availability_delivery = $('.product-details__product-shopping-options table tr[aria-label*="Delivery"] .product-details__product-shopping-options__availability').eq(0).text_sane(); 73 + 74 + // Extract pickup availability - use eq(0) to get only the first matching element 75 + let availability_pickup = $('.product-details__product-shopping-options table tr[aria-label*="Pickup"] .product-details__product-shopping-options__availability').eq(0).text_sane(); 76 + 77 + return { 78 + product_name, 79 + brand, 80 + price, 81 + price_per_unit, 82 + quantity, 83 + availability_delivery, 84 + availability_pickup 85 + };
+3
internal/brightdata/walmart_input.csv
··· 1 + url,zip_code,store_id,shipping_type,max_product 2 + https://www.walmart.com/browse/home/shop-bowls/4044_623679_3480962_3999334?povid=Home_Hubspoke_CookDine_639999DiningEntertaining_Category_BTF_Bowls,20011,,,900 3 + https://www.walmart.com/shop/mothers-day-home-gifts?povid=XCATNav_EvergreenGifting_Mothersdaygifting_Mothersdaygiftfinder__Visualfacet_Home,20011,,delivey,
+11 -2
internal/config/config.go
··· 75 75 } 76 76 77 77 type AlbertsonsConfig struct { 78 - Enable bool `json:"enable"` 78 + Enable bool `json:"enable"` 79 + SearchSubscriptionKey string `json:"search_subscription_key"` 80 + SearchReese84 string `json:"search_reese84"` 79 81 } 80 82 81 83 func (c *AlbertsonsConfig) IsEnabled() bool { 82 84 return c.Enable 85 + } 86 + 87 + // can we dynamically disable if our key expires 88 + func (c *AlbertsonsConfig) HasInventory() bool { 89 + return c.SearchReese84 != "" 83 90 } 84 91 85 92 type PublixConfig struct { ··· 163 170 Enable: envEnabled("WHOLEFOODS_ENABLE"), 164 171 }, 165 172 Albertsons: AlbertsonsConfig{ 166 - Enable: envEnabled("ALBERTSONS_ENABLE"), 173 + Enable: envEnabled("ALBERTSONS_ENABLE"), 174 + SearchSubscriptionKey: os.Getenv("ALBERTSONS_SEARCH_SUBSCRIPTION_KEY"), 175 + SearchReese84: os.Getenv("ALBERTSONS_SEARCH_REESE84"), 167 176 }, 168 177 Publix: PublixConfig{ 169 178 Enable: envEnabled("PUBLIX_ENABLE"),
+21
internal/config/config_test.go
··· 74 74 } 75 75 } 76 76 77 + func TestLoadReadsAlbertsonsSearchCredentials(t *testing.T) { 78 + resetStoreEnvs(t) 79 + t.Setenv("ENABLE_MOCKS", "1") 80 + t.Setenv("ALBERTSONS_SEARCH_SUBSCRIPTION_KEY", "sub-key") 81 + t.Setenv("ALBERTSONS_SEARCH_REESE84", "cookie-value") 82 + 83 + cfg, err := Load() 84 + if err != nil { 85 + t.Fatalf("Load() error = %v", err) 86 + } 87 + 88 + if got, want := cfg.Albertsons.SearchSubscriptionKey, "sub-key"; got != want { 89 + t.Fatalf("expected Albertsons subscription key %q, got %q", want, got) 90 + } 91 + if got, want := cfg.Albertsons.SearchReese84, "cookie-value"; got != want { 92 + t.Fatalf("expected Albertsons reese84 %q, got %q", want, got) 93 + } 94 + } 95 + 77 96 func TestResolvedPublicOriginDefaultsToLocalhostOutsideProd(t *testing.T) { 78 97 cfg := &Config{} 79 98 if got, want := cfg.ResolvedPublicOrigin(), "http://localhost:8080"; got != want { ··· 119 138 "ALDI_ENABLE", 120 139 "WHOLEFOODS_ENABLE", 121 140 "ALBERTSONS_ENABLE", 141 + "ALBERTSONS_SEARCH_SUBSCRIPTION_KEY", 142 + "ALBERTSONS_SEARCH_REESE84", 122 143 "PUBLIX_ENABLE", 123 144 "HEB_ENABLE", 124 145 } {
+6 -3
internal/recipes/staples.go
··· 5 5 "fmt" 6 6 "testing" 7 7 8 + "careme/internal/albertsons" 8 9 "careme/internal/config" 9 10 "careme/internal/kroger" 10 11 "careme/internal/walmart" ··· 38 39 return nil, err 39 40 } 40 41 return routingStaplesProvider{ 41 - backends: defaultStaplesBackends(kclient), 42 + backends: defaultStaplesBackends(cfg, kclient), 42 43 }, nil 43 44 } 44 45 ··· 81 82 return nil, fmt.Errorf("staples provider does not support location %q", locationID) 82 83 } 83 84 84 - func defaultStaplesBackends(krogerClient kroger.ClientWithResponsesInterface) []backendStaplesProvider { 85 + func defaultStaplesBackends(cfg *config.Config, krogerClient kroger.ClientWithResponsesInterface) []backendStaplesProvider { 85 86 return []backendStaplesProvider{ 86 87 kroger.NewStaplesProvider(krogerClient), 87 88 // actowiz.NewStaplesProvider(), 89 + walmart.NewStaplesProvider(), 88 90 wholefoods.NewStaplesProvider(wholefoods.NewClient(nil)), 89 - walmart.NewStaplesProvider(), 91 + albertsons.NewStaplesProvider(cfg.Albertsons), 90 92 } 91 93 } 92 94 ··· 94 96 return []identityProvider{ 95 97 kroger.NewIdentityProvider(), 96 98 // actowiz.NewIdentityProvider(), 99 + albertsons.NewIdentityProvider(), 97 100 wholefoods.NewIdentityProvider(), 98 101 walmart.NewIdentityProvider(), 99 102 }
+11
internal/recipes/staples_test.go
··· 6 6 "testing" 7 7 "time" 8 8 9 + "careme/internal/albertsons" 9 10 "careme/internal/cache" 10 11 "careme/internal/kroger" 11 12 "careme/internal/locations" ··· 121 122 } 122 123 if krogerProvider.calls != 0 || wholeFoodsProvider.calls != 1 { 123 124 t.Fatalf("expected whole foods provider to be selected, got kroger=%d wholefoods=%d", krogerProvider.calls, wholeFoodsProvider.calls) 125 + } 126 + } 127 + 128 + func TestStaplesSignatureForLocation_UsesAlbertsonsIdentityProvider(t *testing.T) { 129 + t.Parallel() 130 + 131 + got := staplesSignatureForLocation("safeway_1142") 132 + want := albertsons.NewIdentityProvider().Signature() 133 + if got != want { 134 + t.Fatalf("unexpected signature: got %q want %q", got, want) 124 135 } 125 136 } 126 137