Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 18 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,14 +104,24 @@ Usage:
# Login to Albert Heijn (opens browser for OAuth)
appie login

# List recent receipts
appie receipt

# List last 5 receipts
appie receipt -n 5

# Show receipt details (items, discounts, payment)
appie receipt <transaction-id>
# Search products
appie search "pindakaas"

# Receipts
appie receipt # list recent receipts
appie receipt show <transaction-id> # show items, discounts, payment

# Orders
appie order # list open orders
appie order show <order-id> # show order contents
appie order add <order-id> <product> # add product (by ID or search term)
appie order rm <order-id> <product-id> # remove product

# Shopping lists
appie list # list all shopping lists
appie list show <list-id> # show items in a list
appie list add <list-id> <product> # add product (by ID or search term)
appie list rm <list-id> <product-id> # remove product
```

Config is stored at `~/.config/appie/config.json` (or `$XDG_CONFIG_HOME/appie/config.json`). Override with `-c`.
Expand Down
30 changes: 30 additions & 0 deletions appie_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,36 @@ func TestGetShoppingList(t *testing.T) {
}
}

func TestGetShoppingListItems(t *testing.T) {
client := testClient(t)
ctx := context.Background()

lists, err := client.GetShoppingLists(ctx, 0)
if err != nil {
t.Fatalf("failed to get shopping lists: %v", err)
}
if len(lists) == 0 {
t.Skip("no shopping lists found")
}

for _, list := range lists {
t.Logf("List: %s (%s, %d items)", list.Name, list.ID, list.ItemCount)

items, err := client.GetShoppingListItems(ctx, list.ID)
if err != nil {
t.Fatalf("failed to get items for list %s: %v", list.Name, err)
}

for i, item := range items {
if i >= 5 {
t.Logf(" ... and %d more", len(items)-5)
break
}
t.Logf(" - [%s] productId=%d qty=%d", item.ID, item.ProductID, item.Quantity)
}
}
}

func TestOrderRoundTrip(t *testing.T) {
client := testClient(t)
ctx := context.Background()
Expand Down
13 changes: 2 additions & 11 deletions bonus.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,22 +42,13 @@ type bonusGroupResponse struct {
SegmentDescription string `json:"segmentDescription"`
DiscountDescription string `json:"discountDescription"`
Category string `json:"category"`
Images []imageResponse `json:"images"`
Images []Image `json:"images"`
Products []productResponse `json:"products"`
ExampleFromPrice float64 `json:"exampleFromPrice"`
ExampleForPrice float64 `json:"exampleForPrice"`
}

func (bg *bonusGroupResponse) toProduct() Product {
var images []Image
for _, img := range bg.Images {
images = append(images, Image{
URL: img.URL,
Width: img.Width,
Height: img.Height,
})
}

return Product{
Title: bg.SegmentDescription,
Category: bg.Category,
Expand All @@ -68,7 +59,7 @@ func (bg *bonusGroupResponse) toProduct() Product {
Now: bg.ExampleForPrice,
Was: bg.ExampleFromPrice,
},
Images: images,
Images: bg.Images,
}
}

Expand Down
18 changes: 9 additions & 9 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ import (
)

const (
defaultBaseURL = "https://api.ah.nl"
defaultUserAgent = "Appie/9.28 (iPhone17,3; iPhone; CPU OS 26_1 like Mac OS X)"
defaultClientID = "appie-ios"
defaultBaseURL = "https://api.ah.nl"
defaultUserAgent = "Appie/9.28 (iPhone17,3; iPhone; CPU OS 26_1 like Mac OS X)"
defaultClientID = "appie-ios"
defaultClientVersion = "9.28"
)

Expand All @@ -26,22 +26,22 @@ const (
//
// Client is safe for concurrent use. Token state is protected by a mutex.
type Client struct {
httpClient *http.Client
baseURL string
userAgent string
clientID string
httpClient *http.Client
baseURL string
userAgent string
clientID string
clientVersion string

mu sync.RWMutex
accessToken string
refreshToken string
memberID string
expiresAt time.Time
orderID string
orderID string // sent as appie-current-order-id header, mirroring the iOS app; the API may use this (not server-side state) to determine the active order
orderHash string

configPath string
loginBaseURL string // overridable for testing; defaults to "https://login.ah.nl"
loginBaseURL string // overridable for testing; defaults to "https://login.ah.nl"
openBrowser func(string) // overridable for testing; nil uses default
logger *log.Logger
}
Expand Down
38 changes: 38 additions & 0 deletions cmd/appie/helpers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package main

import (
"testing"

appie "github.com/gwillem/appie-go"
)

func TestFindList(t *testing.T) {
lists := []appie.ShoppingList{
{ID: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", Name: "Boodschappen"},
{ID: "11111111-2222-3333-4444-555555555555", Name: "Weekmenu"},
}

t.Run("exact match", func(t *testing.T) {
got, err := findList(lists, "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got.Name != "Boodschappen" {
t.Fatalf("got %q, want %q", got.Name, "Boodschappen")
}
})

t.Run("not found", func(t *testing.T) {
_, err := findList(lists, "00000000-0000-0000-0000-000000000000")
if err == nil {
t.Fatal("expected error, got nil")
}
})

t.Run("prefix does not match", func(t *testing.T) {
_, err := findList(lists, "aaaaaaaa")
if err == nil {
t.Fatal("expected error for prefix match, got nil")
}
})
}
Loading
Loading