diff --git a/.env.example b/.env.example index 24c4de0..d4fac2b 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,5 @@ +CONFIG_PATH=/opt/bin/monobank-firefly3-bot/config.json + FIREFLY3_API_URL=https://firefly3.stuzer.link/api FIREFLY3_TOKEN= diff --git a/.gitignore b/.gitignore index 4a0c29b..46d4fe4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /.idea /main -/.env \ No newline at end of file +/.env +/config.json \ No newline at end of file diff --git a/config.go b/config.go index 5e88fbc..d3cce59 100644 --- a/config.go +++ b/config.go @@ -1,130 +1,74 @@ package main -func Configure() ([]ShopConfigItem, map[string]string) { - var ShopConfig []ShopConfigItem - Firefy3AccountsConfig := make(map[string]string) +import ( + "encoding/json" + "io/ioutil" + "os" +) - /** - * Firefly3 accounts - */ - Firefy3AccountsConfig["Mono Black"] = "1" - Firefy3AccountsConfig["Mono White"] = "60" - Firefy3AccountsConfig["Wallet cash (UAH)"] = "4" - - /** - * Bills - */ - ShopConfig = append(ShopConfig, ShopConfigItem{ - Names: []string{"Hetzner"}, - TransactionDescription: "Hetzner: vps2", - }) - - ShopConfig = append(ShopConfig, ShopConfigItem{ - Names: []string{"YouTube"}, - TransactionDescription: "YouTube membership: Latte ASMR", - }) - - ShopConfig = append(ShopConfig, ShopConfigItem{ - Names: []string{"Київстар +380672463500"}, - TransactionDescription: "Kyivstar: +380672463500", - }) - - ShopConfig = append(ShopConfig, ShopConfigItem{ - Names: []string{"Lifecell +380732463500"}, - TransactionDescription: "Lifecell: +380732463500", - }) - - ShopConfig = append(ShopConfig, ShopConfigItem{ - Names: []string{"JetBrains"}, - TransactionDescription: "JetBrains: GoLand", - }) - - ShopConfig = append(ShopConfig, ShopConfigItem{ - Names: []string{"Telegram"}, - TransactionDescription: "Telegram premium", - }) - - /** - * Credit payments - */ - ShopConfig = append(ShopConfig, ShopConfigItem{ - Names: []string{"Платіж Tefal Initial+"}, - TransactionDescription: "Credit payment: TEFAL OptiGrill+ Initial GC706D34", - TransactionDestination: "Credit: TEFAL OptiGrill+ Initial GC706D34", - }) - - ShopConfig = append(ShopConfig, ShopConfigItem{ - Names: []string{"Платіж Dyson"}, - TransactionDescription: "Credit payment: Dyson Supersonic HD07 Nickel/Copper", - TransactionDestination: "Credit: Dyson Supersonic HD07 Nickel/Copper", - }) - - ShopConfig = append(ShopConfig, ShopConfigItem{ - Names: []string{"Платіж Pixel Watch 2"}, - TransactionDescription: "Credit payment: Google Pixel Watch 2 Wi-Fi", - TransactionDestination: "Credit: Google Pixel Watch 2 Wi-Fi", - }) - - ShopConfig = append(ShopConfig, ShopConfigItem{ - Names: []string{"Платіж Apple Watch 9"}, - TransactionDescription: "Credit payment: Apple Watch Series 9", - TransactionDestination: "Credit: Apple Watch Series 9", - }) - - /** - * People - */ - ShopConfig = append(ShopConfig, ShopConfigItem{ - Names: []string{"Олена А."}, - TransactionDescription: "Doctor visit: Елена Ахрипова (психотерапевт)", - }) - - ShopConfig = append(ShopConfig, ShopConfigItem{ - Names: []string{"Ілля Ш."}, - TransactionDescription: "Legal services: Alva Privacy Law Firm", - TransactionDestination: "Legal: Alva Privacy Law Firm", - TransactionCategory: "Legal services", - }) - - /** - * Other - */ - ShopConfig = append(ShopConfig, ShopConfigItem{ - MCCCodes: []int{5411, 5499, 5451, 5422, 5412, 5921}, - Names: []string{"АТБ", "Велмарт", "Novus", "Glovo", "zakaz.ua", "Мегамаркет", "Сільпо", "Фора", "METRO"}, - TransactionDescription: "Groceries", - }) - - ShopConfig = append(ShopConfig, ShopConfigItem{ - Names: []string{"Аптека Доброго Дня", "Аптека оптових цін", "Аптека Копійка", "Аптека Гала", "Аптека АНЦ", "APTEKA 7", "Біла ромашка", "vidshkod ekv apt12", "Будемо Здорові Разом", "Apteka Ants", "Аптека Шар@"}, - TransactionDescription: "Medications", - }) - - ShopConfig = append(ShopConfig, ShopConfigItem{ - MCCCodes: []int{4131, 4111, 4112}, - Names: []string{"Київ Цифровий", "Київпастранс"}, - TransactionDescription: "Public transport", - }) - - ShopConfig = append(ShopConfig, ShopConfigItem{ - Names: []string{"McDonald’s"}, - TransactionDescription: "McDonalds", - }) - - ShopConfig = append(ShopConfig, ShopConfigItem{ - Names: []string{"LeoCafe"}, - TransactionDescription: "Cafe", - }) - - ShopConfig = append(ShopConfig, ShopConfigItem{ - Names: []string{"Bolt food", "Glovo", "Menya Musashi"}, - TransactionDescription: "Restaurant", - }) - - ShopConfig = append(ShopConfig, ShopConfigItem{ - Names: []string{"Lumberjack Barberhouse"}, - TransactionDescription: "Lumberjack: haircut", - }) - - return ShopConfig, Firefy3AccountsConfig +type Config struct { + Accounts []ConfigAccount `json:"accounts"` + TransactionTypes []ConfigTransactionTypes `json:"transaction_types"` +} + +type ConfigAccount struct { + Name string `json:"name"` + Firefly3Id string `json:"firefly3_id,omitempty"` + MonobankId string `json:"monobank_id,omitempty"` +} + +type ConfigTransactionTypes struct { + Names []string `json:"names"` + Firefly3 ConfigTransactionTypeFirefly3 `json:"firefly3,omitempty"` + MccCodes []int `json:"mcc_codes,omitempty"` +} + +type ConfigTransactionTypeFirefly3 struct { + Description string `json:"description"` + Destination string `json:"destination"` + Category string `json:"category"` +} + +func ReadConfig(path string) (Config, error) { + var config Config + + // open file + file, err := os.Open(path) + if err != nil { + return config, err + } + defer file.Close() + + // read file + bytes, err := ioutil.ReadAll(file) + if err != nil { + return config, err + } + + // read file ot config struct + if err := json.Unmarshal(bytes, &config); err != nil { + return config, err + } + + return config, nil +} + +func ConfigGetAccountByName(config Config, q string) ConfigAccount { + for _, row := range config.Accounts { + if row.Name == q { + return row + } + } + + return ConfigAccount{} +} + +func ConfigGetAccountByMonobankId(config Config, q string) ConfigAccount { + for _, row := range config.Accounts { + if row.MonobankId == q { + return row + } + } + + return ConfigAccount{} } diff --git a/config.json b/config.json new file mode 100644 index 0000000..19e0673 --- /dev/null +++ b/config.json @@ -0,0 +1,143 @@ +{ + "accounts": [ + { + "name": "Mono Black", + "firefly3_id": "1", + "monobank_id": "4723djMLsLOCzhoeYjxqRw" + }, + { + "name": "Mono White", + "firefly3_id": "1", + "monobank_id": "jJPAm0cfwAJv3C0I-kYpTA" + }, + { + "name": "Wallet cash (UAH)", + "firefly3_id": "4" + } + ], + "transaction_types": [ + { + "names": ["Hetzner"], + "firefly3": { + "description": "Hetzner: vps2" + } + }, + { + "names": ["YouTube"], + "firefly3": { + "description": "YouTube membership: Latte ASMR" + } + }, + { + "names": ["Київстар +380672463500"], + "firefly3": { + "description": "Kyivstar: +380672463500" + } + }, + { + "names": ["Lifecell +380732463500"], + "firefly3": { + "description": "Lifecell: +380732463500" + } + }, + { + "names": ["JetBrains"], + "firefly3": { + "description": "JetBrains: GoLand" + } + }, + { + "names": ["Telegram"], + "firefly3": { + "description": "Telegram premium" + } + }, + { + "names": ["Платіж Tefal Initial+"], + "firefly3": { + "description": "Credit payment: TEFAL OptiGrill+ Initial GC706D34", + "destination": "Credit: TEFAL OptiGrill+ Initial GC706D34" + } + }, + { + "names": ["Платіж Dyson"], + "firefly3": { + "description": "Credit payment: Dyson Supersonic HD07 Nickel/Copper", + "destination": "Credit: Dyson Supersonic HD07 Nickel/Copper" + } + }, + { + "names": ["Платіж Pixel Watch 2"], + "firefly3": { + "description": "Credit payment: Google Pixel Watch 2 Wi-Fi", + "destination": "Credit: Google Pixel Watch 2 Wi-Fi" + } + }, + { + "names": ["Платіж Apple Watch 9"], + "firefly3": { + "description": "Credit payment: Apple Watch Series 9", + "destination": "Credit: Apple Watch Series 9" + } + }, + { + "names": ["Олена А."], + "firefly3": { + "description": "Doctor visit: Елена Ахрипова (психотерапевт)" + } + }, + { + "names": ["Ілля Ш."], + "firefly3": { + "description": "Legal services: Alva Privacy Law Firm", + "destination": "Legal: Alva Privacy Law Firm", + "category": "Legal services" + } + }, + { + "names": ["АТБ", "Велмарт", "Novus", "Glovo", "zakaz.ua", "Мегамаркет", "Сільпо", "Фора", "METRO"], + "mcc_codes": [5411, 5499, 5451, 5422, 5412, 5921], + "firefly3": { + "description": "Groceries" + } + }, + { + "names": ["Аптека Доброго Дня", "Аптека оптових цін", "Аптека Копійка", "Аптека Гала", "Аптека АНЦ", "APTEKA 7", "Біла ромашка", "vidshkod ekv apt12", "Будемо Здорові Разом", "Apteka Ants", "Аптека Шар@"], + "mcc_codes": [5411, 5499, 5451, 5422, 5412, 5921], + "firefly3": { + "description": "Medications" + } + }, + { + "names": ["Київ Цифровий", "Київпастранс"], + "mcc_codes": [4131, 4111, 4112], + "firefly3": { + "description": "Public transport" + } + }, + { + "names": ["McDonald’s"], + "firefly3": { + "description": "McDonalds" + } + }, + { + "names": ["LeoCafe"], + "firefly3": { + "description": "Cafe" + } + }, + { + "names": ["Bolt food", "Glovo", "Menya Musashi", "Mafia"], + "firefly3": { + "description": "Restaurant" + } + }, + { + "names": ["Lumberjack Barberhouse"], + "firefly3": { + "description": "Lumberjack: haircut" + } + } + ] +} \ No newline at end of file diff --git a/config.json.example b/config.json.example new file mode 100644 index 0000000..41311c3 --- /dev/null +++ b/config.json.example @@ -0,0 +1,48 @@ +{ + "accounts": [ + { + "name": "Card 1", + "firefly3_id": "1", + "monobank_id": "xxxxxxxxxxxxxxxxxxxxxx" + }, + { + "name": "Card 2", + "firefly3_id": "2", + "monobank_id": "xxxxxxxxxxxxxxxxxxxxxx" + }, + { + "name": "Card 3", + "firefly3_id": "3" + } + ], + "transaction_types": [ + { + "names": ["Hetzner"], + "firefly3": { + "description": "Hetzner: vps2" + } + }, + { + "names": ["Платіж Pixel Watch 2"], + "firefly3": { + "description": "Credit payment: Google Pixel Watch 2 Wi-Fi", + "destination": "Credit: Google Pixel Watch 2 Wi-Fi" + } + }, + { + "names": ["Ілля Ш."], + "firefly3": { + "description": "Legal services: Alva Privacy Law Firm", + "destination": "Legal: Alva Privacy Law Firm", + "category": "Legal services" + } + }, + { + "names": ["АТБ", "Велмарт", "Novus", "Glovo", "zakaz.ua", "Мегамаркет", "Сільпо", "Фора", "METRO"], + "mcc_codes": [5411, 5499, 5451, 5422, 5412, 5921], + "firefly3": { + "description": "Groceries" + } + } + ] +} \ No newline at end of file diff --git a/main.go b/main.go index b062930..303ba75 100644 --- a/main.go +++ b/main.go @@ -4,12 +4,11 @@ import ( "context" "encoding/json" "fmt" - "github.com/antihax/optional" "github.com/joho/godotenv" "io" "log" - "main/firefly3" - "main/monobank/api/webhook/models" + firefly3 "main/firefly3" + monobank "main/monobank/api/webhook/models" "math" "net/http" "os" @@ -25,19 +24,11 @@ import ( // curl -X POST https://monobank-firefly3.stuzer.link/webhook -H 'Content-TransactionType: application/json' -d '{"test":123}' -// Configs -var ShopConfig []ShopConfigItem -var Firefy3AccountsConfig map[string]string - -func handleWebhook(w http.ResponseWriter, r *http.Request) { - LogString("-----------------\nwebhook received!") - +func readResponseBody(r *http.Request) (monobank.Transaction, error) { // read body bytes body, err := io.ReadAll(r.Body) if err != nil { - LogString(err.Error()) - w.WriteHeader(http.StatusOK) - return + return monobank.Transaction{}, err } //fmt.Println(string(body)) @@ -49,109 +40,112 @@ func handleWebhook(w http.ResponseWriter, r *http.Request) { // check empty body if len(string(body)) == 0 { - LogString("empty body") - w.WriteHeader(http.StatusOK) - return + return monobank.Transaction{}, err } // parse body - var transaction models.Transaction + var transaction monobank.Transaction err = json.Unmarshal(body, &transaction) + if err != nil { + return monobank.Transaction{}, err + } + + return transaction, nil +} + +func handleWebhook(w http.ResponseWriter, r *http.Request) { + var err error + + firefly3TransactionTypeWithdrawal := firefly3.WITHDRAWAL_TransactionTypeProperty + firefly3TransactionTypeTransfer := firefly3.TRANSFER_TransactionTypeProperty + + // read request + var monobankTransaction monobank.Transaction + monobankTransaction, err = readResponseBody(r) if err != nil { LogString(err.Error()) w.WriteHeader(http.StatusOK) return } - //statement, err := requests.Statement(models2.StatementRequest{ - // Account: transaction.Data.Account, - // From: transaction.Data.StatementItem.Time, - // To: transaction.Data.StatementItem.Time, - //}) - //if err != nil { - // fmt.Printf("%+v", err.Error()) - // w.WriteHeader(http.StatusOK) - // return - //} - //fmt.Printf("%+v", statement) + // get body json string (for logging) + monobankTransactionJson, err := json.Marshal(monobankTransaction) + if err != nil { + LogString(err.Error()) + w.WriteHeader(http.StatusOK) + return + } + + // read config + var config Config + config, err = ReadConfig(os.Getenv("CONFIG_PATH")) + if err != nil { + LogString(err.Error()) + w.WriteHeader(http.StatusOK) + return + } + + // find accounts + account := ConfigGetAccountByMonobankId(config, monobankTransaction.Data.Account) + + // cancel if one of account ids is empty + if len(account.Firefly3Id) == 0 || len(account.MonobankId) == 0 { + LogString("cannot find firefly3 or monobank ids (" + monobankTransaction.Data.Account + ")") + w.WriteHeader(http.StatusOK) + return + } // init firefly3 client clientConf := firefly3.NewConfiguration() clientConf.BasePath = os.Getenv("FIREFLY3_API_URL") clientConf.AddDefaultHeader("Authorization", "Bearer "+os.Getenv("FIREFLY3_TOKEN")) - client := firefly3.NewAPIClient(clientConf) + firefly3Client := firefly3.NewAPIClient(clientConf) - // get firefly3 account - listOpts := firefly3.AccountsApiListAccountOpts{ - Type_: optional.NewInterface("asset"), - } - accounts, _, err := client.AccountsApi.ListAccount(context.Background(), &listOpts) - if err != nil { - LogString(err.Error()) - w.WriteHeader(http.StatusOK) - return - } - - var account firefly3.AccountRead - for _, row := range accounts.Data { - if row.Attributes.Notes == transaction.Data.Account { - account = row - } - } - - if len(account.Id) == 0 { - LogString("unable to find account " + transaction.Data.Account + " in firefly3") - w.WriteHeader(http.StatusOK) - return - } - - // create transaction + // create firefly3 transaction var firefly3Transactions []firefly3.TransactionSplitStore - transactionTypeWithdrawal := firefly3.WITHDRAWAL_TransactionTypeProperty - transactionTypeTransfer := firefly3.TRANSFER_TransactionTypeProperty - firefly3Transaction := firefly3.TransactionSplitStore{ - Type_: &transactionTypeWithdrawal, - Date: time.Unix(int64(transaction.Data.StatementItem.Time), 0).Add(time.Hour * 2), - Notes: string(body), - Amount: strconv.Itoa(int(math.Abs(math.Round(float64(transaction.Data.StatementItem.Amount/100)))) - int(math.Abs(math.Round(float64(transaction.Data.StatementItem.CommissionRate/100))))), - SourceId: account.Id, + Type_: &firefly3TransactionTypeWithdrawal, + Date: time.Unix(int64(monobankTransaction.Data.StatementItem.Time), 0).Add(time.Hour * 2), + Notes: string(monobankTransactionJson), + Amount: strconv.Itoa(int(math.Abs(math.Round(float64(monobankTransaction.Data.StatementItem.Amount/100)))) - int(math.Abs(math.Round(float64(monobankTransaction.Data.StatementItem.CommissionRate/100))))), + SourceId: account.Firefly3Id, } - if slices.Contains([]string{"З чорної картки"}, transaction.Data.StatementItem.Description) { - firefly3Transaction.Type_ = &transactionTypeTransfer + // Special transaction cases + if slices.Contains([]string{"З чорної картки"}, monobankTransaction.Data.StatementItem.Description) { + firefly3Transaction.Type_ = &firefly3TransactionTypeTransfer firefly3Transaction.Description = "Transfer between accounts" - firefly3Transaction.DestinationId = Firefy3AccountsConfig["Mono White"] + firefly3Transaction.DestinationId = ConfigGetAccountByName(config, "Mono White").Firefly3Id firefly3Transactions = append(firefly3Transactions, firefly3Transaction) - } else if slices.Contains([]string{"З білої картки"}, transaction.Data.StatementItem.Description) { - firefly3Transaction.Type_ = &transactionTypeTransfer + } else if slices.Contains([]string{"З білої картки"}, monobankTransaction.Data.StatementItem.Description) { + firefly3Transaction.Type_ = &firefly3TransactionTypeTransfer firefly3Transaction.Description = "Transfer between accounts" - firefly3Transaction.DestinationId = Firefy3AccountsConfig["Mono Black"] + firefly3Transaction.DestinationId = ConfigGetAccountByName(config, "Mono Black").Firefly3Id firefly3Transactions = append(firefly3Transactions, firefly3Transaction) - } else if slices.Contains([]string{"Термінал City24"}, transaction.Data.StatementItem.Description) { - firefly3Transaction.Type_ = &transactionTypeTransfer + } else if slices.Contains([]string{"Термінал City24"}, monobankTransaction.Data.StatementItem.Description) { + firefly3Transaction.Type_ = &firefly3TransactionTypeTransfer firefly3Transaction.Description = "Transfer between accounts" - firefly3Transaction.SourceId = Firefy3AccountsConfig["Wallet cash (UAH)"] + firefly3Transaction.SourceId = ConfigGetAccountByName(config, "Wallet cash (UAH)").Firefly3Id firefly3Transaction.DestinationId = firefly3Transaction.SourceId firefly3Transactions = append(firefly3Transactions, firefly3Transaction) - } else if slices.Contains([]string{"Термінал EasyPay", "City24", "Термінал City24"}, transaction.Data.StatementItem.Description) { - firefly3Transaction.Type_ = &transactionTypeTransfer + } else if slices.Contains([]string{"Термінал EasyPay", "City24", "Термінал City24"}, monobankTransaction.Data.StatementItem.Description) { + firefly3Transaction.Type_ = &firefly3TransactionTypeTransfer firefly3Transaction.Description = "Transfer between accounts" - firefly3Transaction.SourceId = Firefy3AccountsConfig["Wallet cash (UAH)"] // test - firefly3Transaction.DestinationId = account.Id // test + firefly3Transaction.SourceId = ConfigGetAccountByName(config, "Wallet cash (UAH)").Firefly3Id // test + firefly3Transaction.DestinationId = account.Firefly3Id // test firefly3Transactions = append(firefly3Transactions, firefly3Transaction) - } else if slices.Contains([]string{"Банкомат DN00"}, transaction.Data.StatementItem.Description) { - firefly3Transaction.Type_ = &transactionTypeTransfer + } else if slices.Contains([]string{"Банкомат DN00"}, monobankTransaction.Data.StatementItem.Description) { + firefly3Transaction.Type_ = &firefly3TransactionTypeTransfer firefly3Transaction.Description = "Transfer between accounts" - firefly3Transaction.DestinationId = Firefy3AccountsConfig["Wallet cash (UAH)"] // test + firefly3Transaction.DestinationId = ConfigGetAccountByName(config, "Wallet cash (UAH)").Firefly3Id // test firefly3Transactions = append(firefly3Transactions, firefly3Transaction) } else { - for _, row := range ShopConfig { - if slices.Contains(row.Names, transaction.Data.StatementItem.Description) || slices.Contains(row.MCCCodes, transaction.Data.StatementItem.Mcc) { - firefly3Transaction.Description = row.TransactionDescription - firefly3Transaction.DestinationName = row.TransactionDestination - firefly3Transaction.CategoryName = row.TransactionCategory + for _, row := range config.TransactionTypes { + if slices.Contains(row.Names, monobankTransaction.Data.StatementItem.Description) || slices.Contains(row.MccCodes, monobankTransaction.Data.StatementItem.Mcc) { + firefly3Transaction.Description = row.Firefly3.Description + firefly3Transaction.DestinationName = row.Firefly3.Destination + firefly3Transaction.CategoryName = row.Firefly3.Category firefly3Transactions = append(firefly3Transactions, firefly3Transaction) break } @@ -159,14 +153,14 @@ func handleWebhook(w http.ResponseWriter, r *http.Request) { } // record transfer fee - if transaction.Data.StatementItem.CommissionRate > 0 { + if monobankTransaction.Data.StatementItem.CommissionRate > 0 { firefly3Transactions = append(firefly3Transactions, firefly3.TransactionSplitStore{ - Type_: &transactionTypeWithdrawal, - Date: time.Unix(int64(transaction.Data.StatementItem.Time), 0).Add(time.Hour * 2), - Notes: string(body), + Type_: &firefly3TransactionTypeWithdrawal, + Date: time.Unix(int64(monobankTransaction.Data.StatementItem.Time), 0).Add(time.Hour * 2), + Notes: string(monobankTransactionJson), Description: "Transfer fee", - Amount: strconv.Itoa(int(math.Abs(math.Round(float64(transaction.Data.StatementItem.CommissionRate / 100))))), - SourceId: account.Id, + Amount: strconv.Itoa(int(math.Abs(math.Round(float64(monobankTransaction.Data.StatementItem.CommissionRate / 100))))), + SourceId: account.Firefly3Id, }) } @@ -175,7 +169,7 @@ func handleWebhook(w http.ResponseWriter, r *http.Request) { transactionOpts := firefly3.TransactionsApiStoreTransactionOpts{} for _, transaction := range firefly3Transactions { - _, _, err = client.TransactionsApi.StoreTransaction(context.Background(), firefly3.TransactionStore{ + _, _, err = firefly3Client.TransactionsApi.StoreTransaction(context.Background(), firefly3.TransactionStore{ ApplyRules: true, Transactions: []firefly3.TransactionSplitStore{transaction}, }, &transactionOpts) @@ -197,9 +191,6 @@ func main() { log.Fatalf("Error loading .env file") } - // Configure - ShopConfig, Firefy3AccountsConfig = Configure() - // set webhook http.HandleFunc("/webhook", handleWebhook)