Skip to content

Commit

Permalink
support importing transaction by csv/tsv file via command line
Browse files Browse the repository at this point in the history
  • Loading branch information
mayswind committed Sep 1, 2024
1 parent 366311e commit 7c59e83
Show file tree
Hide file tree
Showing 27 changed files with 1,489 additions and 201 deletions.
74 changes: 74 additions & 0 deletions cmd/user_data.go
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,31 @@ var UserData = &cli.Command{
},
},
},
{
Name: "transaction-import",
Usage: "Import transactions to specified user",
Action: bindAction(importUserTransaction),
Flags: []cli.Flag{
&cli.StringFlag{
Name: "username",
Aliases: []string{"n"},
Required: true,
Usage: "Specific user name",
},
&cli.StringFlag{
Name: "file",
Aliases: []string{"f"},
Required: true,
Usage: "Specific import file path (e.g. transaction.csv)",
},
&cli.StringFlag{
Name: "type",
Aliases: []string{"t"},
Required: true,
Usage: "Import file type (supports \"ezbookkeeping_csv\", \"ezbookkeeping_tsv\")",
},
},
},
{
Name: "transaction-export",
Usage: "Export user all transactions to file",
Expand Down Expand Up @@ -639,6 +664,55 @@ func exportUserTransaction(c *core.CliContext) error {
return nil
}

func importUserTransaction(c *core.CliContext) error {
_, err := initializeSystem(c)

if err != nil {
return err
}

username := c.String("username")
filePath := c.String("file")
filetype := c.String("type")

if filePath == "" {
log.BootErrorf(c, "[user_data.importUserTransaction] import file path is not specified")
return os.ErrNotExist
}

fileExists, err := utils.IsExists(filePath)

if !fileExists {
log.BootErrorf(c, "[user_data.importUserTransaction] import file does not exist")
return os.ErrExist
}

if filetype != "ezbookkeeping_csv" && filetype != "ezbookkeeping_tsv" {
log.BootErrorf(c, "[user_data.importUserTransaction] unknown file type \"%s\"", filetype)
return errs.ErrImportFileTypeNotSupported
}

data, err := os.ReadFile(filePath)

if err != nil {
log.BootErrorf(c, "[user_data.importUserTransaction] failed to load import file")
return err
}

log.BootInfof(c, "[user_data.importUserTransaction] start importing transactions to user \"%s\"", username)

err = clis.UserData.ImportTransaction(c, username, filetype, data)

if err != nil {
log.BootErrorf(c, "[user_data.importUserTransaction] error occurs when importing user data")
return err
}

log.BootInfof(c, "[user_data.importUserTransaction] transactions have been imported to user \"%s\"", username)

return nil
}

func printUserInfo(user *models.User) {
fmt.Printf("[Uid] %d\n", user.Uid)
fmt.Printf("[Username] %s\n", user.Username)
Expand Down
8 changes: 4 additions & 4 deletions pkg/api/data_managements.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ const pageCountForDataExport = 1000
// DataManagementsApi represents data management api
type DataManagementsApi struct {
ApiUsingConfig
ezBookKeepingCsvExporter *converters.EzBookKeepingCSVFileExporter
ezBookKeepingTsvExporter *converters.EzBookKeepingTSVFileExporter
ezBookKeepingCsvExporter *converters.EzBookKeepingCSVFileConverter
ezBookKeepingTsvExporter *converters.EzBookKeepingTSVFileConverter
tokens *services.TokenService
users *services.UserService
accounts *services.AccountService
Expand All @@ -38,8 +38,8 @@ var (
ApiUsingConfig: ApiUsingConfig{
container: settings.Container,
},
ezBookKeepingCsvExporter: &converters.EzBookKeepingCSVFileExporter{},
ezBookKeepingTsvExporter: &converters.EzBookKeepingTSVFileExporter{},
ezBookKeepingCsvExporter: &converters.EzBookKeepingCSVFileConverter{},
ezBookKeepingTsvExporter: &converters.EzBookKeepingTSVFileConverter{},
tokens: services.Tokens,
users: services.Users,
accounts: services.Accounts,
Expand Down
112 changes: 108 additions & 4 deletions pkg/cli/user_data.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/services"
"github.com/mayswind/ezbookkeeping/pkg/settings"
"github.com/mayswind/ezbookkeeping/pkg/utils"
"github.com/mayswind/ezbookkeeping/pkg/validators"
)

Expand All @@ -19,8 +20,8 @@ const pageCountForDataExport = 1000
// UserDataCli represents user data cli
type UserDataCli struct {
CliUsingConfig
ezBookKeepingCsvExporter *converters.EzBookKeepingCSVFileExporter
ezBookKeepingTsvExporter *converters.EzBookKeepingTSVFileExporter
ezBookKeepingCsvExporter *converters.EzBookKeepingCSVFileConverter
ezBookKeepingTsvExporter *converters.EzBookKeepingTSVFileConverter
accounts *services.AccountService
transactions *services.TransactionService
categories *services.TransactionCategoryService
Expand All @@ -37,8 +38,8 @@ var (
CliUsingConfig: CliUsingConfig{
container: settings.Container,
},
ezBookKeepingCsvExporter: &converters.EzBookKeepingCSVFileExporter{},
ezBookKeepingTsvExporter: &converters.EzBookKeepingTSVFileExporter{},
ezBookKeepingCsvExporter: &converters.EzBookKeepingCSVFileConverter{},
ezBookKeepingTsvExporter: &converters.EzBookKeepingTSVFileConverter{},
accounts: services.Accounts,
transactions: services.Transactions,
categories: services.TransactionCategories,
Expand Down Expand Up @@ -662,6 +663,73 @@ func (l *UserDataCli) ExportTransaction(c *core.CliContext, username string, fil
return result, nil
}

func (l *UserDataCli) ImportTransaction(c *core.CliContext, username string, fileType string, data []byte) error {
if username == "" {
log.BootErrorf(c, "[user_data.ImportTransaction] user name is empty")
return errs.ErrUsernameIsEmpty
}

var dataImporter converters.DataConverter

if fileType == "ezbookkeeping_csv" {
dataImporter = l.ezBookKeepingCsvExporter
} else if fileType == "ezbookkeeping_tsv" {
dataImporter = l.ezBookKeepingTsvExporter
} else {
return errs.ErrImportFileTypeNotSupported
}

user, err := l.GetUserByUsername(c, username)

if err != nil {
log.BootErrorf(c, "[user_data.ImportTransaction] failed to get user by user name \"%s\", because %s", username, err.Error())
return err
}

accountMap, categoryMap, tagMap, err := l.getUserEssentialDataForImport(c, user.Uid, username)

if err != nil {
log.BootErrorf(c, "[user_data.ImportTransaction] failed to get essential data for user \"%s\", because %s", username, err.Error())
return err
}

newTransactions, newAccounts, newCategories, newTags, err := dataImporter.ParseImportedData(user, data, utils.GetTimezoneOffsetMinutes(time.Local), accountMap, categoryMap, tagMap)

if err != nil {
log.BootErrorf(c, "[user_data.ImportTransaction] failed to parse imported data for \"%s\", because %s", username, err.Error())
return err
}

if len(newTransactions) < 1 {
log.BootErrorf(c, "[user_data.ImportTransaction] there are no transactions in import file")
return errs.ErrOperationFailed
}

if len(newAccounts) > 0 {
log.BootErrorf(c, "[user_data.ImportTransaction] there are %d accounts need to be created, please create them manually", len(newAccounts))
return errs.ErrOperationFailed
}

if len(newCategories) > 0 {
log.BootErrorf(c, "[user_data.ImportTransaction] there are %d transaction categories need to be created, please create them manually", len(newCategories))
return errs.ErrOperationFailed
}

if len(newTags) > 0 {
log.BootErrorf(c, "[user_data.ImportTransaction] there are %d transaction tags need to be created, please create them manually", len(newTags))
return errs.ErrOperationFailed
}

err = l.transactions.BatchCreateTransactions(c, user.Uid, newTransactions)

if err != nil {
log.BootErrorf(c, "[user_data.ImportTransaction] failed to create transaction, because %s", err.Error())
return err
}

return nil
}

func (l *UserDataCli) getUserIdByUsername(c *core.CliContext, username string) (int64, error) {
user, err := l.GetUserByUsername(c, username)

Expand Down Expand Up @@ -718,6 +786,42 @@ func (l *UserDataCli) getUserEssentialData(c *core.CliContext, uid int64, userna
return accountMap, categoryMap, tagMap, tagIndexes, tagIndexesMap, nil
}

func (l *UserDataCli) getUserEssentialDataForImport(c *core.CliContext, uid int64, username string) (accountMap map[string]*models.Account, categoryMap map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag, err error) {
if uid <= 0 {
log.BootErrorf(c, "[user_data.getUserEssentialDataForImport] user uid \"%d\" is invalid", uid)
return nil, nil, nil, errs.ErrUserIdInvalid
}

accounts, err := l.accounts.GetAllAccountsByUid(c, uid)

if err != nil {
log.BootErrorf(c, "[user_data.getUserEssentialDataForImport] failed to get accounts for user \"%s\", because %s", username, err.Error())
return nil, nil, nil, err
}

accountMap = l.accounts.GetAccountNameMapByList(accounts)

categories, err := l.categories.GetAllCategoriesByUid(c, uid, 0, -1)

if err != nil {
log.BootErrorf(c, "[user_data.getUserEssentialDataForImport] failed to get categories for user \"%s\", because %s", username, err.Error())
return nil, nil, nil, err
}

categoryMap = l.categories.GetCategoryNameMapByList(categories)

tags, err := l.tags.GetAllTagsByUid(c, uid)

if err != nil {
log.BootErrorf(c, "[user_data.getUserEssentialDataForImport] failed to get tags for user \"%s\", because %s", username, err.Error())
return nil, nil, nil, err
}

tagMap = l.tags.GetTagNameMapByList(tags)

return accountMap, categoryMap, tagMap, nil
}

func (l *UserDataCli) checkTransactionAccount(c *core.CliContext, transaction *models.Transaction, accountMap map[int64]*models.Account, accountHasChild map[int64]bool) error {
account, exists := accountMap[transaction.AccountId]

Expand Down
5 changes: 4 additions & 1 deletion pkg/converters/data_converter.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ import (
"github.com/mayswind/ezbookkeeping/pkg/models"
)

// DataConverter defines the structure of data exporter
// DataConverter defines the structure of data converter
type DataConverter interface {
// ToExportedContent returns the exported data
ToExportedContent(uid int64, transactions []*models.Transaction, accountMap map[int64]*models.Account, categoryMap map[int64]*models.TransactionCategory, tagMap map[int64]*models.TransactionTag, allTagIndexes map[int64][]int64) ([]byte, error)

// ParseImportedData returns the imported data
ParseImportedData(user *models.User, data []byte, defaultTimezoneOffset int16, accountMap map[string]*models.Account, categoryMap map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) ([]*models.Transaction, []*models.Account, []*models.TransactionCategory, []*models.TransactionTag, error)
}
13 changes: 9 additions & 4 deletions pkg/converters/ezbookkeeping_csv_file.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,19 @@ import (
"github.com/mayswind/ezbookkeeping/pkg/models"
)

// EzBookKeepingCSVFileExporter defines the structure of CSV file exporter
type EzBookKeepingCSVFileExporter struct {
EzBookKeepingPlainFileExporter
// EzBookKeepingCSVFileConverter defines the structure of CSV file converter
type EzBookKeepingCSVFileConverter struct {
EzBookKeepingPlainFileConverter
}

const csvSeparator = ","

// ToExportedContent returns the exported CSV data
func (e *EzBookKeepingCSVFileExporter) ToExportedContent(uid int64, transactions []*models.Transaction, accountMap map[int64]*models.Account, categoryMap map[int64]*models.TransactionCategory, tagMap map[int64]*models.TransactionTag, allTagIndexes map[int64][]int64) ([]byte, error) {
func (e *EzBookKeepingCSVFileConverter) ToExportedContent(uid int64, transactions []*models.Transaction, accountMap map[int64]*models.Account, categoryMap map[int64]*models.TransactionCategory, tagMap map[int64]*models.TransactionTag, allTagIndexes map[int64][]int64) ([]byte, error) {
return e.toExportedContent(uid, csvSeparator, transactions, accountMap, categoryMap, tagMap, allTagIndexes)
}

// ParseImportedData parses transactions of ezbookkeeping CSV data
func (e *EzBookKeepingCSVFileConverter) ParseImportedData(user *models.User, data []byte, defaultTimezoneOffset int16, accountMap map[string]*models.Account, categoryMap map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) ([]*models.Transaction, []*models.Account, []*models.TransactionCategory, []*models.TransactionTag, error) {
return e.parseImportedData(user, csvSeparator, data, defaultTimezoneOffset, accountMap, categoryMap, tagMap)
}
Loading

0 comments on commit 7c59e83

Please sign in to comment.