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>
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
$libalias. No relative imports for lib modules. - Svelte 5: Runes (
$state,$derived,$effect,$props). Events:onclick={fn}. - Types:
export interfacefor models. Frontend usescamelCase, API usessnake_case. Converters live in$lib/models/bill.ts. - Auth: Token stored in
localStoragekeyauth. Always useapiFetch()from$lib/api.tsfor authenticated requests.
Go (server/)
- Module:
billai-server(import path). Use this ingo test/go buildwhen outside the directory. - Layer:
handler→service→adapter/repository→model. No business logic in handlers. - Struct tags: JSON
snake_case,omitemptyfor 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(andcodefor error types)
- Business APIs:
- Time: Custom
LocalTimetype 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_casevars,UPPER_CASEconstants. Prefix private globals with_. - Type hints: Mandatory. Prefer
str | NoneorOptional[str]. - Models:
pydantic.BaseModelfor API schemas. - Cleaners: Extend
BaseCleaner(ABC)fromcleaners/base.py. Category rules inconfig/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 atANALYZER_URLsubprocess: spawnspython 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.jsusesadapter-nodefor Docker SSR.csrf.trustedOrigins: ['*']disables CSRF checks.onwarnignores alla11y_*warnings (chart components).
Deployment
- Gitea Actions self-hosted runner (
.gitea/workflows/deploy.yaml), not GitHub. deploy.shis the manual deployment script (same logic as CI).
Test Coverage
Sparse. Existing tests:
web/src/demo.spec.ts/page.svelte.spec.tsserver/service/changelog_test.goanalyzer/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.txtbefore 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_numberformat (e.g.,server/handler/changelog.go:12) when mentioning code locations.