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
85 changes: 85 additions & 0 deletions .github/workflows/playwright.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
name: Playwright E2E Tests

on:
push:
branches: [main, develop]
paths:
- 'web/**'
- '.github/workflows/playwright.yml'
pull_request:
branches: [main, develop]
paths:
- 'web/**'
- '.github/workflows/playwright.yml'

jobs:
playwright:
name: Playwright Tests
runs-on: ubuntu-latest
timeout-minutes: 15
# Continue on failure - E2E tests may fail without full backend setup
continue-on-error: true
defaults:
run:
working-directory: ./web

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.24'
cache: true

- name: Build Go backend
run: go build -o monarch-sync ./cmd/monarch-sync/
working-directory: .

- name: Start Go backend
run: |
./monarch-sync serve -port 8085 &
sleep 3
working-directory: .
env:
# Use dummy values - tests should mock API responses or be skipped
MONARCH_TOKEN: "dummy-token-for-ci"

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: web/package-lock.json

- name: Install dependencies
run: npm ci

- name: Install Playwright browsers
run: npx playwright install --with-deps chromium

- name: Build Next.js app
run: npm run build

- name: Run Playwright tests
run: npx playwright test --grep-invert "@backend"
env:
CI: true
NEXT_PUBLIC_API_URL: http://localhost:8085

- name: Upload Playwright report
uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: web/playwright-report/
retention-days: 30

- name: Upload test results
uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-test-results
path: web/test-results/
retention-days: 7
101 changes: 92 additions & 9 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
# Developer Guide for AI Assistants

**Last Updated:** October 2024
**Project Status:** Production-ready CLI application
**Last Updated:** December 2024
**Project Status:** Production-ready CLI application with Web UI

## What This Project Is

A working CLI application that syncs Walmart and Costco purchases with Monarch Money, automatically categorizing items and splitting transactions. **This is NOT a web server or API** - it's a command-line tool that runs locally.
A CLI application that syncs Walmart, Costco, and Amazon purchases with Monarch Money, automatically categorizing items and splitting transactions. It now includes:
- **CLI tool** for command-line syncing
- **API server** (`./monarch-sync serve`) for programmatic access
- **Web UI** (Next.js) for monitoring and triggering syncs

## Quick Reference

### Build and Run
```bash
# Build
# Build Go backend
go build -o monarch-sync ./cmd/monarch-sync/

# Run with dry-run (preview, no changes)
Expand All @@ -24,11 +27,31 @@ go build -o monarch-sync ./cmd/monarch-sync/

# Force reprocess already-processed orders
./monarch-sync walmart -force

# Start API server (for web UI)
./monarch-sync serve -port 8085
```

### Web UI
```bash
cd web

# Install dependencies
npm install

# Start development server (port 3000)
npm run dev

# Build for production
npm run build

# Start production server
npm start
```

### Test
```bash
# All tests
# Go tests (all)
go test ./... -v

# Specific layer
Expand All @@ -42,6 +65,33 @@ go test ./... -cover
go test ./... -race
```

### Frontend E2E Tests (Playwright)
```bash
cd web

# Run all E2E tests (requires dev server running or uses webServer config)
npx playwright test

# Run specific test file
npx playwright test navigation.spec.ts

# Run tests with UI mode (interactive)
npx playwright test --ui

# Run tests with visible browser
npx playwright test --headed

# Generate HTML report
npx playwright show-report
```

**Playwright Test Files:**
- `web/e2e/navigation.spec.ts` - Navigation and page loading tests
- `web/e2e/sync.spec.ts` - Sync page functionality
- `web/e2e/dark-mode.spec.ts` - Theme switching tests
- `web/e2e/search.spec.ts` - Search and filtering
- `web/e2e/date-filter.spec.ts` - Date range filtering

### Configuration
The app reads from `config.yaml` or environment variables:
- `MONARCH_TOKEN` - Monarch Money API token (required)
Expand Down Expand Up @@ -73,6 +123,37 @@ internal/

See [docs/architecture.md](docs/architecture.md) for complete details.

### Web Frontend Architecture

```
web/
├── src/
│ ├── app/(app)/ # Next.js App Router pages
│ │ ├── page.tsx # Dashboard
│ │ ├── orders/ # Orders list and detail
│ │ ├── runs/ # Sync runs list
│ │ ├── sync/ # Sync page with job detail
│ │ └── transactions/ # Transactions list and detail
│ ├── components/ # Catalyst UI components
│ └── lib/
│ └── api/ # API client and types
├── e2e/ # Playwright E2E tests
└── playwright.config.ts # Playwright configuration
```

**Frontend Tech Stack:**
- **Next.js 15** with App Router
- **TypeScript** for type safety
- **Tailwind CSS** for styling
- **Catalyst UI** component library
- **Playwright** for E2E testing

**Key Patterns:**
- Server components (page.tsx) for data fetching
- Client components for interactivity (e.g., `orders-table.tsx` with sorting)
- API types in `web/src/lib/api/types.ts`
- Shared table components with sorting in `web/src/components/table.tsx`

## Development Methodology: TDD

### Core Workflow
Expand Down Expand Up @@ -297,12 +378,14 @@ CREATE TABLE sync_runs (

Schema auto-migrates on startup. See [internal/infrastructure/storage/storage.go](internal/infrastructure/storage/storage.go).

## What This Project Is NOT
## Project Scope

- ❌ **NOT a web server** - No HTTP endpoints, no API
- ✅ **CLI tool** - Primary command-line interface
- ✅ **API server** - HTTP endpoints for programmatic access (`./monarch-sync serve`)
- ✅ **Web UI** - Next.js dashboard for monitoring and triggering syncs
- ❌ **NOT a Chrome extension** - Direct provider API integration
- ❌ **NOT a SaaS** - Local CLI tool
- ❌ **NOT real-time** - Manual runs or scheduled via cron
- ❌ **NOT a SaaS** - Local deployment only
- ❌ **NOT real-time** - Manual runs, web-triggered, or scheduled via cron
- ❌ **NOT multi-user** - Single user per config

## Troubleshooting
Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ go 1.24.0
require (
github.com/eshaffer321/costco-go v0.3.4
github.com/eshaffer321/monarchmoney-go v1.0.2
github.com/eshaffer321/walmart-client-go v1.0.8
github.com/eshaffer321/walmart-client-go/v2 v2.0.1
github.com/go-chi/chi/v5 v5.2.3
github.com/mattn/go-sqlite3 v1.14.32
github.com/stretchr/testify v1.11.1
golang.org/x/term v0.35.0
Expand All @@ -15,7 +16,6 @@ require (
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/getsentry/sentry-go v0.36.0 // indirect
github.com/go-chi/chi/v5 v5.2.3 // indirect
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/uuid v1.6.0 // indirect
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ github.com/eshaffer321/costco-go v0.3.4 h1:xRyZGgk63V8A68jEuCR9gvFSk1c1n/lLjEf3E
github.com/eshaffer321/costco-go v0.3.4/go.mod h1:pIIKOjw+KyiQ2+xn9EUdZTIRPbts3b4mU7+OzDY/Jdo=
github.com/eshaffer321/monarchmoney-go v1.0.2 h1:bCENouzURZdBKy8artmRAQNBEUXhIRYi++lP/WPZCz4=
github.com/eshaffer321/monarchmoney-go v1.0.2/go.mod h1:ZKPCYT7NcsKGI+YpJ2EqPtfE3dKfuPbiTUrj6J84ot4=
github.com/eshaffer321/walmart-client-go v1.0.8 h1:F+FHhy+HAI6bTdxCB7hb630JorKvLQBSudBZVmd2y+Y=
github.com/eshaffer321/walmart-client-go v1.0.8/go.mod h1:TuYHoEj2m2EvLV5WOxW5krk1Ycd/CO4o0PkyCrjWJgE=
github.com/eshaffer321/walmart-client-go/v2 v2.0.1 h1:R8NFqKqfdri02Jhmr6jOMpCLAzjdiRbStLtjGKo6WaA=
github.com/eshaffer321/walmart-client-go/v2 v2.0.1/go.mod h1:4PVK9TsqFscTZypC67dgCt/vnPxXdtaheJVM7HOnod0=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/getsentry/sentry-go v0.36.0 h1:UkCk0zV28PiGf+2YIONSSYiYhxwlERE5Li3JPpZqEns=
Expand Down
10 changes: 8 additions & 2 deletions internal/adapters/providers/walmart/order.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
package walmart

import (
"context"
"fmt"
"log/slog"
"time"

"github.com/eshaffer321/monarchmoney-sync-backend/internal/adapters/providers"
walmartclient "github.com/eshaffer321/walmart-client-go"
walmartclient "github.com/eshaffer321/walmart-client-go/v2"
)

// Order wraps a Walmart order and implements providers.Order interface
type Order struct {
walmartOrder *walmartclient.Order
client *walmartclient.WalmartClient
logger *slog.Logger
ctx context.Context

// ledgerCache stores the order ledger to avoid duplicate API calls.
// Note: Assumes single-threaded access per Order instance.
Expand Down Expand Up @@ -206,7 +208,11 @@ func (o *Order) GetFinalCharges() ([]float64, error) {

// Fetch ledger from API
var err error
ledger, err = o.client.GetOrderLedger(o.GetID())
ctx := o.ctx
if ctx == nil {
ctx = context.Background()
}
ledger, err = o.client.GetOrderLedger(ctx, o.GetID())
if err != nil {
return nil, fmt.Errorf("failed to get order ledger: %w", err)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package walmart
import (
"testing"

walmartclient "github.com/eshaffer321/walmart-client-go"
walmartclient "github.com/eshaffer321/walmart-client-go/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
Expand Down
2 changes: 1 addition & 1 deletion internal/adapters/providers/walmart/order_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"time"

"github.com/eshaffer321/monarchmoney-sync-backend/internal/adapters/providers"
walmartclient "github.com/eshaffer321/walmart-client-go"
walmartclient "github.com/eshaffer321/walmart-client-go/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
Expand Down
17 changes: 12 additions & 5 deletions internal/adapters/providers/walmart/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
"time"

"github.com/eshaffer321/monarchmoney-sync-backend/internal/adapters/providers"
walmartclient "github.com/eshaffer321/walmart-client-go"
walmartclient "github.com/eshaffer321/walmart-client-go/v2"
)

// Provider implements the OrderProvider interface for Walmart
Expand Down Expand Up @@ -59,18 +59,23 @@ func (p *Provider) FetchOrders(ctx context.Context, opts providers.FetchOptions)
}

// Fetch purchase history from Walmart API
resp, err := p.client.GetPurchaseHistory(req)
resp, err := p.client.GetPurchaseHistory(ctx, req)
if err != nil {
return nil, fmt.Errorf("failed to fetch walmart orders: %w", err)
}

// Convert OrderSummary to full Orders
var providerOrders []providers.Order
for _, summary := range resp.Data.OrderHistoryV2.OrderGroups {
// Check for context cancellation before each order fetch
if err := ctx.Err(); err != nil {
return providerOrders, fmt.Errorf("cancelled during order fetch: %w", err)
}

// Fetch full order details if requested
if opts.IncludeDetails {
isInStore := summary.FulfillmentType == "IN_STORE"
fullOrder, err := p.client.GetOrder(summary.OrderID, isInStore)
fullOrder, err := p.client.GetOrder(ctx, summary.OrderID, isInStore)
if err != nil {
p.logger.Warn("failed to fetch order details, skipping",
slog.String("order_id", summary.OrderID),
Expand All @@ -82,12 +87,13 @@ func (p *Provider) FetchOrders(ctx context.Context, opts providers.FetchOptions)
walmartOrder: fullOrder,
client: p.client,
logger: p.logger,
ctx: ctx,
})
} else {
// For basic listing, we'd need to create a minimal Order
// For now, always fetch details
isInStore := summary.FulfillmentType == "IN_STORE"
fullOrder, err := p.client.GetOrder(summary.OrderID, isInStore)
fullOrder, err := p.client.GetOrder(ctx, summary.OrderID, isInStore)
if err != nil {
p.logger.Warn("failed to fetch order details, skipping",
slog.String("order_id", summary.OrderID),
Expand All @@ -99,6 +105,7 @@ func (p *Provider) FetchOrders(ctx context.Context, opts providers.FetchOptions)
walmartOrder: fullOrder,
client: p.client,
logger: p.logger,
ctx: ctx,
})
}
}
Expand Down Expand Up @@ -154,7 +161,7 @@ func (p *Provider) HealthCheck(ctx context.Context) error {
MaxTimestamp: &nowTimestamp,
}

_, err := p.client.GetPurchaseHistory(req)
_, err := p.client.GetPurchaseHistory(ctx, req)
if err != nil {
return fmt.Errorf("walmart health check failed: %w", err)
}
Expand Down
1 change: 1 addition & 0 deletions internal/api/dto/responses.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ type ChargeResponse struct {
ChargeSequence int `json:"charge_sequence"`
ChargeAmount float64 `json:"charge_amount"`
ChargeType string `json:"charge_type"`
ChargedAt string `json:"charged_at,omitempty"` // ISO8601 timestamp of when charge occurred
PaymentMethod string `json:"payment_method"`
CardType string `json:"card_type,omitempty"`
CardLastFour string `json:"card_last_four,omitempty"`
Expand Down
Loading
Loading