feat: implement cross-batch Alipay refund reconciliation
When a refund row in an uploaded Alipay bill has no matching expense row in the same batch (because the original purchase was uploaded in a prior batch), the refund is now reconciled against the stored record in bills_cleaned rather than being silently discarded. Changes: - analyzer/cleaners/base.py: add unresolved_refunds list to BaseCleaner - analyzer/cleaners/alipay.py: _aggregate_refunds stores full refund metadata (dict); _process_expenses tracks matched keys and populates self.unresolved_refunds for unmatched refunds - analyzer/server.py: thread unresolved_refunds through do_clean, CleanResponse, and both /clean endpoints - server/adapter/adapter.go: add UnresolvedRefund type and field to CleanResult - server/adapter/http/cleaner.go: deserialize unresolved_refunds from Python response and populate CleanResult - server/repository/repository.go: add ReconcileRefund to BillRepository interface - server/repository/mongo/repository.go: implement ReconcileRefund — full refund soft-deletes the bill, partial refund reduces amount and appends remark with original amount and refund order number - server/handler/upload.go: capture clean result and call ReconcileRefund for each unresolved refund after saving cleaned bills - server/model/response.go: add ReconciledRefundCount to UploadData Also: add CLAUDE.md (@AGENTS.md), update AGENTS.md, fix DailyTrendChart missing-date gap by filling zero-expense dates in daily map. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -13,8 +13,20 @@ type CleanOptions struct {
|
||||
|
||||
// CleanResult 清洗结果
|
||||
type CleanResult struct {
|
||||
BillType string // 检测到的账单类型: alipay/wechat/jd
|
||||
Output string // 脚本输出信息
|
||||
BillType string // 检测到的账单类型: alipay/wechat/jd
|
||||
Output string // 脚本输出信息
|
||||
UnresolvedRefunds []UnresolvedRefund // 本次清洗未在同批次内匹配到对应支出的退款
|
||||
}
|
||||
|
||||
// UnresolvedRefund 本次清洗未在同批次内匹配到对应支出的退款
|
||||
type UnresolvedRefund struct {
|
||||
OrderNo string // 原订单号(去除退款后缀)
|
||||
MerchantOrderNo string // 商家订单号(备用匹配字段)
|
||||
RefundOrderNo string // 退款行自身的完整订单号(用于备注追溯)
|
||||
Amount float64 // 退款金额
|
||||
Time string // 退款时间
|
||||
Merchant string // 交易对方
|
||||
Description string // 商品说明
|
||||
}
|
||||
|
||||
// ConvertResult 格式转换结果
|
||||
|
||||
@@ -29,10 +29,22 @@ type CleanRequest struct {
|
||||
|
||||
// CleanResponse HTTP 清洗响应
|
||||
type CleanResponse struct {
|
||||
Success bool `json:"success"`
|
||||
BillType string `json:"bill_type"`
|
||||
Message string `json:"message"`
|
||||
OutputPath string `json:"output_path,omitempty"`
|
||||
Success bool `json:"success"`
|
||||
BillType string `json:"bill_type"`
|
||||
Message string `json:"message"`
|
||||
OutputPath string `json:"output_path,omitempty"`
|
||||
UnresolvedRefunds []UnresolvedRefund `json:"unresolved_refunds,omitempty"`
|
||||
}
|
||||
|
||||
// UnresolvedRefund 本次清洗未在同批次内匹配到对应支出的退款(与 Python 端 dict 字段对应)
|
||||
type UnresolvedRefund struct {
|
||||
OrderNo string `json:"order_no"`
|
||||
MerchantOrderNo string `json:"merchant_order_no"`
|
||||
RefundOrderNo string `json:"refund_order_no"`
|
||||
Amount float64 `json:"amount"`
|
||||
Time string `json:"time"`
|
||||
Merchant string `json:"merchant"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
// ErrorResponse 错误响应
|
||||
@@ -149,9 +161,23 @@ func (c *Cleaner) Clean(inputPath, outputPath string, opts *adapter.CleanOptions
|
||||
}
|
||||
}
|
||||
|
||||
unresolvedRefunds := make([]adapter.UnresolvedRefund, 0, len(cleanResp.UnresolvedRefunds))
|
||||
for _, ur := range cleanResp.UnresolvedRefunds {
|
||||
unresolvedRefunds = append(unresolvedRefunds, adapter.UnresolvedRefund{
|
||||
OrderNo: ur.OrderNo,
|
||||
MerchantOrderNo: ur.MerchantOrderNo,
|
||||
RefundOrderNo: ur.RefundOrderNo,
|
||||
Amount: ur.Amount,
|
||||
Time: ur.Time,
|
||||
Merchant: ur.Merchant,
|
||||
Description: ur.Description,
|
||||
})
|
||||
}
|
||||
|
||||
return &adapter.CleanResult{
|
||||
BillType: cleanResp.BillType,
|
||||
Output: cleanResp.Message,
|
||||
BillType: cleanResp.BillType,
|
||||
Output: cleanResp.Message,
|
||||
UnresolvedRefunds: unresolvedRefunds,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user