From 9f67d0fd1e98e9dbc12dafb95019838443e5bd21 Mon Sep 17 00:00:00 2001 From: Zhenzhen Zhao <229194950@qq.com> Date: Thu, 20 May 2021 14:28:03 +0800 Subject: [PATCH] feat(wechat): support matching accounts via all rules (#5) * fix(wechat): add order & tx types fixes #3 Signed-off-by: Triple-Z * feat(wechat): support matching accounts via all rules * Rename minusAccount/pusAccount to methoudAccount/targetAccount for WeChat billing. Signed-off-by: Triple-Z * update(example): wechat example --- README.md | 55 +++++++++++++++++++++++ example/wechat/config.yaml | 40 +++++++++++++---- example/wechat/example-wechat-output.bean | 38 ++++++++++++++++ example/wechat/example-wechat-records.csv | 40 +++++++++-------- pkg/analyser/wechat/wechat.go | 35 ++++++++++----- pkg/provider/wechat/config.go | 16 +++---- pkg/provider/wechat/parse.go | 5 ++- pkg/provider/wechat/types.go | 15 +++++-- pkg/provider/wechat/util.go | 14 ++++++ 9 files changed, 207 insertions(+), 51 deletions(-) create mode 100644 example/wechat/example-wechat-output.bean diff --git a/README.md b/README.md index 3161553..c800413 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,12 @@ 架构支持扩展,如需支持新的账单(如银行账单等),可添加 [provider](pkg/provider)。如需支持新的记账语言,可添加 [compiler](pkg/compiler)。 +``` +┌───────────┐ ┌──────────┐ ┌────┐ ┌──────────┐ ┌──────────┐ +│ translate │─▶│ provider │─▶│ IR │─▶│ compiler │─▶│ analyser │ +└───────────┘ └──────────┘ └────┘ └──────────┘ └──────────┘ +``` + ## 安装 ```bash @@ -82,6 +88,7 @@ double-entry-generator translate --config ./example/wechat/config.yaml --provide ## 配置 +### 支付宝 ``` defaultMinusAccount: Liabilities:CreditCard:Test defaultPlusAccount: Expenses:Test @@ -102,6 +109,54 @@ alipay: `alipay` 是蚂蚁账单相关的配置。它提供基于规则的匹配。可以指定 peer(交易对方)和 item(商品名称)的包含匹配。匹配成功则使用规则中定义的 `plusAccount` 和 `minusAccount` 覆盖默认定义。 +### 微信 + +```yaml +defaultMinusAccount: Assets:FIXME +defaultPlusAccount: Expenses:FIXME +defaultCurrency: CNY +title: Test WeChat bills config +wechat: + rules: + - type: 收入 # 微信红包 + method: / + item: / + targetAccount: Income:Wechat:RedPacket + - type: / # 转入零钱通 + peer: / + item: / + targetAccount: Assets:Digital:WeChat:Cash + + - peer: 滴滴 + targetAccount: Expenses:Transport:Taxi + - peer: 公交 + targetAccount: Expenses:Transport:Bus + - peer: 地铁 + targetAccount: Expenses:Transport:Metro + - peer: 中国铁路 + targetAccount: Expenses:Transport:Train + + - method: / # 一般为收入,存入零钱 + methodAccount: Assets:Digital:Wechat:Cash + - method: 零钱 # 零钱/零钱通 + methodAccount: Assets:Digital:Wechat:Cash + - method: 工商银行 + methodAccount: Assets:Bank:CN:ICBC:Savings +``` + +`defaultMinusAccount`, `defaultPlusAccount` 和 `defaultCurrency` 是全局的必填默认值。其中 `defaultMinusAccount` 是默认金额减少的账户,`defaultPlusAccount` 是默认金额增加的账户。 `defaultCurrency` 是默认货币。 + +`wechat` is the provider-specific configuration. WeChat provider has rules matching mechanism. + +微信账单与支付宝不同的是,它提供了“交易方式”字段来标识资金出入账户。这样就可以直接通过“交易方式”,并辅以“收/支”字段确认该账户为增加账户还是减少账户。而复式记账法每笔交易至少需要两个账户,另一个账户则可通过“交易对方”(peer)、“商品”(item)、“收/支”(type)以及“交易方式”(method)的多种包含匹配得出。如支付宝配置类似,匹配成功则使用规则中定义的 `targetAccount` 和 `methodAccount` ,并通过确认该笔交易是收入还是支出,决定 `targetAccount` 和 `methodAccount` 的正负关系,来覆盖默认定义的增减账户。 + +`targetAccount` 与 `methodAccount` 的增减账户关系如下表: + +|收/支|methodAccount|targetAccount| +|----|----|----| +|收入|plusAccount|minusAccount| +|支出|minusAccount|plusAccount| + ## Special Thanks - [dilfish/atb](https://github.com/dilfish/atb) convert alipay bill to beancount version diff --git a/example/wechat/config.yaml b/example/wechat/config.yaml index 3c14212..e823efe 100644 --- a/example/wechat/config.yaml +++ b/example/wechat/config.yaml @@ -1,13 +1,37 @@ -defaultMinusAccount: Liabilities:CreditCard:Test -defaultPlusAccount: Expenses:Test +defaultMinusAccount: Assets:FIXME +defaultPlusAccount: Expenses:FIXME defaultCurrency: CNY title: 测试 wechat: rules: + # type (additional condition) + - type: 收入 # 微信红包 + method: / + item: / + targetAccount: Income:Wechat:RedPacket + - type: / # 转入零钱通 + peer: / + item: / + targetAccount: Assets:Digital:WeChat:Cash + - peer: 云膳过桥米线 - method: 中国银行 - plusAccount: Expenses:Food - minusAccount: Assets:BOC - - type: 收入 - plusAccount: Assets:Wechat - minusAccount: Income:Earnings + targetAccount: Expenses:Food:Meal + - peer: 餐厅 + targetAccount: Expenses:Food:Meal + + - peer: 房东 + type: 支出 + targetAccount: Expenses:Housing:Rent + + - peer: 用户 + type: 收入 + targetAccount: Income:Service + + - method: / # 一般为收入,存入零钱 + methodAccount: Assets:Digital:Wechat:Cash + - method: 零钱 # 零钱/零钱通 + methodAccount: Assets:Digital:Wechat:Cash + - method: 工商银行 + methodAccount: Assets:Bank:CN:ICBC:Savings + - method: 中国银行 + methodAccount: Assets:Bank:CN:BOC:Savings diff --git a/example/wechat/example-wechat-output.bean b/example/wechat/example-wechat-output.bean new file mode 100644 index 0000000..567beaf --- /dev/null +++ b/example/wechat/example-wechat-output.bean @@ -0,0 +1,38 @@ +option "title" "测试" +option "operating_currency" "CNY" + +1970-01-01 open Income:Wechat:RedPacket +1970-01-01 open Assets:Digital:WeChat:Cash +1970-01-01 open Income:Service +1970-01-01 open Assets:Bank:CN:ICBC:Savings +1970-01-01 open Expenses:Food:Meal +1970-01-01 open Expenses:Housing:Rent +1970-01-01 open Assets:Digital:Wechat:Cash +1970-01-01 open Assets:Bank:CN:BOC:Savings +1970-01-01 open Expenses:FIXME +1970-01-01 open Assets:FIXME + +2019-09-26 * "云膳过桥米线(传奇广场店)" "总共消费:28.16" + Expenses:Food:Meal 28.16 CNY + Assets:Bank:CN:BOC:Savings -28.16 CNY + +2019-09-24 * "同性好友" "/" + Assets:Digital:Wechat:Cash 0.35 CNY + Income:Wechat:RedPacket -0.35 CNY + +2021-01-17 * "某餐厅" "收款方备注:二维码收款" + Expenses:Food:Meal 12.00 CNY + Assets:Digital:Wechat:Cash -12.00 CNY + +2021-01-22 * "房东" "转账备注:微信转账" + Expenses:Housing:Rent 500.00 CNY + Assets:Digital:Wechat:Cash -500.00 CNY + +2020-11-27 * "用户A" "收款方备注:二维码收款" + Assets:Digital:Wechat:Cash 23.00 CNY + Income:Service -23.00 CNY + +2021-01-17 * "/" "/" + Assets:Digital:WeChat:Cash 2000.00 CNY + Assets:Bank:CN:ICBC:Savings -2000.00 CNY + diff --git a/example/wechat/example-wechat-records.csv b/example/wechat/example-wechat-records.csv index 9b0a806..d8a555c 100644 --- a/example/wechat/example-wechat-records.csv +++ b/example/wechat/example-wechat-records.csv @@ -1,19 +1,23 @@ -微信支付账单明细,,,,,,,, -微信昵称:[测试],,,,,,,, -起始时间:[2019-08-01 00:00:00] 终止时间:[2019-09-30 23:59:59],,,,,,,, -导出类型:[全部],,,,,,,, -导出时间:[2019-10-09 16:05:22],,,,,,,, -,,,,,,,, -共43笔记录,,,,,,,, -收入:1笔 0.35元,,,,,,,, -支出:1笔 28.16元,,,,,,,, -中性交易:0笔 0.00元,,,,,,,, -注:,,,,,,,, -1. 充值/提现/理财通购买/零钱通存取/信用卡还款等交易,将计入中性交易,,,,,,,, -2. 本明细仅展示当前账单中的交易,不包括已删除的记录,,,,,,,, -3. 本明细仅供个人对账使用,,,,,,,, -,,,,,,,, -----------------------微信支付账单明细列表--------------------,,,,,,,, +微信支付账单明细,,,,,,,,,, +微信昵称:[测试],,,,,,,,,, +起始时间:[2019-08-01 00:00:00] 终止时间:[2019-09-30 23:59:59],,,,,,,,,, +导出类型:[全部],,,,,,,,,, +导出时间:[2019-10-09 16:05:22],,,,,,,,,, +,,,,,,,,,, +共43笔记录,,,,,,,,,, +收入:1笔 0.35元,,,,,,,,,, +支出:1笔 28.16元,,,,,,,,,, +中性交易:0笔 0.00元,,,,,,,,,, +注:,,,,,,,,,, +1. 充值/提现/理财通购买/零钱通存取/信用卡还款等交易,将计入中性交易,,,,,,,,,, +2. 本明细仅展示当前账单中的交易,不包括已删除的记录,,,,,,,,,, +3. 本明细仅供个人对账使用,,,,,,,,,, +,,,,,,,,,, +----------------------微信支付账单明细列表--------------------,,,,,,,,,, 交易时间,交易类型,交易对方,商品,收/支,金额(元),支付方式,当前状态,交易单号,商户单号,备注 -2019-09-26 12:45:27,商户消费,云膳过桥米线(传奇广场店),"总共消费:28.16",支出,¥28.16,中国银行(1234),支付成功,3985734 ,129847129 ,"/" -2019-09-24 10:10:11,微信红包,同性好友,"/",收入,¥0.35,/,已存入零钱,3985734 ,129847129 ,"/" \ No newline at end of file +2019-09-26 12:45:27,商户消费,云膳过桥米线(传奇广场店),总共消费:28.16,支出,¥28.16,中国银行(1234),支付成功,3985734 ,129847129 ,/ +2019-09-24 10:10:11,微信红包,同性好友,/,收入,¥0.35,/,已存入零钱,3985734 ,129847129 ,/ +2021-01-17 18:03:35,扫二维码付款,某餐厅,收款方备注:二维码收款,支出,¥12.00,零钱通,已转账,3985734,129847129,/ +2021-01-22 12:34:56,转账,房东,转账备注:微信转账,支出,¥500.00,零钱通,朋友已收钱,3985734,129847129,/ +2020-11-27 19:29:00,二维码收款,用户A,收款方备注:二维码收款,收入,¥23.00,零钱,已收钱,3985734,/ ,/ +2021-01-17 10:07:31,转入零钱通-来自工商银行(9876),/,/,/,¥2000.00,工商银行(9876),支付成功,3985734,129847129,/ \ No newline at end of file diff --git a/pkg/analyser/wechat/wechat.go b/pkg/analyser/wechat/wechat.go index 9aaca47..ab4bc76 100644 --- a/pkg/analyser/wechat/wechat.go +++ b/pkg/analyser/wechat/wechat.go @@ -20,11 +20,11 @@ func (w Wechat) GetAllCandidateAccounts(cfg *config.Config) map[string]bool { } for _, r := range cfg.Wechat.Rules { - if r.MinusAccount != nil { - uniqMap[*r.MinusAccount] = true + if r.MethodAccount != nil { + uniqMap[*r.MethodAccount] = true } - if r.PlusAccount != nil { - uniqMap[*r.PlusAccount] = true + if r.TargetAccount != nil { + uniqMap[*r.TargetAccount] = true } } uniqMap[cfg.DefaultPlusAccount] = true @@ -38,6 +38,9 @@ func (w Wechat) GetAccounts(o *ir.Order, cfg *config.Config, target, provider st return cfg.DefaultMinusAccount, cfg.DefaultPlusAccount } + resMinus := cfg.DefaultMinusAccount + resPlus := cfg.DefaultPlusAccount + for _, r := range cfg.Wechat.Rules { match := true if r.Peer != nil { @@ -64,16 +67,24 @@ func (w Wechat) GetAccounts(o *ir.Order, cfg *config.Config, target, provider st // TODO(gaocegege): Support it. } if match { - resMinus := cfg.DefaultMinusAccount - resPlus := cfg.DefaultPlusAccount - if r.MinusAccount != nil { - resMinus = *r.MinusAccount + // Support multiple matches, like one rule matches the minus accout, the other rule matches the plus account. + // FIXME(TripleZ): two-layer if... can u refact it? + if r.TargetAccount != nil { + if o.Type == ir.TxTypeRecv { + resMinus = *r.TargetAccount + } else { + resPlus = *r.TargetAccount + } } - if r.PlusAccount != nil { - resPlus = *r.PlusAccount + if r.MethodAccount != nil { + if o.Type == ir.TxTypeRecv { + resPlus = *r.MethodAccount + } else { + resMinus = *r.MethodAccount + } } - return resMinus, resPlus } + } - return cfg.DefaultMinusAccount, cfg.DefaultPlusAccount + return resMinus, resPlus } diff --git a/pkg/provider/wechat/config.go b/pkg/provider/wechat/config.go index 868f86b..c79c545 100644 --- a/pkg/provider/wechat/config.go +++ b/pkg/provider/wechat/config.go @@ -23,12 +23,12 @@ type Config struct { // Rule is the type for match rules. type Rule struct { - Peer *string `yaml:"peer,omitempty"` - Item *string `yaml:"item,omitempty"` - Type *string `yaml:"type,omitempty"` - Method *string `yaml:"method,omitempty"` - StartTime *string `yaml:"startTime,omitempty"` - EndTime *string `yaml:"endTime,omitempty"` - MinusAccount *string `yaml:"minusAccount,omitempty"` - PlusAccount *string `yaml:"plusAccount,omitempty"` + Peer *string `yaml:"peer,omitempty"` + Item *string `yaml:"item,omitempty"` + Type *string `yaml:"type,omitempty"` + Method *string `yaml:"method,omitempty"` + StartTime *string `yaml:"startTime,omitempty"` + EndTime *string `yaml:"endTime,omitempty"` + MethodAccount *string `yaml:"methodAccount,omitempty"` + TargetAccount *string `yaml:"targetAccount,omitempty"` } diff --git a/pkg/provider/wechat/parse.go b/pkg/provider/wechat/parse.go index 4365a52..fe66a2b 100644 --- a/pkg/provider/wechat/parse.go +++ b/pkg/provider/wechat/parse.go @@ -22,7 +22,10 @@ func (w *Wechat) translateToOrders(array []string) error { } bill.TxType = getTxType(array[1]) - if bill.TxType == TxTypeUnknown { + if bill.TxType == TxTypeCash2Cash { + fmt.Printf("Get an unusable tx type, ignore it: %s\n", bill.TxType) + return nil + } else if bill.TxType == TxTypeUnknown { return fmt.Errorf("Failed to get the tx type %s: %v", array[1], err) } bill.Peer = array[2] diff --git a/pkg/provider/wechat/types.go b/pkg/provider/wechat/types.go index fe5ae8a..af1044b 100644 --- a/pkg/provider/wechat/types.go +++ b/pkg/provider/wechat/types.go @@ -39,14 +39,21 @@ type OrderType string const ( OrderTypeSend OrderType = "支出" OrderTypeRecv = "收入" + OrderTypeNil = "/" OrderTypeUnknown = "Unknown" ) type TxType string const ( - TxTypeConsume TxType = "商户消费" - TxTypeLucky = "微信红包" - TxTypeTransfer = "转账" - TxTypeUnknown = "Unknown" + TxTypeConsume TxType = "商户消费" + TxTypeLucky = "微信红包" + TxTypeTransfer = "转账" + TxTypeQRIncome = "二维码收款" + TxTypeQRSend = "扫二维码付款" + TxTypeGroup = "群收款" + TxTypeRefund = "退款" + TxTypeCash2Cash = "转入零钱通-来自零钱" + TxTypeIntoCash = "转入零钱通" + TxTypeUnknown = "Unknown" ) diff --git a/pkg/provider/wechat/util.go b/pkg/provider/wechat/util.go index 6b64140..765efd5 100644 --- a/pkg/provider/wechat/util.go +++ b/pkg/provider/wechat/util.go @@ -8,6 +8,8 @@ func getOrderType(ot string) OrderType { return OrderTypeRecv case string(OrderTypeSend): return OrderTypeSend + case string(OrderTypeNil): + return OrderTypeNil default: return OrderTypeUnknown } @@ -20,6 +22,18 @@ func getTxType(tt string) TxType { return TxTypeConsume } else if strings.Contains(tt, string(TxTypeTransfer)) { return TxTypeTransfer + } else if strings.Contains(tt, string(TxTypeQRIncome)) { + return TxTypeQRIncome + } else if strings.Contains(tt, string(TxTypeQRSend)) { + return TxTypeQRSend + } else if strings.Contains(tt, string(TxTypeGroup)) { + return TxTypeGroup + } else if strings.Contains(tt, string(TxTypeRefund)) { + return TxTypeRefund + } else if strings.Contains(tt, string(TxTypeCash2Cash)) { + return TxTypeCash2Cash + } else if strings.Contains(tt, string(TxTypeIntoCash)) { + return TxTypeIntoCash } else { return TxTypeUnknown }