Files
billai/AGENTS.md
cheliangzhao a2de8c5078 feat: implement WeChat cross-batch refund reconciliation and fix misc issues
WeChat cross-batch refund reconciliation:
- Add OriginalAmount field to CleanedBill for accurate cumulative refund math
- DeduplicateRawFile detects WeChat status-update rows (已退款/已全额退款) and
  emits WechatRefundUpdates for Go-side reconciliation (Scenario 1)
- WechatPy cleaner surfaces -退款 income rows with no same-batch expense match
  as unresolved_refunds for Go ReconcileRefund (Scenario 2)
- Add ReconcileWechatRefund to repository interface and MongoDB implementation
- upload.go step 15 iterates WechatRefundUpdates and reconciles against bills_cleaned

Bug fixes:
- ReviewStats: add nil repo check to prevent panic when DB is not connected
- JWT: remove hardcoded fallback secret; return 500/401 if JWTSecret not configured
- Remove unused parsePageParam dead code and its strconv import
- BillDetailDrawer: show 不计收支 amount in muted gray instead of red
- test_jd_cleaner.py: replace hardcoded D:\Projects\BillAI path with dynamic __file__ resolution

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 21:38:25 +08:00

8.0 KiB

AGENTS.md - AI Coding Agent Guidelines

Guidelines for AI coding agents working on BillAI - a microservices bill analysis system.

Version: See CHANGELOG.md for current version. Latest tag usually matches.

Architecture

  • web/ - SvelteKit 5 + TailwindCSS 4 + TypeScript (Frontend, port 3000)
  • server/ - Go 1.24 + Gin + MongoDB (API, port 8080)
  • analyzer/ - Python 3.12 + FastAPI (Data cleaning, port 8001)

Proxy Caveat: SvelteKit proxies /api/* to Go via web/src/routes/api/[...path]/+server.ts, but only GET and POST are forwarded. The Go backend intentionally uses POST for mutations (update, delete, manual create) to work around this. If you add PUT/PATCH/DELETE endpoints, you must also add them to the proxy.

Build/Lint/Test Commands

Frontend (web/)

npm run dev                  # Start dev server
npm run build                # Production build
npm run check                # TypeScript + Svelte check
npm run lint                 # Prettier --check + ESLint
npm run format               # Prettier --write
npm run test                 # vitest --run (CI mode)
npx vitest run src/xxx.spec.ts    # Run single test file

Note: web/ has a yarn.lock but scripts in package.json use npm run.

Backend (server/)

go run .                     # Start server (reads server/config.yaml)
go build -o server .
go test ./...                # Run all tests
go test -run TestName ./...  # Run single test
go test -v ./handler/...     # Verbose output

Analyzer (analyzer/)

python server.py             # Start FastAPI (has `if __name__ == "__main__"`)
uvicorn server:app --reload  # Requires cwd == analyzer/
pytest                       # Run all tests
pytest test_jd_cleaner.py    # Single test file

Docker

docker compose up -d --build --remove-orphans   # Start/rebuild all
docker compose logs -f server                   # Follow logs
docker compose down                             # Stop services

Code Style & Conventions

TypeScript/Svelte (web/)

  • Formatting: Prettier (tabs, single quotes, trailingComma: none, printWidth 100)
  • Imports: Use $lib alias. No relative imports for lib modules.
  • Svelte 5: Runes ($state, $derived, $effect, $props). Events: onclick={fn}.
  • Types: export interface for models. Frontend uses camelCase, API uses snake_case. Converters live in $lib/models/bill.ts.
  • Auth: Token stored in localStorage key auth. Always use apiFetch() from $lib/api.ts for authenticated requests.

Go (server/)

  • Module: billai-server (import path). Use this in go test / go build when outside the directory.
  • Layer: handlerserviceadapter/repositorymodel. No business logic in handlers.
  • Struct tags: JSON snake_case, omitempty for optional. Pointer types for optional patch fields. Sensitive fields: json:"-".
  • Response shapes:
    • Business APIs: result bool, message string, data *T
    • Auth APIs: success bool, error string, data *T (and code for error types)
  • Time: Custom LocalTime type serializes as "2006-01-02 15:04:05".
  • Soft delete: Never hard-delete. All queries filter is_deleted: false.

Python (analyzer/)

  • Style: PEP 8. snake_case vars, UPPER_CASE constants. Prefix private globals with _.
  • Type hints: Mandatory. Prefer str | None or Optional[str].
  • Models: pydantic.BaseModel for API schemas.
  • Cleaners: Extend BaseCleaner(ABC) from cleaners/base.py. Category rules in config/category.yaml.

Key Patterns & Quirks

API Flow

Browser → SvelteKit proxy → Go (Gin) → handler → service → adapter → Python FastAPI
                                            └→ repository → MongoDB

Authentication

  • JWT (HS256). Header: Authorization: Bearer <token>.
  • middleware.AuthRequired() guards authed routes. Public routes: /api/auth/*, /api/changelog, /health.
  • Frontend apiFetch() intercepts 401 → auth.logout() + redirect /login.

File Processing Pipeline

Upload: ZIP/XLSX → Extract → Convert UTF-8 CSV → Detect bill type (alipay/wechat/jd) → Deduplicate against MongoDB → Clean via Python → Save cleaned data.

Cross-Batch Refund Reconciliation

When the original purchase and its refund are in different upload batches, within-batch logic can't match them. Two reconciliation paths handle this:

Alipay (handler/upload.go step 14): After cleaning, cleaner.unresolved_refunds (refund rows that found no matching expense in the same file) is returned by FastAPI and iterated in Go. ReconcileRefund() looks up the original expense by transaction_id or merchant_order_no and either soft-deletes (full refund) or deducts amount (partial). Alipay refund rows have their own distinct transaction_id (suffixed _<refund_id>), so they pass raw dedup and are naturally idempotent on re-upload.

WeChat (handler/upload.go step 15): WeChat re-exports the same row (same transaction_id) with an updated 当前状态 field (已全额退款 or 已退款(¥X)). DeduplicateRawFile detects these duplicate rows, extracts the refund info into DeduplicateResult.WechatRefundUpdates, and ReconcileWechatRefund() applies the update. WeChat's ¥X is the cumulative total refunded, so CleanedBill.original_amount (set at first save) is used to compute remaining = original_amount - X.

Adapter (Go ↔ Python)

adapter.Cleaner interface. Two modes:

  • http (default): calls FastAPI at ANALYZER_URL
  • subprocess: spawns python analyzer/clean_bill.py

Set via ANALYZER_MODE env var or server/config.yaml analyzer.mode.

Config Precedence

Go backend reads server/config.yaml, but Docker compose sets env vars (ANALYZER_URL, MONGO_URI, JWT_SECRET, etc.) that override it.

SvelteKit Config Notes

  • svelte.config.js uses adapter-node for Docker SSR.
  • csrf.trustedOrigins: ['*'] disables CSRF checks.
  • onwarn ignores all a11y_* warnings (chart components).

Deployment

  • Gitea Actions self-hosted runner (.gitea/workflows/deploy.yaml), not GitHub.
  • deploy.sh is the manual deployment script (same logic as CI).

Test Coverage

Sparse. Existing tests:

  • web/src/demo.spec.ts / page.svelte.spec.ts
  • server/service/changelog_test.go
  • analyzer/test_jd_cleaner.py

Important Files

File Role
web/src/lib/api.ts Central API client, auth injection, all API functions
web/src/lib/stores/auth.ts Auth state, JWT handling, localStorage key auth
web/src/lib/models/bill.ts UIBill model + snake_case ↔ camelCase converters
web/src/routes/api/[...path]/+server.ts SvelteKit → Go proxy (GET/POST only)
server/main.go Entry point, wires adapter + repository + router
server/config.yaml Go backend config (Mongo, auth, paths, analyzer mode)
server/router/router.go Route table, auth group definitions
server/handler/upload.go Full upload pipeline handler
server/handler/bills.go List/filter/update/delete bills
server/model/bill.go Bill models, LocalTime type, BSON/JSON marshaling
server/adapter/adapter.go Cleaner interface definition
server/repository/mongo/repository.go MongoDB implementation, soft-delete queries
analyzer/server.py FastAPI entry, bill detection/clean endpoints
analyzer/cleaners/base.py BaseCleaner ABC
analyzer/category.py Category inference engine
docker-compose.yaml Full stack orchestration

Agent Guidelines

  • Before coding: Search codebase to understand existing patterns and dependencies.
  • Dependencies: Check package.json/go.mod/requirements.txt before adding new packages.
  • Tests: Run the relevant test suite before committing. If no tests exist for your change, verify manually.
  • Git commits: Provide clear messages explaining the "why" of changes.
  • File references: Use relative file_path:line_number format (e.g., server/handler/changelog.go:12) when mentioning code locations.