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
23 changes: 23 additions & 0 deletions docs/bug-fixes.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,29 @@ Each bug fix entry should include:

## Bug Fixes

### 2026-07-16: Completed Walmart in-store receipts were treated as payment pending

**Description:**
Walmart returned completed in-store receipts with a credit-card payment method and final total, but its separate order-ledger endpoint returned no payment methods for those receipts. Itemize treated every empty ledger as an uncharged online order and skipped the in-store purchases indefinitely as `payment pending`.

**Test Case:**
```go
// internal/adapters/providers/walmart/order_multi_delivery_test.go:
// TestOrder_GetFinalCharges/completed_in-store_order_falls_back_to_its_credit-card_total_when_ledger_is_empty
// Expected: a completed IN_STORE order with one CREDITCARD payment returns its receipt total as the bank charge.
```

**Root Cause:**
`GetFinalCharges()` assumed Walmart's order-ledger endpoint was populated for every fulfillment type. Live completed in-store orders retain their payment metadata on the order payload but have an empty ledger response.

**Fix Applied:**
When the ledger is empty, Itemize now uses the receipt total only for an `IN_STORE` order with exactly one `CREDITCARD` payment method and a positive total. Other empty-ledger orders keep the existing payment-pending behavior, avoiding unsafe guesses for split-tender or gift-card purchases.

**Verification:**
- The regression test failed before the fix with `order not yet charged (payment pending)` and passes after it.
- A live run of the rebuilt v0.2.1 source reproduced all three completed in-store receipts being skipped with an empty ledger before the fix.
- The fixed binary dry-ran all three affected receipts against live Walmart and Monarch data, matched `$9.88`, `$32.10`, and `$10.69` transactions, and finished with `Processed=3 Skipped=0 Errors=0` without writing to Monarch.

### 2026-07-15: Walmart detail requests were rejected with HTTP 456

**Description:**
Expand Down
24 changes: 24 additions & 0 deletions internal/adapters/providers/walmart/order.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,14 @@ func (o *Order) GetFinalCharges() ([]float64, error) {

// Extract and validate charges
if len(ledger.PaymentMethods) == 0 {
if charge, ok := o.inStoreCreditCardCharge(); ok {
if o.logger != nil {
o.logger.Debug("Using completed in-store order total because Walmart ledger is empty",
"order_id", o.GetID(),
"charge", charge)
}
return []float64{charge}, nil
}
return nil, fmt.Errorf("order not yet charged (payment pending)")
}

Expand Down Expand Up @@ -237,6 +245,22 @@ func (o *Order) GetFinalCharges() ([]float64, error) {
return positiveCharges, nil
}

// inStoreCreditCardCharge returns the single bank charge represented by a
// completed in-store receipt. Walmart does not populate the order-ledger API
// for these receipts, so the receipt total is the authoritative charge when
// its order payload identifies one credit-card payment method.
func (o *Order) inStoreCreditCardCharge() (float64, bool) {
if o.walmartOrder.Type != "IN_STORE" || len(o.walmartOrder.PaymentMethods) != 1 {
return 0, false
}
if o.walmartOrder.PaymentMethods[0].PaymentType != "CREDITCARD" {
return 0, false
}

charge := o.GetTotal()
return charge, charge > 0
}

// GetRefundCharges returns credit-card refunds for this order as positive amounts.
// Walmart records refunds as negative ledger entries, while Monarch records the
// corresponding bank credits as positive transactions.
Expand Down
80 changes: 80 additions & 0 deletions internal/adapters/providers/walmart/order_multi_delivery_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package walmart

import (
"io"
"log/slog"
"testing"

walmartclient "github.com/eshaffer321/walmart-client-go/v2"
Expand Down Expand Up @@ -120,6 +122,84 @@ func TestOrder_GetFinalCharges(t *testing.T) {
assert.Contains(t, err.Error(), "payment pending")
})

t.Run("completed in-store order falls back to its credit-card total when ledger is empty", func(t *testing.T) {
order := &Order{
walmartOrder: &walmartclient.Order{
ID: "IN-STORE-123",
Type: "IN_STORE",
PriceDetails: &walmartclient.OrderPriceDetails{
GrandTotal: &walmartclient.PriceLineItem{Value: 9.88},
},
PaymentMethods: []walmartclient.OrderPaymentMethod{{PaymentType: "CREDITCARD"}},
},
ledgerCache: &walmartclient.OrderLedger{
OrderID: "IN-STORE-123",
PaymentMethods: []walmartclient.PaymentMethodCharges{},
},
logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
}

charges, err := order.GetFinalCharges()
require.NoError(t, err)
assert.Equal(t, []float64{9.88}, charges)
})

t.Run("empty in-store ledger does not guess an ambiguous charge", func(t *testing.T) {
tests := []struct {
name string
orderType string
paymentMethods []walmartclient.OrderPaymentMethod
total float64
}{
{
name: "online order",
orderType: "GLASS",
paymentMethods: []walmartclient.OrderPaymentMethod{{PaymentType: "CREDITCARD"}},
total: 9.88,
},
{
name: "split tender",
orderType: "IN_STORE",
paymentMethods: []walmartclient.OrderPaymentMethod{
{PaymentType: "CREDITCARD"},
{PaymentType: "GIFTCARD"},
},
total: 9.88,
},
{
name: "gift card only",
orderType: "IN_STORE",
paymentMethods: []walmartclient.OrderPaymentMethod{{PaymentType: "GIFTCARD"}},
total: 9.88,
},
{
name: "non-positive total",
orderType: "IN_STORE",
paymentMethods: []walmartclient.OrderPaymentMethod{{PaymentType: "CREDITCARD"}},
total: 0,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
order := &Order{
walmartOrder: &walmartclient.Order{
ID: "AMBIGUOUS-IN-STORE",
Type: tt.orderType,
PaymentMethods: tt.paymentMethods,
PriceDetails: &walmartclient.OrderPriceDetails{
GrandTotal: &walmartclient.PriceLineItem{Value: tt.total},
},
},
ledgerCache: &walmartclient.OrderLedger{OrderID: "AMBIGUOUS-IN-STORE"},
}

_, err := order.GetFinalCharges()
require.ErrorContains(t, err, "payment pending")
})
}
})

t.Run("error - client not available", func(t *testing.T) {
order := &Order{
walmartOrder: &walmartclient.Order{ID: "TEST001"},
Expand Down