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>
155 lines
8.0 KiB
Markdown
155 lines
8.0 KiB
Markdown
# 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/`)
|
|
```bash
|
|
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/`)
|
|
```bash
|
|
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/`)
|
|
```bash
|
|
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
|
|
```bash
|
|
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:** `handler` → `service` → `adapter`/`repository` → `model`. 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.
|