-
Notifications
You must be signed in to change notification settings - Fork 24
Expand file tree
/
Copy pathshoppinglist.go
More file actions
342 lines (290 loc) · 9.48 KB
/
Copy pathshoppinglist.go
File metadata and controls
342 lines (290 loc) · 9.48 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
package appie
import (
"context"
"fmt"
"net/http"
"strings"
)
// listResponse matches the API response for favorite lists (v3).
type listResponse struct {
ID string `json:"id"`
Description string `json:"description"`
ItemCount int `json:"itemCount"`
HasFavoriteProduct bool `json:"hasFavoriteProduct"`
ProductImages [][]struct {
Width int `json:"width"`
Height int `json:"height"`
URL string `json:"url"`
} `json:"productImages"`
}
// shoppingListItem is the v2 request body format for adding items.
type shoppingListItem struct {
Description string `json:"description"`
ProductID int `json:"productId,omitempty"`
Quantity int `json:"quantity"`
Type string `json:"type"`
OriginCode string `json:"originCode"`
SearchTerm string `json:"searchTerm,omitempty"`
StrikeThrough bool `json:"strikeThrough"`
}
// GetShoppingLists retrieves all favorite lists (v3) for the authenticated user.
// The API quirk requires a productId parameter, but returns all lists regardless.
// Pass 0 to use a default product ID (recommended).
func (c *Client) GetShoppingLists(ctx context.Context, productID int) ([]ShoppingList, error) {
if productID <= 0 {
productID = 1 // Default product ID - API requires it but returns all lists
}
path := fmt.Sprintf("/mobile-services/lists/v3/lists?productId=%d", productID)
var result []listResponse
if err := c.DoRequest(ctx, http.MethodGet, path, nil, &result); err != nil {
return nil, fmt.Errorf("get shopping lists failed: %w", err)
}
lists := make([]ShoppingList, 0, len(result))
for _, r := range result {
lists = append(lists, ShoppingList{
ID: r.ID,
Name: r.Description,
ItemCount: r.ItemCount,
})
}
return lists, nil
}
// GetShoppingListItems retrieves all items for a specific list (v2 GraphQL).
func (c *Client) GetShoppingListItems(ctx context.Context, listID string) ([]ListItem, error) {
const query = `query FavoriteListV2($ids: [String!]!) {
favoriteListV2(ids: $ids) {
id
description
totalSize
items {
id
productId
quantity
}
}
}`
variables := map[string]any{
"ids": []string{strings.ToUpper(listID)},
}
type listItemResult struct {
ID string `json:"id"`
ProductID int `json:"productId"`
Quantity int `json:"quantity"`
}
type listResult struct {
ID string `json:"id"`
Description string `json:"description"`
TotalSize int `json:"totalSize"`
Items []listItemResult `json:"items"`
}
// API may return a single object or array; try array first.
var arrResult struct {
FavoriteListV2 []listResult `json:"favoriteListV2"`
}
if err := c.DoGraphQL(ctx, query, variables, &arrResult); err != nil {
return nil, fmt.Errorf("get shopping list items failed: %w", err)
}
if len(arrResult.FavoriteListV2) == 0 {
return nil, fmt.Errorf("list %s not found", listID)
}
raw := arrResult.FavoriteListV2[0].Items
items := make([]ListItem, 0, len(raw))
for _, item := range raw {
items = append(items, ListItem{
ID: item.ID,
ProductID: item.ProductID,
Quantity: max(item.Quantity, 1),
})
}
return items, nil
}
// getShoppingList retrieves the first (default) favorite list with its items populated.
func (c *Client) getShoppingList(ctx context.Context) (*ShoppingList, error) {
lists, err := c.GetShoppingLists(ctx, 0)
if err != nil {
return nil, err
}
if len(lists) == 0 {
return nil, fmt.Errorf("no shopping lists found")
}
items, err := c.GetShoppingListItems(ctx, lists[0].ID)
if err != nil {
return nil, err
}
lists[0].Items = items
return &lists[0], nil
}
// AddToShoppingList adds products to the main shopping list (v2).
// This uses PATCH /shoppinglist/v2/items.
func (c *Client) AddToShoppingList(ctx context.Context, items []ListItem) error {
v2Items := make([]shoppingListItem, 0, len(items))
for _, item := range items {
v2 := shoppingListItem{
Quantity: max(item.Quantity, 1),
StrikeThrough: false,
}
if item.ProductID > 0 {
v2.ProductID = item.ProductID
v2.Type = "SHOPPABLE"
v2.OriginCode = "PRD"
v2.Description = item.Name
v2.SearchTerm = item.Name
} else {
v2.Type = "SHOPPABLE"
v2.OriginCode = "PRD"
v2.Description = item.Name
}
v2Items = append(v2Items, v2)
}
body := map[string]any{
"items": v2Items,
}
if err := c.DoRequest(ctx, http.MethodPatch, "/mobile-services/shoppinglist/v2/items", body, nil); err != nil {
return fmt.Errorf("add to shopping list failed: %w", err)
}
return nil
}
// AddProductToShoppingList adds a product to the main shopping list.
func (c *Client) AddProductToShoppingList(ctx context.Context, productID int, quantity int) error {
return c.AddToShoppingList(ctx, []ListItem{{
ProductID: productID,
Quantity: max(quantity, 1),
}})
}
// AddFreeTextToShoppingList adds a free-text item (not linked to a product) to the main shopping list.
func (c *Client) AddFreeTextToShoppingList(ctx context.Context, name string, quantity int) error {
return c.AddToShoppingList(ctx, []ListItem{{
Name: name,
Quantity: max(quantity, 1),
}})
}
// AddToFavoriteList adds products to a named favorite list (v3) using GraphQL.
// Each item's ProductID and Quantity are sent. Use GetShoppingLists to get list IDs.
func (c *Client) AddToFavoriteList(ctx context.Context, listID string, items []ListItem) error {
const mutation = `mutation AddProductsToFavoriteList($favoriteListId: String!, $products: [FavoriteListProductMutation!]!) {
favoriteListProductsAddV2(id: $favoriteListId, products: $products) {
__typename
status
errorMessage
}
}`
products := make([]map[string]int, 0, len(items))
for _, item := range items {
products = append(products, map[string]int{
"productId": item.ProductID,
"quantity": max(item.Quantity, 1),
})
}
variables := map[string]any{
"favoriteListId": strings.ToUpper(listID),
"products": products,
}
var result struct {
FavoriteListProductsAddV2 struct {
Status string `json:"status"`
ErrorMessage string `json:"errorMessage"`
} `json:"favoriteListProductsAddV2"`
}
if err := c.DoGraphQL(ctx, mutation, variables, &result); err != nil {
return fmt.Errorf("add to favorite list failed: %w", err)
}
if result.FavoriteListProductsAddV2.Status != "SUCCESS" {
return fmt.Errorf("add to favorite list failed: %s", result.FavoriteListProductsAddV2.ErrorMessage)
}
return nil
}
// RemoveFromFavoriteList removes products from a named favorite list using GraphQL.
// It looks up item IDs by product ID, then calls favoriteListProductsDeleteV2.
func (c *Client) RemoveFromFavoriteList(ctx context.Context, listID string, productIDs []int) error {
items, err := c.GetShoppingListItems(ctx, listID)
if err != nil {
return err
}
want := make(map[int]bool, len(productIDs))
for _, id := range productIDs {
want[id] = true
}
var itemIDs []string
for _, item := range items {
if want[item.ProductID] {
itemIDs = append(itemIDs, item.ID)
}
}
if len(itemIDs) == 0 {
return fmt.Errorf("none of the specified products found in list")
}
const mutation = `mutation DeleteProductsFromFavoriteList($favoriteListId: String!, $itemIds: [String!]!) {
favoriteListProductsDeleteV2(id: $favoriteListId, itemIds: $itemIds) {
status
errorMessage
}
}`
variables := map[string]any{
"favoriteListId": strings.ToUpper(listID),
"itemIds": itemIDs,
}
var result struct {
FavoriteListProductsDeleteV2 struct {
Status string `json:"status"`
ErrorMessage string `json:"errorMessage"`
} `json:"favoriteListProductsDeleteV2"`
}
if err := c.DoGraphQL(ctx, mutation, variables, &result); err != nil {
return fmt.Errorf("remove from favorite list failed: %w", err)
}
if result.FavoriteListProductsDeleteV2.Status != "SUCCESS" {
return fmt.Errorf("remove from favorite list failed: %s", result.FavoriteListProductsDeleteV2.ErrorMessage)
}
return nil
}
// CheckShoppingListItem marks an item as checked (picked) or unchecked.
// Checked items are typically displayed differently in the app UI.
func (c *Client) CheckShoppingListItem(ctx context.Context, itemID string, checked bool) error {
body := map[string]any{
"checked": checked,
}
path := fmt.Sprintf("/mobile-services/lists/v3/lists/items/%s", itemID)
if err := c.DoRequest(ctx, http.MethodPatch, path, body, nil); err != nil {
return fmt.Errorf("check shopping list item failed: %w", err)
}
return nil
}
// ClearShoppingList removes all items from the shopping list.
func (c *Client) ClearShoppingList(ctx context.Context) error {
list, err := c.getShoppingList(ctx)
if err != nil {
return err
}
var productIDs []int
for _, item := range list.Items {
if item.ProductID > 0 {
productIDs = append(productIDs, item.ProductID)
}
}
if len(productIDs) == 0 {
return nil
}
return c.RemoveFromFavoriteList(ctx, list.ID, productIDs)
}
// ShoppingListToOrder adds all unchecked product items from the shopping list to the order.
// Free-text items (without ProductID) are skipped. This is useful for quickly
// converting your shopping list into an order.
func (c *Client) ShoppingListToOrder(ctx context.Context) error {
list, err := c.getShoppingList(ctx)
if err != nil {
return err
}
var orderItems []OrderItem
for _, item := range list.Items {
if !item.Checked && item.ProductID > 0 {
orderItems = append(orderItems, OrderItem{
ProductID: item.ProductID,
Quantity: item.Quantity,
})
}
}
if len(orderItems) == 0 {
return nil
}
return c.AddToOrder(ctx, orderItems)
}