Skip to content

Commit

Permalink
feat(wechat): support matching accounts via all rules (#5)
Browse files Browse the repository at this point in the history
* fix(wechat): add order & tx types

fixes #3

Signed-off-by: Triple-Z <me@triplez.cn>

* feat(wechat): support matching accounts via all rules

* Rename minusAccount/pusAccount to
  methoudAccount/targetAccount for WeChat billing.

Signed-off-by: Triple-Z <me@triplez.cn>

* update(example): wechat example
  • Loading branch information
Triple-Z authored May 20, 2021
1 parent aa57692 commit 9f67d0f
Show file tree
Hide file tree
Showing 9 changed files with 207 additions and 51 deletions.
55 changes: 55 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@

架构支持扩展,如需支持新的账单(如银行账单等),可添加 [provider](pkg/provider)。如需支持新的记账语言,可添加 [compiler](pkg/compiler)

```
┌───────────┐ ┌──────────┐ ┌────┐ ┌──────────┐ ┌──────────┐
│ translate │─▶│ provider │─▶│ IR │─▶│ compiler │─▶│ analyser │
└───────────┘ └──────────┘ └────┘ └──────────┘ └──────────┘
```

## 安装

```bash
Expand Down Expand Up @@ -82,6 +88,7 @@ double-entry-generator translate --config ./example/wechat/config.yaml --provide

## 配置

### 支付宝
```
defaultMinusAccount: Liabilities:CreditCard:Test
defaultPlusAccount: Expenses:Test
Expand All @@ -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
40 changes: 32 additions & 8 deletions example/wechat/config.yaml
Original file line number Diff line number Diff line change
@@ -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
38 changes: 38 additions & 0 deletions example/wechat/example-wechat-output.bean
Original file line number Diff line number Diff line change
@@ -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

40 changes: 22 additions & 18 deletions example/wechat/example-wechat-records.csv
Original file line number Diff line number Diff line change
@@ -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 ,"/"
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,/
35 changes: 23 additions & 12 deletions pkg/analyser/wechat/wechat.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand All @@ -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
}
16 changes: 8 additions & 8 deletions pkg/provider/wechat/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
5 changes: 4 additions & 1 deletion pkg/provider/wechat/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
15 changes: 11 additions & 4 deletions pkg/provider/wechat/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
14 changes: 14 additions & 0 deletions pkg/provider/wechat/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ func getOrderType(ot string) OrderType {
return OrderTypeRecv
case string(OrderTypeSend):
return OrderTypeSend
case string(OrderTypeNil):
return OrderTypeNil
default:
return OrderTypeUnknown
}
Expand All @@ -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
}
Expand Down

0 comments on commit 9f67d0f

Please sign in to comment.