From 76af5d946abaec16b9c5fede605b3f7534482f41 Mon Sep 17 00:00:00 2001 From: MaysWind Date: Wed, 24 Dec 2025 00:33:47 +0800 Subject: [PATCH] use the daylight saving time zone as default time zone rather than the current standard time zone during the DST --- pkg/api/accounts.go | 12 +- pkg/api/data_managements.go | 12 +- pkg/api/large_language_models.go | 8 +- pkg/api/transactions.go | 6 +- pkg/cli/user_data.go | 2 +- ...ipay_transaction_data_csv_file_importer.go | 5 +- ...transaction_data_csv_file_importer_test.go | 56 ++++----- ...eancount_transaction_data_file_importer.go | 6 +- ...unt_transaction_data_file_importer_test.go | 33 ++--- .../camt_statement_transaction_data_table.go | 2 +- .../camt_transaction_data_file_importer.go | 6 +- ...amt_transaction_data_file_importer_test.go | 43 +++---- .../data_table_transaction_data_exporter.go | 5 +- .../data_table_transaction_data_importer.go | 11 +- .../converter/transaction_data_converter.go | 4 +- ...ult_transaction_data_json_file_importer.go | 7 +- ...t_transaction_data_plain_text_converter.go | 6 +- ...nsaction_data_plain_text_converter_test.go | 61 ++++----- ...stom_transaction_data_dsv_file_importer.go | 5 +- ...transaction_data_dsv_file_importer_test.go | 95 +++++++------- ...ustom_transaction_plain_text_data_table.go | 2 +- ..._app_transaction_data_csv_file_importer.go | 5 +- ...transaction_data_csv_file_importer_test.go | 58 ++++----- ...oud_transaction_data_xlsx_file_importer.go | 6 +- ...ransaction_data_xlsx_file_importer_test.go | 2 +- ..._web_transaction_data_xls_file_importer.go | 6 +- ...transaction_data_xls_file_importer_test.go | 2 +- ...yiii_transaction_data_csv_file_importer.go | 5 +- ...transaction_data_csv_file_importer_test.go | 57 ++++----- .../fireflyiii_transaction_data_row_parser.go | 2 +- .../gnucash_transaction_data_file_importer.go | 6 +- ...ash_transaction_data_file_importer_test.go | 43 +++---- .../gnucash/gnucash_transaction_data_table.go | 2 +- .../iif/iif_transaction_data_file_importer.go | 6 +- ...iif_transaction_data_file_importer_test.go | 79 ++++++------ ...ance_transaction_data_csv_file_importer.go | 5 +- ...transaction_data_csv_file_importer_test.go | 54 ++++---- .../mt/mt_transaction_data_file_importer.go | 6 +- .../mt_transaction_data_file_importer_test.go | 15 +-- .../ofx/ofx_transaction_data_file_importer.go | 6 +- ...ofx_transaction_data_file_importer_test.go | 37 +++--- .../qif/qif_transaction_data_file_importer.go | 6 +- ...qif_transaction_data_file_importer_test.go | 43 +++---- ..._pay_transaction_data_csv_file_importer.go | 5 +- ...transaction_data_csv_file_importer_test.go | 46 +++---- ...pay_transaction_data_xlsx_file_importer.go | 6 +- pkg/mcp/add_transaction_tool_handler.go | 2 +- pkg/services/accounts.go | 13 +- pkg/utils/datetimes.go | 25 ++-- pkg/utils/datetimes_test.go | 85 ++++++++++--- .../base/AccountBalanceTrendsChartBase.ts | 22 ++-- src/components/base/DateRangeSelectionBase.ts | 50 +++++--- src/components/base/DateTimeSelectionBase.ts | 19 +++ .../base/MonthRangeSelectionBase.ts | 7 +- src/components/common/DateTimePicker.vue | 16 +-- src/components/common/MonthPicker.vue | 10 +- src/components/common/TransactionCalendar.vue | 16 +-- .../desktop/DateRangeSelectionDialog.vue | 20 +-- src/components/desktop/DateTimeSelect.vue | 24 ++-- src/components/desktop/TrendsChart.vue | 19 +-- .../mobile/DateRangeSelectionSheet.vue | 20 +-- .../mobile/DateTimeSelectionSheet.vue | 13 +- src/components/mobile/TrendsBarChart.vue | 18 +-- src/lib/datetime.ts | 117 ++++++++++++++---- src/lib/services.ts | 7 +- src/lib/transaction.ts | 15 +-- src/locales/helpers.ts | 82 ++++++------ src/models/transaction.ts | 37 +++--- src/stores/transaction.ts | 26 ++-- src/views/base/AboutPageBase.ts | 6 +- src/views/base/ExchangeRatesPageBase.ts | 11 +- src/views/base/HomePageBase.ts | 24 ++-- .../base/accounts/AccountEditPageBase.ts | 46 ++++++- .../ReconciliationStatementPageBase.ts | 39 +++--- .../base/settings/AppSettingsPageBase.ts | 3 +- .../StatisticsTransactionPageBase.ts | 22 ++-- .../transactions/TransactionEditPageBase.ts | 46 ++++--- .../transactions/TransactionListPageBase.ts | 43 ++++--- src/views/desktop/HomePage.vue | 6 +- .../accounts/list/dialogs/EditDialog.vue | 12 +- .../dialogs/ReconciliationStatementDialog.vue | 4 +- src/views/desktop/insights/ExplorePage.vue | 9 +- .../insights/tabs/ExploreDataTableTab.vue | 22 ++-- .../cards/MonthlyIncomeAndExpenseCard.vue | 10 +- src/views/desktop/transactions/ListPage.vue | 10 +- .../tabs/ImportTransactionCheckDataTab.vue | 20 +-- ...mportTransactionExecuteCustomScriptTab.vue | 7 +- .../transactions/list/dialogs/EditDialog.vue | 32 ++--- .../settings/tabs/UserSecuritySettingTab.vue | 7 +- src/views/mobile/SettingsPage.vue | 11 +- src/views/mobile/accounts/EditPage.vue | 23 ++-- .../accounts/ReconciliationStatementPage.vue | 11 +- .../mobile/settings/TextSizeSettingsPage.vue | 19 +-- src/views/mobile/transactions/EditPage.vue | 33 +++-- src/views/mobile/transactions/ListPage.vue | 17 ++- src/views/mobile/users/SessionListPage.vue | 10 +- 96 files changed, 1179 insertions(+), 882 deletions(-) diff --git a/pkg/api/accounts.go b/pkg/api/accounts.go index ebb22ff1..44e94ab8 100644 --- a/pkg/api/accounts.go +++ b/pkg/api/accounts.go @@ -150,10 +150,10 @@ func (a *AccountsApi) AccountCreateHandler(c *core.WebContext) (any, *errs.Error return nil, errs.NewIncompleteOrIncorrectSubmissionError(err) } - _, utcOffset, err := c.GetClientTimezone() + clientTimezone, _, err := c.GetClientTimezone() if err != nil { - log.Warnf(c, "[accounts.AccountCreateHandler] cannot get client timezone offset, because %s", err.Error()) + log.Warnf(c, "[accounts.AccountCreateHandler] cannot get client timezone, because %s", err.Error()) return nil, errs.ErrClientTimezoneOffsetInvalid } @@ -278,7 +278,7 @@ func (a *AccountsApi) AccountCreateHandler(c *core.WebContext) (any, *errs.Error } } - err = a.accounts.CreateAccounts(c, mainAccount, accountCreateReq.BalanceTime, childrenAccounts, childrenAccountBalanceTimes, utcOffset) + err = a.accounts.CreateAccounts(c, mainAccount, accountCreateReq.BalanceTime, childrenAccounts, childrenAccountBalanceTimes, clientTimezone) if err != nil { log.Errorf(c, "[accounts.AccountCreateHandler] failed to create account \"id:%d\" for user \"uid:%d\", because %s", mainAccount.AccountId, uid, err.Error()) @@ -315,10 +315,10 @@ func (a *AccountsApi) AccountModifyHandler(c *core.WebContext) (any, *errs.Error return nil, errs.ErrAccountIdInvalid } - _, utcOffset, err := c.GetClientTimezone() + clientTimezone, _, err := c.GetClientTimezone() if err != nil { - log.Warnf(c, "[accounts.AccountModifyHandler] cannot get client timezone offset, because %s", err.Error()) + log.Warnf(c, "[accounts.AccountModifyHandler] cannot get client timezone, because %s", err.Error()) return nil, errs.ErrClientTimezoneOffsetInvalid } @@ -521,7 +521,7 @@ func (a *AccountsApi) AccountModifyHandler(c *core.WebContext) (any, *errs.Error } } - err = a.accounts.ModifyAccounts(c, mainAccount, toUpdateAccounts, toAddAccounts, toAddAccountBalanceTimes, toDeleteAccountIds, utcOffset) + err = a.accounts.ModifyAccounts(c, mainAccount, toUpdateAccounts, toAddAccounts, toAddAccountBalanceTimes, toDeleteAccountIds, clientTimezone) if err != nil { log.Errorf(c, "[accounts.AccountModifyHandler] failed to update account \"id:%d\" for user \"uid:%d\", because %s", accountModifyReq.Id, uid, err.Error()) diff --git a/pkg/api/data_managements.go b/pkg/api/data_managements.go index c269510f..15740a10 100644 --- a/pkg/api/data_managements.go +++ b/pkg/api/data_managements.go @@ -302,11 +302,11 @@ func (a *DataManagementsApi) getExportedFileContent(c *core.WebContext, fileType return nil, "", errs.NewIncompleteOrIncorrectSubmissionError(err) } - timezone, _, err := c.GetClientTimezone() + clientTimezone, _, err := c.GetClientTimezone() if err != nil { - log.Warnf(c, "[data_managements.getExportedFileContent] cannot get client timezone offset, because %s", err.Error()) - timezone = time.Local + log.Warnf(c, "[data_managements.getExportedFileContent] cannot get client timezone, because %s", err.Error()) + clientTimezone = time.Local } uid := c.GetCurrentUid() @@ -413,13 +413,13 @@ func (a *DataManagementsApi) getExportedFileContent(c *core.WebContext, fileType return nil, "", errs.Or(err, errs.ErrOperationFailed) } - fileName := a.getFileName(user, timezone, fileType) + fileName := a.getFileName(user, clientTimezone, fileType) return result, fileName, nil } -func (a *DataManagementsApi) getFileName(user *models.User, timezone *time.Location, fileExtension string) string { - currentTime := utils.FormatUnixTimeToLongDateTimeWithoutSecond(time.Now().Unix(), timezone) +func (a *DataManagementsApi) getFileName(user *models.User, clientTimezone *time.Location, fileExtension string) string { + currentTime := utils.FormatUnixTimeToLongDateTimeWithoutSecond(time.Now().Unix(), clientTimezone) currentTime = strings.Replace(currentTime, "-", "_", -1) currentTime = strings.Replace(currentTime, " ", "_", -1) currentTime = strings.Replace(currentTime, ":", "_", -1) diff --git a/pkg/api/large_language_models.go b/pkg/api/large_language_models.go index 26450878..9f354807 100644 --- a/pkg/api/large_language_models.go +++ b/pkg/api/large_language_models.go @@ -47,7 +47,7 @@ func (a *LargeLanguageModelsApi) RecognizeReceiptImageHandler(c *core.WebContext return nil, errs.ErrLargeLanguageModelProviderNotEnabled } - clientTimezone, utcOffset, err := c.GetClientTimezone() + clientTimezone, _, err := c.GetClientTimezone() if err != nil { log.Warnf(c, "[large_language_models.RecognizeReceiptImageHandler] cannot get client timezone, because %s", err.Error()) @@ -238,10 +238,10 @@ func (a *LargeLanguageModelsApi) RecognizeReceiptImageHandler(c *core.WebContext return nil, errs.Or(err, errs.ErrOperationFailed) } - return a.parseRecognizedReceiptImageResponse(c, uid, utcOffset, result, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap) + return a.parseRecognizedReceiptImageResponse(c, uid, clientTimezone, result, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap) } -func (a *LargeLanguageModelsApi) parseRecognizedReceiptImageResponse(c *core.WebContext, uid int64, utcOffset int16, recognizedResult *models.RecognizedReceiptImageResult, accountMap map[string]*models.Account, expenseCategoryMap map[string]*models.TransactionCategory, incomeCategoryMap map[string]*models.TransactionCategory, transferCategoryMap map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (*models.RecognizedReceiptImageResponse, *errs.Error) { +func (a *LargeLanguageModelsApi) parseRecognizedReceiptImageResponse(c *core.WebContext, uid int64, clientTimezone *time.Location, recognizedResult *models.RecognizedReceiptImageResult, accountMap map[string]*models.Account, expenseCategoryMap map[string]*models.TransactionCategory, incomeCategoryMap map[string]*models.TransactionCategory, transferCategoryMap map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (*models.RecognizedReceiptImageResponse, *errs.Error) { recognizedReceiptImageResponse := &models.RecognizedReceiptImageResponse{ Type: models.TRANSACTION_TYPE_EXPENSE, } @@ -290,7 +290,7 @@ func (a *LargeLanguageModelsApi) parseRecognizedReceiptImageResponse(c *core.Web if len(recognizedResult.Time) > 0 { longDateTime := a.getLongDateTime(recognizedResult.Time) - timestamp, err := utils.ParseFromLongDateTime(longDateTime, utcOffset) + timestamp, err := utils.ParseFromLongDateTimeInTimeZone(longDateTime, clientTimezone) if err != nil { log.Warnf(c, "[large_language_models.parseRecognizedReceiptImageResponse] recoginzed time \"%s\" is invalid", recognizedResult.Time) diff --git a/pkg/api/transactions.go b/pkg/api/transactions.go index 1d0cd808..29af069c 100644 --- a/pkg/api/transactions.go +++ b/pkg/api/transactions.go @@ -1488,10 +1488,10 @@ func (a *TransactionsApi) TransactionParseImportFileHandler(c *core.WebContext) return nil, errs.ErrParameterInvalid } - _, utcOffset, err := c.GetClientTimezone() + clientTimezone, _, err := c.GetClientTimezone() if err != nil { - log.Warnf(c, "[transactions.TransactionParseImportFileHandler] cannot get client timezone offset, because %s", err.Error()) + log.Warnf(c, "[transactions.TransactionParseImportFileHandler] cannot get client timezone, because %s", err.Error()) return nil, errs.ErrClientTimezoneOffsetInvalid } @@ -1688,7 +1688,7 @@ func (a *TransactionsApi) TransactionParseImportFileHandler(c *core.WebContext) tagMap := a.transactionTags.GetVisibleTagNameMapByList(tags) - parsedTransactions, _, _, _, _, _, err := dataImporter.ParseImportedData(c, user, fileData, utcOffset, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap) + parsedTransactions, _, _, _, _, _, err := dataImporter.ParseImportedData(c, user, fileData, clientTimezone, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap) if err != nil { log.Errorf(c, "[transactions.TransactionParseImportFileHandler] failed to parse imported data for user \"uid:%d\", because %s", user.Uid, err.Error()) diff --git a/pkg/cli/user_data.go b/pkg/cli/user_data.go index c53488cc..cb071cde 100644 --- a/pkg/cli/user_data.go +++ b/pkg/cli/user_data.go @@ -819,7 +819,7 @@ func (l *UserDataCli) ImportTransaction(c *core.CliContext, username string, fil return err } - parsedTransactions, newAccounts, newSubExpenseCategories, newSubIncomeCategories, newSubTransferCategories, newTags, err := dataImporter.ParseImportedData(c, user, data, utils.GetTimezoneOffsetMinutes(time.Local), converter.DefaultImporterOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap) + parsedTransactions, newAccounts, newSubExpenseCategories, newSubIncomeCategories, newSubTransferCategories, newTags, err := dataImporter.ParseImportedData(c, user, data, time.Local, converter.DefaultImporterOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap) if err != nil { log.CliErrorf(c, "[user_data.ImportTransaction] failed to parse imported data for \"%s\", because %s", username, err.Error()) diff --git a/pkg/converters/alipay/alipay_transaction_data_csv_file_importer.go b/pkg/converters/alipay/alipay_transaction_data_csv_file_importer.go index 508e2ad0..5502c82f 100644 --- a/pkg/converters/alipay/alipay_transaction_data_csv_file_importer.go +++ b/pkg/converters/alipay/alipay_transaction_data_csv_file_importer.go @@ -2,6 +2,7 @@ package alipay import ( "bytes" + "time" "golang.org/x/text/encoding/simplifiedchinese" "golang.org/x/text/transform" @@ -53,7 +54,7 @@ type alipayTransactionDataCsvFileImporter struct { } // ParseImportedData returns the imported data by parsing the alipay transaction csv data -func (c *alipayTransactionDataCsvFileImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezoneOffset int16, additionalOptions converter.TransactionDataImporterOptions, accountMap map[string]*models.Account, expenseCategoryMap map[string]map[string]*models.TransactionCategory, incomeCategoryMap map[string]map[string]*models.TransactionCategory, transferCategoryMap map[string]map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionTag, error) { +func (c *alipayTransactionDataCsvFileImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezone *time.Location, additionalOptions converter.TransactionDataImporterOptions, accountMap map[string]*models.Account, expenseCategoryMap map[string]map[string]*models.TransactionCategory, incomeCategoryMap map[string]map[string]*models.TransactionCategory, transferCategoryMap map[string]map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionTag, error) { enc := simplifiedchinese.GB18030 reader := transform.NewReader(bytes.NewReader(data), enc.NewDecoder()) @@ -83,5 +84,5 @@ func (c *alipayTransactionDataCsvFileImporter) ParseImportedData(ctx core.Contex transactionDataTable := datatable.CreateNewTransactionDataTableFromCommonDataTable(commonDataTable, alipayTransactionSupportedColumns, transactionRowParser) dataTableImporter := converter.CreateNewSimpleImporterWithTypeNameMapping(alipayTransactionTypeNameMapping) - return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezoneOffset, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap) + return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezone, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap) } diff --git a/pkg/converters/alipay/alipay_transaction_data_csv_file_importer_test.go b/pkg/converters/alipay/alipay_transaction_data_csv_file_importer_test.go index 65c6bd37..24afbe20 100644 --- a/pkg/converters/alipay/alipay_transaction_data_csv_file_importer_test.go +++ b/pkg/converters/alipay/alipay_transaction_data_csv_file_importer_test.go @@ -36,7 +36,7 @@ func TestAlipayCsvFileImporterParseImportedData_MinimumValidData(t *testing.T) { "------------------------------------------------------------------------------------\n") assert.Nil(t, err) - allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := importer.ParseImportedData(context, user, []byte(data), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := importer.ParseImportedData(context, user, []byte(data), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 4, len(allNewTransactions)) @@ -113,7 +113,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseRefundTransaction(t *testin "------------------------------------------------------------------------------------\n") assert.Nil(t, err) - allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(data1), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(data1), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, int64(1234567890), allNewTransactions[0].Uid) @@ -133,7 +133,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseRefundTransaction(t *testin "------------------------------------------------------------------------------------\n") assert.Nil(t, err) - allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data2), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data2), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, int64(1234567890), allNewTransactions[0].Uid) @@ -164,7 +164,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseInvestmentRefundTransaction "2024-09-01 01:00:00,Test Account2,xxx-买入,不计收支,0.01,Test Account,退款成功,\n" + "2024-09-01 02:00:00,Test Account2,xxx-买入退款,不计收支,0.01,Test Account,退款成功,\n") - allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(data1), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(data1), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 2, len(allNewTransactions)) @@ -202,7 +202,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseInvalidTime(t *testing.T) { "------------------------------------------------------------------------------------\n") assert.Nil(t, err) - _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data1), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data1), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message) data2, err := simplifiedchinese.GB18030.NewEncoder().String("支付宝交易记录明细查询\n" + @@ -214,7 +214,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseInvalidTime(t *testing.T) { "------------------------------------------------------------------------------------\n") assert.Nil(t, err) - _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data2), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data2), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message) } @@ -236,7 +236,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseInvalidType(t *testing.T) { "------------------------------------------------------------------------------------\n") assert.Nil(t, err) - _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrNotFoundTransactionDataInFile.Message) } @@ -259,7 +259,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseAccountName(t *testing.T) { "------------------------------------------------------------------------------------\n") assert.Nil(t, err) - allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(data1), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(data1), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) @@ -275,7 +275,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseAccountName(t *testing.T) { "------------------------------------------------------------------------------------\n") assert.Nil(t, err) - allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data2), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data2), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) @@ -291,7 +291,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseAccountName(t *testing.T) { "------------------------------------------------------------------------------------\n") assert.Nil(t, err) - allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data3), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data3), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) @@ -308,7 +308,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseAccountName(t *testing.T) { "------------------------------------------------------------------------------------\n") assert.Nil(t, err) - allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data4), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data4), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) @@ -325,7 +325,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseAccountName(t *testing.T) { "------------------------------------------------------------------------------------\n") assert.Nil(t, err) - allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data5), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data5), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) @@ -342,7 +342,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseAccountName(t *testing.T) { "------------------------------------------------------------------------------------\n") assert.Nil(t, err) - allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data6), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data6), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) @@ -359,7 +359,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseAccountName(t *testing.T) { "------------------------------------------------------------------------------------\n") assert.Nil(t, err) - allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data7), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data7), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) @@ -389,7 +389,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseCategory(t *testing.T) { "2024-09-01 23:59:59,Test Category3,充值-普通充值,不计收支,0.05,交易成功,\n") assert.Nil(t, err) - allNewTransactions, _, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, _, err := importer.ParseImportedData(context, user, []byte(data1), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + allNewTransactions, _, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, _, err := importer.ParseImportedData(context, user, []byte(data1), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 3, len(allNewTransactions)) @@ -435,7 +435,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseRelatedAccount(t *testing.T "2024-09-01 08:00:00,Test Account4,信用卡还款,不计收支,0.01,Test Account,还款成功,repayment,\n") assert.Nil(t, err) - allNewTransactions, allNewAccounts, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(data1), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + allNewTransactions, allNewAccounts, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(data1), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 9, len(allNewTransactions)) @@ -547,7 +547,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseDescription(t *testing.T) { "------------------------------------------------------------------------------------\n") assert.Nil(t, err) - allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(data1), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(data1), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) @@ -562,7 +562,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseDescription(t *testing.T) { "------------------------------------------------------------------------------------\n") assert.Nil(t, err) - allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data2), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data2), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) @@ -587,7 +587,7 @@ func TestAlipayCsvFileImporterParseImportedData_SkipClosedIncomeOrTransferTransa "2024-09-01 23:59:59 ,充值-普通充值 ,0.05 ,不计收支 ,交易关闭 ,\n" + "------------------------------------------------------------------------------------\n") assert.Nil(t, err) - _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrNotFoundTransactionDataInFile.Message) } @@ -608,7 +608,7 @@ func TestAlipayCsvFileImporterParseImportedData_SkipUnknownProductTransferTransa "2024-09-01 23:59:59 ,xxxx ,0.05 ,不计收支 ,交易成功 ,\n" + "------------------------------------------------------------------------------------\n") assert.Nil(t, err) - _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrNotFoundTransactionDataInFile.Message) } @@ -629,7 +629,7 @@ func TestAlipayCsvFileImporterParseImportedData_SkipUnknownStatusTransaction(t * "2024-09-01 01:23:45 ,xxxx ,0.12 ,收入 ,xxxx ,\n" + "------------------------------------------------------------------------------------\n") assert.Nil(t, err) - _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrNotFoundTransactionDataInFile.Message) } @@ -648,10 +648,10 @@ func TestAlipayCsvFileImporterParseImportedData_MissingFileHeader(t *testing.T) "------------------------------------------------------------------------------------\n") assert.Nil(t, err) - _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrInvalidFileHeader.Message) - _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(""), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrInvalidFileHeader.Message) } @@ -672,7 +672,7 @@ func TestAlipayCsvFileImporterParseImportedData_MissingRequiredColumn(t *testing "金额(元),收/支 ,交易状态 ,\n" + "0.12 ,收入 ,交易成功 ,\n" + "------------------------------------------------------------------------------------\n") - _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data1), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data1), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message) // Missing Amount Column @@ -683,7 +683,7 @@ func TestAlipayCsvFileImporterParseImportedData_MissingRequiredColumn(t *testing "交易创建时间 ,收/支 ,交易状态 ,\n" + "2024-09-01 12:34:56 ,收入 ,交易成功 ,\n" + "------------------------------------------------------------------------------------\n") - _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data2), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data2), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message) // Missing Status Column @@ -694,7 +694,7 @@ func TestAlipayCsvFileImporterParseImportedData_MissingRequiredColumn(t *testing "交易创建时间 ,金额(元),收/支 ,\n" + "2024-09-01 12:34:56 ,0.12 ,收入 ,\n" + "------------------------------------------------------------------------------------\n") - _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data3), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data3), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message) // Missing Type Column @@ -705,7 +705,7 @@ func TestAlipayCsvFileImporterParseImportedData_MissingRequiredColumn(t *testing "交易创建时间 ,金额(元),交易状态 ,\n" + "2024-09-01 12:34:56 ,0.12 ,交易成功 ,\n" + "------------------------------------------------------------------------------------\n") - _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data4), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data4), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message) } @@ -724,6 +724,6 @@ func TestAlipayCsvFileImporterParseImportedData_NoTransactionData(t *testing.T) "---------------------------------交易记录明细列表------------------------------------\n" + "交易创建时间 ,金额(元),收/支 ,交易状态 ,\n" + "------------------------------------------------------------------------------------\n") - _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data1), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data1), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrNotFoundTransactionDataInFile.Message) } diff --git a/pkg/converters/beancount/beancount_transaction_data_file_importer.go b/pkg/converters/beancount/beancount_transaction_data_file_importer.go index 5b7b7536..cf6eab93 100644 --- a/pkg/converters/beancount/beancount_transaction_data_file_importer.go +++ b/pkg/converters/beancount/beancount_transaction_data_file_importer.go @@ -1,6 +1,8 @@ package beancount import ( + "time" + "github.com/mayswind/ezbookkeeping/pkg/converters/converter" "github.com/mayswind/ezbookkeeping/pkg/core" "github.com/mayswind/ezbookkeeping/pkg/models" @@ -24,7 +26,7 @@ var ( ) // ParseImportedData returns the imported data by parsing the Beancount transaction data -func (c *beancountTransactionDataImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezoneOffset int16, additionalOptions converter.TransactionDataImporterOptions, accountMap map[string]*models.Account, expenseCategoryMap map[string]map[string]*models.TransactionCategory, incomeCategoryMap map[string]map[string]*models.TransactionCategory, transferCategoryMap map[string]map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionTag, error) { +func (c *beancountTransactionDataImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezone *time.Location, additionalOptions converter.TransactionDataImporterOptions, accountMap map[string]*models.Account, expenseCategoryMap map[string]map[string]*models.TransactionCategory, incomeCategoryMap map[string]map[string]*models.TransactionCategory, transferCategoryMap map[string]map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionTag, error) { beancountDataReader, err := createNewBeancountDataReader(ctx, data) if err != nil { @@ -45,5 +47,5 @@ func (c *beancountTransactionDataImporter) ParseImportedData(ctx core.Context, u dataTableImporter := converter.CreateNewImporterWithTypeNameMapping(beancountTransactionTypeNameMapping, "", "", BEANCOUNT_TRANSACTION_TAG_SEPARATOR) - return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezoneOffset, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap) + return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezone, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap) } diff --git a/pkg/converters/beancount/beancount_transaction_data_file_importer_test.go b/pkg/converters/beancount/beancount_transaction_data_file_importer_test.go index 11e14579..fd3892b6 100644 --- a/pkg/converters/beancount/beancount_transaction_data_file_importer_test.go +++ b/pkg/converters/beancount/beancount_transaction_data_file_importer_test.go @@ -2,6 +2,7 @@ package beancount import ( "testing" + "time" "github.com/stretchr/testify/assert" @@ -33,7 +34,7 @@ func TestBeancountTransactionDataFileParseImportedData_MinimumValidData(t *testi " Expenses:TestCategory2 1.00 CNY\n"+ "2024-09-04 *\n"+ " Assets:TestAccount -0.05 CNY\n"+ - " Assets:TestAccount2 0.05 CNY\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + " Assets:TestAccount2 0.05 CNY\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) @@ -112,7 +113,7 @@ func TestBeancountTransactionDataFileParseImportedData_MinimumValidData2(t *test " Assets:TestAccount -1.00 CNY\n"+ "2024-09-04 *\n"+ " Assets:TestAccount2 0.05 CNY\n"+ - " Assets:TestAccount -0.05 CNY\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + " Assets:TestAccount -0.05 CNY\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) @@ -182,7 +183,7 @@ func TestBeancountTransactionDataFileParseImportedData_ParseInvalidTime(t *testi _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte( "2024/09/01 *\n"+ " Equity:Opening-Balances -123.45 CNY\n"+ - " Assets:TestAccount 123.45 CNY\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + " Assets:TestAccount 123.45 CNY\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrNotFoundTransactionDataInFile.Message) } @@ -198,7 +199,7 @@ func TestBeancountTransactionDataFileParseImportedData_ParseValidCurrency(t *tes allNewTransactions, allNewAccounts, _, _, _, _, err := importer.ParseImportedData(context, user, []byte( "2024-09-01 * \"Payee Name\" \"Hello\nWorld\"\n"+ " Assets:TestAccount -0.12 USD\n"+ - " Assets:TestAccount2 0.84 CNY\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + " Assets:TestAccount2 0.84 CNY\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) @@ -234,13 +235,13 @@ func TestBeancountTransactionDataFileParseImportedData_ParseInvalidAmount(t *tes _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte( "2024-09-01 *\n"+ " Equity:Opening-Balances -abc CNY\n"+ - " Assets:TestAccount abc CNY\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + " Assets:TestAccount abc CNY\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrAmountInvalid.Message) _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte( "2024-09-01 *\n"+ " Equity:Opening-Balances -1/0 CNY\n"+ - " Assets:TestAccount 1/0 CNY\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + " Assets:TestAccount 1/0 CNY\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrAmountInvalid.Message) } @@ -259,7 +260,7 @@ func TestBeancountTransactionDataFileParseImportedData_ParseDescription(t *testi " Assets:TestAccount 123.45 CNY\n"+ "2024-09-02 * \"Payee Name\" \"Hello\nWorld\"\n"+ " Income:TestCategory -0.12 CNY\n"+ - " Assets:TestAccount 0.12 CNY\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + " Assets:TestAccount 0.12 CNY\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) @@ -281,25 +282,25 @@ func TestBeancountTransactionDataFileParseImportedData_InvalidTransaction(t *tes _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte( "2024-09-02 * \"Payee Name\" \"Hello\nWorld\"\n"+ " Assets:TestAccount 0.11 CNY\n"+ - " Assets:TestAccount2 0.11 CNY\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + " Assets:TestAccount2 0.11 CNY\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrInvalidBeancountFile.Message) _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte( "2024-09-02 * \"Payee Name\" \"Hello\nWorld\"\n"+ " Expenses:TestCategory -0.11 CNY\n"+ - " Expenses:TestCategory2 0.11 CNY\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + " Expenses:TestCategory2 0.11 CNY\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrThereAreNotSupportedTransactionType.Message) _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte( "2024-09-02 * \"Payee Name\" \"Hello\nWorld\"\n"+ " Income:TestCategory -0.11 CNY\n"+ - " Income:TestCategory2 0.11 CNY\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + " Income:TestCategory2 0.11 CNY\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrThereAreNotSupportedTransactionType.Message) _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte( "2024-09-02 * \"Payee Name\" \"Hello\nWorld\"\n"+ " Equity:TestCategory -0.11 CNY\n"+ - " Equity:TestCategory2 0.11 CNY\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + " Equity:TestCategory2 0.11 CNY\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrThereAreNotSupportedTransactionType.Message) } @@ -316,7 +317,7 @@ func TestBeancountTransactionDataFileParseImportedData_NotSupportedToParseSplitT "2024-09-02 * \"Payee Name\" \"Hello\nWorld\"\n"+ " Assets:TestAccount -0.23 CNY\n"+ " Assets:TestAccount2 0.11 CNY\n"+ - " Assets:TestAccount3 0.12 CNY\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + " Assets:TestAccount3 0.12 CNY\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrNotSupportedSplitTransactions.Message) } @@ -333,27 +334,27 @@ func TestBeancountTransactionDataFileParseImportedData_MissingTransactionRequire _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte( "* \"narration\"\n"+ " Equity:Opening-Balances -123.45 CNY\n"+ - " Assets:TestAccount 123.45 CNY\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + " Assets:TestAccount 123.45 CNY\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrNotFoundTransactionDataInFile.Message) // Missing Account Name _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte( "2024-09-01 * \"narration\"\n"+ " Equity:Opening-Balances -123.45 CNY\n"+ - " 123.45 CNY\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + " 123.45 CNY\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrInvalidBeancountFile.Message) // Missing Amount _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte( "2024-09-01 * \"narration\"\n"+ " Equity:Opening-Balances\n"+ - " Assets:TestAccount\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + " Assets:TestAccount\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrInvalidBeancountFile.Message) // Missing Commodity _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte( "2024-09-01 * \"narration\"\n"+ " Equity:Opening-Balances -123.45\n"+ - " Assets:TestAccount 123.45\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + " Assets:TestAccount 123.45\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrInvalidBeancountFile.Message) } diff --git a/pkg/converters/camt/camt_statement_transaction_data_table.go b/pkg/converters/camt/camt_statement_transaction_data_table.go index 9a090aad..9cdca0bc 100644 --- a/pkg/converters/camt/camt_statement_transaction_data_table.go +++ b/pkg/converters/camt/camt_statement_transaction_data_table.go @@ -231,7 +231,7 @@ func (t *camtStatementTransactionDataRowIterator) parseTransaction(ctx core.Cont } data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME] = utils.FormatUnixTimeToLongDateTime(dateTime.Unix(), dateTime.Location()) - data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE] = utils.FormatTimezoneOffset(dateTime.Location()) + data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE] = utils.FormatTimezoneOffset(dateTime.Unix(), dateTime.Location()) } else if entry.BookingDate != nil && entry.BookingDate.Date != "" { data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME] = fmt.Sprintf("%s 00:00:00", entry.BookingDate.Date) data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE] = datatable.TRANSACTION_DATA_TABLE_TIMEZONE_NOT_AVAILABLE diff --git a/pkg/converters/camt/camt_transaction_data_file_importer.go b/pkg/converters/camt/camt_transaction_data_file_importer.go index 4299c06b..f8374e92 100644 --- a/pkg/converters/camt/camt_transaction_data_file_importer.go +++ b/pkg/converters/camt/camt_transaction_data_file_importer.go @@ -1,6 +1,8 @@ package camt import ( + "time" + "github.com/mayswind/ezbookkeeping/pkg/converters/converter" "github.com/mayswind/ezbookkeeping/pkg/core" "github.com/mayswind/ezbookkeeping/pkg/models" @@ -23,7 +25,7 @@ var ( ) // ParseImportedData returns the imported data by parsing the camt.053 file transaction data -func (c *camt053TransactionDataImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezoneOffset int16, additionalOptions converter.TransactionDataImporterOptions, accountMap map[string]*models.Account, expenseCategoryMap map[string]map[string]*models.TransactionCategory, incomeCategoryMap map[string]map[string]*models.TransactionCategory, transferCategoryMap map[string]map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionTag, error) { +func (c *camt053TransactionDataImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezone *time.Location, additionalOptions converter.TransactionDataImporterOptions, accountMap map[string]*models.Account, expenseCategoryMap map[string]map[string]*models.TransactionCategory, incomeCategoryMap map[string]map[string]*models.TransactionCategory, transferCategoryMap map[string]map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionTag, error) { camt053DataReader, err := createNewCamt053FileReader(data) if err != nil { @@ -44,5 +46,5 @@ func (c *camt053TransactionDataImporter) ParseImportedData(ctx core.Context, use dataTableImporter := converter.CreateNewSimpleImporterWithTypeNameMapping(camtTransactionTypeNameMapping) - return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezoneOffset, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap) + return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezone, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap) } diff --git a/pkg/converters/camt/camt_transaction_data_file_importer_test.go b/pkg/converters/camt/camt_transaction_data_file_importer_test.go index 78ee47ca..7f8a48bc 100644 --- a/pkg/converters/camt/camt_transaction_data_file_importer_test.go +++ b/pkg/converters/camt/camt_transaction_data_file_importer_test.go @@ -2,6 +2,7 @@ package camt import ( "testing" + "time" "github.com/stretchr/testify/assert" @@ -65,7 +66,7 @@ func TestCamt053TransactionDataFileParseImportedData_MinimumValidData(t *testing - `), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + `), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) @@ -158,7 +159,7 @@ func TestCamt053TransactionDataFileParseImportedData_ParseValidTransactionTime(t - `), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + `), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 3, len(allNewTransactions)) @@ -197,7 +198,7 @@ func TestCamt053TransactionDataFileParseImportedData_ParseInvalidTransactionTime - `), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + `), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message) _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte( @@ -220,7 +221,7 @@ func TestCamt053TransactionDataFileParseImportedData_ParseInvalidTransactionTime - `), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + `), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message) _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte( @@ -243,7 +244,7 @@ func TestCamt053TransactionDataFileParseImportedData_ParseInvalidTransactionTime - `), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + `), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message) _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte( @@ -266,7 +267,7 @@ func TestCamt053TransactionDataFileParseImportedData_ParseInvalidTransactionTime - `), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + `), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message) } @@ -315,7 +316,7 @@ func TestCamt053TransactionDataFileParseImportedData_ParseTransactionValidAmount - `), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + `), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 2, len(allNewTransactions)) @@ -366,7 +367,7 @@ func TestCamt053TransactionDataFileParseImportedData_ParseTransactionValidAmount - `), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + `), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 2, len(allNewTransactions)) @@ -404,7 +405,7 @@ func TestCamt053TransactionDataFileParseImportedData_ParseTransactionValidAmount - `), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + `), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) @@ -431,7 +432,7 @@ func TestCamt053TransactionDataFileParseImportedData_ParseTransactionValidAmount - `), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + `), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) @@ -468,7 +469,7 @@ func TestCamt053TransactionDataFileParseImportedData_ParseTransactionInvalidAmou - `), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + `), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrAmountInvalid.Message) _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte( @@ -499,7 +500,7 @@ func TestCamt053TransactionDataFileParseImportedData_ParseTransactionInvalidAmou - `), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + `), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrAmountInvalid.Message) _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte( @@ -530,7 +531,7 @@ func TestCamt053TransactionDataFileParseImportedData_ParseTransactionInvalidAmou - `), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + `), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrAmountInvalid.Message) } @@ -573,7 +574,7 @@ func TestCamt053TransactionDataFileParseImportedData_ParseDescription(t *testing - `), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + `), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) @@ -608,7 +609,7 @@ func TestCamt053TransactionDataFileParseImportedData_ParseDescription(t *testing - `), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + `), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) @@ -635,7 +636,7 @@ func TestCamt053TransactionDataFileParseImportedData_ParseDescription(t *testing - `), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + `), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) @@ -665,7 +666,7 @@ func TestCamt053TransactionDataFileParseImportedData_MissingAccountNode(t *testi - `), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + `), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrMissingAccountData.Message) } @@ -695,7 +696,7 @@ func TestCamt053TransactionDataFileParseImportedData_MissingTransactionRequiredN - `), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + `), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrMissingTransactionTime.Message) _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte( @@ -717,7 +718,7 @@ func TestCamt053TransactionDataFileParseImportedData_MissingTransactionRequiredN - `), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + `), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrTransactionTypeInvalid.Message) _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte( @@ -739,7 +740,7 @@ func TestCamt053TransactionDataFileParseImportedData_MissingTransactionRequiredN - `), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + `), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrAmountInvalid.Message) _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte( @@ -761,6 +762,6 @@ func TestCamt053TransactionDataFileParseImportedData_MissingTransactionRequiredN - `), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + `), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message) } diff --git a/pkg/converters/converter/data_table_transaction_data_exporter.go b/pkg/converters/converter/data_table_transaction_data_exporter.go index da8425d3..e855f1e9 100644 --- a/pkg/converters/converter/data_table_transaction_data_exporter.go +++ b/pkg/converters/converter/data_table_transaction_data_exporter.go @@ -28,10 +28,11 @@ func (c *DataTableTransactionDataExporter) BuildExportedContent(ctx core.Context } dataRowMap := make(map[datatable.TransactionDataTableColumn]string, 15) + transactionUnixTime := utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime) transactionTimeZone := time.FixedZone("Transaction Timezone", int(transaction.TimezoneUtcOffset)*60) - dataRowMap[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME] = utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime), transactionTimeZone) - dataRowMap[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE] = utils.FormatTimezoneOffset(transactionTimeZone) + dataRowMap[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME] = utils.FormatUnixTimeToLongDateTime(transactionUnixTime, transactionTimeZone) + dataRowMap[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE] = utils.FormatTimezoneOffset(transactionUnixTime, transactionTimeZone) dataRowMap[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = dataTableBuilder.ReplaceDelimiters(c.getDisplayTransactionTypeName(transaction.Type)) dataRowMap[datatable.TRANSACTION_DATA_TABLE_CATEGORY] = c.getExportedTransactionCategoryName(dataTableBuilder, transaction.CategoryId, categoryMap) dataRowMap[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY] = c.getExportedTransactionSubCategoryName(dataTableBuilder, transaction.CategoryId, categoryMap) diff --git a/pkg/converters/converter/data_table_transaction_data_importer.go b/pkg/converters/converter/data_table_transaction_data_importer.go index 5ef84ab9..fde1ffa4 100644 --- a/pkg/converters/converter/data_table_transaction_data_importer.go +++ b/pkg/converters/converter/data_table_transaction_data_importer.go @@ -3,6 +3,7 @@ package converter import ( "sort" "strings" + "time" "github.com/mayswind/ezbookkeeping/pkg/converters/datatable" "github.com/mayswind/ezbookkeeping/pkg/core" @@ -29,7 +30,7 @@ type DataTableTransactionDataImporter struct { } // ParseImportedData returns the imported transaction data -func (c *DataTableTransactionDataImporter) ParseImportedData(ctx core.Context, user *models.User, dataTable datatable.TransactionDataTable, defaultTimezoneOffset int16, additionalOptions TransactionDataImporterOptions, accountMap map[string]*models.Account, expenseCategoryMap map[string]map[string]*models.TransactionCategory, incomeCategoryMap map[string]map[string]*models.TransactionCategory, transferCategoryMap map[string]map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionTag, error) { +func (c *DataTableTransactionDataImporter) ParseImportedData(ctx core.Context, user *models.User, dataTable datatable.TransactionDataTable, defaultTimezone *time.Location, additionalOptions TransactionDataImporterOptions, accountMap map[string]*models.Account, expenseCategoryMap map[string]map[string]*models.TransactionCategory, incomeCategoryMap map[string]map[string]*models.TransactionCategory, transferCategoryMap map[string]map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionTag, error) { if dataTable.TransactionRowCount() < 1 { log.Errorf(ctx, "[data_table_transaction_data_importer.ParseImportedData] cannot parse import data for user \"uid:%d\", because data table row count is less 1", user.Uid) return nil, nil, nil, nil, nil, nil, errs.ErrNotFoundTransactionDataInFile @@ -94,7 +95,7 @@ func (c *DataTableTransactionDataImporter) ParseImportedData(ctx core.Context, u continue } - timezoneOffset := defaultTimezoneOffset + timezone := defaultTimezone if dataTable.HasColumn(datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE) && dataRow.GetData(datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE) != datatable.TRANSACTION_DATA_TABLE_TIMEZONE_NOT_AVAILABLE { @@ -105,10 +106,10 @@ func (c *DataTableTransactionDataImporter) ParseImportedData(ctx core.Context, u return nil, nil, nil, nil, nil, nil, errs.ErrTransactionTimeZoneInvalid } - timezoneOffset = utils.GetTimezoneOffsetMinutes(transactionTimezone) + timezone = transactionTimezone } - transactionTime, err := utils.ParseFromLongDateTime(dataRow.GetData(datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME), timezoneOffset) + transactionTime, err := utils.ParseFromLongDateTimeInTimeZone(dataRow.GetData(datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME), timezone) if err != nil { log.Errorf(ctx, "[data_table_transaction_data_importer.ParseImportedData] cannot parse time \"%s\" in data row \"index:%d\" for user \"uid:%d\", because %s", dataRow.GetData(datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME), dataRowIndex, user.Uid, err.Error()) @@ -373,7 +374,7 @@ func (c *DataTableTransactionDataImporter) ParseImportedData(ctx core.Context, u Type: transactionDbType, CategoryId: categoryId, TransactionTime: utils.GetMinTransactionTimeFromUnixTime(transactionTime.Unix()), - TimezoneUtcOffset: timezoneOffset, + TimezoneUtcOffset: utils.GetTimezoneOffsetMinutes(transactionTime.Unix(), timezone), AccountId: account.AccountId, Amount: amount, HideAmount: false, diff --git a/pkg/converters/converter/transaction_data_converter.go b/pkg/converters/converter/transaction_data_converter.go index 3bf8c24d..23f96553 100644 --- a/pkg/converters/converter/transaction_data_converter.go +++ b/pkg/converters/converter/transaction_data_converter.go @@ -1,6 +1,8 @@ package converter import ( + "time" + "github.com/mayswind/ezbookkeeping/pkg/core" "github.com/mayswind/ezbookkeeping/pkg/models" ) @@ -14,7 +16,7 @@ type TransactionDataExporter interface { // TransactionDataImporter defines the structure of transaction data importer type TransactionDataImporter interface { // ParseImportedData returns the imported data - ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezoneOffset int16, additionalOptions TransactionDataImporterOptions, accountMap map[string]*models.Account, expenseCategoryMap map[string]map[string]*models.TransactionCategory, incomeCategoryMap map[string]map[string]*models.TransactionCategory, transferCategoryMap map[string]map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionTag, error) + ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezone *time.Location, additionalOptions TransactionDataImporterOptions, accountMap map[string]*models.Account, expenseCategoryMap map[string]map[string]*models.TransactionCategory, incomeCategoryMap map[string]map[string]*models.TransactionCategory, transferCategoryMap map[string]map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionTag, error) } // TransactionDataConverter defines the structure of transaction data converter diff --git a/pkg/converters/default/default_transaction_data_json_file_importer.go b/pkg/converters/default/default_transaction_data_json_file_importer.go index 9ffd0f1d..2a53eedb 100644 --- a/pkg/converters/default/default_transaction_data_json_file_importer.go +++ b/pkg/converters/default/default_transaction_data_json_file_importer.go @@ -35,7 +35,7 @@ var ( ) // ParseImportedData returns the imported data by parsing the transaction json data -func (c *defaultTransactionDataJsonImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezoneOffset int16, additionalOptions converter.TransactionDataImporterOptions, accountMap map[string]*models.Account, expenseCategoryMap map[string]map[string]*models.TransactionCategory, incomeCategoryMap map[string]map[string]*models.TransactionCategory, transferCategoryMap map[string]map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionTag, error) { +func (c *defaultTransactionDataJsonImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezone *time.Location, additionalOptions converter.TransactionDataImporterOptions, accountMap map[string]*models.Account, expenseCategoryMap map[string]map[string]*models.TransactionCategory, incomeCategoryMap map[string]map[string]*models.TransactionCategory, transferCategoryMap map[string]map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionTag, error) { var importRequest models.ImportTransactionRequest if err := json.Unmarshal(data, &importRequest); err != nil { @@ -55,7 +55,7 @@ func (c *defaultTransactionDataJsonImporter) ParseImportedData(ctx core.Context, ezbookkeepingTagSeparator, ) - return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezoneOffset, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap) + return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezone, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap) } func (c *defaultTransactionDataJsonImporter) createNewDefaultTransactionDataTable(importRequest models.ImportTransactionRequest) (datatable.TransactionDataTable, error) { @@ -75,10 +75,11 @@ func (c *defaultTransactionDataJsonImporter) createNewDefaultTransactionDataTabl } timezone := time.FixedZone("Transaction Timezone", utcOffset*60) + timezoneOffset := utils.FormatTimezoneOffset(time.Now().Unix(), timezone) row := make(map[datatable.TransactionDataTableColumn]string, len(allJsonDataSupportedColumns)) row[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME] = transaction.Time - row[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE] = utils.FormatTimezoneOffset(timezone) + row[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE] = timezoneOffset row[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = transaction.Type row[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY] = transaction.CategoryName row[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = transaction.SourceAccountName diff --git a/pkg/converters/default/default_transaction_data_plain_text_converter.go b/pkg/converters/default/default_transaction_data_plain_text_converter.go index 825d2c8d..c7cca507 100644 --- a/pkg/converters/default/default_transaction_data_plain_text_converter.go +++ b/pkg/converters/default/default_transaction_data_plain_text_converter.go @@ -1,6 +1,8 @@ package _default import ( + "time" + "github.com/mayswind/ezbookkeeping/pkg/converters/converter" "github.com/mayswind/ezbookkeeping/pkg/converters/datatable" "github.com/mayswind/ezbookkeeping/pkg/core" @@ -84,7 +86,7 @@ func (c *defaultTransactionDataPlainTextConverter) ToExportedContent(ctx core.Co } // ParseImportedData returns the imported data by parsing the transaction plain text data -func (c *defaultTransactionDataPlainTextConverter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezoneOffset int16, additionalOptions converter.TransactionDataImporterOptions, accountMap map[string]*models.Account, expenseCategoryMap map[string]map[string]*models.TransactionCategory, incomeCategoryMap map[string]map[string]*models.TransactionCategory, transferCategoryMap map[string]map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionTag, error) { +func (c *defaultTransactionDataPlainTextConverter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezone *time.Location, additionalOptions converter.TransactionDataImporterOptions, accountMap map[string]*models.Account, expenseCategoryMap map[string]map[string]*models.TransactionCategory, incomeCategoryMap map[string]map[string]*models.TransactionCategory, transferCategoryMap map[string]map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionTag, error) { dataTable, err := createNewDefaultPlainTextDataTable( string(data), c.columnSeparator, @@ -104,5 +106,5 @@ func (c *defaultTransactionDataPlainTextConverter) ParseImportedData(ctx core.Co ezbookkeepingTagSeparator, ) - return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezoneOffset, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap) + return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezone, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap) } diff --git a/pkg/converters/default/default_transaction_data_plain_text_converter_test.go b/pkg/converters/default/default_transaction_data_plain_text_converter_test.go index 31428c28..ee5a2e87 100644 --- a/pkg/converters/default/default_transaction_data_plain_text_converter_test.go +++ b/pkg/converters/default/default_transaction_data_plain_text_converter_test.go @@ -2,6 +2,7 @@ package _default import ( "testing" + "time" "github.com/stretchr/testify/assert" @@ -139,7 +140,7 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_MinimumValidDat "2024-09-01 00:00:00,Balance Modification,,Test Account,123.45,,\n"+ "2024-09-01 01:23:45,Income,Test Category,Test Account,0.12,,\n"+ "2024-09-01 12:34:56,Expense,Test Category2,Test Account,1.00,,\n"+ - "2024-09-01 23:59:59,Transfer,Test Category3,Test Account,0.05,Test Account2,0.05"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 23:59:59,Transfer,Test Category3,Test Account,0.05,Test Account2,0.05"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) @@ -207,11 +208,11 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseInvalidTim } _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+ - "2024-09-01T12:34:56,Expense,Test Category,Test Account,123.45,,"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01T12:34:56,Expense,Test Category,Test Account,123.45,,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message) _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+ - "09/01/2024 12:34:56,Expense,Test Category,Test Account,123.45,,"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "09/01/2024 12:34:56,Expense,Test Category,Test Account,123.45,,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message) } @@ -225,7 +226,7 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseInvalidTyp } _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+ - "2024-09-01 12:34:56,Type,Test Category,Test Account,123.45,,"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 12:34:56,Type,Test Category,Test Account,123.45,,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrTransactionTypeInvalid.Message) } @@ -239,19 +240,19 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseValidTimez } allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("Time,Timezone,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+ - "2024-09-01 12:34:56,-10:00,Expense,Test Category,Test Account,123.45,,"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 12:34:56,-10:00,Expense,Test Category,Test Account,123.45,,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, int64(1725230096), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime)) allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("Time,Timezone,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+ - "2024-09-01 12:34:56,+00:00,Expense,Test Category,Test Account,123.45,,"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 12:34:56,+00:00,Expense,Test Category,Test Account,123.45,,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, int64(1725194096), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime)) allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("Time,Timezone,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+ - "2024-09-01 12:34:56,+12:45,Expense,Test Category,Test Account,123.45,,"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 12:34:56,+12:45,Expense,Test Category,Test Account,123.45,,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, int64(1725148196), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime)) @@ -267,7 +268,7 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseInvalidTim } _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("Time,Timezone,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+ - "2024-09-01 12:34:56,Asia/Shanghai,Expense,Test Category,Test Account,123.45,,"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 12:34:56,Asia/Shanghai,Expense,Test Category,Test Account,123.45,,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrTransactionTimeZoneInvalid.Message) } @@ -282,7 +283,7 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseValidAccou allNewTransactions, allNewAccounts, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount\n"+ "2024-09-01 01:23:45,Balance Modification,,Test Account,USD,123.45,,,\n"+ - "2024-09-01 12:34:56,Transfer,Test Category2,Test Account,USD,1.23,Test Account2,EUR,1.10"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 12:34:56,Transfer,Test Category2,Test Account,USD,1.23,Test Account2,EUR,1.10"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) @@ -309,12 +310,12 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseInvalidAcc _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount\n"+ "2024-09-01 01:23:45,Balance Modification,,Test Account,USD,123.45,,,\n"+ - "2024-09-01 12:34:56,Transfer,Test Category3,Test Account,CNY,1.23,Test Account2,EUR,1.10"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 12:34:56,Transfer,Test Category3,Test Account,CNY,1.23,Test Account2,EUR,1.10"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message) _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount\n"+ "2024-09-01 01:23:45,Balance Modification,,Test Account,USD,123.45,,,\n"+ - "2024-09-01 12:34:56,Transfer,Test Category3,Test Account2,CNY,1.23,Test Account,EUR,1.10"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 12:34:56,Transfer,Test Category3,Test Account2,CNY,1.23,Test Account,EUR,1.10"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message) } @@ -328,11 +329,11 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseNotSupport } _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount\n"+ - "2024-09-01 01:23:45,Balance Modification,,Test Account,XXX,123.45,,,"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 01:23:45,Balance Modification,,Test Account,XXX,123.45,,,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message) _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount\n"+ - "2024-09-01 01:23:45,Transfer,Test Category,Test Account,USD,123.45,Test Account2,XXX,123.45"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 01:23:45,Transfer,Test Category,Test Account,USD,123.45,Test Account2,XXX,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message) } @@ -346,11 +347,11 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseInvalidAmo } _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+ - "2024-09-01 12:34:56,Expense,Test Category,Test Account,123 45,,"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 12:34:56,Expense,Test Category,Test Account,123 45,,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrAmountInvalid.Message) _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+ - "2024-09-01 12:34:56,Transfer,Test Category,Test Account,123.45,Test Account2,123 45"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 12:34:56,Transfer,Test Category,Test Account,123.45,Test Account2,123 45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrAmountInvalid.Message) } @@ -364,14 +365,14 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseNoAmount2( } allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2\n"+ - "2024-09-01 12:34:56,Expense,Test Category,Test Account,123.45,"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 12:34:56,Expense,Test Category,Test Account,123.45,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, int64(12345), allNewTransactions[0].Amount) assert.Equal(t, int64(0), allNewTransactions[0].RelatedAccountAmount) allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2\n"+ - "2024-09-01 12:34:56,Transfer,Test Category,Test Account,123.45,Test Account2"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 12:34:56,Transfer,Test Category,Test Account,123.45,Test Account2"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, int64(12345), allNewTransactions[0].Amount) @@ -388,7 +389,7 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseValidGeogr } allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount,Geographic Location\n"+ - "2024-09-01 12:34:56,Expense,Test Category,Test Account,123.45,,,123.45 45.56"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 12:34:56,Expense,Test Category,Test Account,123.45,,,123.45 45.56"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) @@ -406,18 +407,18 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseInvalidGeo } allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount,Geographic Location\n"+ - "2024-09-01 12:34:56,Expense,Test Category,Test Account,123.45,,,1"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 12:34:56,Expense,Test Category,Test Account,123.45,,,1"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, float64(0), allNewTransactions[0].GeoLongitude) assert.Equal(t, float64(0), allNewTransactions[0].GeoLatitude) _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount,Geographic Location\n"+ - "2024-09-01 12:34:56,Expense,Test Category,Test Account,123.45,,,a b"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 12:34:56,Expense,Test Category,Test Account,123.45,,,a b"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrGeographicLocationInvalid.Message) _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount,Geographic Location\n"+ - "2024-09-01 12:34:56,Expense,Test Category,Test Account,123.45,,,1 "), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 12:34:56,Expense,Test Category,Test Account,123.45,,,1 "), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrGeographicLocationInvalid.Message) } @@ -431,7 +432,7 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseTag(t *tes } _, _, _, _, _, allNewTags, err := importer.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount,Tags\n"+ - "2024-09-01 00:00:00,Balance Modification,,Test Account,123.45,,,foo;;bar.;#test;hello\tworld;;"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 00:00:00,Balance Modification,,Test Account,123.45,,,foo;;bar.;#test;hello\tworld;;"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) @@ -460,7 +461,7 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseDescriptio } allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount,Description\n"+ - "2024-09-01 12:34:56,Expense,Test Category,Test Account,123.45,,,foo bar\t#test"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 12:34:56,Expense,Test Category,Test Account,123.45,,,foo bar\t#test"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) @@ -476,7 +477,7 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_MissingFileHead DefaultCurrency: "CNY", } - _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(""), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrNotFoundTransactionDataInFile.Message) } @@ -491,31 +492,31 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_MissingRequired // Missing Time Column _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("Timezone,Type,Category,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount,Geographic Location,Tags,Description\n"+ - "+08:00,Balance Modification,,Test Sub Category,Test Account,CNY,123.45,,,,,,"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "+08:00,Balance Modification,,Test Sub Category,Test Account,CNY,123.45,,,,,,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message) // Missing Type Column _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("Time,Category,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount,Geographic Location,Tags,Description\n"+ - "2024-09-01 00:00:00,+08:00,Test Category,Test Sub Category,Test Account,CNY,123.45,,,,,,"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 00:00:00,+08:00,Test Category,Test Sub Category,Test Account,CNY,123.45,,,,,,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message) // Missing Sub Category Column _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("Time,Type,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount,Geographic Location,Tags,Description\n"+ - "2024-09-01 00:00:00,+08:00,Balance Modification,Test Account,CNY,123.45,,,,,,"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 00:00:00,+08:00,Balance Modification,Test Account,CNY,123.45,,,,,,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message) // Missing Account Name Column _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("Time,Timezone,Type,Category,Sub Category,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount,Geographic Location,Tags,Description\n"+ - "2024-09-01 00:00:00,+08:00,Balance Modification,,Test Sub Category,CNY,123.45,,,,,,"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 00:00:00,+08:00,Balance Modification,,Test Sub Category,CNY,123.45,,,,,,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message) // Missing Amount Column _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("Time,Timezone,Type,Category,Sub Category,Account,Account Currency,Account2,Account2 Currency,Account2 Amount,Geographic Location,Tags,Description\n"+ - "2024-09-01 00:00:00,+08:00,Balance Modification,,Test Sub Category,Test Account,CNY,,,,,,"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 00:00:00,+08:00,Balance Modification,,Test Sub Category,Test Account,CNY,,,,,,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message) // Missing Account2 Name Column _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("Time,Timezone,Type,Category,Sub Category,Account,Account Currency,Amount,Account2 Currency,Account2 Amount,Geographic Location,Tags,Description\n"+ - "2024-09-01 00:00:00,+08:00,Balance Modification,,Test Sub Category,Test Account,CNY,123.45,,,,,"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 00:00:00,+08:00,Balance Modification,,Test Sub Category,Test Account,CNY,123.45,,,,,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message) } diff --git a/pkg/converters/dsv/custom_transaction_data_dsv_file_importer.go b/pkg/converters/dsv/custom_transaction_data_dsv_file_importer.go index a3c92b66..330232b1 100644 --- a/pkg/converters/dsv/custom_transaction_data_dsv_file_importer.go +++ b/pkg/converters/dsv/custom_transaction_data_dsv_file_importer.go @@ -5,6 +5,7 @@ import ( "encoding/csv" "io" "strings" + "time" "golang.org/x/text/encoding" "golang.org/x/text/encoding/charmap" @@ -148,7 +149,7 @@ func (c *customTransactionDataDsvFileImporter) ParseDsvFileLines(ctx core.Contex } // ParseImportedData returns the imported data by parsing the custom transaction dsv data -func (c *customTransactionDataDsvFileImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezoneOffset int16, additionalOptions converter.TransactionDataImporterOptions, accountMap map[string]*models.Account, expenseCategoryMap map[string]map[string]*models.TransactionCategory, incomeCategoryMap map[string]map[string]*models.TransactionCategory, transferCategoryMap map[string]map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionTag, error) { +func (c *customTransactionDataDsvFileImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezone *time.Location, additionalOptions converter.TransactionDataImporterOptions, accountMap map[string]*models.Account, expenseCategoryMap map[string]map[string]*models.TransactionCategory, incomeCategoryMap map[string]map[string]*models.TransactionCategory, transferCategoryMap map[string]map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionTag, error) { allLines, err := c.ParseDsvFileLines(ctx, data) if err != nil { @@ -159,7 +160,7 @@ func (c *customTransactionDataDsvFileImporter) ParseImportedData(ctx core.Contex transactionDataTable := CreateNewCustomPlainTextDataTable(dataTable, c.columnIndexMapping, c.transactionTypeNameMapping, c.timeFormat, c.timezoneFormat, c.amountDecimalSeparator, c.amountDigitGroupingSymbol) dataTableImporter := converter.CreateNewImporterWithTypeNameMapping(customTransactionTypeNameMapping, c.geoLocationSeparator, c.geoLocationOrder, c.transactionTagSeparator) - return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezoneOffset, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap) + return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezone, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap) } // IsDelimiterSeparatedValuesFileType returns whether the file type is the delimiter-separated values file type diff --git a/pkg/converters/dsv/custom_transaction_data_dsv_file_importer_test.go b/pkg/converters/dsv/custom_transaction_data_dsv_file_importer_test.go index c719bcb4..9092edea 100644 --- a/pkg/converters/dsv/custom_transaction_data_dsv_file_importer_test.go +++ b/pkg/converters/dsv/custom_transaction_data_dsv_file_importer_test.go @@ -2,6 +2,7 @@ package dsv import ( "testing" + "time" "github.com/stretchr/testify/assert" @@ -92,7 +93,7 @@ func TestCustomTransactionDataDsvFileImporter_MinimumValidData(t *testing.T) { "2024-09-01 00:00:00,B,123.45\n"+ "2024-09-01 01:23:45,I,0.12\n"+ "2024-09-01 12:34:56,E,1.00\n"+ - "2024-09-01 23:59:59,T,0.05"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 23:59:59,T,0.05"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) @@ -184,7 +185,7 @@ func TestCustomTransactionDataDsvFileImporter_WithAllSupportedColumns(t *testing "\"2024-09-01 00:00:00\",\"+08:00\",\"Balance Modification\",\"\",\"\",\"Test Account\",\"CNY\",\"123.45\",\"\",\"\",\"\",\"\",\"\",\"\"\n"+ "\"2024-09-01 01:23:45\",\"+08:00\",\"Income\",\"Test Category\",\"Test Sub Category\",\"Test Account\",\"CNY\",\"0.12\",\"\",\"\",\"\",\"123.450000 45.670000\",\"Test Tag;Test Tag2\",\"Hello World\"\n"+ "\"2024-09-01 12:34:56\",\"+00:00\",\"Expense\",\"Test Category2\",\"Test Sub Category2\",\"Test Account\",\"CNY\",\"1.00\",\"\",\"\",\"\",\"\",\"Test Tag\",\"Foo#Bar\"\n"+ - "\"2024-09-01 23:59:59\",\"-05:00\",\"Transfer\",\"Test Category3\",\"Test Sub Category3\",\"Test Account\",\"CNY\",\"0.05\",\"Test Account2\",\"USD\",\"0.35\",\"\",\"Test Tag2\",\"foo\tbar\""), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "\"2024-09-01 23:59:59\",\"-05:00\",\"Transfer\",\"Test Category3\",\"Test Sub Category3\",\"Test Account\",\"CNY\",\"0.05\",\"Test Account2\",\"USD\",\"0.35\",\"\",\"Test Tag2\",\"foo\tbar\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) @@ -273,11 +274,11 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidTime(t *testing.T) { } _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte( - "2024-09-01T12:34:56,E,123.45"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01T12:34:56,E,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message) _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte( - "09/01/2024 12:34:56,E,123.45"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "09/01/2024 12:34:56,E,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message) } @@ -304,7 +305,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseTransactionWithoutType(t *tes } _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte( - "2024-09-01 12:34:56,A,123.45"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 12:34:56,A,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrNotFoundTransactionDataInFile.Message) } @@ -328,7 +329,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidType(t *testing.T) { } _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte( - "2024-09-01 12:34:56,B,123.45"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 12:34:56,B,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrTransactionTypeInvalid.Message) } @@ -352,19 +353,19 @@ func TestCustomTransactionDataDsvFileImporter_ParseTimeWithTimezone(t *testing.T } allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte( - "2024-09-01 12:34:56-10:00,E,123.45"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 12:34:56-10:00,E,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, int64(1725230096), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime)) allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte( - "2024-09-01 12:34:56+00:00,E,123.45"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 12:34:56+00:00,E,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, int64(1725194096), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime)) allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte( - "2024-09-01 12:34:56+12:45,E,123.45"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 12:34:56+12:45,E,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, int64(1725148196), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime)) @@ -390,19 +391,19 @@ func TestCustomTransactionDataDsvFileImporter_ParseTimeWithTimezone2(t *testing. } allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte( - "2024-09-01 12:34:56-1000,E,123.45"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 12:34:56-1000,E,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, int64(1725230096), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime)) allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte( - "2024-09-01 12:34:56+0000,E,123.45"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 12:34:56+0000,E,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, int64(1725194096), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime)) allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte( - "2024-09-01 12:34:56+1245,E,123.45"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 12:34:56+1245,E,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, int64(1725148196), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime)) @@ -429,19 +430,19 @@ func TestCustomTransactionDataDsvFileImporter_ParseValidTimezone(t *testing.T) { } allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte( - "2024-09-01 12:34:56,-10:00,E,123.45"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 12:34:56,-10:00,E,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, int64(1725230096), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime)) allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte( - "2024-09-01 12:34:56,+00:00,E,123.45"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 12:34:56,+00:00,E,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, int64(1725194096), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime)) allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte( - "2024-09-01 12:34:56,+12:45,E,123.45"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 12:34:56,+12:45,E,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, int64(1725148196), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime)) @@ -468,19 +469,19 @@ func TestCustomTransactionDataDsvFileImporter_ParseValidTimezone2(t *testing.T) } allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte( - "2024-09-01 12:34:56,-1000,E,123.45"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 12:34:56,-1000,E,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, int64(1725230096), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime)) allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte( - "2024-09-01 12:34:56,+0000,E,123.45"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 12:34:56,+0000,E,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, int64(1725194096), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime)) allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte( - "2024-09-01 12:34:56,+1245,E,123.45"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 12:34:56,+1245,E,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, int64(1725148196), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime)) @@ -507,7 +508,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidTimezoneFormat(t *test } _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte( - "2024-09-01 12:34:56,CST,E,123.45"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 12:34:56,CST,E,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrImportFileTransactionTimezoneFormatInvalid.Message) } @@ -532,11 +533,11 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidTimezone(t *testing.T) } _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte( - "2024-09-01 12:34:56,Asia/Shanghai,E,123.45"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 12:34:56,Asia/Shanghai,E,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrTransactionTimeZoneInvalid.Message) _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte( - "2024-09-01 12:34:56,-0700,E,123.45"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 12:34:56,-0700,E,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrTransactionTimeZoneInvalid.Message) } @@ -561,11 +562,11 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidTimezone2(t *testing.T } _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte( - "2024-09-01 12:34:56,Asia/Shanghai,E,123.45"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 12:34:56,Asia/Shanghai,E,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrTransactionTimeZoneInvalid.Message) _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte( - "2024-09-01 12:34:56,0700,E,123.45"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 12:34:56,0700,E,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrTransactionTimeZoneInvalid.Message) } @@ -589,7 +590,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseAmountWithCustomFormat(t *tes } allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte( - "2024-09-01 12:34:56\tE\t1.234,56"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 12:34:56\tE\t1.234,56"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, int64(123456), allNewTransactions[0].Amount) @@ -615,7 +616,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidAmountWithCustomFormat } _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte( - "2024-09-01 12:34:56\tE\t1.234,56"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 12:34:56\tE\t1.234,56"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrAmountInvalid.Message) } @@ -639,7 +640,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidAmountWithCustomFormat } _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte( - "2024-09-01 12:34:56\tE\t1.234,56"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 12:34:56\tE\t1.234,56"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrAmountInvalid.Message) } @@ -670,7 +671,7 @@ func TestCustomTransactionDataDsvFileImporter_ParsePrimaryCategory(t *testing.T) "2024-09-01 00:00:00,B,,123.45\n"+ "2024-09-01 01:23:45,I,Test Category,0.12\n"+ "2024-09-01 12:34:56,E,Test Category2,1.00\n"+ - "2024-09-01 23:59:59,T,Test Category3,0.05"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 23:59:59,T,Test Category3,0.05"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) @@ -737,7 +738,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseValidAccountCurrency(t *testi allNewTransactions, allNewAccounts, _, _, _, _, err := importer.ParseImportedData(context, user, []byte( "2024-09-01 01:23:45,B,Test Account,USD,123.45,,,\n"+ - "2024-09-01 12:34:56,T,Test Account,USD,1.23,Test Account2,EUR,1.10"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 12:34:56,T,Test Account,USD,1.23,Test Account2,EUR,1.10"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) @@ -780,12 +781,12 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidAccountCurrency(t *tes _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte( "2024-09-01 01:23:45,B,Test Account,USD,123.45,,,\n"+ - "2024-09-01 12:34:56,T,Test Account,CNY,1.23,Test Account2,EUR,1.10"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 12:34:56,T,Test Account,CNY,1.23,Test Account2,EUR,1.10"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message) _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte( "2024-09-01 01:23:45,B,Test Account,USD,123.45,,,\n"+ - "2024-09-01 12:34:56,T,Test Account2,CNY,1.23,Test Account,EUR,1.10"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 12:34:56,T,Test Account2,CNY,1.23,Test Account,EUR,1.10"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message) } @@ -815,11 +816,11 @@ func TestCustomTransactionDataDsvFileImporter_ParseNotSupportedCurrency(t *testi } _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte( - "2024-09-01 01:23:45,B,Test Account,XXX,123.45,,,"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 01:23:45,B,Test Account,XXX,123.45,,,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message) _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte( - "2024-09-01 01:23:45,T,Test Account,USD,123.45,Test Account2,XXX,123.45"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 01:23:45,T,Test Account,USD,123.45,Test Account2,XXX,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message) } @@ -850,7 +851,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseValidAmount(t *testing.T) { "2024-09-01 00:00:00,B,123.45000000,\n"+ "2024-09-01 01:23:45,I,0.12000000,\n"+ "2024-09-01 12:34:56,E,1.00000000,\n"+ - "2024-09-01 23:59:59,T,0.05000000,0.35000000"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 23:59:59,T,0.05000000,0.35000000"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) @@ -895,7 +896,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseAmountWithSpaceDigitGroupingS // normal space allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte( - "2024-09-01 00:00:00,E,1 234,\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 00:00:00,E,1 234,\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) @@ -904,7 +905,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseAmountWithSpaceDigitGroupingS // no-break space (NBSP) allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte( - "2024-09-01 00:00:00,E,1 234,\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 00:00:00,E,1 234,\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) @@ -913,7 +914,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseAmountWithSpaceDigitGroupingS // narrow no-break space (NNBSP) allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte( - "2024-09-01 00:00:00,E,1 234,\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 00:00:00,E,1 234,\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) @@ -922,7 +923,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseAmountWithSpaceDigitGroupingS // figure space allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte( - "2024-09-01 00:00:00,E,1 234,\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 00:00:00,E,1 234,\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) @@ -954,11 +955,11 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidAmount(t *testing.T) { } _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte( - "2024-09-01 12:34:56,E,Test Account,123 45,,"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 12:34:56,E,Test Account,123 45,,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrAmountInvalid.Message) _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte( - "2024-09-01 12:34:56,T,Test Account,123.45,Test Account2,123 45"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 12:34:56,T,Test Account,123.45,Test Account2,123 45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrAmountInvalid.Message) } @@ -985,14 +986,14 @@ func TestCustomTransactionDataDsvFileImporter_ParseNoAmount2(t *testing.T) { } allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte( - "2024-09-01 12:34:56,E,Test Account,123.45,"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 12:34:56,E,Test Account,123.45,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, int64(12345), allNewTransactions[0].Amount) assert.Equal(t, int64(0), allNewTransactions[0].RelatedAccountAmount) allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte( - "2024-09-01 12:34:56,T,Test Account,123.45,Test Account2"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 12:34:56,T,Test Account,123.45,Test Account2"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, int64(12345), allNewTransactions[0].Amount) @@ -1020,7 +1021,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseValidGeographicLocation(t *te } allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte( - "2024-09-01 12:34:56,E,123.45,123.45;45.56"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 12:34:56,E,123.45,123.45;45.56"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) @@ -1049,14 +1050,14 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidGeographicLocation(t * } allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte( - "2024-09-01 12:34:56,E,123.45,,,1"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 12:34:56,E,123.45,,,1"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, float64(0), allNewTransactions[0].GeoLongitude) assert.Equal(t, float64(0), allNewTransactions[0].GeoLatitude) _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte( - "2024-09-01 12:34:56,E,123.45,a b"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 12:34:56,E,123.45,a b"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrGeographicLocationInvalid.Message) } @@ -1081,7 +1082,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseTag(t *testing.T) { } _, _, _, _, _, allNewTags, err := importer.ParseImportedData(context, user, []byte( - "2024-09-01 00:00:00,E,123.45,foo;;bar.;#test;hello\tworld;;"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 00:00:00,E,123.45,foo;;bar.;#test;hello\tworld;;"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) @@ -1121,7 +1122,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseTagWithoutSeparator(t *testin } _, _, _, _, _, allNewTags, err := importer.ParseImportedData(context, user, []byte( - "2024-09-01 00:00:00,E,123.45,foo;;bar.;#test;hello\tworld;;"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 00:00:00,E,123.45,foo;;bar.;#test;hello\tworld;;"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) @@ -1152,7 +1153,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseDescription(t *testing.T) { } allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte( - "2024-09-01 12:34:56,T,123.45,foo bar\t#test"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "2024-09-01 12:34:56,T,123.45,foo bar\t#test"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) diff --git a/pkg/converters/dsv/custom_transaction_plain_text_data_table.go b/pkg/converters/dsv/custom_transaction_plain_text_data_table.go index f8ac3cfb..4d4fdc41 100644 --- a/pkg/converters/dsv/custom_transaction_plain_text_data_table.go +++ b/pkg/converters/dsv/custom_transaction_plain_text_data_table.go @@ -147,7 +147,7 @@ func (t *customPlainTextDataRowIterator) parseTransaction(ctx core.Context, user rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME] = utils.FormatUnixTimeToLongDateTime(dateTime.Unix(), dateTime.Location()) if t.transactionDataTable.timeFormatIncludeTimezone { - rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE] = utils.FormatTimezoneOffset(dateTime.Location()) + rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE] = utils.FormatTimezoneOffset(dateTime.Unix(), dateTime.Location()) } } diff --git a/pkg/converters/feidee/feidee_mymoney_app_transaction_data_csv_file_importer.go b/pkg/converters/feidee/feidee_mymoney_app_transaction_data_csv_file_importer.go index 2bd0f4da..ffc75f58 100644 --- a/pkg/converters/feidee/feidee_mymoney_app_transaction_data_csv_file_importer.go +++ b/pkg/converters/feidee/feidee_mymoney_app_transaction_data_csv_file_importer.go @@ -3,6 +3,7 @@ package feidee import ( "bytes" "strings" + "time" "golang.org/x/text/encoding/unicode" "golang.org/x/text/transform" @@ -61,7 +62,7 @@ var ( ) // ParseImportedData returns the imported data by parsing the feidee mymoney app transaction csv data -func (c *feideeMymoneyAppTransactionDataCsvFileImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezoneOffset int16, additionalOptions converter.TransactionDataImporterOptions, accountMap map[string]*models.Account, expenseCategoryMap map[string]map[string]*models.TransactionCategory, incomeCategoryMap map[string]map[string]*models.TransactionCategory, transferCategoryMap map[string]map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionTag, error) { +func (c *feideeMymoneyAppTransactionDataCsvFileImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezone *time.Location, additionalOptions converter.TransactionDataImporterOptions, accountMap map[string]*models.Account, expenseCategoryMap map[string]map[string]*models.TransactionCategory, incomeCategoryMap map[string]map[string]*models.TransactionCategory, transferCategoryMap map[string]map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionTag, error) { fallback := unicode.UTF8.NewDecoder() reader := transform.NewReader(bytes.NewReader(data), unicode.BOMOverride(fallback)) @@ -97,7 +98,7 @@ func (c *feideeMymoneyAppTransactionDataCsvFileImporter) ParseImportedData(ctx c dataTableImporter := converter.CreateNewSimpleImporterWithTypeNameMapping(feideeMymoneyTransactionTypeNameMapping) - return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezoneOffset, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap) + return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezone, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap) } func (c *feideeMymoneyAppTransactionDataCsvFileImporter) createNewFeideeMymoneyAppTransactionDataTable(ctx core.Context, commonDataTable datatable.CommonDataTable) (datatable.TransactionDataTable, error) { diff --git a/pkg/converters/feidee/feidee_mymoney_app_transaction_data_csv_file_importer_test.go b/pkg/converters/feidee/feidee_mymoney_app_transaction_data_csv_file_importer_test.go index 75279e42..3fa14004 100644 --- a/pkg/converters/feidee/feidee_mymoney_app_transaction_data_csv_file_importer_test.go +++ b/pkg/converters/feidee/feidee_mymoney_app_transaction_data_csv_file_importer_test.go @@ -31,7 +31,7 @@ func TestFeideeMymoneyCsvFileImporterParseImportedData_MinimumValidData(t *testi "\"转出\",\"2024-09-01 23:59:59\",\"Test Category3\",\"Test Account\",\"0.05\",\"\",\"00000000-0000-0000-0000-000000000001\"\n"+ "\"转入\",\"2024-09-01 23:59:59\",\"Test Category3\",\"Test Account2\",\"0.05\",\"\",\"00000000-0000-0000-0000-000000000001\"\n"+ "\"转入\",\"2024-09-02 23:59:59\",\"Test Category3\",\"Test Account\",\"0.5\",\"\",\"00000000-0000-0000-0000-000000000002\"\n"+ - "\"转出\",\"2024-09-02 23:59:59\",\"Test Category3\",\"Test Account2\",\"0.5\",\"\",\"00000000-0000-0000-0000-000000000002\""), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "\"转出\",\"2024-09-02 23:59:59\",\"Test Category3\",\"Test Account2\",\"0.5\",\"\",\"00000000-0000-0000-0000-000000000002\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) @@ -122,7 +122,7 @@ func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseOutstandingBalanceMo allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, _, _, err := importer.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+ "\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+ "\"负债变更\",\"2024-09-01 00:00:00\",\"\",\"Test Account\",\"123.45\",\"\",\"\"\n"+ - "\"负债变更\",\"2024-09-01 01:00:00\",\"\",\"Test Account2\",\"-0.12\",\"\",\"\"\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "\"负债变更\",\"2024-09-01 01:00:00\",\"\",\"Test Account2\",\"-0.12\",\"\",\"\"\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) @@ -171,12 +171,12 @@ func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseInvalidTime(t *testi _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+ "\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+ - "\"收入\",\"2024-09-01T12:34:56\",\"Test Category\",\"Test Account\",\"0.12\",\"\",\"\""), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "\"收入\",\"2024-09-01T12:34:56\",\"Test Category\",\"Test Account\",\"0.12\",\"\",\"\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message) _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+ "\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+ - "\"收入\",\"09/01/2024 12:34:56\",\"Test Category\",\"Test Account\",\"0.12\",\"\",\"\""), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "\"收入\",\"09/01/2024 12:34:56\",\"Test Category\",\"Test Account\",\"0.12\",\"\",\"\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message) } @@ -191,7 +191,7 @@ func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseInvalidType(t *testi _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+ "\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+ - "\"Type\",\"2024-09-01 12:34:56\",\"Test Category\",\"Test Account\",\"0.12\",\"\",\"\""), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "\"Type\",\"2024-09-01 12:34:56\",\"Test Category\",\"Test Account\",\"0.12\",\"\",\"\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrTransactionTypeInvalid.Message) } @@ -208,7 +208,7 @@ func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseValidAccountCurrency "\"交易类型\",\"日期\",\"子类别\",\"账户\",\"账户币种\",\"金额\",\"备注\",\"关联Id\"\n"+ "\"余额变更\",\"2024-09-01 01:23:45\",\"\",\"Test Account\",\"USD\",\"123.45\",\"\",\"\"\n"+ "\"转出\",\"2024-09-01 12:34:56\",\"Test Category3\",\"Test Account\",\"USD\",\"1.23\",\"\",\"00000000-0000-0000-0000-000000000001\"\n"+ - "\"转入\",\"2024-09-01 12:34:56\",\"Test Category3\",\"Test Account2\",\"EUR\",\"1.10\",\"\",\"00000000-0000-0000-0000-000000000001\""), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "\"转入\",\"2024-09-01 12:34:56\",\"Test Category3\",\"Test Account2\",\"EUR\",\"1.10\",\"\",\"00000000-0000-0000-0000-000000000001\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) @@ -237,14 +237,14 @@ func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseInvalidAccountCurren "\"交易类型\",\"日期\",\"子类别\",\"账户\",\"账户币种\",\"金额\",\"备注\",\"关联Id\"\n"+ "\"余额变更\",\"2024-09-01 01:23:45\",\"\",\"Test Account\",\"USD\",\"123.45\",\"\",\"\"\n"+ "\"转出\",\"2024-09-01 12:34:56\",\"Test Category3\",\"Test Account\",\"CNY\",\"1.23\",\"\",\"00000000-0000-0000-0000-000000000001\"\n"+ - "\"转入\",\"2024-09-01 12:34:56\",\"Test Category3\",\"Test Account2\",\"EUR\",\"1.10\",\"\",\"00000000-0000-0000-0000-000000000001\""), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "\"转入\",\"2024-09-01 12:34:56\",\"Test Category3\",\"Test Account2\",\"EUR\",\"1.10\",\"\",\"00000000-0000-0000-0000-000000000001\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message) _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+ "\"交易类型\",\"日期\",\"子类别\",\"账户\",\"账户币种\",\"金额\",\"备注\",\"关联Id\"\n"+ "\"余额变更\",\"2024-09-01 01:23:45\",\"\",\"Test Account\",\"USD\",\"123.45\",\"\",\"\"\n"+ "\"转出\",\"2024-09-01 12:34:56\",\"Test Category3\",\"Test Account2\",\"CNY\",\"1.23\",\"\",\"00000000-0000-0000-0000-000000000001\"\n"+ - "\"转入\",\"2024-09-01 12:34:56\",\"Test Category3\",\"Test Account\",\"EUR\",\"1.10\",\"\",\"00000000-0000-0000-0000-000000000001\""), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "\"转入\",\"2024-09-01 12:34:56\",\"Test Category3\",\"Test Account\",\"EUR\",\"1.10\",\"\",\"00000000-0000-0000-0000-000000000001\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message) } @@ -259,19 +259,19 @@ func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseNotSupportedCurrency _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+ "\"交易类型\",\"日期\",\"子类别\",\"账户\",\"账户币种\",\"金额\",\"备注\",\"关联Id\"\n"+ - "\"余额变更\",\"2024-09-01 01:23:45\",\"\",\"Test Account\",\"XXX\",\"123.45\",\"\",\"\""), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "\"余额变更\",\"2024-09-01 01:23:45\",\"\",\"Test Account\",\"XXX\",\"123.45\",\"\",\"\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message) _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+ "\"交易类型\",\"日期\",\"子类别\",\"账户\",\"账户币种\",\"金额\",\"备注\",\"关联Id\"\n"+ "\"转出\",\"2024-09-01 12:34:56\",\"Test Category\",\"Test Account\",\"USD\",\"123.45\",\"\",\"00000000-0000-0000-0000-000000000001\"\n"+ - "\"转入\",\"2024-09-01 12:34:56\",\"Test Category\",\"Test Account2\",\"XXX\",\"123.45\",\"\",\"00000000-0000-0000-0000-000000000001\""), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "\"转入\",\"2024-09-01 12:34:56\",\"Test Category\",\"Test Account2\",\"XXX\",\"123.45\",\"\",\"00000000-0000-0000-0000-000000000001\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message) _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+ "\"交易类型\",\"日期\",\"子类别\",\"账户\",\"账户币种\",\"金额\",\"备注\",\"关联Id\"\n"+ "\"转出\",\"2024-09-01 12:34:56\",\"Test Category\",\"Test Account\",\"XXX\",\"123.45\",\"\",\"00000000-0000-0000-0000-000000000001\"\n"+ - "\"转入\",\"2024-09-01 12:34:56\",\"Test Category\",\"Test Account2\",\"USD\",\"123.45\",\"\",\"00000000-0000-0000-0000-000000000001\""), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "\"转入\",\"2024-09-01 12:34:56\",\"Test Category\",\"Test Account2\",\"USD\",\"123.45\",\"\",\"00000000-0000-0000-0000-000000000001\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message) } @@ -286,24 +286,24 @@ func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseInvalidAmount(t *tes _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+ "\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+ - "\"余额变更\",\"2024-09-01 01:23:45\",\"\",\"Test Account\",\"123 45\",\"\",\"\""), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "\"余额变更\",\"2024-09-01 01:23:45\",\"\",\"Test Account\",\"123 45\",\"\",\"\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrAmountInvalid.Message) _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+ "\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+ - "\"负债变更\",\"2024-09-01 01:23:45\",\"\",\"Test Account\",\"123 45\",\"\",\"\""), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "\"负债变更\",\"2024-09-01 01:23:45\",\"\",\"Test Account\",\"123 45\",\"\",\"\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrAmountInvalid.Message) _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+ "\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+ "\"转出\",\"2024-09-01 12:34:56\",\"Test Category\",\"Test Account\",\"123 45\",\"\",\"00000000-0000-0000-0000-000000000001\"\n"+ - "\"转入\",\"2024-09-01 12:34:56\",\"Test Category\",\"Test Account2\",\"123.45\",\"\",\"00000000-0000-0000-0000-000000000001\""), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "\"转入\",\"2024-09-01 12:34:56\",\"Test Category\",\"Test Account2\",\"123.45\",\"\",\"00000000-0000-0000-0000-000000000001\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrAmountInvalid.Message) _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+ "\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+ "\"转出\",\"2024-09-01 12:34:56\",\"Test Category\",\"Test Account\",\"123.45\",\"\",\"00000000-0000-0000-0000-000000000001\"\n"+ - "\"转入\",\"2024-09-01 12:34:56\",\"Test Category\",\"Test Account2\",\"123 45\",\"\",\"00000000-0000-0000-0000-000000000001\""), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "\"转入\",\"2024-09-01 12:34:56\",\"Test Category\",\"Test Account2\",\"123 45\",\"\",\"00000000-0000-0000-0000-000000000001\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrAmountInvalid.Message) } @@ -319,7 +319,7 @@ func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseDescription(t *testi allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+ "\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+ "\"支出\",\"2024-09-01 12:34:56\",\"Test Category\",\"Test Account\",\"123.45\",\"Test\n"+ - "A new line break\",\"\""), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "A new line break\",\"\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) @@ -337,7 +337,7 @@ func TestFeideeMymoneyCsvFileImporterParseImportedData_WithAdditionalOptions(t * allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+ "\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\",\"成员\",\"项目\",\"商家\"\n"+ - "\"支出\",\"2024-09-01 12:34:56\",\"Test Category\",\"Test Account\",\"123.45\",\"\",\"\",\"test1\",\"test2\",\"test3\""), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "\"支出\",\"2024-09-01 12:34:56\",\"Test Category\",\"Test Account\",\"123.45\",\"\",\"\",\"test1\",\"test2\",\"test3\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) @@ -345,7 +345,7 @@ func TestFeideeMymoneyCsvFileImporterParseImportedData_WithAdditionalOptions(t * allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+ "\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\",\"成员\",\"项目\",\"商家\"\n"+ - "\"支出\",\"2024-09-01 12:34:56\",\"Test Category\",\"Test Account\",\"123.45\",\"\",\"\",\"test1\",\"test2\",\"test3\""), 0, converter.DefaultImporterOptions.WithMemberAsTag().WithProjectAsTag().WithMerchantAsTag(), nil, nil, nil, nil, nil) + "\"支出\",\"2024-09-01 12:34:56\",\"Test Category\",\"Test Account\",\"123.45\",\"\",\"\",\"test1\",\"test2\",\"test3\""), time.UTC, converter.DefaultImporterOptions.WithMemberAsTag().WithProjectAsTag().WithMerchantAsTag(), nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) @@ -366,18 +366,18 @@ func TestFeideeMymoneyCsvFileImporterParseImportedData_InvalidRelatedId(t *testi _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+ "\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+ - "\"转出\",\"2024-09-01 23:59:59\",\"Test Category3\",\"Test Account\",\"0.05\",\"\",\"\""), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "\"转出\",\"2024-09-01 23:59:59\",\"Test Category3\",\"Test Account\",\"0.05\",\"\",\"\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrRelatedIdCannotBeBlank.Message) _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+ "\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+ - "\"转入\",\"2024-09-01 23:59:59\",\"Test Category3\",\"Test Account\",\"0.05\",\"\",\"\""), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "\"转入\",\"2024-09-01 23:59:59\",\"Test Category3\",\"Test Account\",\"0.05\",\"\",\"\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrRelatedIdCannotBeBlank.Message) _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+ "\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+ "\"转出\",\"2024-09-01 23:59:59\",\"Test Category3\",\"Test Account\",\"0.05\",\"\",\"00000000-0000-0000-0000-000000000001\"\n"+ - "\"转入\",\"2024-09-02 23:59:59\",\"Test Category3\",\"Test Account\",\"0.5\",\"\",\"00000000-0000-0000-0000-000000000002\""), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "\"转入\",\"2024-09-02 23:59:59\",\"Test Category3\",\"Test Account\",\"0.5\",\"\",\"00000000-0000-0000-0000-000000000002\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrFoundRecordNotHasRelatedRecord.Message) } @@ -390,10 +390,10 @@ func TestFeideeMymoneyCsvFileImporterParseImportedData_MissingFileHeader(t *test DefaultCurrency: "CNY", } _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+ - "\"余额变更\",\"2024-09-01 00:00:00\",\"\",\"Test Account\",\"123.45\",\"\",\"\"\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "\"余额变更\",\"2024-09-01 00:00:00\",\"\",\"Test Account\",\"123.45\",\"\",\"\"\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrInvalidFileHeader.Message) - _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(""), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrInvalidFileHeader.Message) } @@ -409,36 +409,36 @@ func TestFeideeMymoneyCsvFileImporterParseImportedData_MissingRequiredColumn(t * // Missing Time Column _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+ "\"交易类型\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+ - "\"余额变更\",\"\",\"Test Account\",\"123.45\",\"\",\"\"\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "\"余额变更\",\"\",\"Test Account\",\"123.45\",\"\",\"\"\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message) // Missing Type Column _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+ "\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+ - "\"2024-09-01 00:00:00\",\"Test Category\",\"Test Account\",\"123.45\",\"\",\"\"\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "\"2024-09-01 00:00:00\",\"Test Category\",\"Test Account\",\"123.45\",\"\",\"\"\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message) // Missing Sub Category Column _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+ "\"交易类型\",\"日期\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+ - "\"余额变更\",\"2024-09-01 00:00:00\",\"Test Account\",\"123.45\",\"\",\"\"\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "\"余额变更\",\"2024-09-01 00:00:00\",\"Test Account\",\"123.45\",\"\",\"\"\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message) // Missing Account Name Column _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+ "\"交易类型\",\"日期\",\"子类别\",\"金额\",\"备注\",\"关联Id\"\n"+ - "\"余额变更\",\"2024-09-01 00:00:00\",\"\",\"123.45\",\"\",\"\"\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "\"余额变更\",\"2024-09-01 00:00:00\",\"\",\"123.45\",\"\",\"\"\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message) // Missing Amount Column _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+ "\"交易类型\",\"日期\",\"子类别\",\"账户\",\"备注\",\"关联Id\"\n"+ - "\"余额变更\",\"2024-09-01 00:00:00\",\"\",\"Test Account\",\"\",\"\"\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "\"余额变更\",\"2024-09-01 00:00:00\",\"\",\"Test Account\",\"\",\"\"\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message) // Missing Related ID Column _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+ "\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\"\n"+ - "\"余额变更\",\"2024-09-01 00:00:00\",\"\",\"Test Account\",\"123.45\",\"\"\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "\"余额变更\",\"2024-09-01 00:00:00\",\"\",\"Test Account\",\"123.45\",\"\"\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message) } diff --git a/pkg/converters/feidee/feidee_mymoney_elecloud_transaction_data_xlsx_file_importer.go b/pkg/converters/feidee/feidee_mymoney_elecloud_transaction_data_xlsx_file_importer.go index b6ebccef..6e13d84f 100644 --- a/pkg/converters/feidee/feidee_mymoney_elecloud_transaction_data_xlsx_file_importer.go +++ b/pkg/converters/feidee/feidee_mymoney_elecloud_transaction_data_xlsx_file_importer.go @@ -1,6 +1,8 @@ package feidee import ( + "time" + "github.com/mayswind/ezbookkeeping/pkg/converters/converter" "github.com/mayswind/ezbookkeeping/pkg/converters/datatable" "github.com/mayswind/ezbookkeeping/pkg/converters/excel" @@ -34,7 +36,7 @@ var ( ) // ParseImportedData returns the imported data by parsing the feidee mymoney (elecloud) transaction xlsx data -func (c *feideeMymoneyElecloudTransactionDataXlsxFileImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezoneOffset int16, additionalOptions converter.TransactionDataImporterOptions, accountMap map[string]*models.Account, expenseCategoryMap map[string]map[string]*models.TransactionCategory, incomeCategoryMap map[string]map[string]*models.TransactionCategory, transferCategoryMap map[string]map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionTag, error) { +func (c *feideeMymoneyElecloudTransactionDataXlsxFileImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezone *time.Location, additionalOptions converter.TransactionDataImporterOptions, accountMap map[string]*models.Account, expenseCategoryMap map[string]map[string]*models.TransactionCategory, incomeCategoryMap map[string]map[string]*models.TransactionCategory, transferCategoryMap map[string]map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionTag, error) { dataTable, err := excel.CreateNewExcelOOXMLFileBasicDataTable(data, true) if err != nil { @@ -45,5 +47,5 @@ func (c *feideeMymoneyElecloudTransactionDataXlsxFileImporter) ParseImportedData transactionDataTable := datatable.CreateNewTransactionDataTableFromBasicDataTableWithRowParser(dataTable, feideeMymoneyElecloudDataColumnNameMapping, transactionRowParser) dataTableImporter := converter.CreateNewSimpleImporter(feideeMymoneyElecloudTransactionTypeNameMapping) - return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezoneOffset, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap) + return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezone, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap) } diff --git a/pkg/converters/feidee/feidee_mymoney_elecloud_transaction_data_xlsx_file_importer_test.go b/pkg/converters/feidee/feidee_mymoney_elecloud_transaction_data_xlsx_file_importer_test.go index 06e7cc08..2402c36c 100644 --- a/pkg/converters/feidee/feidee_mymoney_elecloud_transaction_data_xlsx_file_importer_test.go +++ b/pkg/converters/feidee/feidee_mymoney_elecloud_transaction_data_xlsx_file_importer_test.go @@ -25,7 +25,7 @@ func TestFeideeMymoneyElecloudTransactionDataXlsxImporterParseImportedData_Minim testdata, err := os.ReadFile("../../../testdata/feidee_mymoney_elecloud_test_file.xlsx") assert.Nil(t, err) - allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := importer.ParseImportedData(context, user, testdata, 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := importer.ParseImportedData(context, user, testdata, time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 7, len(allNewTransactions)) diff --git a/pkg/converters/feidee/feidee_mymoney_web_transaction_data_xls_file_importer.go b/pkg/converters/feidee/feidee_mymoney_web_transaction_data_xls_file_importer.go index 89709a1b..c2cd9e00 100644 --- a/pkg/converters/feidee/feidee_mymoney_web_transaction_data_xls_file_importer.go +++ b/pkg/converters/feidee/feidee_mymoney_web_transaction_data_xls_file_importer.go @@ -1,6 +1,8 @@ package feidee import ( + "time" + "github.com/mayswind/ezbookkeeping/pkg/converters/converter" "github.com/mayswind/ezbookkeeping/pkg/converters/datatable" "github.com/mayswind/ezbookkeeping/pkg/converters/excel" @@ -33,7 +35,7 @@ var ( ) // ParseImportedData returns the imported data by parsing the feidee mymoney (web) transaction xls data -func (c *feideeMymoneyWebTransactionDataXlsFileImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezoneOffset int16, additionalOptions converter.TransactionDataImporterOptions, accountMap map[string]*models.Account, expenseCategoryMap map[string]map[string]*models.TransactionCategory, incomeCategoryMap map[string]map[string]*models.TransactionCategory, transferCategoryMap map[string]map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionTag, error) { +func (c *feideeMymoneyWebTransactionDataXlsFileImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezone *time.Location, additionalOptions converter.TransactionDataImporterOptions, accountMap map[string]*models.Account, expenseCategoryMap map[string]map[string]*models.TransactionCategory, incomeCategoryMap map[string]map[string]*models.TransactionCategory, transferCategoryMap map[string]map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionTag, error) { dataTable, err := excel.CreateNewExcelMSCFBFileBasicDataTable(data, true) if err != nil { @@ -44,5 +46,5 @@ func (c *feideeMymoneyWebTransactionDataXlsFileImporter) ParseImportedData(ctx c transactionDataTable := datatable.CreateNewTransactionDataTableFromBasicDataTableWithRowParser(dataTable, feideeMymoneyWebDataColumnNameMapping, transactionRowParser) dataTableImporter := converter.CreateNewSimpleImporterWithTypeNameMapping(feideeMymoneyTransactionTypeNameMapping) - return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezoneOffset, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap) + return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezone, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap) } diff --git a/pkg/converters/feidee/feidee_mymoney_web_transaction_data_xls_file_importer_test.go b/pkg/converters/feidee/feidee_mymoney_web_transaction_data_xls_file_importer_test.go index 84e6fc72..730df34e 100644 --- a/pkg/converters/feidee/feidee_mymoney_web_transaction_data_xls_file_importer_test.go +++ b/pkg/converters/feidee/feidee_mymoney_web_transaction_data_xls_file_importer_test.go @@ -25,7 +25,7 @@ func TestFeideeMymoneyTransactionDataXlsImporterParseImportedData_MinimumValidDa testdata, err := os.ReadFile("../../../testdata/feidee_mymoney_test_file.xls") assert.Nil(t, err) - allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := importer.ParseImportedData(context, user, testdata, 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := importer.ParseImportedData(context, user, testdata, time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 7, len(allNewTransactions)) diff --git a/pkg/converters/fireflyIII/fireflyiii_transaction_data_csv_file_importer.go b/pkg/converters/fireflyIII/fireflyiii_transaction_data_csv_file_importer.go index 4b91efe0..44a7d727 100644 --- a/pkg/converters/fireflyIII/fireflyiii_transaction_data_csv_file_importer.go +++ b/pkg/converters/fireflyIII/fireflyiii_transaction_data_csv_file_importer.go @@ -2,6 +2,7 @@ package fireflyIII import ( "bytes" + "time" "github.com/mayswind/ezbookkeeping/pkg/converters/converter" "github.com/mayswind/ezbookkeeping/pkg/converters/csv" @@ -40,7 +41,7 @@ var ( ) // ParseImportedData returns the imported data by parsing the firefly III transaction csv data -func (c *fireflyIIITransactionDataCsvFileImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezoneOffset int16, additionalOptions converter.TransactionDataImporterOptions, accountMap map[string]*models.Account, expenseCategoryMap map[string]map[string]*models.TransactionCategory, incomeCategoryMap map[string]map[string]*models.TransactionCategory, transferCategoryMap map[string]map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionTag, error) { +func (c *fireflyIIITransactionDataCsvFileImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezone *time.Location, additionalOptions converter.TransactionDataImporterOptions, accountMap map[string]*models.Account, expenseCategoryMap map[string]map[string]*models.TransactionCategory, incomeCategoryMap map[string]map[string]*models.TransactionCategory, transferCategoryMap map[string]map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionTag, error) { reader := bytes.NewReader(data) dataTable, err := csv.CreateNewCsvBasicDataTable(ctx, reader, true) @@ -52,5 +53,5 @@ func (c *fireflyIIITransactionDataCsvFileImporter) ParseImportedData(ctx core.Co transactionDataTable := datatable.CreateNewTransactionDataTableFromBasicDataTableWithRowParser(dataTable, fireflyIIITransactionDataColumnNameMapping, transactionRowParser) dataTableImporter := converter.CreateNewImporterWithTypeNameMapping(fireflyIIITransactionTypeNameMapping, "", "", ",") - return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezoneOffset, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap) + return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezone, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap) } diff --git a/pkg/converters/fireflyIII/fireflyiii_transaction_data_csv_file_importer_test.go b/pkg/converters/fireflyIII/fireflyiii_transaction_data_csv_file_importer_test.go index 80978315..d96573ca 100644 --- a/pkg/converters/fireflyIII/fireflyiii_transaction_data_csv_file_importer_test.go +++ b/pkg/converters/fireflyIII/fireflyiii_transaction_data_csv_file_importer_test.go @@ -2,6 +2,7 @@ package fireflyIII import ( "testing" + "time" "github.com/stretchr/testify/assert" @@ -25,7 +26,7 @@ func TestFireFlyIIICsvFileimporterParseImportedData_MinimumValidData(t *testing. "\"Opening balance\",123.45,2024-09-01T00:00:00+08:00,\"Initial balance for \"\"Test Account\"\"\",\"Test Account\",\n"+ "Deposit,0.12,2024-09-01T01:23:45+08:00,\"A revenue account\",\"Test Account\",\"Test Category\"\n"+ "Withdrawal,-1.00,2024-09-01T12:34:56+08:00,\"Test Account\",\"A expense account\",\"Test Category2\"\n"+ - "Transfer,0.05,2024-09-01T23:59:59+08:00,\"Test Account\",\"Test Account2\",\"Test Category3\""), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "Transfer,0.05,2024-09-01T23:59:59+08:00,\"Test Account\",\"Test Account2\",\"Test Category3\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) @@ -93,11 +94,11 @@ func TestFireFlyIIICsvFileimporterParseImportedData_ParseInvalidTime(t *testing. } _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name,category\n"+ - "Withdrawal,-1.00,2024-09-01T12:34:56,\"Test Account\",\"A expense account\",\"Test Category\""), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "Withdrawal,-1.00,2024-09-01T12:34:56,\"Test Account\",\"A expense account\",\"Test Category\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message) _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name,category\n"+ - "Withdrawal,-1.00,2024-09-01 12:34:56+08:00,\"Test Account\",\"A expense account\",\"Test Category\""), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "Withdrawal,-1.00,2024-09-01 12:34:56+08:00,\"Test Account\",\"A expense account\",\"Test Category\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message) } @@ -111,7 +112,7 @@ func TestFireFlyIIICsvFileimporterParseImportedData_ParseInvalidType(t *testing. } _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name,category\n"+ - "Type,123.45,2024-09-01T12:34:56+08:00,\"Test Account\",\"A expense account\",\"Test Category\""), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "Type,123.45,2024-09-01T12:34:56+08:00,\"Test Account\",\"A expense account\",\"Test Category\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrTransactionTypeInvalid.Message) } @@ -125,14 +126,14 @@ func TestFireFlyIIICsvFileimporterParseImportedData_ParseAccountNameAsCategoryNa } allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name,category\n"+ - "Withdrawal,-1.00,2024-09-01T12:34:56+08:00,\"Test Account\",\"A expense account\",\"\""), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "Withdrawal,-1.00,2024-09-01T12:34:56+08:00,\"Test Account\",\"A expense account\",\"\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, "A expense account", allNewTransactions[0].OriginalCategoryName) allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name,category\n"+ - "Deposit,10.00,2024-09-01T12:34:56+08:00,\"A revenue account\",\"Test Account\",\"\""), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "Deposit,10.00,2024-09-01T12:34:56+08:00,\"A revenue account\",\"Test Account\",\"\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) @@ -149,19 +150,19 @@ func TestFireFlyIIICsvFileimporterParseImportedData_ParseValidTimezone(t *testin } allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name,category\n"+ - "Withdrawal,-1.00,2024-09-01T12:34:56-10:00,\"Test Account\",\"A expense account\",\"Test Category\""), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "Withdrawal,-1.00,2024-09-01T12:34:56-10:00,\"Test Account\",\"A expense account\",\"Test Category\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, int64(1725230096), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime)) allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name,category\n"+ - "Withdrawal,-1.00,2024-09-01T12:34:56+00:00,\"Test Account\",\"A expense account\",\"Test Category\""), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "Withdrawal,-1.00,2024-09-01T12:34:56+00:00,\"Test Account\",\"A expense account\",\"Test Category\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, int64(1725194096), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime)) allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name,category\n"+ - "Withdrawal,-1.00,2024-09-01T12:34:56+12:45,\"Test Account\",\"A expense account\",\"Test Category\""), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "Withdrawal,-1.00,2024-09-01T12:34:56+12:45,\"Test Account\",\"A expense account\",\"Test Category\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, int64(1725148196), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime)) @@ -178,7 +179,7 @@ func TestFireFlyIIICsvFileimporterParseImportedData_ParseValidAccountCurrency(t allNewTransactions, allNewAccounts, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("type,amount,foreign_amount,date,currency_code,foreign_currency_code,source_name,destination_name,category\n"+ "\"Opening balance\",123.45,,2024-09-01T00:00:00+08:00,USD,,\"Initial balance for \"\"Test Account\"\"\",\"Test Account\",\n"+ - "Transfer,1.23,1.10,2024-09-01T23:59:59+08:00,USD,EUR,\"Test Account\",\"Test Account2\",\"Test Category2\""), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "Transfer,1.23,1.10,2024-09-01T23:59:59+08:00,USD,EUR,\"Test Account\",\"Test Account2\",\"Test Category2\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) @@ -204,7 +205,7 @@ func TestFireFlyIIICsvFileimporterParseImportedData_ParseValidForeignAmountAndCu } allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("type,amount,foreign_amount,date,currency_code,foreign_currency_code,source_name,destination_name,category\n"+ - "Transfer,10.00,15.00,2024-09-01T12:34:56+08:00,USD,EUR,\"Test Account\",\"Test Account2\",\"Test Category\""), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "Transfer,10.00,15.00,2024-09-01T12:34:56+08:00,USD,EUR,\"Test Account\",\"Test Account2\",\"Test Category\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) @@ -215,7 +216,7 @@ func TestFireFlyIIICsvFileimporterParseImportedData_ParseValidForeignAmountAndCu assert.Equal(t, "EUR", allNewTransactions[0].OriginalDestinationAccountCurrency) allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("type,amount,date,currency_code,foreign_currency_code,source_name,destination_name,category\n"+ - "Transfer,10.00,2024-09-01T12:34:56+08:00,USD,EUR,\"Test Account\",\"Test Account2\",\"Test Category\""), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "Transfer,10.00,2024-09-01T12:34:56+08:00,USD,EUR,\"Test Account\",\"Test Account2\",\"Test Category\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) @@ -225,7 +226,7 @@ func TestFireFlyIIICsvFileimporterParseImportedData_ParseValidForeignAmountAndCu assert.Equal(t, "EUR", allNewTransactions[0].OriginalDestinationAccountCurrency) allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("type,amount,date,currency_code,foreign_currency_code,source_name,destination_name,category\n"+ - "Transfer,10.00,2024-09-01T12:34:56+08:00,USD,,\"Test Account\",\"Test Account2\",\"Test Category\""), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "Transfer,10.00,2024-09-01T12:34:56+08:00,USD,,\"Test Account\",\"Test Account2\",\"Test Category\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) @@ -244,12 +245,12 @@ func TestFireFlyIIICsvFileimporterParseImportedData_ParseInvalidAccountCurrency( _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("type,amount,foreign_amount,date,currency_code,foreign_currency_code,source_name,destination_name,category\n"+ "\"Opening balance\",123.45,,2024-09-01T00:00:00+08:00,USD,,\"Initial balance for \"\"Test Account\"\"\",\"Test Account\",\n"+ - "Transfer,1.23,1.10,2024-09-01T23:59:59+08:00,CNY,EUR,\"Test Account\",\"Test Account2\",\"Test Category3\""), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "Transfer,1.23,1.10,2024-09-01T23:59:59+08:00,CNY,EUR,\"Test Account\",\"Test Account2\",\"Test Category3\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message) _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("type,amount,foreign_amount,date,currency_code,foreign_currency_code,source_name,destination_name,category\n"+ "\"Opening balance\",123.45,,2024-09-01T00:00:00+08:00,USD,,\"Initial balance for \"\"Test Account\"\"\",\"Test Account\",\n"+ - "Transfer,1.23,1.10,2024-09-01T23:59:59+08:00,CNY,EUR,\"Test Account2\",\"Test Account\",\"Test Category3\""), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "Transfer,1.23,1.10,2024-09-01T23:59:59+08:00,CNY,EUR,\"Test Account2\",\"Test Account\",\"Test Category3\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message) } @@ -263,11 +264,11 @@ func TestFireFlyIIICsvFileimporterParseImportedData_ParseNotSupportedCurrency(t } _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("type,amount,foreign_amount,date,currency_code,foreign_currency_code,source_name,destination_name,category\n"+ - "\"Opening balance\",123.45,,2024-09-01T00:00:00+08:00,XXX,,\"Initial balance for \"\"Test Account\"\"\",\"Test Account\",\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "\"Opening balance\",123.45,,2024-09-01T00:00:00+08:00,XXX,,\"Initial balance for \"\"Test Account\"\"\",\"Test Account\",\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message) _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("type,amount,foreign_amount,date,currency_code,foreign_currency_code,source_name,destination_name,category\n"+ - "Transfer,123.45,123.45,2024-09-01T23:59:59+08:00,USD,XXX,\"Test Account\",\"Test Account2\",\"Test Category2\""), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "Transfer,123.45,123.45,2024-09-01T23:59:59+08:00,USD,XXX,\"Test Account\",\"Test Account2\",\"Test Category2\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message) } @@ -281,11 +282,11 @@ func TestFireFlyIIICsvFileimporterParseImportedData_ParseInvalidAmount(t *testin } _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name,category\n"+ - "Withdrawal,-123 45,2024-09-01T12:34:56+08:00,\"Test Account\",\"A expense account\",\"Test Category\"\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "Withdrawal,-123 45,2024-09-01T12:34:56+08:00,\"Test Account\",\"A expense account\",\"Test Category\"\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrAmountInvalid.Message) _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("type,amount,foreign_amount,date,source_name,destination_name,category\n"+ - "Transfer,123.45,123 45,2024-09-01T23:59:59+08:00,\"Test Account\",\"Test Account2\",\"Test Category2\""), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "Transfer,123.45,123 45,2024-09-01T23:59:59+08:00,\"Test Account\",\"Test Account2\",\"Test Category2\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrAmountInvalid.Message) } @@ -299,7 +300,7 @@ func TestFireFlyIIICsvFileimporterParseImportedData_ParseDescription(t *testing. } allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("type,amount,description,date,source_name,destination_name,category\n"+ - "Withdrawal,-123.45,\"foo bar\t#test\",2024-09-01T12:34:56+08:00,\"Test Account\",\"A expense account\",\"Test Category\"\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "Withdrawal,-123.45,\"foo bar\t#test\",2024-09-01T12:34:56+08:00,\"Test Account\",\"A expense account\",\"Test Category\"\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) @@ -316,7 +317,7 @@ func TestFireFlyIIICsvFileimporterParseImportedData_ParseTags(t *testing.T) { } allNewTransactions, _, _, _, _, allNewTags, err := importer.ParseImportedData(context, user, []byte("type,amount,tags,date,source_name,destination_name,category\n"+ - "Withdrawal,-123.45,\"tag1,tag2,tag3\",2024-09-01T12:34:56+08:00,\"Test Account\",\"A expense account\",\"Test Category\"\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "Withdrawal,-123.45,\"tag1,tag2,tag3\",2024-09-01T12:34:56+08:00,\"Test Account\",\"A expense account\",\"Test Category\"\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) @@ -338,7 +339,7 @@ func TestFireFlyIIICsvFileimporterParseImportedData_MissingFileHeader(t *testing DefaultCurrency: "CNY", } - _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(""), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrNotFoundTransactionDataInFile.Message) } @@ -353,31 +354,31 @@ func TestFireFlyIIICsvFileimporterParseImportedData_MissingRequiredColumn(t *tes // Missing Time Column _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("type,amount,source_name,destination_name,category\n"+ - "\"Opening balance\",123.45,\"Initial balance for \"\"Test Account\"\"\",\"Test Account\",\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "\"Opening balance\",123.45,\"Initial balance for \"\"Test Account\"\"\",\"Test Account\",\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message) // Missing Type Column _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("amount,date,source_name,destination_name,category\n"+ - "123.45,2024-09-01T00:00:00+08:00,\"Initial balance for \"\"Test Account\"\"\",\"Test Account\",\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "123.45,2024-09-01T00:00:00+08:00,\"Initial balance for \"\"Test Account\"\"\",\"Test Account\",\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message) // Missing Sub Category Column _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name\n"+ - "\"Opening balance\",123.45,2024-09-01T00:00:00+08:00,\"Initial balance for \"\"Test Account\"\"\",\"Test Account\"\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "\"Opening balance\",123.45,2024-09-01T00:00:00+08:00,\"Initial balance for \"\"Test Account\"\"\",\"Test Account\"\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message) // Missing Account Name Column _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("type,amount,date,destination_name,category\n"+ - "\"Opening balance\",123.45,2024-09-01T00:00:00+08:00,\"Test Account\",\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "\"Opening balance\",123.45,2024-09-01T00:00:00+08:00,\"Test Account\",\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message) // Missing Amount Column _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("type,date,source_name,destination_name,category\n"+ - "\"Opening balance\",2024-09-01T00:00:00+08:00,\"Initial balance for \"\"Test Account\"\"\",\"Test Account\",\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "\"Opening balance\",2024-09-01T00:00:00+08:00,\"Initial balance for \"\"Test Account\"\"\",\"Test Account\",\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message) // Missing Account2 Name Column _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("type,amount,date,source_name,category\n"+ - "\"Opening balance\",123.45,2024-09-01T00:00:00+08:00,\"Initial balance for \"\"Test Account\"\"\",\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "\"Opening balance\",123.45,2024-09-01T00:00:00+08:00,\"Initial balance for \"\"Test Account\"\"\",\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message) } diff --git a/pkg/converters/fireflyIII/fireflyiii_transaction_data_row_parser.go b/pkg/converters/fireflyIII/fireflyiii_transaction_data_row_parser.go index b0676300..5be6b0ff 100644 --- a/pkg/converters/fireflyIII/fireflyiii_transaction_data_row_parser.go +++ b/pkg/converters/fireflyIII/fireflyiii_transaction_data_row_parser.go @@ -44,7 +44,7 @@ func (p *fireflyIIITransactionDataRowParser) Parse(data map[datatable.Transactio } rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME] = utils.FormatUnixTimeToLongDateTime(dateTime.Unix(), dateTime.Location()) - rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE] = utils.FormatTimezoneOffset(dateTime.Location()) + rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE] = utils.FormatTimezoneOffset(dateTime.Unix(), dateTime.Location()) } // trim trailing zero in decimal diff --git a/pkg/converters/gnucash/gnucash_transaction_data_file_importer.go b/pkg/converters/gnucash/gnucash_transaction_data_file_importer.go index adfd5d68..450adc14 100644 --- a/pkg/converters/gnucash/gnucash_transaction_data_file_importer.go +++ b/pkg/converters/gnucash/gnucash_transaction_data_file_importer.go @@ -1,6 +1,8 @@ package gnucash import ( + "time" + "github.com/mayswind/ezbookkeeping/pkg/converters/converter" "github.com/mayswind/ezbookkeeping/pkg/core" "github.com/mayswind/ezbookkeeping/pkg/models" @@ -24,7 +26,7 @@ var ( ) // ParseImportedData returns the imported data by parsing the gnucash transaction data -func (c *gnucashTransactionDataImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezoneOffset int16, additionalOptions converter.TransactionDataImporterOptions, accountMap map[string]*models.Account, expenseCategoryMap map[string]map[string]*models.TransactionCategory, incomeCategoryMap map[string]map[string]*models.TransactionCategory, transferCategoryMap map[string]map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionTag, error) { +func (c *gnucashTransactionDataImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezone *time.Location, additionalOptions converter.TransactionDataImporterOptions, accountMap map[string]*models.Account, expenseCategoryMap map[string]map[string]*models.TransactionCategory, incomeCategoryMap map[string]map[string]*models.TransactionCategory, transferCategoryMap map[string]map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionTag, error) { gnucashDataReader, err := createNewGnuCashDatabaseReader(data) if err != nil { @@ -45,5 +47,5 @@ func (c *gnucashTransactionDataImporter) ParseImportedData(ctx core.Context, use dataTableImporter := converter.CreateNewSimpleImporterWithTypeNameMapping(gnucashTransactionTypeNameMapping) - return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezoneOffset, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap) + return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezone, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap) } diff --git a/pkg/converters/gnucash/gnucash_transaction_data_file_importer_test.go b/pkg/converters/gnucash/gnucash_transaction_data_file_importer_test.go index 65b28e38..c795ca9e 100644 --- a/pkg/converters/gnucash/gnucash_transaction_data_file_importer_test.go +++ b/pkg/converters/gnucash/gnucash_transaction_data_file_importer_test.go @@ -4,6 +4,7 @@ import ( "bytes" "compress/gzip" "testing" + "time" "github.com/stretchr/testify/assert" @@ -194,7 +195,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_MinimumValidData(t *tes DefaultCurrency: "CNY", } - allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := importer.ParseImportedData(context, user, []byte(gnucashMinimumValidDataCase), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := importer.ParseImportedData(context, user, []byte(gnucashMinimumValidDataCase), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) checkParsedMinimumValidData(t, allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags) @@ -218,7 +219,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_GzippedMinimumValidData assert.Nil(t, err) gzippedData := buffer.Bytes() - allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := importer.ParseImportedData(context, user, gzippedData, 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := importer.ParseImportedData(context, user, gzippedData, time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) checkParsedMinimumValidData(t, allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags) @@ -357,7 +358,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_MinimumValidDataWithRev " \n"+ "\n"+ "\n"+ - "\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) checkParsedMinimumValidData(t, allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags) @@ -389,7 +390,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseInvalidTime(t *tes " \n"+ " \n"+ "\n"+ - gnucashCommonValidDataCaseFooter), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + gnucashCommonValidDataCaseFooter), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message) _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte( @@ -409,7 +410,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseInvalidTime(t *tes " \n"+ " \n"+ "\n"+ - gnucashCommonValidDataCaseFooter), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + gnucashCommonValidDataCaseFooter), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message) } @@ -439,7 +440,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseValidTimezone(t *t " \n"+ " \n"+ "\n"+ - gnucashCommonValidDataCaseFooter), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + gnucashCommonValidDataCaseFooter), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, int64(1725230096), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime)) @@ -461,7 +462,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseValidTimezone(t *t " \n"+ " \n"+ "\n"+ - gnucashCommonValidDataCaseFooter), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + gnucashCommonValidDataCaseFooter), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, int64(1725148196), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime)) @@ -558,7 +559,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseValidAccountCurren " \n"+ "\n"+ "\n"+ - "\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) @@ -600,7 +601,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseValidAmount(t *tes " \n"+ " \n"+ "\n"+ - gnucashCommonValidDataCaseFooter), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + gnucashCommonValidDataCaseFooter), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) @@ -623,7 +624,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseValidAmount(t *tes " \n"+ " \n"+ "\n"+ - gnucashCommonValidDataCaseFooter), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + gnucashCommonValidDataCaseFooter), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) @@ -656,7 +657,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseInvalidAmount(t *t " \n"+ " \n"+ "\n"+ - gnucashCommonValidDataCaseFooter), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + gnucashCommonValidDataCaseFooter), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrAmountInvalid.Message) _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte( @@ -676,7 +677,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseInvalidAmount(t *t " \n"+ " \n"+ "\n"+ - gnucashCommonValidDataCaseFooter), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + gnucashCommonValidDataCaseFooter), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrAmountInvalid.Message) _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte( @@ -696,7 +697,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseInvalidAmount(t *t " \n"+ " \n"+ "\n"+ - gnucashCommonValidDataCaseFooter), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + gnucashCommonValidDataCaseFooter), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrAmountInvalid.Message) } @@ -727,7 +728,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseDescription(t *tes " \n"+ " \n"+ "\n"+ - gnucashCommonValidDataCaseFooter), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + gnucashCommonValidDataCaseFooter), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) @@ -756,7 +757,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_SkipZeroAmountTransacti " \n"+ " \n"+ "\n"+ - gnucashCommonValidDataCaseFooter), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + gnucashCommonValidDataCaseFooter), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrNotFoundTransactionDataInFile.Message) } @@ -806,7 +807,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_NotSupportedToParseSpli " \n"+ " \n"+ "\n"+ - gnucashCommonValidDataCaseFooter), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + gnucashCommonValidDataCaseFooter), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrNotSupportedSplitTransactions.Message) } @@ -873,7 +874,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_MissingAccountRequiredN " \n"+ " \n"+ "\n"+ - gnucashCommonValidDataCaseFooter), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + gnucashCommonValidDataCaseFooter), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message) } @@ -901,7 +902,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_MissingTransactionRequi " \n"+ " \n"+ "\n"+ - gnucashCommonValidDataCaseFooter), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + gnucashCommonValidDataCaseFooter), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrMissingTransactionTime.Message) // Missing Transaction Splits Node @@ -912,7 +913,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_MissingTransactionRequi " 2024-09-01 00:00:00 +0000\n"+ " \n"+ "\n"+ - gnucashCommonValidDataCaseFooter), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + gnucashCommonValidDataCaseFooter), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrInvalidGnuCashFile.Message) // Missing Transaction Split Quantity Node @@ -931,7 +932,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_MissingTransactionRequi " \n"+ " \n"+ "\n"+ - gnucashCommonValidDataCaseFooter), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + gnucashCommonValidDataCaseFooter), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrAmountInvalid.Message) // Missing Transaction Split Account Node @@ -951,7 +952,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_MissingTransactionRequi " \n"+ " \n"+ "\n"+ - gnucashCommonValidDataCaseFooter), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + gnucashCommonValidDataCaseFooter), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrMissingAccountData.Message) } diff --git a/pkg/converters/gnucash/gnucash_transaction_data_table.go b/pkg/converters/gnucash/gnucash_transaction_data_table.go index d881d4ce..dbd8adcc 100644 --- a/pkg/converters/gnucash/gnucash_transaction_data_table.go +++ b/pkg/converters/gnucash/gnucash_transaction_data_table.go @@ -124,7 +124,7 @@ func (t *gnucashTransactionDataRowIterator) parseTransaction(ctx core.Context, u } data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME] = utils.FormatUnixTimeToLongDateTime(dateTime.Unix(), dateTime.Location()) - data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE] = utils.FormatTimezoneOffset(dateTime.Location()) + data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE] = utils.FormatTimezoneOffset(dateTime.Unix(), dateTime.Location()) if len(gnucashTransaction.Splits) == 2 { splitData1 := gnucashTransaction.Splits[0] diff --git a/pkg/converters/iif/iif_transaction_data_file_importer.go b/pkg/converters/iif/iif_transaction_data_file_importer.go index fca7ba98..f2b4e6f3 100644 --- a/pkg/converters/iif/iif_transaction_data_file_importer.go +++ b/pkg/converters/iif/iif_transaction_data_file_importer.go @@ -1,6 +1,8 @@ package iif import ( + "time" + "github.com/mayswind/ezbookkeeping/pkg/converters/converter" "github.com/mayswind/ezbookkeeping/pkg/core" "github.com/mayswind/ezbookkeeping/pkg/models" @@ -23,7 +25,7 @@ var ( ) // ParseImportedData returns the imported data by parsing the intuit interchange format (iif) data -func (c *iifTransactionDataFileImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezoneOffset int16, additionalOptions converter.TransactionDataImporterOptions, accountMap map[string]*models.Account, expenseCategoryMap map[string]map[string]*models.TransactionCategory, incomeCategoryMap map[string]map[string]*models.TransactionCategory, transferCategoryMap map[string]map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionTag, error) { +func (c *iifTransactionDataFileImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezone *time.Location, additionalOptions converter.TransactionDataImporterOptions, accountMap map[string]*models.Account, expenseCategoryMap map[string]map[string]*models.TransactionCategory, incomeCategoryMap map[string]map[string]*models.TransactionCategory, transferCategoryMap map[string]map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionTag, error) { iifDataReader := createNewIifDataReader(data) accountDatasets, transactionDatasets, err := iifDataReader.read(ctx) @@ -39,5 +41,5 @@ func (c *iifTransactionDataFileImporter) ParseImportedData(ctx core.Context, use dataTableImporter := converter.CreateNewSimpleImporterWithTypeNameMapping(iifTransactionTypeNameMapping) - return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezoneOffset, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap) + return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezone, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap) } diff --git a/pkg/converters/iif/iif_transaction_data_file_importer_test.go b/pkg/converters/iif/iif_transaction_data_file_importer_test.go index 9281d630..577b52e1 100644 --- a/pkg/converters/iif/iif_transaction_data_file_importer_test.go +++ b/pkg/converters/iif/iif_transaction_data_file_importer_test.go @@ -2,6 +2,7 @@ package iif import ( "testing" + "time" "github.com/stretchr/testify/assert" @@ -50,7 +51,7 @@ func TestIIFTransactionDataFileParseImportedData_MinimumValidData(t *testing.T) "ENDTRNS\t\t\t\t\n"+ "TRNS\tCREDIT CARD\t09/07/2024\tTest Category2\t34.56\n"+ "SPL\tCREDIT CARD\t09/07/2024\tTest Account2\t-34.56\n"+ - "ENDTRNS\t\t\t\t\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "ENDTRNS\t\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) @@ -147,7 +148,7 @@ func TestIIFTransactionDataFileParseImportedData_MinimumValidDataWithoutAccountD "!ENDTRNS\t\t\t\n"+ "TRNS\t09/01/2024\tTest Account\t123.45\n"+ "SPL\t09/01/2024\tTest Category\t-123.45\n"+ - "ENDTRNS\t\t\t\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "ENDTRNS\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) @@ -203,7 +204,7 @@ func TestIIFTransactionDataFileParseImportedData_MultipleDataset(t *testing.T) { "ENDTRNS\t\t\t\t\n"+ "!ACCNT\tTEST\tNAME\tACCNTTYPE\n"+ "ACCNT\t\tTest Category\tINC\n"+ - "ACCNT\t\tTest Category2\tEXP\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "ACCNT\t\tTest Category2\tEXP\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) @@ -268,7 +269,7 @@ func TestIIFTransactionDataFileParseImportedData_ParseCategoryAndSubCategory(t * "ENDTRNS\t\t\t\n"+ "TRNS\t09/02/2024\tTest Account2\t-123.45\n"+ "SPL\t09/02/2024\tTest Parent Category2:Test Category2\t123.45\n"+ - "ENDTRNS\t\t\t\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "ENDTRNS\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) @@ -323,7 +324,7 @@ func TestIIFTransactionDataFileParseImportedData_ParseYearMonthDayFormatTime(t * "ENDTRNS\t\t\t\n"+ "TRNS\t2024/9/4\tTest Account\t123.45\n"+ "SPL\t2024/9/4\tTest Account2\t-123.45\n"+ - "ENDTRNS\t\t\t\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "ENDTRNS\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) @@ -355,7 +356,7 @@ func TestIIFTransactionDataFileParseImportedData_ParseShortMonthDayYearFormatTim "ENDTRNS\t\t\t\n"+ "TRNS\t9/3/2024\tTest Account\t123.45\n"+ "SPL\t9/3/2024\tTest Account2\t-123.45\n"+ - "ENDTRNS\t\t\t\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "ENDTRNS\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) @@ -386,7 +387,7 @@ func TestIIFTransactionDataFileParseImportedData_ParseShortMonthDayTwoDigitsYear "ENDTRNS\t\t\t\n"+ "TRNS\t24/9/3\tTest Account\t123.45\n"+ "SPL\t24/9/3\tTest Account2\t-123.45\n"+ - "ENDTRNS\t\t\t\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "ENDTRNS\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) @@ -411,7 +412,7 @@ func TestIIFTransactionDataFileParseImportedData_ParseInvalidTime(t *testing.T) "!ENDTRNS\t\t\t\n"+ "TRNS\t09-01-2024\tTest Account\t123.45\n"+ "SPL\t09-01-2024\tTest Account2\t-123.45\n"+ - "ENDTRNS\t\t\t\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "ENDTRNS\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message) _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte( @@ -420,7 +421,7 @@ func TestIIFTransactionDataFileParseImportedData_ParseInvalidTime(t *testing.T) "!ENDTRNS\t\t\t\n"+ "TRNS\t2024-09-01\tTest Account\t123.45\n"+ "SPL\t2024-09-01\tTest Account2\t-123.45\n"+ - "ENDTRNS\t\t\t\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "ENDTRNS\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message) _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte( @@ -429,7 +430,7 @@ func TestIIFTransactionDataFileParseImportedData_ParseInvalidTime(t *testing.T) "!ENDTRNS\t\t\t\n"+ "TRNS\t9/24\tTest Account\t123.45\n"+ "SPL\t9/24\tTest Account2\t-123.45\n"+ - "ENDTRNS\t\t\t\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "ENDTRNS\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message) } @@ -448,7 +449,7 @@ func TestIIFTransactionDataFileParseImportedData_ParseAmountWithThousandsSeparat "!ENDTRNS\t\t\t\n"+ "TRNS\t9/01/2024\tTest Account\t123,456.78\n"+ "SPL\t9/01/2024\tTest Account2\t-123,456.78\n"+ - "ENDTRNS\t\t\t\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "ENDTRNS\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) @@ -471,7 +472,7 @@ func TestIIFTransactionDataFileParseImportedData_ParseInvalidAmount(t *testing.T "!ENDTRNS\t\t\t\n"+ "TRNS\t09/01/2024\tTest Account\t123 45\n"+ "SPL\t09/01/2024\tTest Account2\t-123.45\n"+ - "ENDTRNS\t\t\t\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "ENDTRNS\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrAmountInvalid.Message) _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte( @@ -480,7 +481,7 @@ func TestIIFTransactionDataFileParseImportedData_ParseInvalidAmount(t *testing.T "!ENDTRNS\t\t\t\n"+ "TRNS\t09/01/2024\tTest Account\t123.45\n"+ "SPL\t09/01/2024\tTest Account2\t-123 45\n"+ - "ENDTRNS\t\t\t\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "ENDTRNS\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrAmountInvalid.Message) } @@ -499,7 +500,7 @@ func TestIIFTransactionDataFileParseImportedData_ParseDescription(t *testing.T) "!ENDTRNS\t\t\t\t\t\n"+ "TRNS\t09/01/2024\tTest Account\t\"Test\"\t123.45\t\"foo bar\t#test\"\n"+ "SPL\t09/01/2024\tTest Account2\t\t-123.45\t\n"+ - "ENDTRNS\t\t\t\t\t\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "ENDTRNS\t\t\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) @@ -511,7 +512,7 @@ func TestIIFTransactionDataFileParseImportedData_ParseDescription(t *testing.T) "!ENDTRNS\t\t\t\t\t\n"+ "TRNS\t09/01/2024\tTest Account\tTest\t123.45\t\n"+ "SPL\t09/01/2024\tTest Account2\t\t-123.45\t\n"+ - "ENDTRNS\t\t\t\t\t\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "ENDTRNS\t\t\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) @@ -553,7 +554,7 @@ func TestIIFTransactionDataFileParseImportedData_ParseSplitTransaction(t *testin "TRNS\t09/05/2024\tTest Category2\t100.00\n"+ "SPL\t09/05/2024\tTest Account3\t-40.00\n"+ "SPL\t09/05/2024\tTest Account4\t-60.00\n"+ - "ENDTRNS\t\t\t\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "ENDTRNS\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) @@ -667,7 +668,7 @@ func TestIIFTransactionDataFileParseImportedData_ParseSplitTransactionDescriptio "TRNS\t09/01/2024\tTest Account\t\"Test\"\t123.45\t\"foo bar\t#test\"\n"+ "SPL\t09/01/2024\tTest Account2\t\t-100.00\t\"foo\ttest#bar\"\n"+ "SPL\t09/01/2024\tTest Account3\t\t-23.45\t\n"+ - "ENDTRNS\t\t\t\t\t\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "ENDTRNS\t\t\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 2, len(allNewTransactions)) @@ -682,7 +683,7 @@ func TestIIFTransactionDataFileParseImportedData_ParseSplitTransactionDescriptio "SPL\t09/01/2024\tTest Account2\t\t-100.00\t\"test\"\n"+ "SPL\t09/01/2024\tTest Account3\tfoo\t-12.34\t\n"+ "SPL\t09/01/2024\tTest Account4\t\t-11.11\t\n"+ - "ENDTRNS\t\t\t\t\t\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "ENDTRNS\t\t\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 3, len(allNewTransactions)) @@ -708,7 +709,7 @@ func TestIIFTransactionDataFileParseImportedData_NotSupportedSplitTransaction(t "TRNS\tBEGINBALCHECK\t09/01/2024\tTest Account\t123.45\n"+ "SPL\tBEGINBALCHECK\t09/01/2024\tTest Account2\t-100.00\n"+ "SPL\tBEGINBALCHECK\t09/01/2024\tTest Account3\t-23.45\n"+ - "ENDTRNS\t\t\t\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "ENDTRNS\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrNotSupportedSplitTransactions.Message) // Transaction with invalid amount @@ -719,7 +720,7 @@ func TestIIFTransactionDataFileParseImportedData_NotSupportedSplitTransaction(t "TRNS\t09/01/2024\tTest Account\t123 45\n"+ "SPL\t09/01/2024\tTest Account2\t-100.00\n"+ "SPL\t09/01/2024\tTest Account3\t-23.45\n"+ - "ENDTRNS\t\t\t\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "ENDTRNS\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrAmountInvalid.Message) // Transaction split data with invalid amount @@ -730,7 +731,7 @@ func TestIIFTransactionDataFileParseImportedData_NotSupportedSplitTransaction(t "TRNS\t09/01/2024\tTest Account\t123.45\n"+ "SPL\t09/01/2024\tTest Account2\t-100 00\n"+ "SPL\t09/01/2024\tTest Account3\t-23.45\n"+ - "ENDTRNS\t\t\t\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "ENDTRNS\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrAmountInvalid.Message) // Transaction amount not equal to sum of split data amount @@ -741,7 +742,7 @@ func TestIIFTransactionDataFileParseImportedData_NotSupportedSplitTransaction(t "TRNS\t09/01/2024\tTest Account\t123.00\n"+ "SPL\t09/01/2024\tTest Account2\t-100.00\n"+ "SPL\t09/01/2024\tTest Account3\t-23.45\n"+ - "ENDTRNS\t\t\t\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "ENDTRNS\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrNotSupportedSplitTransactions.Message) } @@ -760,7 +761,7 @@ func TestIIFTransactionDataFileParseImportedData_InvalidDataLines(t *testing.T) "!SPL\tDATE\tACCNT\tAMOUNT\n"+ "!ENDTRNS\t\t\t\n"+ "SPL\t09/01/2024\tTest Account2\t-123.45\n"+ - "ENDTRNS\t\t\t\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "ENDTRNS\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrInvalidIIFFile.Message) // Missing Transaction And Split Line @@ -768,7 +769,7 @@ func TestIIFTransactionDataFileParseImportedData_InvalidDataLines(t *testing.T) "!TRNS\tDATE\tACCNT\tAMOUNT\n"+ "!SPL\tDATE\tACCNT\tAMOUNT\n"+ "!ENDTRNS\t\t\t\n"+ - "ENDTRNS\t\t\t\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "ENDTRNS\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrInvalidIIFFile.Message) // Missing Split Line @@ -777,7 +778,7 @@ func TestIIFTransactionDataFileParseImportedData_InvalidDataLines(t *testing.T) "!SPL\tDATE\tACCNT\tAMOUNT\n"+ "!ENDTRNS\t\t\t\n"+ "TRNS\t09/01/2024\tTest Account\t123.45\n"+ - "ENDTRNS\t\t\t\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "ENDTRNS\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrInvalidIIFFile.Message) // Missing Transaction End Line @@ -786,7 +787,7 @@ func TestIIFTransactionDataFileParseImportedData_InvalidDataLines(t *testing.T) "!SPL\tDATE\tACCNT\tAMOUNT\n"+ "!ENDTRNS\t\t\t\n"+ "TRNS\t09/01/2024\tTest Account\t123.45\n"+ - "SPL\t09/01/2024\tTest Account2\t-123.45\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "SPL\t09/01/2024\tTest Account2\t-123.45\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrInvalidIIFFile.Message) // Missing Transaction End Line (following is another header) @@ -797,7 +798,7 @@ func TestIIFTransactionDataFileParseImportedData_InvalidDataLines(t *testing.T) "TRNS\t09/01/2024\tTest Account\t123.45\n"+ "SPL\t09/01/2024\tTest Account2\t-123.45\n"+ "!ACCNT\tNAME\tACCNTTYPE\n"+ - "ACCNT\tTest Account\tBANK\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "ACCNT\tTest Account\tBANK\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrInvalidIIFFile.Message) // Invalid Line @@ -808,7 +809,7 @@ func TestIIFTransactionDataFileParseImportedData_InvalidDataLines(t *testing.T) "TRNS\t09/01/2024\tTest Account\t123.45\n"+ "SPL\t09/01/2024\tTest Account2\t-123.45\n"+ "TEST\t\t\t\n"+ - "ENDTRNS\t\t\t\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "ENDTRNS\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrInvalidIIFFile.Message) // Repeat Transaction Line @@ -819,7 +820,7 @@ func TestIIFTransactionDataFileParseImportedData_InvalidDataLines(t *testing.T) "TRNS\t09/01/2024\tTest Account\t123.45\n"+ "TRNS\t09/01/2024\tTest Account\t123.45\n"+ "SPL\t09/01/2024\tTest Account2\t-123.45\n"+ - "ENDTRNS\t\t\t\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "ENDTRNS\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrInvalidIIFFile.Message) // Repeat Transaction End Line @@ -831,7 +832,7 @@ func TestIIFTransactionDataFileParseImportedData_InvalidDataLines(t *testing.T) "TRNS\t09/01/2024\tTest Account\t123.45\n"+ "SPL\t09/01/2024\tTest Account2\t-123.45\n"+ "ENDTRNS\t\t\t\n"+ - "ENDTRNS\t\t\t\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "ENDTRNS\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrInvalidIIFFile.Message) } @@ -848,25 +849,25 @@ func TestIIFTransactionDataFileParseImportedData_InvalidHeaderLines(t *testing.T _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte( "TRNS\t09/01/2024\tTest Account\t123.45\n"+ "SPL\t09/01/2024\tTest Account2\t-123.45\n"+ - "ENDTRNS\t\t\t\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "ENDTRNS\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrInvalidIIFFile.Message) // Missing Transaction Sample Line _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte( "!SPL\tDATE\tACCNT\tAMOUNT\n"+ - "!ENDTRNS\t\t\t\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "!ENDTRNS\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrInvalidIIFFile.Message) // Missing Split Sample Line _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte( "!TRNS\tDATE\tACCNT\tAMOUNT\n"+ - "!ENDTRNS\t\t\t\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "!ENDTRNS\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrInvalidIIFFile.Message) // Missing Transaction End Sample Line _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte( "!TRNS\tDATE\tACCNT\tAMOUNT\n"+ - "!SPL\tDATE\tACCNT\tAMOUNT\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "!SPL\tDATE\tACCNT\tAMOUNT\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrInvalidIIFFile.Message) // Missing Transaction End Sample Line (following is data line) @@ -875,14 +876,14 @@ func TestIIFTransactionDataFileParseImportedData_InvalidHeaderLines(t *testing.T "!SPL\tDATE\tACCNT\tAMOUNT\n"+ "TRNS\t09/01/2024\tTest Account\t123.45\n"+ "SPL\t09/01/2024\tTest Account2\t-123.45\n"+ - "ENDTRNS\t\t\t\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "ENDTRNS\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrInvalidIIFFile.Message) // Invalid Sample Line _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte( "!TRNS\tDATE\tACCNT\tAMOUNT\n"+ "!TEST\tDATE\tACCNT\tAMOUNT\n"+ - "!ENDTRNS\t\t\t\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "!ENDTRNS\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrInvalidIIFFile.Message) } @@ -902,7 +903,7 @@ func TestIIFTransactionDataFileParseImportedData_MissingRequiredColumn(t *testin "!ENDTRNS\t\t\t\n"+ "TRNS\tTest Account\t123.45\n"+ "SPL\tTest Account2\t-123.45\n"+ - "ENDTRNS\t\t\t\t\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "ENDTRNS\t\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message) // Missing Account Column @@ -912,7 +913,7 @@ func TestIIFTransactionDataFileParseImportedData_MissingRequiredColumn(t *testin "!ENDTRNS\t\t\t\n"+ "TRNS\t09/01/2024\t123.45\n"+ "SPL\t09/01/2024\t-123.45\n"+ - "ENDTRNS\t\t\t\t\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "ENDTRNS\t\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message) // Missing Amount Column @@ -922,6 +923,6 @@ func TestIIFTransactionDataFileParseImportedData_MissingRequiredColumn(t *testin "!ENDTRNS\t\t\t\n"+ "TRNS\t09/01/2024\tTest Account\n"+ "SPL\t09/01/2024\tTest Account2\n"+ - "ENDTRNS\t\t\t\t\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "ENDTRNS\t\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message) } diff --git a/pkg/converters/jdcom/jdcom_finance_transaction_data_csv_file_importer.go b/pkg/converters/jdcom/jdcom_finance_transaction_data_csv_file_importer.go index 9741dceb..980fd3f0 100644 --- a/pkg/converters/jdcom/jdcom_finance_transaction_data_csv_file_importer.go +++ b/pkg/converters/jdcom/jdcom_finance_transaction_data_csv_file_importer.go @@ -2,6 +2,7 @@ package jdcom import ( "bytes" + "time" "golang.org/x/text/encoding/unicode" "golang.org/x/text/transform" @@ -27,7 +28,7 @@ var ( ) // ParseImportedData returns the imported data by parsing the jd.com finance transaction csv data -func (c *jdComFinanceTransactionDataCsvFileImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezoneOffset int16, additionalOptions converter.TransactionDataImporterOptions, accountMap map[string]*models.Account, expenseCategoryMap map[string]map[string]*models.TransactionCategory, incomeCategoryMap map[string]map[string]*models.TransactionCategory, transferCategoryMap map[string]map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionTag, error) { +func (c *jdComFinanceTransactionDataCsvFileImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezone *time.Location, additionalOptions converter.TransactionDataImporterOptions, accountMap map[string]*models.Account, expenseCategoryMap map[string]map[string]*models.TransactionCategory, incomeCategoryMap map[string]map[string]*models.TransactionCategory, transferCategoryMap map[string]map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionTag, error) { fallback := unicode.UTF8.NewDecoder() reader := transform.NewReader(bytes.NewReader(data), unicode.BOMOverride(fallback)) @@ -60,5 +61,5 @@ func (c *jdComFinanceTransactionDataCsvFileImporter) ParseImportedData(ctx core. transactionDataTable := datatable.CreateNewTransactionDataTableFromCommonDataTable(commonDataTable, jdComFinanceTransactionSupportedColumns, transactionRowParser) dataTableImporter := converter.CreateNewSimpleImporterWithTypeNameMapping(jdComFinanceTransactionTypeNameMapping) - return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezoneOffset, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap) + return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezone, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap) } diff --git a/pkg/converters/jdcom/jdcom_finance_transaction_data_csv_file_importer_test.go b/pkg/converters/jdcom/jdcom_finance_transaction_data_csv_file_importer_test.go index 3f698115..be365f54 100644 --- a/pkg/converters/jdcom/jdcom_finance_transaction_data_csv_file_importer_test.go +++ b/pkg/converters/jdcom/jdcom_finance_transaction_data_csv_file_importer_test.go @@ -31,7 +31,7 @@ func TestJDComFinanceCsvFileImporterParseImportedData_MinimumValidData(t *testin "2025-09-01 12:34:56,xxx,xxx,123.45,银行卡,交易成功,支出,其他网购\n" + "2025-09-01 23:59:59,xxx,京东钱包余额充值,0.05,银行卡,交易成功,不计收支,余额\n" + "2025-09-02 23:59:59,xxx,京东余额提现,0.03,银行卡,交易成功,不计收支,余额\n" - allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := importer.ParseImportedData(context, user, []byte(data), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := importer.ParseImportedData(context, user, []byte(data), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 4, len(allNewTransactions)) @@ -111,7 +111,7 @@ func TestJDComFinanceCsvFileImporterParseImportedData_ParseRefundTransaction(t * "2025-09-01 02:34:56,xxx,xxx,0.12(已全额退款),银行卡,交易成功,不计收支\n" + "2025-09-02 01:23:45,xxx,xxx,3.45,银行卡,退款成功,不计收支\n" + "2025-09-02 02:34:56,xxx,xxx,123.45(已退款3.45),银行卡,交易成功,支出\n" - allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(data1), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(data1), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, int64(1234567890), allNewTransactions[0].Uid) @@ -154,7 +154,7 @@ func TestJDComFinanceCsvFileImporterParseImportedData_ParseInvalidTime(t *testin "\n" + "交易时间,商户名称,交易说明,金额,收/付款方式,交易状态,收/支\n" + "2025-09-01T01:23:45,xxx,xxx,0.12,银行卡,交易成功,支出\n" - _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(data1), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(data1), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message) data2 := "导出信息:\n" + @@ -163,7 +163,7 @@ func TestJDComFinanceCsvFileImporterParseImportedData_ParseInvalidTime(t *testin "\n" + "交易时间,商户名称,交易说明,金额,收/付款方式,交易状态,收/支\n" + "09/01/2025 01:23:45,xxx,xxx,0.12,银行卡,交易成功,支出\n" - _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data2), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data2), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message) } @@ -182,7 +182,7 @@ func TestJDComFinanceCsvFileImporterParseImportedData_ParseInvalidType(t *testin "\n" + "交易时间,商户名称,交易说明,金额,收/付款方式,交易状态,收/支\n" + "2025-09-01 01:23:45,xxx,xxx,0.12,银行卡,交易成功,转账\n" - _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(data), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(data), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrNotFoundTransactionDataInFile.Message) } @@ -201,7 +201,7 @@ func TestJDComFinanceCsvFileImporterParseImportedData_ParseInvalidAmount(t *test "\n" + "交易时间,商户名称,交易说明,金额,收/付款方式,交易状态,收/支\n" + "2025-09-01 01:23:45,xxx,xxx,¥0.12,银行卡,交易成功,支出\n" - _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(data), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(data), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrAmountInvalid.Message) } @@ -221,7 +221,7 @@ func TestJDComFinanceCsvFileImporterParseImportedData_ParseAccountName(t *testin "\n" + "交易时间,商户名称,交易说明,金额,收/付款方式,交易状态,收/支,交易分类\n" + "2025-09-01 01:23:45,xxx,京东钱包余额充值,0.05,银行卡,交易成功,不计收支,余额\n" - allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(data1), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(data1), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) @@ -236,7 +236,7 @@ func TestJDComFinanceCsvFileImporterParseImportedData_ParseAccountName(t *testin "\n" + "交易时间,商户名称,交易说明,金额,收/付款方式,交易状态,收/支,交易分类\n" + "2025-09-01 01:23:45,xxx,京东余额提现,0.05,银行卡,交易成功,不计收支,余额\n" - allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data2), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data2), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) @@ -253,7 +253,7 @@ func TestJDComFinanceCsvFileImporterParseImportedData_ParseAccountName(t *testin "2025-09-01 01:23:45,xxx,京东小金库-转入,0.05,余额,交易成功,不计收支,小金库\n" assert.Nil(t, err) - allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data3), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data3), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) @@ -270,7 +270,7 @@ func TestJDComFinanceCsvFileImporterParseImportedData_ParseAccountName(t *testin "2025-09-01 01:23:45,xxx,京东小金库-转出,0.05,余额,交易成功,不计收支,小金库\n" assert.Nil(t, err) - allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data4), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data4), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) @@ -287,7 +287,7 @@ func TestJDComFinanceCsvFileImporterParseImportedData_ParseAccountName(t *testin "2025-09-01 01:23:45,xxx,价保退款,0.05,银行卡,交易成功,不计收支,其他\n" assert.Nil(t, err) - allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data5), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data5), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) @@ -303,7 +303,7 @@ func TestJDComFinanceCsvFileImporterParseImportedData_ParseAccountName(t *testin "2025-09-01 01:23:45,xxx,白条主动还款,0.05,银行卡,交易成功,不计收支,白条\n" assert.Nil(t, err) - allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data6), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data6), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) @@ -327,7 +327,7 @@ func TestJDComFinanceCsvFileImporterParseImportedData_ParseDescription(t *testin "\n" + "交易时间,商户名称,交易说明,金额,收/付款方式,交易状态,收/支\n" + "2025-09-01 01:23:45,xxx,,0.12,银行卡,交易成功,支出\n" - allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(data1), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(data1), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) @@ -339,7 +339,7 @@ func TestJDComFinanceCsvFileImporterParseImportedData_ParseDescription(t *testin "\n" + "交易时间,商户名称,交易说明,交易说明,金额,收/付款方式,交易状态,收/支,备注\n" + "2025-09-01 01:23:45,xxx,xxx,Test,0.12,银行卡,交易成功,支出,\"foo\"\"bar,\ntest\"\n" - allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data2), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data2), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, "foo\"bar,\ntest", allNewTransactions[0].Comment) @@ -349,7 +349,7 @@ func TestJDComFinanceCsvFileImporterParseImportedData_ParseDescription(t *testin "\n" + "交易时间,商户名称,交易说明,交易说明,金额,收/付款方式,交易状态,收/支,备注\n" + "2025-09-01 01:23:45,xxx,xxx,Test,0.12,银行卡,交易成功,支出,\n" - allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data3), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data3), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, "Test", allNewTransactions[0].Comment) } @@ -369,7 +369,7 @@ func TestJDComFinanceCsvFileImporterParseImportedData_SkipUnknownStatusTransacti "\n" + "交易时间,商户名称,交易说明,金额,收/付款方式,交易状态,收/支\n" + "2025-09-01 01:23:45,xxx,xxx,0.12,银行卡,xxxx,支出\n" - _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(data), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(data), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrNotFoundTransactionDataInFile.Message) } @@ -388,7 +388,7 @@ func TestJDComFinanceCsvFileImporterParseImportedData_SkipUnknownMemoTransferTra "\n" + "交易时间,商户名称,交易说明,金额,收/付款方式,交易状态,收/支\n" + "2025-09-01 01:23:45,xxx,xxx,0.12,银行卡,交易成功,不计收支\n" - _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(data), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(data), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrNotFoundTransactionDataInFile.Message) } @@ -403,10 +403,10 @@ func TestJDComFinanceCsvFileImporterParseImportedData_MissingFileHeader(t *testi data := "交易时间,商户名称,交易说明,金额,收/付款方式,交易状态,收/支\n" + "2025-09-01 01:23:45,xxx,xxx,0.12,银行卡,交易成功,支出\n" - _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(data), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(data), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrInvalidFileHeader.Message) - _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(""), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrInvalidFileHeader.Message) } @@ -426,7 +426,7 @@ func TestJDComFinanceCsvFileImporterParseImportedData_MissingRequiredColumn(t *t "\n" + "商户名称,交易说明,金额,收/付款方式,交易状态,收/支\n" + "xxx,xxx,0.12,银行卡,交易成功,支出\n" - _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(data1), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(data1), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrInvalidFileHeader.Message) // Missing Merchant Name Column @@ -436,7 +436,7 @@ func TestJDComFinanceCsvFileImporterParseImportedData_MissingRequiredColumn(t *t "\n" + "交易时间,交易说明,金额,收/付款方式,交易状态,收/支\n" + "2025-09-01 01:23:45,xxx,0.12,银行卡,交易成功,支出\n" - _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data2), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data2), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message) // Missing Transaction Memo Column @@ -446,7 +446,7 @@ func TestJDComFinanceCsvFileImporterParseImportedData_MissingRequiredColumn(t *t "\n" + "交易时间,商户名称,金额,收/付款方式,交易状态,收/支\n" + "2025-09-01 01:23:45,xxx,0.12,银行卡,交易成功,支出\n" - _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data3), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data3), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message) // Missing Amount Column @@ -456,7 +456,7 @@ func TestJDComFinanceCsvFileImporterParseImportedData_MissingRequiredColumn(t *t "\n" + "交易时间,商户名称,交易说明,收/付款方式,交易状态,收/支\n" + "2025-09-01 01:23:45,xxx,xxx,银行卡,交易成功,支出\n" - _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data4), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data4), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message) // Missing Related Account Column @@ -466,7 +466,7 @@ func TestJDComFinanceCsvFileImporterParseImportedData_MissingRequiredColumn(t *t "\n" + "交易时间,商户名称,交易说明,金额,交易状态,收/支\n" + "2025-09-01 01:23:45,xxx,xxx,0.12,交易成功,支出\n" - _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data5), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data5), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message) // Missing Status Column @@ -476,7 +476,7 @@ func TestJDComFinanceCsvFileImporterParseImportedData_MissingRequiredColumn(t *t "\n" + "交易时间,商户名称,交易说明,金额,收/付款方式,收/支\n" + "2025-09-01 01:23:45,xxx,xxx,0.12,银行卡,支出\n" - _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data6), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data6), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message) // Missing Type Column @@ -486,7 +486,7 @@ func TestJDComFinanceCsvFileImporterParseImportedData_MissingRequiredColumn(t *t "\n" + "交易时间,商户名称,交易说明,金额,收/付款方式,交易状态\n" + "2025-09-01 01:23:45,xxx,xxx,0.12,银行卡,交易成功\n" - _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data7), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data7), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message) } @@ -504,6 +504,6 @@ func TestJDComFinanceCsvFileImporterParseImportedData_NoTransactionData(t *testi "日期区间:2025-01-01 至 2025-09-01\n" + "\n" + "交易时间,商户名称,交易说明,金额,收/付款方式,交易状态,收/支\n" - _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(data), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(data), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrNotFoundTransactionDataInFile.Message) } diff --git a/pkg/converters/mt/mt_transaction_data_file_importer.go b/pkg/converters/mt/mt_transaction_data_file_importer.go index 435d749d..5f45ce2c 100644 --- a/pkg/converters/mt/mt_transaction_data_file_importer.go +++ b/pkg/converters/mt/mt_transaction_data_file_importer.go @@ -1,6 +1,8 @@ package mt import ( + "time" + "github.com/mayswind/ezbookkeeping/pkg/converters/converter" "github.com/mayswind/ezbookkeeping/pkg/core" "github.com/mayswind/ezbookkeeping/pkg/models" @@ -22,7 +24,7 @@ var ( ) // ParseImportedData returns the imported data by parsing the mt940 file statement data -func (c *mt940TransactionDataFileImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezoneOffset int16, additionalOptions converter.TransactionDataImporterOptions, accountMap map[string]*models.Account, expenseCategoryMap map[string]map[string]*models.TransactionCategory, incomeCategoryMap map[string]map[string]*models.TransactionCategory, transferCategoryMap map[string]map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionTag, error) { +func (c *mt940TransactionDataFileImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezone *time.Location, additionalOptions converter.TransactionDataImporterOptions, accountMap map[string]*models.Account, expenseCategoryMap map[string]map[string]*models.TransactionCategory, incomeCategoryMap map[string]map[string]*models.TransactionCategory, transferCategoryMap map[string]map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionTag, error) { mt940DataReader := createNewMT940FileReader(data) mt940Data, err := mt940DataReader.read(ctx) @@ -38,5 +40,5 @@ func (c *mt940TransactionDataFileImporter) ParseImportedData(ctx core.Context, u dataTableImporter := converter.CreateNewSimpleImporterWithTypeNameMapping(mt940TransactionTypeNameMapping) - return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezoneOffset, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap) + return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezone, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap) } diff --git a/pkg/converters/mt/mt_transaction_data_file_importer_test.go b/pkg/converters/mt/mt_transaction_data_file_importer_test.go index 162eafa3..7d0e92cc 100644 --- a/pkg/converters/mt/mt_transaction_data_file_importer_test.go +++ b/pkg/converters/mt/mt_transaction_data_file_importer_test.go @@ -2,6 +2,7 @@ package mt import ( "testing" + "time" "github.com/stretchr/testify/assert" @@ -32,7 +33,7 @@ func TestMT940TransactionDataFileParseImportedData_MinimumValidData(t *testing.T :61:2506020603D234,56NTRFFOOBAR :86:Transaction 2 :62F:C250601CNY123,45 - -}`), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + -}`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) @@ -92,7 +93,7 @@ func TestMT940TransactionDataFileParseImportedData_ParseTransactionValidAmountAn :61:250603C1,NTRFTEST :86:Transaction 3 :62F:C250601CNY123,45 - -}`), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + -}`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 3, len(allNewTransactions)) @@ -118,7 +119,7 @@ func TestMT940TransactionDataFileParseImportedData_ParseTransactionInvalidAmount :60F:C250601CNY123,45 :61:2506010602C123 45NTRFTEST :62F:C250601CNY123,45 - -}`), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + -}`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrInvalidMT940File.Message) _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte( @@ -129,7 +130,7 @@ func TestMT940TransactionDataFileParseImportedData_ParseTransactionInvalidAmount :60F:C250601CNY123,45 :61:2506010602C12.45NTRFTEST :62F:C250601CNY123,45 - -}`), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + -}`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrInvalidMT940File.Message) } @@ -157,7 +158,7 @@ func TestMT940TransactionDataFileParseImportedData_ParseTransactionType(t *testi :61:250604RD123,45NTRFTEST :86:Transaction 4 :62F:C250601CNY123,45 - -}`), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + -}`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 4, len(allNewTransactions)) @@ -187,7 +188,7 @@ func TestMT940TransactionDataFileParseImportedData_ParseDescription(t *testing.T Part 2 Part 3 :62F:C250601CNY123,45 - -}`), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + -}`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) @@ -210,6 +211,6 @@ func TestMT940TransactionDataFileParseImportedData_MissingRequiredField(t *testi :28C:123/1 :61:250601C123,45NTRFTEST :86:Transaction 1 - -}`), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + -}`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message) } diff --git a/pkg/converters/ofx/ofx_transaction_data_file_importer.go b/pkg/converters/ofx/ofx_transaction_data_file_importer.go index 3c1d0329..c189b0ac 100644 --- a/pkg/converters/ofx/ofx_transaction_data_file_importer.go +++ b/pkg/converters/ofx/ofx_transaction_data_file_importer.go @@ -1,6 +1,8 @@ package ofx import ( + "time" + "github.com/mayswind/ezbookkeeping/pkg/converters/converter" "github.com/mayswind/ezbookkeeping/pkg/core" "github.com/mayswind/ezbookkeeping/pkg/models" @@ -23,7 +25,7 @@ var ( ) // ParseImportedData returns the imported data by parsing the open financial exchange (ofx) file transaction data -func (c *ofxTransactionDataImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezoneOffset int16, additionalOptions converter.TransactionDataImporterOptions, accountMap map[string]*models.Account, expenseCategoryMap map[string]map[string]*models.TransactionCategory, incomeCategoryMap map[string]map[string]*models.TransactionCategory, transferCategoryMap map[string]map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionTag, error) { +func (c *ofxTransactionDataImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezone *time.Location, additionalOptions converter.TransactionDataImporterOptions, accountMap map[string]*models.Account, expenseCategoryMap map[string]map[string]*models.TransactionCategory, incomeCategoryMap map[string]map[string]*models.TransactionCategory, transferCategoryMap map[string]map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionTag, error) { ofxDataReader, err := createNewOFXFileReader(ctx, data) if err != nil { @@ -44,5 +46,5 @@ func (c *ofxTransactionDataImporter) ParseImportedData(ctx core.Context, user *m dataTableImporter := converter.CreateNewSimpleImporterWithTypeNameMapping(ofxTransactionTypeNameMapping) - return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezoneOffset, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap) + return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezone, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap) } diff --git a/pkg/converters/ofx/ofx_transaction_data_file_importer_test.go b/pkg/converters/ofx/ofx_transaction_data_file_importer_test.go index fb7722f7..41fc193b 100644 --- a/pkg/converters/ofx/ofx_transaction_data_file_importer_test.go +++ b/pkg/converters/ofx/ofx_transaction_data_file_importer_test.go @@ -2,6 +2,7 @@ package ofx import ( "testing" + "time" "github.com/stretchr/testify/assert" @@ -77,7 +78,7 @@ func TestOFXTransactionDataFileParseImportedData_MinimumValidData(t *testing.T) " \n"+ " \n"+ " \n"+ - ""), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + ""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) @@ -211,7 +212,7 @@ func TestOFXTransactionDataFileParseImportedData_ParseAccountTo(t *testing.T) { " \n"+ " \n"+ " \n"+ - ""), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + ""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) @@ -296,7 +297,7 @@ func TestOFXTransactionDataFileParseImportedData_ParseValidTransactionTime(t *te " \n"+ " \n"+ " \n"+ - ""), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + ""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) @@ -338,7 +339,7 @@ func TestOFXTransactionDataFileParseImportedData_ParseInvalidTransactionTime(t * " \n"+ " \n"+ " \n"+ - ""), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + ""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message) _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte( @@ -360,7 +361,7 @@ func TestOFXTransactionDataFileParseImportedData_ParseInvalidTransactionTime(t * " \n"+ " \n"+ " \n"+ - ""), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + ""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message) _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte( @@ -382,7 +383,7 @@ func TestOFXTransactionDataFileParseImportedData_ParseInvalidTransactionTime(t * " \n"+ " \n"+ " \n"+ - ""), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + ""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message) _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte( @@ -404,7 +405,7 @@ func TestOFXTransactionDataFileParseImportedData_ParseInvalidTransactionTime(t * " \n"+ " \n"+ " \n"+ - ""), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + ""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message) } @@ -436,7 +437,7 @@ func TestOFXTransactionDataFileParseImportedData_ParseAmount_CommaAsDecimalPoint " \n"+ " \n"+ " \n"+ - ""), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + ""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) @@ -472,7 +473,7 @@ func TestOFXTransactionDataFileParseImportedData_ParseInvalidAmount(t *testing.T " \n"+ " \n"+ " \n"+ - ""), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + ""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrAmountInvalid.Message) } @@ -505,7 +506,7 @@ func TestOFXTransactionDataFileParseImportedData_ParseTransactionCurrency(t *tes " \n"+ " \n"+ " \n"+ - ""), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + ""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) @@ -542,7 +543,7 @@ func TestOFXTransactionDataFileParseImportedData_ParseDescription(t *testing.T) " \n"+ " \n"+ " \n"+ - ""), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + ""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) @@ -568,7 +569,7 @@ func TestOFXTransactionDataFileParseImportedData_ParseDescription(t *testing.T) " \n"+ " \n"+ " \n"+ - ""), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + ""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) @@ -596,7 +597,7 @@ func TestOFXTransactionDataFileParseImportedData_ParseDescription(t *testing.T) " \n"+ " \n"+ " \n"+ - ""), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + ""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) @@ -629,7 +630,7 @@ func TestOFXTransactionDataFileParseImportedData_MissingAccountFromNode(t *testi " \n"+ " \n"+ " \n"+ - ""), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + ""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrMissingAccountData.Message) } @@ -661,7 +662,7 @@ func TestOFXTransactionDataFileParseImportedData_MissingCurrencyNode(t *testing. " \n"+ " \n"+ " \n"+ - ""), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + ""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message) } @@ -693,7 +694,7 @@ func TestOFXTransactionDataFileParseImportedData_MissingTransactionRequiredNode( " \n"+ " \n"+ " \n"+ - ""), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + ""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrMissingTransactionTime.Message) // Missing Transaction Type Node @@ -715,7 +716,7 @@ func TestOFXTransactionDataFileParseImportedData_MissingTransactionRequiredNode( " \n"+ " \n"+ " \n"+ - ""), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + ""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrTransactionTypeInvalid.Message) // Missing Amount Node @@ -737,6 +738,6 @@ func TestOFXTransactionDataFileParseImportedData_MissingTransactionRequiredNode( " \n"+ " \n"+ " \n"+ - ""), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + ""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrAmountInvalid.Message) } diff --git a/pkg/converters/qif/qif_transaction_data_file_importer.go b/pkg/converters/qif/qif_transaction_data_file_importer.go index ff04e57b..8f5f3918 100644 --- a/pkg/converters/qif/qif_transaction_data_file_importer.go +++ b/pkg/converters/qif/qif_transaction_data_file_importer.go @@ -1,6 +1,8 @@ package qif import ( + "time" + "github.com/mayswind/ezbookkeeping/pkg/converters/converter" "github.com/mayswind/ezbookkeeping/pkg/core" "github.com/mayswind/ezbookkeeping/pkg/models" @@ -35,7 +37,7 @@ var ( ) // ParseImportedData returns the imported data by parsing the quicken interchange format (qif) transaction data -func (c *qifTransactionDataImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezoneOffset int16, additionalOptions converter.TransactionDataImporterOptions, accountMap map[string]*models.Account, expenseCategoryMap map[string]map[string]*models.TransactionCategory, incomeCategoryMap map[string]map[string]*models.TransactionCategory, transferCategoryMap map[string]map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionTag, error) { +func (c *qifTransactionDataImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezone *time.Location, additionalOptions converter.TransactionDataImporterOptions, accountMap map[string]*models.Account, expenseCategoryMap map[string]map[string]*models.TransactionCategory, incomeCategoryMap map[string]map[string]*models.TransactionCategory, transferCategoryMap map[string]map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionTag, error) { qifDataReader := createNewQifDataReader(data) qifData, err := qifDataReader.read(ctx) @@ -51,5 +53,5 @@ func (c *qifTransactionDataImporter) ParseImportedData(ctx core.Context, user *m dataTableImporter := converter.CreateNewSimpleImporterWithTypeNameMapping(qifTransactionTypeNameMapping) - return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezoneOffset, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap) + return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezone, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap) } diff --git a/pkg/converters/qif/qif_transaction_data_file_importer_test.go b/pkg/converters/qif/qif_transaction_data_file_importer_test.go index 6c199ea6..77f026f7 100644 --- a/pkg/converters/qif/qif_transaction_data_file_importer_test.go +++ b/pkg/converters/qif/qif_transaction_data_file_importer_test.go @@ -2,6 +2,7 @@ package qif import ( "testing" + "time" "github.com/stretchr/testify/assert" @@ -43,7 +44,7 @@ func TestQIFTransactionDataFileParseImportedData_MinimumValidData(t *testing.T) "D2024-09-05\n"+ "T0.06\n"+ "L[Test Account2]\n"+ - "^\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "^\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) @@ -138,7 +139,7 @@ func TestQIFTransactionDataFileParseImportedData_ParseYearMonthDayDateFormatTime "^\n"+ "D2024'9.5\n"+ "T-123.45\n"+ - "^\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "^\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) @@ -176,7 +177,7 @@ func TestQIFTransactionDataFileParseImportedData_ParseMonthDayYearDateFormatTime "^\n"+ "D9.5'2024\n"+ "T-123.45\n"+ - "^\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "^\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) @@ -214,7 +215,7 @@ func TestQIFTransactionDataFileParseImportedData_ParseDayYearMonthDateFormatTime "^\n"+ "D5'9.2024\n"+ "T-123.45\n"+ - "^\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "^\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) @@ -252,7 +253,7 @@ func TestQIFTransactionDataFileParseImportedData_ParseShortYearMonthDayDateForma "^\n"+ "D24'9.5\n"+ "T-123.45\n"+ - "^\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "^\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) @@ -278,7 +279,7 @@ func TestQIFTransactionDataFileParseImportedData_ParseInvalidTime(t *testing.T) "!Type:Bank\n"+ "D2024 09 01\n"+ "T-123.45\n"+ - "^\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "^\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message) } @@ -295,7 +296,7 @@ func TestQIFTransactionDataFileParseImportedData_ParseAmountWithThousandsSeparat "!Type:Bank\n"+ "D2024-09-01\n"+ "T-123,456.78\n"+ - "^\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "^\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) @@ -317,7 +318,7 @@ func TestQIFTransactionDataFileParseImportedData_ParseInvalidAmount(t *testing.T "!Type:Bank\n"+ "D2024-09-01\n"+ "T-123 45\n"+ - "^\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "^\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrAmountInvalid.Message) } @@ -334,7 +335,7 @@ func TestQIFTransactionDataFileParseImportedData_ParseAccountType(t *testing.T) "!Type:Cash\n"+ "D2024-09-01\n"+ "T-123.45\n"+ - "^\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "^\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) @@ -347,7 +348,7 @@ func TestQIFTransactionDataFileParseImportedData_ParseAccountType(t *testing.T) "!Type:CCard\n"+ "D2024-09-01\n"+ "T-123.45\n"+ - "^\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "^\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) @@ -360,7 +361,7 @@ func TestQIFTransactionDataFileParseImportedData_ParseAccountType(t *testing.T) "!Type:Oth A\n"+ "D2024-09-01\n"+ "T-123.45\n"+ - "^\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "^\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) @@ -373,7 +374,7 @@ func TestQIFTransactionDataFileParseImportedData_ParseAccountType(t *testing.T) "!Type:Oth L\n"+ "D2024-09-01\n"+ "T-123.45\n"+ - "^\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "^\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) @@ -399,7 +400,7 @@ func TestQIFTransactionDataFileParseImportedData_ParseAccount(t *testing.T) { "!Type:Bank\n"+ "D2024-09-01\n"+ "T-123.45\n"+ - "^\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "^\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) @@ -425,7 +426,7 @@ func TestQIFTransactionDataFileParseImportedData_ParseAmountWithLeadingPlusSign( "!Type:Bank\n"+ "D2024-09-01\n"+ "T+123.45\n"+ - "^\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "^\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) @@ -446,7 +447,7 @@ func TestQIFTransactionDataFileParseImportedData_ParseSubCategory(t *testing.T) "D2024-09-01\n"+ "T-123.45\n"+ "LTest Category:Sub Category\n"+ - "^\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "^\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) @@ -477,7 +478,7 @@ func TestQIFTransactionDataFileParseImportedData_ParseDescription(t *testing.T) "D2024-09-02\n"+ "T-234.56\n"+ "PTest2\n"+ - "^\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "^\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 2, len(allNewTransactions)) @@ -494,7 +495,7 @@ func TestQIFTransactionDataFileParseImportedData_ParseDescription(t *testing.T) "D2024-09-02\n"+ "T-234.56\n"+ "PTest2\n"+ - "^\n"), 0, converter.DefaultImporterOptions.WithPayeeAsDescription(), nil, nil, nil, nil, nil) + "^\n"), time.UTC, converter.DefaultImporterOptions.WithPayeeAsDescription(), nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 2, len(allNewTransactions)) @@ -516,7 +517,7 @@ func TestQIFTransactionDataFileParseImportedData_WithAdditionalOptions(t *testin "D2024-09-01\n"+ "T-123.45\n"+ "PTest2\n"+ - "^\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "^\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) @@ -527,7 +528,7 @@ func TestQIFTransactionDataFileParseImportedData_WithAdditionalOptions(t *testin "D2024-09-01\n"+ "T-123.45\n"+ "PTest2\n"+ - "^\n"), 0, converter.DefaultImporterOptions.WithPayeeAsTag(), nil, nil, nil, nil, nil) + "^\n"), time.UTC, converter.DefaultImporterOptions.WithPayeeAsTag(), nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) @@ -548,13 +549,13 @@ func TestQIFTransactionDataFileParseImportedData_MissingRequiredFields(t *testin _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte( "!Type:Bank\n"+ "T-123.45\n"+ - "^\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "^\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrMissingTransactionTime.Message) // Missing Amount Field _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte( "!Type:Bank\n"+ "D2024-09-01\n"+ - "^\n"), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + "^\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrAmountInvalid.Message) } diff --git a/pkg/converters/wechat/wechat_pay_transaction_data_csv_file_importer.go b/pkg/converters/wechat/wechat_pay_transaction_data_csv_file_importer.go index 0c7eeec7..5ce150f0 100644 --- a/pkg/converters/wechat/wechat_pay_transaction_data_csv_file_importer.go +++ b/pkg/converters/wechat/wechat_pay_transaction_data_csv_file_importer.go @@ -2,6 +2,7 @@ package wechat import ( "bytes" + "time" "golang.org/x/text/encoding/unicode" "golang.org/x/text/transform" @@ -27,7 +28,7 @@ var ( ) // ParseImportedData returns the imported data by parsing the wechat pay transaction csv data -func (c *wechatPayTransactionDataCsvFileImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezoneOffset int16, additionalOptions converter.TransactionDataImporterOptions, accountMap map[string]*models.Account, expenseCategoryMap map[string]map[string]*models.TransactionCategory, incomeCategoryMap map[string]map[string]*models.TransactionCategory, transferCategoryMap map[string]map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionTag, error) { +func (c *wechatPayTransactionDataCsvFileImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezone *time.Location, additionalOptions converter.TransactionDataImporterOptions, accountMap map[string]*models.Account, expenseCategoryMap map[string]map[string]*models.TransactionCategory, incomeCategoryMap map[string]map[string]*models.TransactionCategory, transferCategoryMap map[string]map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionTag, error) { fallback := unicode.UTF8.NewDecoder() reader := transform.NewReader(bytes.NewReader(data), unicode.BOMOverride(fallback)) @@ -58,5 +59,5 @@ func (c *wechatPayTransactionDataCsvFileImporter) ParseImportedData(ctx core.Con transactionDataTable := datatable.CreateNewTransactionDataTableFromCommonDataTable(commonDataTable, wechatPayTransactionSupportedColumns, transactionRowParser) dataTableImporter := converter.CreateNewSimpleImporterWithTypeNameMapping(wechatPayTransactionTypeNameMapping) - return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezoneOffset, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap) + return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezone, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap) } diff --git a/pkg/converters/wechat/wechat_pay_transaction_data_csv_file_importer_test.go b/pkg/converters/wechat/wechat_pay_transaction_data_csv_file_importer_test.go index 5c46eb6c..fa52ed2c 100644 --- a/pkg/converters/wechat/wechat_pay_transaction_data_csv_file_importer_test.go +++ b/pkg/converters/wechat/wechat_pay_transaction_data_csv_file_importer_test.go @@ -32,7 +32,7 @@ func TestWeChatPayCsvFileImporterParseImportedData_MinimumValidData(t *testing.T "2024-09-01 12:34:56,商户消费,支出,¥123.45,支付成功\n" + "2024-09-01 23:59:59,零钱充值,/,¥0.05,充值完成\n" + "2024-09-02 23:59:59,零钱提现,/,¥0.03,提现已到账\n" - allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := importer.ParseImportedData(context, user, []byte(data), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := importer.ParseImportedData(context, user, []byte(data), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 4, len(allNewTransactions)) @@ -109,7 +109,7 @@ func TestWeChatPayCsvFileImporterParseImportedData_ParseRefundTransaction(t *tes "----------------------微信支付账单明细列表--------------------,,,,\n" + "交易时间,交易类型,收/支,金额(元),当前状态\n" + "2024-09-01 01:23:45,xxx-退款,收入,¥0.12,已全额退款\n" - allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(data1), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(data1), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, int64(1234567890), allNewTransactions[0].Uid) @@ -136,7 +136,7 @@ func TestWeChatPayCsvFileImporterParseImportedData_ParseInvalidTime(t *testing.T "----------------------微信支付账单明细列表--------------------,,,,\n" + "交易时间,交易类型,收/支,金额(元),当前状态\n" + "2024-09-01T01:23:45,二维码收款,收入,¥0.12,已收钱\n" - _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(data1), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(data1), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message) data2 := "微信支付账单明细,,,,\n" + @@ -146,7 +146,7 @@ func TestWeChatPayCsvFileImporterParseImportedData_ParseInvalidTime(t *testing.T "----------------------微信支付账单明细列表--------------------,,,,\n" + "交易时间,交易类型,收/支,金额(元),当前状态\n" + "09/01/2024 12:34:56,二维码收款,收入,¥0.12,已收钱\n" - _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data2), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data2), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message) } @@ -166,7 +166,7 @@ func TestWeChatPayCsvFileImporterParseImportedData_ParseInvalidType(t *testing.T "----------------------微信支付账单明细列表--------------------,,,,\n" + "交易时间,交易类型,收/支,金额(元),当前状态\n" + "2024-09-01T01:23:45,xxx,,¥0.12,支付成功\n" - _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(data), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(data), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrNotFoundTransactionDataInFile.Message) } @@ -186,7 +186,7 @@ func TestWeChatPayCsvFileImporterParseImportedData_ParseInvalidAmount(t *testing "----------------------微信支付账单明细列表--------------------,,,,\n" + "交易时间,交易类型,收/支,金额(元),当前状态\n" + "2024-09-01 01:23:45,二维码收款,收入,¥,已收钱\n" - _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(data), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(data), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrAmountInvalid.Message) } @@ -207,7 +207,7 @@ func TestWeChatPayCsvFileImporterParseImportedData_ParseAccountName(t *testing.T "----------------------微信支付账单明细列表--------------------,,,,\n" + "交易时间,交易类型,收/支,金额(元),支付方式,当前状态\n" + "2024-09-01 01:23:45,二维码收款,收入,¥0.12,/,已收钱\n" - allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(data1), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(data1), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) @@ -223,7 +223,7 @@ func TestWeChatPayCsvFileImporterParseImportedData_ParseAccountName(t *testing.T "2024-09-01 01:23:45,xxx-退款,收入,¥0.12,test,已全额退款\n" assert.Nil(t, err) - allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data2), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data2), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) @@ -239,7 +239,7 @@ func TestWeChatPayCsvFileImporterParseImportedData_ParseAccountName(t *testing.T "2024-09-01 23:59:59,零钱充值,/,¥0.05,test,充值完成\n" assert.Nil(t, err) - allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data3), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data3), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) @@ -256,7 +256,7 @@ func TestWeChatPayCsvFileImporterParseImportedData_ParseAccountName(t *testing.T "2024-09-02 23:59:59,零钱提现,/,¥0.03,test,提现已到账\n" assert.Nil(t, err) - allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data4), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data4), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) @@ -273,7 +273,7 @@ func TestWeChatPayCsvFileImporterParseImportedData_ParseAccountName(t *testing.T "2024-09-03 23:59:59,信用卡还款,/,¥0.01,零钱,支付成功\n" assert.Nil(t, err) - allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data5), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data5), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) @@ -297,7 +297,7 @@ func TestWeChatPayCsvFileImporterParseImportedData_ParseDescription(t *testing.T "----------------------微信支付账单明细列表--------------------,,,,\n" + "交易时间,交易类型,收/支,金额(元),当前状态,备注\n" + "2024-09-01 01:23:45,二维码收款,收入,¥0.12,已收钱,\"/\"\n" - allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(data1), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(data1), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Nil(t, err) assert.Equal(t, 1, len(allNewTransactions)) @@ -310,7 +310,7 @@ func TestWeChatPayCsvFileImporterParseImportedData_ParseDescription(t *testing.T "----------------------微信支付账单明细列表--------------------,,,,\n" + "交易时间,交易类型,商品,收/支,金额(元),当前状态,备注\n" + "2024-09-01 01:23:45,二维码收款,Test,收入,¥0.12,已收钱,\"foo\"\"bar,\ntest\"\n" - allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data2), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data2), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, "foo\"bar,\ntest", allNewTransactions[0].Comment) @@ -321,7 +321,7 @@ func TestWeChatPayCsvFileImporterParseImportedData_ParseDescription(t *testing.T "----------------------微信支付账单明细列表--------------------,,,,\n" + "交易时间,交易类型,商品,收/支,金额(元),当前状态,备注\n" + "2024-09-01 01:23:45,二维码收款,Test,收入,¥0.12,已收钱,\"\"\n" - allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data3), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data3), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, "Test", allNewTransactions[0].Comment) } @@ -342,7 +342,7 @@ func TestWeChatPayCsvFileImporterParseImportedData_SkipUnknownTransferTransactio "----------------------微信支付账单明细列表--------------------,,,,\n" + "交易时间,交易类型,收/支,金额(元),当前状态\n" + "2024-09-01 23:59:59,/,/,¥0.05,充值完成\n" - _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(data), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(data), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrNotFoundTransactionDataInFile.Message) } @@ -357,10 +357,10 @@ func TestWeChatPayCsvFileImporterParseImportedData_MissingFileHeader(t *testing. data := "交易时间,交易类型,收/支,金额(元),当前状态\n" + "2024-09-01 01:23:45,二维码收款,收入,¥0.12,已收钱\n" - _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(data), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(data), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrInvalidFileHeader.Message) - _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(""), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrInvalidFileHeader.Message) } @@ -381,7 +381,7 @@ func TestWeChatPayCsvFileImporterParseImportedData_MissingRequiredColumn(t *test "----------------------微信支付账单明细列表--------------------,,,,\n" + "交易类型,收/支,金额(元),当前状态\n" + "二维码收款,收入,¥0.12,已收钱\n" - _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(data1), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(data1), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message) // Missing Category Column @@ -392,7 +392,7 @@ func TestWeChatPayCsvFileImporterParseImportedData_MissingRequiredColumn(t *test "----------------------微信支付账单明细列表--------------------,,,,\n" + "交易时间,收/支,金额(元),当前状态\n" + "2024-09-01 01:23:45,收入,¥0.12,已收钱\n" - _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data2), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data2), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message) // Missing Type Column @@ -403,7 +403,7 @@ func TestWeChatPayCsvFileImporterParseImportedData_MissingRequiredColumn(t *test "----------------------微信支付账单明细列表--------------------,,,,\n" + "交易时间,交易类型,金额(元),当前状态\n" + "2024-09-01 01:23:45,二维码收款,¥0.12,已收钱\n" - _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data3), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data3), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message) // Missing Amount Column @@ -414,7 +414,7 @@ func TestWeChatPayCsvFileImporterParseImportedData_MissingRequiredColumn(t *test "----------------------微信支付账单明细列表--------------------,,,,\n" + "交易时间,交易类型,收/支,当前状态\n" + "2024-09-01 01:23:45,二维码收款,收入,已收钱\n" - _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data4), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data4), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message) // Missing Status Column @@ -425,7 +425,7 @@ func TestWeChatPayCsvFileImporterParseImportedData_MissingRequiredColumn(t *test "----------------------微信支付账单明细列表--------------------,,,,\n" + "交易时间,交易类型,收/支,金额(元)\n" + "2024-09-01 01:23:45,二维码收款,收入,¥0.12\n" - _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data5), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data5), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message) } @@ -444,6 +444,6 @@ func TestWeChatPayCsvFileImporterParseImportedData_NoTransactionData(t *testing. ",,,,\n" + "----------------------微信支付账单明细列表--------------------,,,,\n" + "交易时间,交易类型,收/支,金额(元),当前状态\n" - _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(data), 0, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) + _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(data), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil) assert.EqualError(t, err, errs.ErrNotFoundTransactionDataInFile.Message) } diff --git a/pkg/converters/wechat/wechat_pay_transaction_data_xlsx_file_importer.go b/pkg/converters/wechat/wechat_pay_transaction_data_xlsx_file_importer.go index 51f281cc..9bd19a9c 100644 --- a/pkg/converters/wechat/wechat_pay_transaction_data_xlsx_file_importer.go +++ b/pkg/converters/wechat/wechat_pay_transaction_data_xlsx_file_importer.go @@ -1,6 +1,8 @@ package wechat import ( + "time" + "github.com/mayswind/ezbookkeeping/pkg/converters/converter" "github.com/mayswind/ezbookkeeping/pkg/converters/datatable" "github.com/mayswind/ezbookkeeping/pkg/converters/excel" @@ -21,7 +23,7 @@ var ( ) // ParseImportedData returns the imported data by parsing the wechat pay transaction csv data -func (c *wechatPayTransactionDataXlsxFileImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezoneOffset int16, additionalOptions converter.TransactionDataImporterOptions, accountMap map[string]*models.Account, expenseCategoryMap map[string]map[string]*models.TransactionCategory, incomeCategoryMap map[string]map[string]*models.TransactionCategory, transferCategoryMap map[string]map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionTag, error) { +func (c *wechatPayTransactionDataXlsxFileImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezone *time.Location, additionalOptions converter.TransactionDataImporterOptions, accountMap map[string]*models.Account, expenseCategoryMap map[string]map[string]*models.TransactionCategory, incomeCategoryMap map[string]map[string]*models.TransactionCategory, transferCategoryMap map[string]map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionTag, error) { xlsxDataTable, err := excel.CreateNewExcelOOXMLFileBasicDataTable(data, false) if err != nil { @@ -49,5 +51,5 @@ func (c *wechatPayTransactionDataXlsxFileImporter) ParseImportedData(ctx core.Co transactionDataTable := datatable.CreateNewTransactionDataTableFromCommonDataTable(commonDataTable, wechatPayTransactionSupportedColumns, transactionRowParser) dataTableImporter := converter.CreateNewSimpleImporterWithTypeNameMapping(wechatPayTransactionTypeNameMapping) - return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezoneOffset, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap) + return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezone, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap) } diff --git a/pkg/mcp/add_transaction_tool_handler.go b/pkg/mcp/add_transaction_tool_handler.go index 9b16fe9d..b5061053 100644 --- a/pkg/mcp/add_transaction_tool_handler.go +++ b/pkg/mcp/add_transaction_tool_handler.go @@ -266,7 +266,7 @@ func (h *mcpAddTransactionToolHandler) createNewTransactionModel(uid int64, addT Type: transactionDbType, CategoryId: categoryId, TransactionTime: utils.GetMinTransactionTimeFromUnixTime(transactionTime.Unix()), - TimezoneUtcOffset: utils.GetTimezoneOffsetMinutes(transactionTime.Location()), + TimezoneUtcOffset: utils.GetTimezoneOffsetMinutes(transactionTime.Unix(), transactionTime.Location()), AccountId: sourceAccountId, Amount: amount, HideAmount: false, diff --git a/pkg/services/accounts.go b/pkg/services/accounts.go index cc46637b..4d03965e 100644 --- a/pkg/services/accounts.go +++ b/pkg/services/accounts.go @@ -219,7 +219,7 @@ func (s *AccountService) GetMaxSubAccountDisplayOrder(c core.Context, uid int64, } // CreateAccounts saves a new account model to database -func (s *AccountService) CreateAccounts(c core.Context, mainAccount *models.Account, mainAccountBalanceTime int64, childrenAccounts []*models.Account, childrenAccountBalanceTimes []int64, utcOffset int16) error { +func (s *AccountService) CreateAccounts(c core.Context, mainAccount *models.Account, mainAccountBalanceTime int64, childrenAccounts []*models.Account, childrenAccountBalanceTimes []int64, clientTimezone *time.Location) error { if mainAccount.Uid <= 0 { return errs.ErrUserIdInvalid } @@ -266,11 +266,14 @@ func (s *AccountService) CreateAccounts(c core.Context, mainAccount *models.Acco } transactionTime := defaultTransactionTime + transactionUtcOffset := utils.GetTimezoneOffsetMinutes(now, clientTimezone) if i == 0 && mainAccountBalanceTime > 0 { transactionTime = utils.GetMinTransactionTimeFromUnixTime(mainAccountBalanceTime) + transactionUtcOffset = utils.GetTimezoneOffsetMinutes(mainAccountBalanceTime, clientTimezone) } else if i > 0 && len(childrenAccountBalanceTimes) > i-1 && childrenAccountBalanceTimes[i-1] > 0 { transactionTime = utils.GetMinTransactionTimeFromUnixTime(childrenAccountBalanceTimes[i-1]) + transactionUtcOffset = utils.GetTimezoneOffsetMinutes(childrenAccountBalanceTimes[i-1], clientTimezone) } else { defaultTransactionTime++ } @@ -281,7 +284,7 @@ func (s *AccountService) CreateAccounts(c core.Context, mainAccount *models.Acco Deleted: false, Type: models.TRANSACTION_DB_TYPE_MODIFY_BALANCE, TransactionTime: transactionTime, - TimezoneUtcOffset: utcOffset, + TimezoneUtcOffset: transactionUtcOffset, AccountId: allAccounts[i].AccountId, Amount: allAccounts[i].Balance, RelatedAccountId: allAccounts[i].AccountId, @@ -366,7 +369,7 @@ func (s *AccountService) CreateAccounts(c core.Context, mainAccount *models.Acco } // ModifyAccounts saves an existed account model to database -func (s *AccountService) ModifyAccounts(c core.Context, mainAccount *models.Account, updateAccounts []*models.Account, addSubAccounts []*models.Account, addSubAccountBalanceTimes []int64, removeSubAccountIds []int64, utcOffset int16) error { +func (s *AccountService) ModifyAccounts(c core.Context, mainAccount *models.Account, updateAccounts []*models.Account, addSubAccounts []*models.Account, addSubAccountBalanceTimes []int64, removeSubAccountIds []int64, clientTimezone *time.Location) error { if mainAccount.Uid <= 0 { return errs.ErrUserIdInvalid } @@ -407,9 +410,11 @@ func (s *AccountService) ModifyAccounts(c core.Context, mainAccount *models.Acco } transactionTime := defaultTransactionTime + transactionUtcOffset := utils.GetTimezoneOffsetMinutes(now, clientTimezone) if len(addSubAccountBalanceTimes) > i && addSubAccountBalanceTimes[i] > 0 { transactionTime = utils.GetMinTransactionTimeFromUnixTime(addSubAccountBalanceTimes[i]) + transactionUtcOffset = utils.GetTimezoneOffsetMinutes(addSubAccountBalanceTimes[i], clientTimezone) } else { defaultTransactionTime++ } @@ -420,7 +425,7 @@ func (s *AccountService) ModifyAccounts(c core.Context, mainAccount *models.Acco Deleted: false, Type: models.TRANSACTION_DB_TYPE_MODIFY_BALANCE, TransactionTime: transactionTime, - TimezoneUtcOffset: utcOffset, + TimezoneUtcOffset: transactionUtcOffset, AccountId: childAccount.AccountId, Amount: childAccount.Balance, RelatedAccountId: childAccount.AccountId, diff --git a/pkg/utils/datetimes.go b/pkg/utils/datetimes.go index d64b6e58..40c7e422 100644 --- a/pkg/utils/datetimes.go +++ b/pkg/utils/datetimes.go @@ -224,12 +224,17 @@ func ParseFromLongDateTimeToMaxUnixTime(t string) (time.Time, error) { return time.ParseInLocation(longDateTimeFormat, t, timezone) } -// ParseFromLongDateTime parses a formatted string in long date time format -func ParseFromLongDateTime(t string, utcOffset int16) (time.Time, error) { +// ParseFromLongDateTimeInFixedUtcOffset parses a formatted string in long date time format +func ParseFromLongDateTimeInFixedUtcOffset(t string, utcOffset int16) (time.Time, error) { timezone := time.FixedZone("Timezone", int(utcOffset)*60) return time.ParseInLocation(longDateTimeFormat, t, timezone) } +// ParseFromLongDateTimeInTimeZone parses a formatted string in long date time format +func ParseFromLongDateTimeInTimeZone(t string, timezone *time.Location) (time.Time, error) { + return time.ParseInLocation(longDateTimeFormat, t, timezone) +} + // ParseFromLongDateTimeWithTimezone parses a formatted string in long date time format func ParseFromLongDateTimeWithTimezone(t string) (time.Time, error) { return time.Parse(longDateTimeWithTimezoneFormat, t) @@ -245,14 +250,14 @@ func ParseFromLongDateTimeWithTimezoneRFC3339Format(t string) (time.Time, error) return time.Parse(longDateTimeWithTimezoneRFC3339Format, t) } -// ParseFromLongDateTimeWithoutSecond parses a formatted string in long date time format (no second) -func ParseFromLongDateTimeWithoutSecond(t string, utcOffset int16) (time.Time, error) { +// ParseFromLongDateTimeWithoutSecondInFixedUtcOffset parses a formatted string in long date time format (no second) with fixed UTC offset +func ParseFromLongDateTimeWithoutSecondInFixedUtcOffset(t string, utcOffset int16) (time.Time, error) { timezone := time.FixedZone("Timezone", int(utcOffset)*60) return time.ParseInLocation(longDateTimeWithoutSecondFormat, t, timezone) } -// ParseFromShortDateTime parses a formatted string in short date time format -func ParseFromShortDateTime(t string, utcOffset int16) (time.Time, error) { +// ParseFromShortDateTimeInFixedUtcOffset parses a formatted string in short date time format with fixed UTC offset +func ParseFromShortDateTimeInFixedUtcOffset(t string, utcOffset int16) (time.Time, error) { timezone := time.FixedZone("Timezone", int(utcOffset)*60) return time.ParseInLocation(shortDateTimeFormat, t, timezone) } @@ -282,8 +287,8 @@ func IsUnixTimeEqualsYearAndMonth(unixTime int64, timezone *time.Location, year } // GetTimezoneOffsetMinutes returns offset minutes according specified timezone -func GetTimezoneOffsetMinutes(timezone *time.Location) int16 { - _, tzOffset := time.Now().In(timezone).Zone() +func GetTimezoneOffsetMinutes(unixTime int64, timezone *time.Location) int16 { + _, tzOffset := parseFromUnixTime(unixTime).In(timezone).Zone() tzMinuteOffset := int16(tzOffset / 60) return tzMinuteOffset @@ -298,8 +303,8 @@ func GetServerTimezoneOffsetMinutes() int16 { } // FormatTimezoneOffset returns "+/-HH:MM" format of timezone -func FormatTimezoneOffset(timezone *time.Location) string { - tzMinutesOffset := GetTimezoneOffsetMinutes(timezone) +func FormatTimezoneOffset(unixTime int64, timezone *time.Location) string { + tzMinutesOffset := GetTimezoneOffsetMinutes(unixTime, timezone) sign := "+" hourAbsOffset := tzMinutesOffset / 60 diff --git a/pkg/utils/datetimes_test.go b/pkg/utils/datetimes_test.go index 3f5d4d4c..262267f9 100644 --- a/pkg/utils/datetimes_test.go +++ b/pkg/utils/datetimes_test.go @@ -215,15 +215,36 @@ func TestParseFromLongDateTimeToMaxUnixTime(t *testing.T) { assert.Equal(t, expectedValue, actualValue) } -func TestParseFromLongDateTime(t *testing.T) { +func TestParseFromLongDateTimeInFixedUtcOffset(t *testing.T) { expectedValue := int64(1617228083) - actualTime, err := ParseFromLongDateTime("2021-04-01 06:01:23", 480) + actualTime, err := ParseFromLongDateTimeInFixedUtcOffset("2021-04-01 06:01:23", 480) assert.Equal(t, nil, err) actualValue := actualTime.Unix() assert.Equal(t, expectedValue, actualValue) } +func TestParseFromLongDateTimeInTimeZone(t *testing.T) { + londonLocation, err := time.LoadLocation("Europe/London") + assert.Equal(t, nil, err) + + // during standard time (UTC+0) + expectedValue := int64(1577858483) + actualTime, err := ParseFromLongDateTimeInTimeZone("2020-01-01 06:01:23", londonLocation) + assert.Equal(t, nil, err) + + actualValue := actualTime.Unix() + assert.Equal(t, expectedValue, actualValue) + + // during daylight saving time (UTC+1) + expectedValue = int64(1619845283) + actualTime, err = ParseFromLongDateTimeInTimeZone("2021-05-01 06:01:23", londonLocation) + assert.Equal(t, nil, err) + + actualValue = actualTime.Unix() + assert.Equal(t, expectedValue, actualValue) +} + func TestParseFromLongDateTimeWithTimezone(t *testing.T) { expectedValue := int64(1617238883) actualTime, err := ParseFromLongDateTimeWithTimezone("2021-04-01 06:01:23+05:00") @@ -251,18 +272,18 @@ func TestParseFromLongDateTimeWithTimezoneRFC3339Format(t *testing.T) { assert.Equal(t, expectedValue, actualValue) } -func TestParseFromLongDateTimeWithoutSecond(t *testing.T) { +func TestParseFromLongDateTimeWithoutSecondInFixedUtcOffset(t *testing.T) { expectedValue := int64(1691947440) - actualTime, err := ParseFromLongDateTimeWithoutSecond("2023-08-13 17:24", 0) + actualTime, err := ParseFromLongDateTimeWithoutSecondInFixedUtcOffset("2023-08-13 17:24", 0) assert.Equal(t, nil, err) actualValue := actualTime.Unix() assert.Equal(t, expectedValue, actualValue) } -func TestParseFromShortDateTime(t *testing.T) { +func TestParseFromShortDateTimeInFixedUtcOffset(t *testing.T) { expectedValue := int64(1617228083) - actualTime, err := ParseFromShortDateTime("2021-4-1 6:1:23", 480) + actualTime, err := ParseFromShortDateTimeInFixedUtcOffset("2021-4-1 6:1:23", 480) assert.Equal(t, nil, err) actualValue := actualTime.Unix() @@ -312,52 +333,82 @@ func TestIsUnixTimeEqualsYearAndMonth(t *testing.T) { assert.Equal(t, false, actualValue) } -func TestGetTimezoneOffsetMinutes(t *testing.T) { +func TestGetTimezoneOffsetMinutes_FixedTimezone(t *testing.T) { timezone := time.FixedZone("Test Timezone", 120*60) expectedValue := int16(120) - actualValue := GetTimezoneOffsetMinutes(timezone) + actualValue := GetTimezoneOffsetMinutes(time.Now().Unix(), timezone) assert.Equal(t, expectedValue, actualValue) timezone = time.FixedZone("Test Timezone", 345*60) expectedValue = int16(345) - actualValue = GetTimezoneOffsetMinutes(timezone) + actualValue = GetTimezoneOffsetMinutes(time.Now().Unix(), timezone) assert.Equal(t, expectedValue, actualValue) timezone = time.FixedZone("Test Timezone", -720*60) expectedValue = int16(-720) - actualValue = GetTimezoneOffsetMinutes(timezone) + actualValue = GetTimezoneOffsetMinutes(time.Now().Unix(), timezone) assert.Equal(t, expectedValue, actualValue) timezone = time.FixedZone("Test Timezone", 0) expectedValue = int16(0) - actualValue = GetTimezoneOffsetMinutes(timezone) + actualValue = GetTimezoneOffsetMinutes(time.Now().Unix(), timezone) assert.Equal(t, expectedValue, actualValue) } -func TestFormatTimezoneOffset(t *testing.T) { +func TestGetTimezoneOffsetMinutes_TimezoneWithDST(t *testing.T) { + londonLocation, err := time.LoadLocation("Europe/London") + assert.Equal(t, nil, err) + + // during standard time (UTC+0) + expectedValue := int16(0) + actualValue := GetTimezoneOffsetMinutes(1577858483, londonLocation) // 2020-01-01 06:01:23 +00:00 + assert.Equal(t, expectedValue, actualValue) + + // during daylight saving time (UTC+1) + expectedValue = int16(60) + actualValue = GetTimezoneOffsetMinutes(1619845283, londonLocation) // 2021-05-01 06:01:23 +01:00 + assert.Equal(t, expectedValue, actualValue) +} + +func TestFormatTimezoneOffset_FixedTimezone(t *testing.T) { timezone := time.FixedZone("Test Timezone", 120*60) expectedValue := "+02:00" - actualValue := FormatTimezoneOffset(timezone) + actualValue := FormatTimezoneOffset(time.Now().Unix(), timezone) assert.Equal(t, expectedValue, actualValue) timezone = time.FixedZone("Test Timezone", 345*60) expectedValue = "+05:45" - actualValue = FormatTimezoneOffset(timezone) + actualValue = FormatTimezoneOffset(time.Now().Unix(), timezone) assert.Equal(t, expectedValue, actualValue) timezone = time.FixedZone("Test Timezone", -720*60) expectedValue = "-12:00" - actualValue = FormatTimezoneOffset(timezone) + actualValue = FormatTimezoneOffset(time.Now().Unix(), timezone) assert.Equal(t, expectedValue, actualValue) timezone = time.FixedZone("Test Timezone", -150*60) expectedValue = "-02:30" - actualValue = FormatTimezoneOffset(timezone) + actualValue = FormatTimezoneOffset(time.Now().Unix(), timezone) assert.Equal(t, expectedValue, actualValue) timezone = time.FixedZone("Test Timezone", 0) expectedValue = "+00:00" - actualValue = FormatTimezoneOffset(timezone) + actualValue = FormatTimezoneOffset(time.Now().Unix(), timezone) + assert.Equal(t, expectedValue, actualValue) +} + +func TestFormatTimezoneOffset_TimezoneWithDST(t *testing.T) { + londonLocation, err := time.LoadLocation("Europe/London") + assert.Equal(t, nil, err) + + // during standard time (UTC+0) + expectedValue := "+00:00" + actualValue := FormatTimezoneOffset(1577858483, londonLocation) // 2020-01-01 06:01:23 +00:00 + assert.Equal(t, expectedValue, actualValue) + + // during daylight saving time (UTC+1) + expectedValue = "+01:00" + actualValue = FormatTimezoneOffset(1619845283, londonLocation) // 2021-05-01 06:01:23 +01:00 assert.Equal(t, expectedValue, actualValue) } diff --git a/src/components/base/AccountBalanceTrendsChartBase.ts b/src/components/base/AccountBalanceTrendsChartBase.ts index 87b7c44c..8a08c4fd 100644 --- a/src/components/base/AccountBalanceTrendsChartBase.ts +++ b/src/components/base/AccountBalanceTrendsChartBase.ts @@ -17,6 +17,7 @@ import type { TransactionReconciliationStatementResponseItem } from '@/models/tr import { isArray } from '@/lib/common.ts'; import { sumAmounts } from '@/lib/numeral.ts'; import { + parseDateTimeFromUnixTime, getGregorianCalendarYearAndMonthFromUnixTime, getYearFirstUnixTimeBySpecifiedUnixTime, getQuarterFirstUnixTimeBySpecifiedUnixTime, @@ -52,11 +53,11 @@ export interface CommonAccountBalanceTrendsChartProps { export function useAccountBalanceTrendsChartBase(props: CommonAccountBalanceTrendsChartProps) { const { - formatUnixTimeToShortDate, - formatUnixTimeToGregorianLikeShortYear, - formatUnixTimeToGregorianLikeShortYearMonth, - formatUnixTimeToGregorianLikeYearQuarter, - formatUnixTimeToGregorianLikeFiscalYear + formatDateTimeToShortDate, + formatDateTimeToGregorianLikeShortYear, + formatDateTimeToGregorianLikeShortYearMonth, + formatDateTimeToGregorianLikeYearQuarter, + formatDateTimeToGregorianLikeFiscalYear } = useI18n(); const dataDateRange = computed(() => { @@ -150,19 +151,20 @@ export function useAccountBalanceTrendsChartBase(props: CommonAccountBalanceTren for (const dateRange of allDateRanges.value) { const dataItems = dayDataItemsMap[dateRange.minUnixTime]; + const minDateTime = parseDateTimeFromUnixTime(dateRange.minUnixTime); let displayDate = ''; if (props.dateAggregationType === ChartDateAggregationType.Year.type) { - displayDate = formatUnixTimeToGregorianLikeShortYear(dateRange.minUnixTime); + displayDate = formatDateTimeToGregorianLikeShortYear(minDateTime); } else if (props.dateAggregationType === ChartDateAggregationType.FiscalYear.type) { - displayDate = formatUnixTimeToGregorianLikeFiscalYear(dateRange.minUnixTime); + displayDate = formatDateTimeToGregorianLikeFiscalYear(minDateTime); } else if (props.dateAggregationType === ChartDateAggregationType.Quarter.type) { - displayDate = formatUnixTimeToGregorianLikeYearQuarter(dateRange.minUnixTime); + displayDate = formatDateTimeToGregorianLikeYearQuarter(minDateTime); } else if (props.dateAggregationType === ChartDateAggregationType.Month.type) { - displayDate = formatUnixTimeToGregorianLikeShortYearMonth(dateRange.minUnixTime); + displayDate = formatDateTimeToGregorianLikeShortYearMonth(minDateTime); } else if (props.dateAggregationType === ChartDateAggregationType.Day.type) { - displayDate = formatUnixTimeToShortDate(dateRange.minUnixTime); + displayDate = formatDateTimeToShortDate(minDateTime); } else { return ret; } diff --git a/src/components/base/DateRangeSelectionBase.ts b/src/components/base/DateRangeSelectionBase.ts index e55fc6fe..d03f38ea 100644 --- a/src/components/base/DateRangeSelectionBase.ts +++ b/src/components/base/DateRangeSelectionBase.ts @@ -1,16 +1,23 @@ import { ref, computed } from 'vue'; -import { type TimeRangeAndDateType, type PresetDateRange, type UnixTimeRange, type WeekDayValue, DateRange } from '@/core/datetime.ts'; +import { + type DateTime, + type UnixTimeRange, + type TimeRangeAndDateType, + type PresetDateRange, + type WeekDayValue, + DateRange, +} from '@/core/datetime.ts'; import { getCurrentUnixTime, getLocalDatetimeFromUnixTime, getUnixTimeFromLocalDatetime, getTodayFirstUnixTime, - getDummyUnixTimeForLocalUsage, - getActualUnixTimeForStore, - getTimezoneOffsetMinutes, - getBrowserTimezoneOffsetMinutes, + getSameDateTimeWithCurrentTimezone, + getSameDateTimeWithBrowserTimezone, + parseDateTimeFromUnixTime, + parseDateTimeFromUnixTimeWithBrowserTimezone, getDateRangeByDateType } from '@/lib/datetime.ts'; @@ -45,23 +52,21 @@ function getDateRangeFromProps(props: CommonDateRangeSelectionProps): { minDate: } export function useDateRangeSelectionBase(props: CommonDateRangeSelectionProps) { - const { tt, formatUnixTimeToLongDateTime } = useI18n(); + const { tt, formatDateTimeToLongDateTime } = useI18n(); const userStore = useUserStore(); const { minDate, maxDate } = getDateRangeFromProps(props); const dateRange = ref([ - getLocalDatetimeFromUnixTime(getDummyUnixTimeForLocalUsage(minDate, getTimezoneOffsetMinutes(), getBrowserTimezoneOffsetMinutes())), - getLocalDatetimeFromUnixTime(getDummyUnixTimeForLocalUsage(maxDate, getTimezoneOffsetMinutes(), getBrowserTimezoneOffsetMinutes())) + getLocalDatetimeFromSameDateTimeOfUnixTime(minDate), + getLocalDatetimeFromSameDateTimeOfUnixTime(maxDate) ]); const firstDayOfWeek = computed(() => userStore.currentUserFirstDayOfWeek); const beginDateTime = computed(() => { - const actualBeginUnixTime = getActualUnixTimeForStore(getUnixTimeFromLocalDatetime(dateRange.value[0] as Date), getTimezoneOffsetMinutes(), getBrowserTimezoneOffsetMinutes()); - return formatUnixTimeToLongDateTime(actualBeginUnixTime); + return formatDateTimeToLongDateTime(getDateTimeFromSameDateTimeOfLocalDatetime(dateRange.value[0] as Date)); }); const endDateTime = computed(() => { - const actualEndUnixTime = getActualUnixTimeForStore(getUnixTimeFromLocalDatetime(dateRange.value[1] as Date), getTimezoneOffsetMinutes(), getBrowserTimezoneOffsetMinutes()); - return formatUnixTimeToLongDateTime(actualEndUnixTime); + return formatDateTimeToLongDateTime(getDateTimeFromSameDateTimeOfLocalDatetime(dateRange.value[1] as Date)); }); const presetRanges = computed(() => { const presetRanges:PresetDateRange[] = []; @@ -82,8 +87,8 @@ export function useDateRangeSelectionBase(props: CommonDateRangeSelectionProps) presetRanges.push({ label: tt(dateRangeType.name), value: [ - getLocalDatetimeFromUnixTime(getDummyUnixTimeForLocalUsage(dateRange.minTime, getTimezoneOffsetMinutes(), getBrowserTimezoneOffsetMinutes())), - getLocalDatetimeFromUnixTime(getDummyUnixTimeForLocalUsage(dateRange.maxTime, getTimezoneOffsetMinutes(), getBrowserTimezoneOffsetMinutes())) + getLocalDatetimeFromSameDateTimeOfUnixTime(dateRange.minTime), + getLocalDatetimeFromSameDateTimeOfUnixTime(dateRange.maxTime) ] }); }); @@ -91,6 +96,14 @@ export function useDateRangeSelectionBase(props: CommonDateRangeSelectionProps) return presetRanges; }); + function getLocalDatetimeFromSameDateTimeOfUnixTime(unixTime: number): Date { + return getLocalDatetimeFromUnixTime(getSameDateTimeWithBrowserTimezone(parseDateTimeFromUnixTime(unixTime)).getUnixTime()); + } + + function getDateTimeFromSameDateTimeOfLocalDatetime(localDatetime: Date): DateTime { + return getSameDateTimeWithCurrentTimezone(parseDateTimeFromUnixTimeWithBrowserTimezone(getUnixTimeFromLocalDatetime(localDatetime))); + } + function getFinalDateRange(): UnixTimeRange | null { if (!dateRange.value[0] || !dateRange.value[1]) { return null; @@ -99,16 +112,13 @@ export function useDateRangeSelectionBase(props: CommonDateRangeSelectionProps) const currentMinDate = dateRange.value[0]; const currentMaxDate = dateRange.value[1]; - let minUnixTime = getUnixTimeFromLocalDatetime(currentMinDate); - let maxUnixTime = getUnixTimeFromLocalDatetime(currentMaxDate); + const minUnixTime = getDateTimeFromSameDateTimeOfLocalDatetime(currentMinDate).getUnixTime(); + const maxUnixTime = getDateTimeFromSameDateTimeOfLocalDatetime(currentMaxDate).getUnixTime(); if (minUnixTime < 0 || maxUnixTime < 0) { throw new Error('Date is too early'); } - minUnixTime = getActualUnixTimeForStore(minUnixTime, getTimezoneOffsetMinutes(), getBrowserTimezoneOffsetMinutes()); - maxUnixTime = getActualUnixTimeForStore(maxUnixTime, getTimezoneOffsetMinutes(), getBrowserTimezoneOffsetMinutes()); - return { minUnixTime, maxUnixTime @@ -123,6 +133,8 @@ export function useDateRangeSelectionBase(props: CommonDateRangeSelectionProps) endDateTime, presetRanges, // functions + getLocalDatetimeFromSameDateTimeOfUnixTime, + getDateTimeFromSameDateTimeOfLocalDatetime, getFinalDateRange }; } diff --git a/src/components/base/DateTimeSelectionBase.ts b/src/components/base/DateTimeSelectionBase.ts index 9c51afa0..e88b9453 100644 --- a/src/components/base/DateTimeSelectionBase.ts +++ b/src/components/base/DateTimeSelectionBase.ts @@ -5,6 +5,15 @@ import { useI18n } from '@/locales/helpers.ts'; import { type NameValue } from '@/core/base.ts'; import { NumeralSystem } from '@/core/numeral.ts'; +import { + getLocalDatetimeFromUnixTime, + getUnixTimeFromLocalDatetime, + getSameDateTimeWithBrowserTimezone, + getSameDateTimeWithTimezoneOffset, + parseDateTimeFromUnixTimeWithBrowserTimezone, + parseDateTimeFromUnixTimeWithTimezoneOffset +} from '@/lib/datetime.ts'; + export interface TimePickerValue { value: string; itemsIndex: number; @@ -30,6 +39,14 @@ export function useDateTimeSelectionBase() { const numeralSystem = computed(() => getCurrentNumeralSystemType()); const meridiemItems = computed(() => getAllMeridiemIndicators()); + function getLocalDatetimeFromSameDateTimeOfUnixTime(unixTime: number, utcOffset: number): Date { + return getLocalDatetimeFromUnixTime(getSameDateTimeWithBrowserTimezone(parseDateTimeFromUnixTimeWithTimezoneOffset(unixTime, utcOffset)).getUnixTime()); + } + + function getUnixTimeFromSameDateTimeOfLocalDatetime(localDatetime: Date, utcOffset: number): number { + return getSameDateTimeWithTimezoneOffset(parseDateTimeFromUnixTimeWithBrowserTimezone(getUnixTimeFromLocalDatetime(localDatetime)), utcOffset).getUnixTime(); + } + function getDisplayTimeValue(value: number, forceTwoDigits: boolean): string { let textualValue = value.toString(); @@ -89,6 +106,8 @@ export function useDateTimeSelectionBase() { // computed meridiemItems, // functions + getLocalDatetimeFromSameDateTimeOfUnixTime, + getUnixTimeFromSameDateTimeOfLocalDatetime, getDisplayTimeValue, generateAllHours, generateAllMinutesOrSeconds diff --git a/src/components/base/MonthRangeSelectionBase.ts b/src/components/base/MonthRangeSelectionBase.ts index 5c52257d..f9971dc9 100644 --- a/src/components/base/MonthRangeSelectionBase.ts +++ b/src/components/base/MonthRangeSelectionBase.ts @@ -7,6 +7,7 @@ import { getYear0BasedMonthObjectFromString, getYearMonthStringFromYear0BasedMonthObject, getCurrentUnixTime, + parseDateTimeFromUnixTime, getThisYearFirstUnixTime, getYearMonthFirstUnixTime, getYearMonthLastUnixTime @@ -49,7 +50,7 @@ function getMonthRangeFromProps(props: CommonMonthRangeSelectionProps): { minDat } export function useMonthRangeSelectionBase(props: CommonMonthRangeSelectionProps) { - const { formatUnixTimeToGregorianLikeLongYearMonth } = useI18n(); + const { formatDateTimeToGregorianLikeLongYearMonth } = useI18n(); const { minDate, maxDate } = getMonthRangeFromProps(props); const dateRange = ref([ @@ -57,8 +58,8 @@ export function useMonthRangeSelectionBase(props: CommonMonthRangeSelectionProps maxDate ]); - const beginDateTime = computed(() => formatUnixTimeToGregorianLikeLongYearMonth(getYearMonthFirstUnixTime(dateRange.value[0] as Year0BasedMonth))); - const endDateTime = computed(() => formatUnixTimeToGregorianLikeLongYearMonth(getYearMonthLastUnixTime(dateRange.value[1] as Year0BasedMonth))); + const beginDateTime = computed(() => formatDateTimeToGregorianLikeLongYearMonth(parseDateTimeFromUnixTime(getYearMonthFirstUnixTime(dateRange.value[0] as Year0BasedMonth)))); + const endDateTime = computed(() => formatDateTimeToGregorianLikeLongYearMonth(parseDateTimeFromUnixTime(getYearMonthLastUnixTime(dateRange.value[1] as Year0BasedMonth)))); function getFinalMonthRange(): { minYearMonth: TextualYearMonth | '', maxYearMonth: TextualYearMonth | '' } | null { if (!dateRange.value[0] || !dateRange.value[1]) { diff --git a/src/components/common/DateTimePicker.vue b/src/components/common/DateTimePicker.vue index 83601371..bbf1e6bc 100644 --- a/src/components/common/DateTimePicker.vue +++ b/src/components/common/DateTimePicker.vue @@ -88,9 +88,9 @@ const { getCurrentNumeralSystemType, isLongDateMonthAfterYear, isLongTime24HourFormat, - getCalendarDisplayShortYearFromUnixTime, - getCalendarDisplayShortMonthFromUnixTime, - getCalendarDisplayDayOfMonthFromUnixTime, + getCalendarDisplayShortYearFromDateTime, + getCalendarDisplayShortMonthFromDateTime, + getCalendarDisplayDayOfMonthFromDateTime, getCalendarAlternateDate } = useI18n(); @@ -138,21 +138,21 @@ function switchView(viewType: MenuView): void { } function getDisplayYear(year: number): string { - return getCalendarDisplayShortYearFromUnixTime(getYearMonthDayDateTime(year, 1, 1).getUnixTime(), actualNumeralSystem.value); + return getCalendarDisplayShortYearFromDateTime(getYearMonthDayDateTime(year, 1, 1), actualNumeralSystem.value); } function getDisplayMonth(month: number): string { if (isArray(dateTime.value)) { - return getCalendarDisplayShortMonthFromUnixTime(getYearMonthDayDateTime(dateTime.value[0]!.getFullYear(), month + 1, 1).getUnixTime(), actualNumeralSystem.value); + return getCalendarDisplayShortMonthFromDateTime(getYearMonthDayDateTime(dateTime.value[0]!.getFullYear(), month + 1, 1), actualNumeralSystem.value); } else if (dateTime.value) { - return getCalendarDisplayShortMonthFromUnixTime(getYearMonthDayDateTime(dateTime.value.getFullYear(), month + 1, 1).getUnixTime(), actualNumeralSystem.value); + return getCalendarDisplayShortMonthFromDateTime(getYearMonthDayDateTime(dateTime.value.getFullYear(), month + 1, 1), actualNumeralSystem.value); } else { - return getCalendarDisplayShortMonthFromUnixTime(getYearMonthDayDateTime(new Date().getFullYear(), month + 1, 1).getUnixTime(), actualNumeralSystem.value); + return getCalendarDisplayShortMonthFromDateTime(getYearMonthDayDateTime(new Date().getFullYear(), month + 1, 1), actualNumeralSystem.value); } } function getDisplayDay(date: Date): string { - return getCalendarDisplayDayOfMonthFromUnixTime(getYearMonthDayDateTime(date.getFullYear(), date.getMonth() + 1, date.getDate()).getUnixTime(), actualNumeralSystem.value); + return getCalendarDisplayDayOfMonthFromDateTime(getYearMonthDayDateTime(date.getFullYear(), date.getMonth() + 1, date.getDate()), actualNumeralSystem.value); } defineExpose({ diff --git a/src/components/common/MonthPicker.vue b/src/components/common/MonthPicker.vue index 4a38216d..e2823188 100644 --- a/src/components/common/MonthPicker.vue +++ b/src/components/common/MonthPicker.vue @@ -54,8 +54,8 @@ const emit = defineEmits<{ const { isLongDateMonthAfterYear, - getCalendarDisplayShortYearFromUnixTime, - getCalendarDisplayShortMonthFromUnixTime + getCalendarDisplayShortYearFromDateTime, + getCalendarDisplayShortMonthFromDateTime } = useI18n(); const yearRange = getAllowedYearRange(); @@ -96,14 +96,14 @@ function getYear0BasedMonthFromMonthSelectionValue(value: MonthSelectionValue): } function getDisplayYear(year: number): string { - return getCalendarDisplayShortYearFromUnixTime(getYearMonthDayDateTime(year, 1, 1).getUnixTime()); + return getCalendarDisplayShortYearFromDateTime(getYearMonthDayDateTime(year, 1, 1)); } function getDisplayMonth(month: number): string { if (isArray(dateTime.value)) { - return getCalendarDisplayShortMonthFromUnixTime(getYearMonthDayDateTime(dateTime.value[0]!.year, month + 1, 1).getUnixTime()); + return getCalendarDisplayShortMonthFromDateTime(getYearMonthDayDateTime(dateTime.value[0]!.year, month + 1, 1)); } else { - return getCalendarDisplayShortMonthFromUnixTime(getYearMonthDayDateTime(dateTime.value.year, month + 1, 1).getUnixTime()); + return getCalendarDisplayShortMonthFromDateTime(getYearMonthDayDateTime(dateTime.value.year, month + 1, 1)); } } diff --git a/src/components/common/TransactionCalendar.vue b/src/components/common/TransactionCalendar.vue index 741227f7..3a17c7e2 100644 --- a/src/components/common/TransactionCalendar.vue +++ b/src/components/common/TransactionCalendar.vue @@ -38,14 +38,7 @@ import type { CalendarAlternateDate, TextualYearMonthDay, WeekDayValue } from '@ import { INCOMPLETE_AMOUNT_SUFFIX } from '@/consts/numeral.ts'; import { arrangeArrayWithNewStartIndex } from '@/lib/common.ts'; -import { - getTimezoneOffsetMinutes, - getBrowserTimezoneOffsetMinutes, - getUnixTimeFromLocalDatetime, - getActualUnixTimeForStore, - getYearMonthDayDateTime, - parseDateTimeFromUnixTime -} from '@/lib/datetime.ts'; +import { getYearMonthDayDateTime } from '@/lib/datetime.ts'; const props = defineProps<{ modelValue: TextualYearMonthDay | ''; @@ -67,7 +60,7 @@ const emit = defineEmits<{ const { getAllLongWeekdayNames, getAllShortWeekdayNames, - getCalendarDisplayDayOfMonthFromUnixTime, + getCalendarDisplayDayOfMonthFromDateTime, getCalendarAlternateDates, formatAmountToLocalizedNumeralsWithCurrency } = useI18n(); @@ -105,8 +98,7 @@ const alternateDates = computed | undefined> }); function noTransactionInMonthDay(date: Date): boolean { - const dateTime = parseDateTimeFromUnixTime(getActualUnixTimeForStore(getUnixTimeFromLocalDatetime(date), getTimezoneOffsetMinutes(), getBrowserTimezoneOffsetMinutes())); - return !props.dailyTotalAmounts || !props.dailyTotalAmounts[dateTime.getGregorianCalendarDay()]; + return !props.dailyTotalAmounts || !props.dailyTotalAmounts[date.getDate()]; } function getDisplayMonthTotalAmount(amount: number, currency: string | false, symbol: string, incomplete: boolean): string { @@ -115,7 +107,7 @@ function getDisplayMonthTotalAmount(amount: number, currency: string | false, sy } function getDisplayDay(date: Date): string { - return getCalendarDisplayDayOfMonthFromUnixTime(getYearMonthDayDateTime(date.getFullYear(), date.getMonth() + 1, date.getDate()).getUnixTime()); + return getCalendarDisplayDayOfMonthFromDateTime(getYearMonthDayDateTime(date.getFullYear(), date.getMonth() + 1, date.getDate())); } diff --git a/src/components/desktop/DateRangeSelectionDialog.vue b/src/components/desktop/DateRangeSelectionDialog.vue index 3687b219..4317c1c4 100644 --- a/src/components/desktop/DateRangeSelectionDialog.vue +++ b/src/components/desktop/DateRangeSelectionDialog.vue @@ -43,13 +43,6 @@ import { type CommonDateRangeSelectionProps, useDateRangeSelectionBase } from '@ import { ThemeType } from '@/core/theme.ts'; -import { - getLocalDatetimeFromUnixTime, - getDummyUnixTimeForLocalUsage, - getTimezoneOffsetMinutes, - getBrowserTimezoneOffsetMinutes -} from '@/lib/datetime.ts'; - interface DesktopDateRangeSelectionProps extends CommonDateRangeSelectionProps { persistent?: boolean; } @@ -64,7 +57,14 @@ const emit = defineEmits<{ const theme = useTheme(); const { tt } = useI18n(); -const { dateRange, beginDateTime, endDateTime, presetRanges, getFinalDateRange } = useDateRangeSelectionBase(props); +const { + dateRange, + beginDateTime, + endDateTime, + presetRanges, + getLocalDatetimeFromSameDateTimeOfUnixTime, + getFinalDateRange +} = useDateRangeSelectionBase(props); const isDarkMode = computed(() => theme.global.name.value === ThemeType.Dark); const showState = computed({ @@ -94,13 +94,13 @@ function cancel(): void { watch(() => props.minTime, (newValue) => { if (newValue) { - dateRange.value[0] = getLocalDatetimeFromUnixTime(getDummyUnixTimeForLocalUsage(newValue, getTimezoneOffsetMinutes(), getBrowserTimezoneOffsetMinutes())); + dateRange.value[0] = getLocalDatetimeFromSameDateTimeOfUnixTime(newValue); } }); watch(() => props.maxTime, (newValue) => { if (newValue) { - dateRange.value[1] = getLocalDatetimeFromUnixTime(getDummyUnixTimeForLocalUsage(newValue, getTimezoneOffsetMinutes(), getBrowserTimezoneOffsetMinutes())); + dateRange.value[1] = getLocalDatetimeFromSameDateTimeOfUnixTime(newValue); } }); diff --git a/src/components/desktop/DateTimeSelect.vue b/src/components/desktop/DateTimeSelect.vue index 08308d84..43ef7912 100644 --- a/src/components/desktop/DateTimeSelect.vue +++ b/src/components/desktop/DateTimeSelect.vue @@ -94,12 +94,9 @@ import { } from '@/core/datetime.ts'; import { getHourIn12HourFormat, - getTimezoneOffsetMinutes, - getBrowserTimezoneOffsetMinutes, getLocalDatetimeFromUnixTime, - getUnixTimeFromLocalDatetime, - getActualUnixTimeForStore, - getDummyUnixTimeForLocalUsage, + getSameDateTimeWithBrowserTimezone, + parseDateTimeFromUnixTimeWithTimezoneOffset, parseDateTimeFromKnownDateTimeFormat, getAMOrPM, getCombinedDateAndTimeValues @@ -108,6 +105,7 @@ import { setChildInputFocus } from '@/lib/ui/desktop.ts'; const props = defineProps<{ modelValue: number; + timezoneUtcOffset: number; disabled?: boolean; readonly?: boolean; label?: string; @@ -124,7 +122,7 @@ const { getCurrentNumeralSystemType, parseDateTimeFromLongDateTime, parseDateTimeFromShortDateTime, - formatUnixTimeToLongDateTime + formatDateTimeToLongDateTime } = useI18n(); const { @@ -133,6 +131,8 @@ const { isMinuteTwoDigits, isSecondTwoDigits, isMeridiemIndicatorFirst, + getLocalDatetimeFromSameDateTimeOfUnixTime, + getUnixTimeFromSameDateTimeOfLocalDatetime, getDisplayTimeValue, generateAllHours, generateAllMinutesOrSeconds @@ -147,10 +147,10 @@ const numeralSystem = computed(() => getCurrentNumeralSystemType( const dateTime = computed({ get: () => { - return getLocalDatetimeFromUnixTime(props.modelValue); + return getLocalDatetimeFromSameDateTimeOfUnixTime(props.modelValue, props.timezoneUtcOffset); }, set: (value: Date) => { - const unixTime = getUnixTimeFromLocalDatetime(value); + const unixTime = getUnixTimeFromSameDateTimeOfLocalDatetime(value, props.timezoneUtcOffset); if (unixTime < 0) { emit('error', 'Date is too early'); @@ -161,7 +161,7 @@ const dateTime = computed({ } }); -const displayTime = computed(() => formatUnixTimeToLongDateTime(getActualUnixTimeForStore(getUnixTimeFromLocalDatetime(dateTime.value), getTimezoneOffsetMinutes(), getBrowserTimezoneOffsetMinutes()))); +const displayTime = computed(() => formatDateTimeToLongDateTime(parseDateTimeFromUnixTimeWithTimezoneOffset(props.modelValue, props.timezoneUtcOffset))); const hourItems = computed(() => generateAllHours(1, isHourTwoDigits.value)); const minuteItems = computed(() => generateAllMinutesOrSeconds(1, isMinuteTwoDigits.value)); @@ -252,7 +252,7 @@ function onPaste(event: ClipboardEvent): void { dt = parseDateTimeFromKnownDateTimeFormat(text, formats[0] as KnownDateTimeFormat); if (dt) { - dateTime.value = getLocalDatetimeFromUnixTime(getDummyUnixTimeForLocalUsage(dt.getUnixTime(), getTimezoneOffsetMinutes(), getBrowserTimezoneOffsetMinutes())); + dateTime.value = getLocalDatetimeFromUnixTime(getSameDateTimeWithBrowserTimezone(dt).getUnixTime()); return; } } @@ -260,14 +260,14 @@ function onPaste(event: ClipboardEvent): void { dt = parseDateTimeFromLongDateTime(text); if (dt) { - dateTime.value = getLocalDatetimeFromUnixTime(getDummyUnixTimeForLocalUsage(dt.getUnixTime(), getTimezoneOffsetMinutes(), getBrowserTimezoneOffsetMinutes())); + dateTime.value = getLocalDatetimeFromUnixTime(getSameDateTimeWithBrowserTimezone(dt).getUnixTime()); return; } dt = parseDateTimeFromShortDateTime(text); if (dt) { - dateTime.value = getLocalDatetimeFromUnixTime(getDummyUnixTimeForLocalUsage(dt.getUnixTime(), getTimezoneOffsetMinutes(), getBrowserTimezoneOffsetMinutes())); + dateTime.value = getLocalDatetimeFromUnixTime(getSameDateTimeWithBrowserTimezone(dt).getUnixTime()); return; } diff --git a/src/components/desktop/TrendsChart.vue b/src/components/desktop/TrendsChart.vue index 1f2bad3c..4359969b 100644 --- a/src/components/desktop/TrendsChart.vue +++ b/src/components/desktop/TrendsChart.vue @@ -43,6 +43,7 @@ import { isNumber } from '@/lib/common.ts'; import { + parseDateTimeFromUnixTime, getYearMonthFirstUnixTime, getYearMonthLastUnixTime, getDateTypeByDateRange, @@ -95,11 +96,11 @@ const theme = useTheme(); const { tt, getCurrentLanguageTextDirection, - formatUnixTimeToShortDate, - formatUnixTimeToGregorianLikeShortYear, - formatUnixTimeToGregorianLikeShortYearMonth, + formatDateTimeToShortDate, + formatDateTimeToGregorianLikeShortYear, + formatDateTimeToGregorianLikeShortYearMonth, formatYearQuarterToGregorianLikeYearQuarter, - formatUnixTimeToGregorianLikeFiscalYear, + formatDateTimeToGregorianLikeFiscalYear, formatAmountToWesternArabicNumeralsWithoutDigitGrouping, formatAmountToLocalizedNumeralsWithCurrency } = useI18n(); @@ -151,16 +152,18 @@ const allDisplayDateRanges = computed(() => { const allDisplayDateRanges: string[] = []; for (const dateRange of allDateRanges.value) { + const minDateTime = parseDateTimeFromUnixTime(dateRange.minUnixTime); + if (props.dateAggregationType === ChartDateAggregationType.Year.type) { - allDisplayDateRanges.push(formatUnixTimeToGregorianLikeShortYear(dateRange.minUnixTime)); + allDisplayDateRanges.push(formatDateTimeToGregorianLikeShortYear(minDateTime)); } else if (props.dateAggregationType === ChartDateAggregationType.FiscalYear.type && 'year' in dateRange) { - allDisplayDateRanges.push(formatUnixTimeToGregorianLikeFiscalYear(dateRange.minUnixTime)); + allDisplayDateRanges.push(formatDateTimeToGregorianLikeFiscalYear(minDateTime)); } else if (props.dateAggregationType === ChartDateAggregationType.Quarter.type && 'quarter' in dateRange) { allDisplayDateRanges.push(formatYearQuarterToGregorianLikeYearQuarter(dateRange.year, dateRange.quarter)); } else if (props.dateAggregationType === ChartDateAggregationType.Month.type) { - allDisplayDateRanges.push(formatUnixTimeToGregorianLikeShortYearMonth(dateRange.minUnixTime)); + allDisplayDateRanges.push(formatDateTimeToGregorianLikeShortYearMonth(minDateTime)); } else if (props.dateAggregationType === ChartDateAggregationType.Day.type && props.chartMode === 'daily') { - allDisplayDateRanges.push(formatUnixTimeToShortDate(dateRange.minUnixTime)); + allDisplayDateRanges.push(formatDateTimeToShortDate(minDateTime)); } } diff --git a/src/components/mobile/DateRangeSelectionSheet.vue b/src/components/mobile/DateRangeSelectionSheet.vue index 88daebdb..b140793a 100644 --- a/src/components/mobile/DateRangeSelectionSheet.vue +++ b/src/components/mobile/DateRangeSelectionSheet.vue @@ -45,13 +45,6 @@ import { type CommonDateRangeSelectionProps, useDateRangeSelectionBase } from '@ import { useEnvironmentsStore } from '@/stores/environment.ts'; -import { - getLocalDatetimeFromUnixTime, - getDummyUnixTimeForLocalUsage, - getTimezoneOffsetMinutes, - getBrowserTimezoneOffsetMinutes -} from '@/lib/datetime.ts'; - type DateTimePickerType = InstanceType; const props = defineProps(); @@ -62,7 +55,14 @@ const emit = defineEmits<{ const { tt } = useI18n(); const { showToast } = useI18nUIComponents(); -const { dateRange, beginDateTime, endDateTime, presetRanges, getFinalDateRange } = useDateRangeSelectionBase(props); +const { + dateRange, + beginDateTime, + endDateTime, + presetRanges, + getLocalDatetimeFromSameDateTimeOfUnixTime, + getFinalDateRange +} = useDateRangeSelectionBase(props); const environmentsStore = useEnvironmentsStore(); @@ -91,11 +91,11 @@ function cancel(): void { function onSheetOpen(): void { if (props.minTime) { - dateRange.value[0] = getLocalDatetimeFromUnixTime(getDummyUnixTimeForLocalUsage(props.minTime, getTimezoneOffsetMinutes(), getBrowserTimezoneOffsetMinutes())); + dateRange.value[0] = getLocalDatetimeFromSameDateTimeOfUnixTime(props.minTime); } if (props.maxTime) { - dateRange.value[1] = getLocalDatetimeFromUnixTime(getDummyUnixTimeForLocalUsage(props.maxTime, getTimezoneOffsetMinutes(), getBrowserTimezoneOffsetMinutes())); + dateRange.value[1] = getLocalDatetimeFromSameDateTimeOfUnixTime(props.maxTime); } window.dispatchEvent(new Event('resize')); // fix vue-datepicker preset max-width diff --git a/src/components/mobile/DateTimeSelectionSheet.vue b/src/components/mobile/DateTimeSelectionSheet.vue index 59a8b842..82ca3748 100644 --- a/src/components/mobile/DateTimeSelectionSheet.vue +++ b/src/components/mobile/DateTimeSelectionSheet.vue @@ -111,9 +111,7 @@ import { NumeralSystem } from '@/core/numeral.ts'; import { isDefined } from '@/lib/common.ts'; import { getHourIn12HourFormat, - getLocalDatetimeFromUnixTime, getCurrentUnixTime, - getUnixTimeFromLocalDatetime, getAMOrPM, getCombinedDateAndTimeValues } from '@/lib/datetime.ts'; @@ -122,6 +120,7 @@ type DateTimePickerType = InstanceType; const props = defineProps<{ modelValue: number; + timezoneUtcOffset: number; initMode?: string; show: boolean; }>(); @@ -144,6 +143,8 @@ const { isSecondTwoDigits, isMeridiemIndicatorFirst, meridiemItems, + getLocalDatetimeFromSameDateTimeOfUnixTime, + getUnixTimeFromSameDateTimeOfLocalDatetime, getDisplayTimeValue, generateAllHours, generateAllMinutesOrSeconds @@ -160,7 +161,7 @@ let resetTimePickerItemPositionItemsLastOffsetTop: number | undefined = undefine let resetTimePickerItemPositionCheckedFrames: number | undefined = undefined; const mode = ref(props.initMode || 'time'); -const dateTime = ref(getLocalDatetimeFromUnixTime(props.modelValue || getCurrentUnixTime())); +const dateTime = ref(getLocalDatetimeFromSameDateTimeOfUnixTime(props.modelValue || getCurrentUnixTime(), props.timezoneUtcOffset)); const timePickerContainerHeight = ref(undefined); const timePickerItemHeight = ref(undefined); @@ -213,7 +214,7 @@ function switchMode(): void { } function setCurrentTime(): void { - dateTime.value = getLocalDatetimeFromUnixTime(getCurrentUnixTime()); + dateTime.value = getLocalDatetimeFromSameDateTimeOfUnixTime(getCurrentUnixTime(), props.timezoneUtcOffset); if (mode.value === 'time') { scrollAllTimeSelectedItems(); @@ -225,7 +226,7 @@ function confirm(): void { return; } - const unixTime = getUnixTimeFromLocalDatetime(dateTime.value); + const unixTime = getUnixTimeFromSameDateTimeOfLocalDatetime(dateTime.value, props.timezoneUtcOffset); if (unixTime < 0) { showToast('Date is too early'); @@ -420,7 +421,7 @@ function onSheetOpen(): void { mode.value = props.initMode || 'time'; if (props.modelValue) { - dateTime.value = getLocalDatetimeFromUnixTime(props.modelValue); + dateTime.value = getLocalDatetimeFromSameDateTimeOfUnixTime(props.modelValue, props.timezoneUtcOffset); } if (mode.value === 'time') { diff --git a/src/components/mobile/TrendsBarChart.vue b/src/components/mobile/TrendsBarChart.vue index 561ec30e..df5375ae 100644 --- a/src/components/mobile/TrendsBarChart.vue +++ b/src/components/mobile/TrendsBarChart.vue @@ -144,6 +144,7 @@ import { isNumber } from '@/lib/common.ts'; import { + parseDateTimeFromUnixTime, getYearMonthFirstUnixTime, getYearMonthLastUnixTime, getDateTypeByDateRange, @@ -200,11 +201,11 @@ const emit = defineEmits<{ const { tt, - formatUnixTimeToShortDate, - formatUnixTimeToGregorianLikeShortYear, - formatUnixTimeToGregorianLikeShortYearMonth, + formatDateTimeToShortDate, + formatDateTimeToGregorianLikeShortYear, + formatDateTimeToGregorianLikeShortYearMonth, formatYearQuarterToGregorianLikeYearQuarter, - formatUnixTimeToGregorianLikeFiscalYear, + formatDateTimeToGregorianLikeFiscalYear, formatAmountToLocalizedNumeralsWithCurrency } = useI18n(); @@ -324,18 +325,19 @@ const allDisplayDataItems = computed(() => { dateRangeKey = `${dateRange.year}-${dateRange.month}-${dateRange.day}`; } + const minDateTime = parseDateTimeFromUnixTime(dateRange.minUnixTime); let displayDateRange = ''; if (props.dateAggregationType === ChartDateAggregationType.Year.type) { - displayDateRange = formatUnixTimeToGregorianLikeShortYear(dateRange.minUnixTime); + displayDateRange = formatDateTimeToGregorianLikeShortYear(minDateTime); } else if (props.dateAggregationType === ChartDateAggregationType.FiscalYear.type) { - displayDateRange = formatUnixTimeToGregorianLikeFiscalYear(dateRange.minUnixTime); + displayDateRange = formatDateTimeToGregorianLikeFiscalYear(minDateTime); } else if (props.dateAggregationType === ChartDateAggregationType.Quarter.type && 'quarter' in dateRange) { displayDateRange = formatYearQuarterToGregorianLikeYearQuarter(dateRange.year, dateRange.quarter); } else if (props.dateAggregationType === ChartDateAggregationType.Month.type) { - displayDateRange = formatUnixTimeToGregorianLikeShortYearMonth(dateRange.minUnixTime); + displayDateRange = formatDateTimeToGregorianLikeShortYearMonth(minDateTime); } else if (props.dateAggregationType === ChartDateAggregationType.Day.type && props.chartMode === 'daily') { - displayDateRange = formatUnixTimeToShortDate(dateRange.minUnixTime); + displayDateRange = formatDateTimeToShortDate(minDateTime); } const dataItems = allDateRangeItemsMap[dateRangeKey] || []; diff --git a/src/lib/datetime.ts b/src/lib/datetime.ts index fbfc7050..f38739b8 100644 --- a/src/lib/datetime.ts +++ b/src/lib/datetime.ts @@ -69,7 +69,31 @@ interface DateTimeFormatResult { hasNumeral?: boolean; } -type DateTimeTokenFormatFunction = (d: MomentDateTime, options: DateTimeFormatOptions) => DateTimeFormatResult +type DateTimeTokenFormatFunction = (d: MomentDateTime, options: DateTimeFormatOptions) => DateTimeFormatResult; + +const westernmostTimezoneUtcOffset: number = -720; // Etc/GMT+12 (UTC-12:00) +const easternmostTimezoneUtcOffset: number = 840; // Pacific/Kiritimati (UTC+14:00) + +function getFixedTimezoneName(utcOffset: number): string { + return `Fixed/Timezone${utcOffset}`; +} + +(function initFixedTimezone(): void { + for (let utcOffset = westernmostTimezoneUtcOffset; utcOffset <= easternmostTimezoneUtcOffset; utcOffset += 15) { + const timezoneName = getFixedTimezoneName(utcOffset); + + if (moment.tz.zone(timezoneName)) { + continue; + } + + moment.tz.add(moment.tz.pack({ + name: timezoneName, + abbrs: [`FIX${utcOffset}`], + offsets: [-utcOffset], + untils: [0] + })); + } +})(); class MomentDateTime implements DateTime { private static readonly tokenFormatFuncs: Record = { @@ -501,28 +525,29 @@ export function getUtcOffsetByUtcOffsetMinutes(utcOffsetMinutes: number): string } } -export function getTimezoneOffset(timezone?: string): string { - return getUtcOffsetByUtcOffsetMinutes(getTimezoneOffsetMinutes(timezone)); +export function getTimezoneOffset(unixTime: number, timezone?: string): string { + return getUtcOffsetByUtcOffsetMinutes(getTimezoneOffsetMinutes(unixTime, timezone)); } -export function getTimezoneOffsetMinutes(timezone?: string): number { +export function getTimezoneOffsetMinutes(unixTime: number, timezone?: string): number { if (timezone) { - return moment().tz(timezone).utcOffset(); + return moment.unix(unixTime).tz(timezone).utcOffset(); } else { - return moment().utcOffset(); + return moment.unix(unixTime).utcOffset(); } } -export function getBrowserTimezoneOffset(): string { - return getUtcOffsetByUtcOffsetMinutes(getBrowserTimezoneOffsetMinutes()); +export function getBrowserTimezoneOffset(unixTime: number): string { + return getUtcOffsetByUtcOffsetMinutes(getBrowserTimezoneOffsetMinutes(unixTime)); } -export function getBrowserTimezoneOffsetMinutes(): number { - return -new Date().getTimezoneOffset(); +export function getBrowserTimezoneOffsetMinutes(unixTime: number): number { + const date = getLocalDatetimeFromUnixTime(getSameDateTimeWithBrowserTimezone(parseDateTimeFromUnixTime(unixTime)).getUnixTime()); + return -date.getTimezoneOffset(); } -export function guessTimezoneName(): string { - return moment.tz.guess(true); +export function getBrowserTimezoneName(): string { + return new Intl.DateTimeFormat().resolvedOptions().timeZone; } export function getLocalDatetimeFromUnixTime(unixTime: number): Date { @@ -533,12 +558,46 @@ export function getUnixTimeFromLocalDatetime(datetime: Date): number { return Math.floor(datetime.getTime() / 1000); } -export function getActualUnixTimeForStore(unixTime: number, utcOffset: number, currentUtcOffset: number): number { - return unixTime - (utcOffset - currentUtcOffset) * 60; +export function getSameDateTimeWithCurrentTimezone(dateTime: DateTime): DateTime { + const newDateTime = moment().set({ + year: dateTime.getGregorianCalendarYear(), + month: dateTime.getGregorianCalendarMonth() - 1, + date: dateTime.getGregorianCalendarDay(), + hour: dateTime.getHour(), + minute: dateTime.getMinute(), + second: dateTime.getSecond(), + millisecond: 0 + }); + + return MomentDateTime.of(newDateTime); } -export function getDummyUnixTimeForLocalUsage(unixTime: number, utcOffset: number, currentUtcOffset: number): number { - return unixTime + (utcOffset - currentUtcOffset) * 60; +export function getSameDateTimeWithBrowserTimezone(dateTime: DateTime): DateTime { + const newDateTime = moment().tz(getBrowserTimezoneName()).set({ + year: dateTime.getGregorianCalendarYear(), + month: dateTime.getGregorianCalendarMonth() - 1, + date: dateTime.getGregorianCalendarDay(), + hour: dateTime.getHour(), + minute: dateTime.getMinute(), + second: dateTime.getSecond(), + millisecond: 0, + }); + + return MomentDateTime.of(newDateTime); +} + +export function getSameDateTimeWithTimezoneOffset(dateTime: DateTime, utcOffset: number): DateTime { + const newDateTime = moment().tz(getFixedTimezoneName(utcOffset)).set({ + year: dateTime.getGregorianCalendarYear(), + month: dateTime.getGregorianCalendarMonth() - 1, + date: dateTime.getGregorianCalendarDay(), + hour: dateTime.getHour(), + minute: dateTime.getMinute(), + second: dateTime.getSecond(), + millisecond: 0 + }); + + return MomentDateTime.of(newDateTime); } export function getCurrentDateTime(): DateTime { @@ -554,18 +613,18 @@ export function getYearMonthDayDateTime(year: number, month: number, day: number return MomentDateTime.of(date); } -export function parseDateTimeFromUnixTime(unixTime: number, utcOffset?: number, currentUtcOffset?: number): DateTime { - if (isNumber(utcOffset)) { - if (!isNumber(currentUtcOffset)) { - currentUtcOffset = getTimezoneOffsetMinutes(); - } - - unixTime = getDummyUnixTimeForLocalUsage(unixTime, utcOffset, currentUtcOffset); - } - +export function parseDateTimeFromUnixTime(unixTime: number): DateTime { return MomentDateTime.of(moment.unix(unixTime)); } +export function parseDateTimeFromUnixTimeWithBrowserTimezone(unixTime: number): DateTime { + return MomentDateTime.of(moment.unix(unixTime).tz(getBrowserTimezoneName())); +} + +export function parseDateTimeFromUnixTimeWithTimezoneOffset(unixTime: number, utcOffset: number): DateTime { + return MomentDateTime.of(moment.unix(unixTime).tz(getFixedTimezoneName(utcOffset))); +} + export function parseDateTimeFromKnownDateTimeFormat(dateTime: string, format: KnownDateTimeFormat): DateTime | undefined { const m = moment(dateTime, format.format); @@ -586,8 +645,12 @@ export function parseDateTimeFromString(dateTime: string, format: string): DateT return MomentDateTime.of(m); } -export function formatUnixTime(unixTime: number, format: string, options: DateTimeFormatOptions, utcOffset?: number, currentUtcOffset?: number): string { - return parseDateTimeFromUnixTime(unixTime, utcOffset, currentUtcOffset).format(format, options); +export function formatDateTime(dateTime: DateTime, format: string, options: DateTimeFormatOptions): string { + return dateTime.format(format, options); +} + +export function formatUnixTime(unixTime: number, format: string, options: DateTimeFormatOptions): string { + return parseDateTimeFromUnixTime(unixTime).format(format, options); } export function formatCurrentTime(format: string, options: DateTimeFormatOptions): string { diff --git a/src/lib/services.ts b/src/lib/services.ts index f53d1f7d..75d7accf 100644 --- a/src/lib/services.ts +++ b/src/lib/services.ts @@ -178,7 +178,8 @@ import { } from './server_settings.ts'; import { getTimezoneOffsetMinutes, - guessTimezoneName + getBrowserTimezoneName, + getCurrentUnixTime } from './datetime.ts'; import { generateRandomUUID } from './misc.ts'; import { getBasePath } from './web.ts'; @@ -208,12 +209,12 @@ axios.interceptors.request.use((config: ApiRequestConfig) => { config.headers.Authorization = `Bearer ${token}`; } - config.headers['X-Timezone-Offset'] = getTimezoneOffsetMinutes(); + config.headers['X-Timezone-Offset'] = getTimezoneOffsetMinutes(getCurrentUnixTime()); let timezoneName = getTimeZone(); if (!timezoneName || timezoneName.trim().length < 1) { - timezoneName = guessTimezoneName(); + timezoneName = getBrowserTimezoneName(); } config.headers['X-Timezone-Name'] = timezoneName; diff --git a/src/lib/transaction.ts b/src/lib/transaction.ts index e13d564f..59a02dfd 100644 --- a/src/lib/transaction.ts +++ b/src/lib/transaction.ts @@ -11,8 +11,7 @@ import { isNumber } from './common.ts'; import { - getBrowserTimezoneOffsetMinutes, - getDummyUnixTimeForLocalUsage + getTimezoneOffsetMinutes } from './datetime.ts'; import { categoryTypeToTransactionType, @@ -33,9 +32,10 @@ export interface SetTransactionOptions { comment?: string; } -export function setTransactionModelByTransaction(transaction: Transaction, transaction2: Transaction | null | undefined, allCategories: Record, allCategoriesMap: Record, allVisibleAccounts: Account[], allAccountsMap: Record, allTagsMap: Record, defaultAccountId: string, options: SetTransactionOptions, setContextData: boolean, convertContextTime: boolean): void { +export function setTransactionModelByTransaction(transaction: Transaction, transaction2: Transaction | null | undefined, allCategories: Record, allCategoriesMap: Record, allVisibleAccounts: Account[], allAccountsMap: Record, allTagsMap: Record, defaultAccountId: string, options: SetTransactionOptions, setContextData: boolean): void { if (isDefined(options.time)) { transaction.time = options.time; + transaction.utcOffset = getTimezoneOffsetMinutes(transaction.time, transaction.timeZone); } if (!options.type && options.categoryId && options.categoryId !== '0' && allCategoriesMap[options.categoryId]) { @@ -172,14 +172,9 @@ export function setTransactionModelByTransaction(transaction: Transaction, trans } if (setContextData) { - transaction.utcOffset = transaction2.utcOffset; + transaction.time = transaction2.time; transaction.timeZone = transaction2.timeZone; - - if (convertContextTime) { - transaction.time = getDummyUnixTimeForLocalUsage(transaction2.time, transaction.utcOffset, getBrowserTimezoneOffsetMinutes()); - } else { - transaction.time = transaction2.time; - } + transaction.utcOffset = transaction2.utcOffset; } transaction.sourceAccountId = transaction2.sourceAccountId; diff --git a/src/locales/helpers.ts b/src/locales/helpers.ts index b1deaff0..e16eaada 100644 --- a/src/locales/helpers.ts +++ b/src/locales/helpers.ts @@ -187,6 +187,7 @@ import { formatCurrentTime, formatGregorianCalendarYearDashMonthDashDay, formatGregorianCalendarMonthDashDay, + formatDateTime, formatUnixTime, getBrowserTimezoneOffset, getBrowserTimezoneOffsetMinutes, @@ -202,7 +203,7 @@ import { getTimeDifferenceHoursAndMinutes, getTimezoneOffset, getTimezoneOffsetMinutes, - guessTimezoneName, + getBrowserTimezoneName, isDateRangeMatchFullMonths, isDateRangeMatchFullYears, isPM @@ -1159,20 +1160,20 @@ export function useI18n() { return allRecentMonthDateRanges; } - function getAllTimezones(includeSystemDefault?: boolean): LocalizedTimezoneInfo[] { + function getAllTimezones(unixTime: number, includeSystemDefault?: boolean): LocalizedTimezoneInfo[] { const numeralSystem = getCurrentNumeralSystemType(); - const defaultTimezoneOffset = numeralSystem.replaceWesternArabicDigitsToLocalizedDigits(getBrowserTimezoneOffset()); - const defaultTimezoneOffsetMinutes = getBrowserTimezoneOffsetMinutes(); + const defaultTimezoneOffset = numeralSystem.replaceWesternArabicDigitsToLocalizedDigits(getBrowserTimezoneOffset(unixTime)); + const defaultTimezoneOffsetMinutes = getBrowserTimezoneOffsetMinutes(unixTime); const allTimezoneInfos: LocalizedTimezoneInfo[] = []; for (const timezoneInfo of ALL_TIMEZONES) { - const utcOffset = (timezoneInfo.timezoneName !== UTC_TIMEZONE.timezoneName ? numeralSystem.replaceWesternArabicDigitsToLocalizedDigits(getTimezoneOffset(timezoneInfo.timezoneName)) : ''); + const utcOffset = (timezoneInfo.timezoneName !== UTC_TIMEZONE.timezoneName ? numeralSystem.replaceWesternArabicDigitsToLocalizedDigits(getTimezoneOffset(unixTime, timezoneInfo.timezoneName)) : ''); const displayName = t(`timezone.${timezoneInfo.displayName}`); allTimezoneInfos.push({ name: timezoneInfo.timezoneName, utcOffset: utcOffset, - utcOffsetMinutes: getTimezoneOffsetMinutes(timezoneInfo.timezoneName), + utcOffsetMinutes: getTimezoneOffsetMinutes(unixTime, timezoneInfo.timezoneName), displayName: displayName, displayNameWithUtcOffset: `(UTC${utcOffset}) ${displayName}` }); @@ -1206,7 +1207,7 @@ export function useI18n() { function getAllTimezoneTypesUsedForStatistics(currentTimezone?: string): TypeAndDisplayName[] { const numeralSystem = getCurrentNumeralSystemType(); - const currentTimezoneOffset = numeralSystem.replaceWesternArabicDigitsToLocalizedDigits(getTimezoneOffset(currentTimezone)); + const currentTimezoneOffset = numeralSystem.replaceWesternArabicDigitsToLocalizedDigits(getTimezoneOffset(getCurrentUnixTime(), currentTimezone)); return [ { @@ -1789,12 +1790,11 @@ export function useI18n() { return formatGregorianCalendarMonthDashDay(monthDay, getLocalizedLongMonthDayFormat(), getDateTimeFormatOptions({ calendarType: gregorianLikeCalendarType, numeralSystem: numeralSystem })); } - function formatUnixTimeToGregorianLikeYearQuarter(unixTime: number): string { + function formatDateTimeToGregorianLikeYearQuarter(dateTime: DateTime): string { const gregorianLikeCalendarType = getGregorianLikeCalendarType(); const dateTimeFormatOptions = getDateTimeFormatOptions({ calendarType: gregorianLikeCalendarType }); - const date = parseDateTimeFromUnixTime(unixTime); - const year = date.getLocalizedCalendarYear(dateTimeFormatOptions); - const quarter = date.getLocalizedCalendarQuarter(dateTimeFormatOptions); + const year = dateTime.getLocalizedCalendarYear(dateTimeFormatOptions); + const quarter = dateTime.getLocalizedCalendarQuarter(dateTimeFormatOptions); return formatYearQuarter(year, quarter); } @@ -1887,9 +1887,9 @@ export function useI18n() { return `${displayStartTime} ~ ${displayEndTime}`; } - function getTimezoneDifferenceDisplayText(utcOffset: number): string { + function getTimezoneDifferenceDisplayText(unixTime: number, utcOffset: number): string { const numeralSystem = getCurrentNumeralSystemType(); - const defaultTimezoneOffset = getTimezoneOffsetMinutes(); + const defaultTimezoneOffset = getTimezoneOffsetMinutes(unixTime); const offsetTime = getTimeDifferenceHoursAndMinutes(utcOffset - defaultTimezoneOffset); if (utcOffset > defaultTimezoneOffset) { @@ -2284,19 +2284,11 @@ export function useI18n() { } function setTimeZone(timezone: string): void { - let timezoneOffsetMinutes = getBrowserTimezoneOffsetMinutes(); - if (timezone) { - timezoneOffsetMinutes = getTimezoneOffsetMinutes(timezone); + moment.tz.setDefault(timezone); + } else { + moment.tz.setDefault(); } - - moment.tz.add(moment.tz.pack({ - name: 'Fixed/Timezone', - abbrs: ['FIX'], - offsets: [-timezoneOffsetMinutes], - untils: [0] - })); - moment.tz.setDefault('Fixed/Timezone'); } function initLocale(lastUserLanguage?: string, timezone?: string): LocaleDefaultSettings | null { @@ -2317,7 +2309,7 @@ export function useI18n() { logger.info(`Current timezone is ${timezone}`); setTimeZone(timezone); } else { - logger.info(`No timezone is set, use browser default ${getTimezoneOffset()} (maybe ${guessTimezoneName()})`); + logger.info(`No timezone is set, use browser default ${getTimezoneOffset(getCurrentUnixTime())} (${getBrowserTimezoneName()})`); setTimeZone(''); } @@ -2423,36 +2415,36 @@ export function useI18n() { isLongTimeMinuteTwoDigits, isLongTimeSecondTwoDigits, // format date time (by calendar display type) functions - getCalendarDisplayShortYearFromUnixTime: (unixTime: number, numeralSystem?: NumeralSystem, utcOffset?: number, currentUtcOffset?: number) => formatUnixTime(unixTime, getLocalizedShortYearFormat(), getDateTimeFormatOptions({ calendarType: getCurrentCalendarDisplayType().primaryCalendarType, numeralSystem: numeralSystem }), utcOffset, currentUtcOffset), - getCalendarDisplayShortMonthFromUnixTime: (unixTime: number, numeralSystem?: NumeralSystem, utcOffset?: number, currentUtcOffset?: number) => formatUnixTime(unixTime, 'MMM', getDateTimeFormatOptions({ calendarType: getCurrentCalendarDisplayType().primaryCalendarType, numeralSystem: numeralSystem }), utcOffset, currentUtcOffset), - getCalendarDisplayDayOfMonthFromUnixTime: (unixTime: number, numeralSystem?: NumeralSystem, utcOffset?: number, currentUtcOffset?: number) => formatUnixTime(unixTime, getLocalizedShortDayFormat(), getDateTimeFormatOptions({ calendarType: getCurrentCalendarDisplayType().primaryCalendarType, numeralSystem: numeralSystem }), utcOffset, currentUtcOffset), + getCalendarDisplayShortYearFromDateTime: (dateTime: DateTime, numeralSystem?: NumeralSystem) => formatDateTime(dateTime, getLocalizedShortYearFormat(), getDateTimeFormatOptions({ calendarType: getCurrentCalendarDisplayType().primaryCalendarType, numeralSystem: numeralSystem })), + getCalendarDisplayShortMonthFromDateTime: (dateTime: DateTime, numeralSystem?: NumeralSystem) => formatDateTime(dateTime, 'MMM', getDateTimeFormatOptions({ calendarType: getCurrentCalendarDisplayType().primaryCalendarType, numeralSystem: numeralSystem })), + getCalendarDisplayDayOfMonthFromDateTime: (dateTime: DateTime, numeralSystem?: NumeralSystem) => formatDateTime(dateTime, getLocalizedShortDayFormat(), getDateTimeFormatOptions({ calendarType: getCurrentCalendarDisplayType().primaryCalendarType, numeralSystem: numeralSystem })), // format date time (by date display type) functions parseDateTimeFromLongDateTime: (dateTime: string) => parseDateTimeFromString(dateTime, getLocalizedLongDateFormat() + ' ' + getLocalizedLongTimeFormat()), parseDateTimeFromShortDateTime: (dateTime: string) => parseDateTimeFromString(dateTime, getLocalizedShortDateFormat() + ' ' + getLocalizedShortTimeFormat()), - formatUnixTimeToLongDateTime: (unixTime: number, utcOffset?: number, currentUtcOffset?: number) => formatUnixTime(unixTime, getLocalizedLongDateFormat() + ' ' + getLocalizedLongTimeFormat(), getDateTimeFormatOptions(), utcOffset, currentUtcOffset), - formatUnixTimeToShortDateTime: (unixTime: number, utcOffset?: number, currentUtcOffset?: number) => formatUnixTime(unixTime, getLocalizedShortDateFormat() + ' ' + getLocalizedShortTimeFormat(), getDateTimeFormatOptions(), utcOffset, currentUtcOffset), - formatUnixTimeToLongDate: (unixTime: number, utcOffset?: number, currentUtcOffset?: number) => formatUnixTime(unixTime, getLocalizedLongDateFormat(), getDateTimeFormatOptions(), utcOffset, currentUtcOffset), - formatUnixTimeToShortDate: (unixTime: number, utcOffset?: number, currentUtcOffset?: number) => formatUnixTime(unixTime, getLocalizedShortDateFormat(), getDateTimeFormatOptions(), utcOffset, currentUtcOffset), - formatUnixTimeToLongMonthDay: (unixTime: number, utcOffset?: number, currentUtcOffset?: number) => formatUnixTime(unixTime, getLocalizedLongMonthDayFormat(), getDateTimeFormatOptions(), utcOffset, currentUtcOffset), - formatUnixTimeToShortMonthDay: (unixTime: number, utcOffset?: number, currentUtcOffset?: number) => formatUnixTime(unixTime, getLocalizedShortMonthDayFormat(), getDateTimeFormatOptions(), utcOffset, currentUtcOffset), - formatUnixTimeToLongTime: (unixTime: number, utcOffset?: number, currentUtcOffset?: number) => formatUnixTime(unixTime, getLocalizedLongTimeFormat(), getDateTimeFormatOptions(), utcOffset, currentUtcOffset), - formatUnixTimeToShortTime: (unixTime: number, utcOffset?: number, currentUtcOffset?: number) => formatUnixTime(unixTime, getLocalizedShortTimeFormat(), getDateTimeFormatOptions(), utcOffset, currentUtcOffset), + formatDateTimeToLongDateTime: (dateTime: DateTime) => formatDateTime(dateTime, getLocalizedLongDateFormat() + ' ' + getLocalizedLongTimeFormat(), getDateTimeFormatOptions()), + formatDateTimeToShortDateTime: (dateTime: DateTime) => formatDateTime(dateTime, getLocalizedShortDateFormat() + ' ' + getLocalizedShortTimeFormat(), getDateTimeFormatOptions()), + formatDateTimeToLongDate: (dateTime: DateTime) => formatDateTime(dateTime, getLocalizedLongDateFormat(), getDateTimeFormatOptions()), + formatDateTimeToShortDate: (dateTime: DateTime) => formatDateTime(dateTime, getLocalizedShortDateFormat(), getDateTimeFormatOptions()), + formatDateTimeToLongMonthDay: (dateTime: DateTime) => formatDateTime(dateTime, getLocalizedLongMonthDayFormat(), getDateTimeFormatOptions()), + formatDateTimeToShortMonthDay: (dateTime: DateTime) => formatDateTime(dateTime, getLocalizedShortMonthDayFormat(), getDateTimeFormatOptions()), + formatDateTimeToLongTime: (dateTime: DateTime) => formatDateTime(dateTime, getLocalizedLongTimeFormat(), getDateTimeFormatOptions()), + formatDateTimeToShortTime: (dateTime: DateTime) => formatDateTime(dateTime, getLocalizedShortTimeFormat(), getDateTimeFormatOptions()), formatGregorianTextualYearMonthDayToLongDate: (date: TextualYearMonthDay) => formatGregorianCalendarYearDashMonthDashDay(date, getLocalizedLongDateFormat(), getDateTimeFormatOptions()), // format date time (Gregorian calendar and Gregorian-like calendar) functions - formatUnixTimeToGregorianLikeLongYear: (unixTime: number, utcOffset?: number, currentUtcOffset?: number) => formatUnixTime(unixTime, getLocalizedLongYearFormat(), getDateTimeFormatOptions({ calendarType: getGregorianLikeCalendarType() }), utcOffset, currentUtcOffset), - formatUnixTimeToGregorianLikeShortYear: (unixTime: number, utcOffset?: number, currentUtcOffset?: number) => formatUnixTime(unixTime, getLocalizedShortYearFormat(), getDateTimeFormatOptions({ calendarType: getGregorianLikeCalendarType() }), utcOffset, currentUtcOffset), - formatUnixTimeToGregorianLikeLongYearMonth: (unixTime: number, utcOffset?: number, currentUtcOffset?: number) => formatUnixTime(unixTime, getLocalizedLongYearMonthFormat(), getDateTimeFormatOptions({ calendarType: getGregorianLikeCalendarType() }), utcOffset, currentUtcOffset), - formatUnixTimeToGregorianLikeShortYearMonth: (unixTime: number, utcOffset?: number, currentUtcOffset?: number) => formatUnixTime(unixTime, getLocalizedShortYearMonthFormat(), getDateTimeFormatOptions({ calendarType: getGregorianLikeCalendarType() }), utcOffset, currentUtcOffset), - formatUnixTimeToGregorianLikeLongMonth: (unixTime: number, utcOffset?: number, currentUtcOffset?: number) => formatUnixTime(unixTime, 'MMMM', getDateTimeFormatOptions({ calendarType: getGregorianLikeCalendarType() }), utcOffset, currentUtcOffset), - formatUnixTimeToGregorianLikeShortMonth: (unixTime: number, utcOffset?: number, currentUtcOffset?: number) => formatUnixTime(unixTime, 'MMM', getDateTimeFormatOptions({ calendarType: getGregorianLikeCalendarType() }), utcOffset, currentUtcOffset), + formatDateTimeToGregorianLikeLongYear: (dateTime: DateTime) => formatDateTime(dateTime, getLocalizedLongYearFormat(), getDateTimeFormatOptions({ calendarType: getGregorianLikeCalendarType() })), + formatDateTimeToGregorianLikeShortYear: (dateTime: DateTime) => formatDateTime(dateTime, getLocalizedShortYearFormat(), getDateTimeFormatOptions({ calendarType: getGregorianLikeCalendarType() })), + formatDateTimeToGregorianLikeLongYearMonth: (dateTime: DateTime) => formatDateTime(dateTime, getLocalizedLongYearMonthFormat(), getDateTimeFormatOptions({ calendarType: getGregorianLikeCalendarType() })), + formatDateTimeToGregorianLikeShortYearMonth: (dateTime: DateTime) => formatDateTime(dateTime, getLocalizedShortYearMonthFormat(), getDateTimeFormatOptions({ calendarType: getGregorianLikeCalendarType() })), + formatDateTimeToGregorianLikeLongMonth: (dateTime: DateTime) => formatDateTime(dateTime, 'MMMM', getDateTimeFormatOptions({ calendarType: getGregorianLikeCalendarType() })), + formatDateTimeToGregorianLikeShortMonth: (dateTime: DateTime) => formatDateTime(dateTime, 'MMM', getDateTimeFormatOptions({ calendarType: getGregorianLikeCalendarType() })), formatGregorianTextualMonthDayToGregorianLikeLongMonthDay, - formatUnixTimeToGregorianLikeYearQuarter, + formatDateTimeToGregorianLikeYearQuarter, formatYearQuarterToGregorianLikeYearQuarter, - formatUnixTimeToGregorianLikeFiscalYear, + formatDateTimeToGregorianLikeFiscalYear: (dateTime: DateTime) => formatUnixTimeToGregorianLikeFiscalYear(dateTime.getUnixTime()), formatGregorianYearToGregorianLikeFiscalYear, formatFiscalYearStartToGregorianLikeLongMonth, // format date time (Gregorian calendar) functions - formatUnixTimeToGregorianDefaultDateTime: (unixTime: number, utcOffset?: number, currentUtcOffset?: number) => formatUnixTime(unixTime, KnownDateTimeFormat.DefaultDateTime.format, getDateTimeFormatOptions({ numeralSystem: NumeralSystem.WesternArabicNumerals, calendarType: CalendarType.Gregorian }), utcOffset, currentUtcOffset), + formatDateTimeToGregorianDefaultDateTime: (dateTime: DateTime) => formatDateTime(dateTime, KnownDateTimeFormat.DefaultDateTime.format, getDateTimeFormatOptions({ numeralSystem: NumeralSystem.WesternArabicNumerals, calendarType: CalendarType.Gregorian })), // other format date time functions formatDateRange, getTimezoneDifferenceDisplayText, diff --git a/src/models/transaction.ts b/src/models/transaction.ts index b37bce7c..164be8b8 100644 --- a/src/models/transaction.ts +++ b/src/models/transaction.ts @@ -226,11 +226,11 @@ export class Transaction implements TransactionInfoResponse { this._displayDayOfWeek = displayDayOfWeek; } - public toCreateRequest(clientSessionId: string, actualTime?: number): TransactionCreateRequest { + public toCreateRequest(clientSessionId: string): TransactionCreateRequest { return { type: this.type, categoryId: this.getCategoryId(), - time: actualTime ? actualTime : this.time, + time: this.time, utcOffset: this.utcOffset, sourceAccountId: this.sourceAccountId, destinationAccountId: this.type === TransactionType.Transfer ? this.destinationAccountId : '0', @@ -245,7 +245,7 @@ export class Transaction implements TransactionInfoResponse { }; } - public toModifyRequest(actualTime?: number): TransactionModifyRequest { + public toModifyRequest(): TransactionModifyRequest { let categoryId = this.getCategoryId(); if (this.type === TransactionType.ModifyBalance) { @@ -255,7 +255,7 @@ export class Transaction implements TransactionInfoResponse { return { id: this.id, categoryId: categoryId, - time: actualTime ? actualTime : this.time, + time: this.time, utcOffset: this.utcOffset, sourceAccountId: this.sourceAccountId, destinationAccountId: this.type === TransactionType.Transfer ? this.destinationAccountId : '0', @@ -677,20 +677,20 @@ export const ALL_TRANSACTION_AMOUNTS_REQUEST_TYPE = [ export type TransactionAmountsRequestType = typeof ALL_TRANSACTION_AMOUNTS_REQUEST_TYPE[number]; -export const LATEST_12MONTHS_TRANSACTION_AMOUNTS_REQUEST_TYPES: PartialRecord = { - 'monthBeforeLast10Months': 11, - 'monthBeforeLast9Months': 10, - 'monthBeforeLast8Months': 9, - 'monthBeforeLast7Months': 8, - 'monthBeforeLast6Months': 7, - 'monthBeforeLast5Months': 6, - 'monthBeforeLast4Months': 5, - 'monthBeforeLast3Months': 4, - 'monthBeforeLast2Months': 3, - 'monthBeforeLastMonth': 2, - 'lastMonth': 1, - 'thisMonth': 0 -}; +export const LATEST_12MONTHS_TRANSACTION_AMOUNTS_REQUEST_TYPES: TransactionAmountsRequestType[] = [ + 'monthBeforeLast10Months', + 'monthBeforeLast9Months', + 'monthBeforeLast8Months', + 'monthBeforeLast7Months', + 'monthBeforeLast6Months', + 'monthBeforeLast5Months', + 'monthBeforeLast4Months', + 'monthBeforeLast3Months', + 'monthBeforeLast2Months', + 'monthBeforeLastMonth', + 'lastMonth', + 'thisMonth' +]; export interface TransactionAmountsRequestParams extends PartialRecord { readonly useTransactionTimezone: boolean; @@ -1009,7 +1009,6 @@ export interface TransactionOverviewResponseItem { export interface TransactionMonthlyIncomeAndExpenseData { readonly monthStartTime: number; - readonly monthsBeforeCurrentMonth: number; readonly incomeAmount: number; readonly expenseAmount: number; readonly incompleteIncomeAmount: boolean; diff --git a/src/stores/transaction.ts b/src/stores/transaction.ts index 5c805261..d398e63b 100644 --- a/src/stores/transaction.ts +++ b/src/stores/transaction.ts @@ -53,12 +53,7 @@ import { splitItemsToMap, countSplitItems } from '@/lib/common.ts'; -import { - getTimezoneOffsetMinutes, - getBrowserTimezoneOffsetMinutes, - getActualUnixTimeForStore, - parseDateTimeFromUnixTime -} from '@/lib/datetime.ts'; +import { parseDateTimeFromUnixTimeWithTimezoneOffset } from '@/lib/datetime.ts'; import { getAmountWithDecimalNumberCount } from '@/lib/numeral.ts'; import { getCurrencyFraction } from '@/lib/currency.ts'; import { getFirstVisibleCategoryId } from '@/lib/category.ts'; @@ -185,14 +180,13 @@ export const useTransactionsStore = defineStore('transactions', () => { } if (transactionPageWrapper.items && transactionPageWrapper.items.length) { - const currentUtcOffset = getTimezoneOffsetMinutes(settingsStore.appSettings.timeZone); let currentMonthListIndex = -1; let currentMonthList: TransactionMonthList | null = null; for (const [item, index] of itemAndIndex(transactionPageWrapper.items)) { - fillTransactionObject(item, currentUtcOffset); + fillTransactionObject(item); - const transactionTime = parseDateTimeFromUnixTime(item.time, item.utcOffset, currentUtcOffset); + const transactionTime = parseDateTimeFromUnixTimeWithTimezoneOffset(item.time, item.utcOffset); const transactionYear = transactionTime.getGregorianCalendarYear(); const transactionMonth = transactionTime.getGregorianCalendarMonth(); const transactionYearDashMonth = transactionTime.getGregorianCalendarYearDashMonth(); @@ -264,8 +258,7 @@ export const useTransactionsStore = defineStore('transactions', () => { } function updateTransactionInTransactionList({ currentTransaction, defaultCurrency }: { currentTransaction: Transaction, defaultCurrency: string }): void { - const currentUtcOffset = getTimezoneOffsetMinutes(settingsStore.appSettings.timeZone); - const transactionTime = parseDateTimeFromUnixTime(currentTransaction.time, currentTransaction.utcOffset, currentUtcOffset); + const transactionTime = parseDateTimeFromUnixTimeWithTimezoneOffset(currentTransaction.time, currentTransaction.utcOffset); const transactionYear = transactionTime.getGregorianCalendarYear(); const transactionMonth = transactionTime.getGregorianCalendarMonth(); @@ -276,7 +269,7 @@ export const useTransactionsStore = defineStore('transactions', () => { for (const [transaction, transactionIndex] of itemAndIndex(transactionMonthList.items)) { if (transaction.id === currentTransaction.id) { - fillTransactionObject(currentTransaction, currentUtcOffset); + fillTransactionObject(currentTransaction); if (transactionYear !== transactionMonthList.year || transactionMonth !== transactionMonthList.month || @@ -445,12 +438,12 @@ export const useTransactionsStore = defineStore('transactions', () => { } } - function fillTransactionObject(transaction: Transaction, currentUtcOffset: number): void { + function fillTransactionObject(transaction: Transaction): void { if (!transaction) { return; } - const transactionTime = parseDateTimeFromUnixTime(transaction.time, transaction.utcOffset, currentUtcOffset); + const transactionTime = parseDateTimeFromUnixTimeWithTimezoneOffset(transaction.time, transaction.utcOffset); transaction.setDisplayDate(transactionTime.getGregorianCalendarYearDashMonthDashDay(), transactionTime.getGregorianCalendarDay(), transactionTime.getWeekDay()); if (transaction.sourceAccountId) { @@ -1026,7 +1019,6 @@ export const useTransactionsStore = defineStore('transactions', () => { function saveTransaction({ transaction, defaultCurrency, isEdit, clientSessionId }: { transaction: Transaction, defaultCurrency: string, isEdit: boolean, clientSessionId: string }): Promise { return new Promise((resolve, reject) => { - const actualTime = getActualUnixTimeForStore(transaction.time, transaction.utcOffset, getBrowserTimezoneOffsetMinutes()); let promise: ApiResponsePromise; if (transaction.type !== TransactionType.Expense && @@ -1041,9 +1033,9 @@ export const useTransactionsStore = defineStore('transactions', () => { } if (!isEdit) { - promise = services.addTransaction(transaction.toCreateRequest(clientSessionId, actualTime)); + promise = services.addTransaction(transaction.toCreateRequest(clientSessionId)); } else { - promise = services.modifyTransaction(transaction.toModifyRequest(actualTime)); + promise = services.modifyTransaction(transaction.toModifyRequest()); } promise.then(response => { diff --git a/src/views/base/AboutPageBase.ts b/src/views/base/AboutPageBase.ts index 4e89f36a..cca8358b 100644 --- a/src/views/base/AboutPageBase.ts +++ b/src/views/base/AboutPageBase.ts @@ -9,6 +9,7 @@ import type { VersionInfo } from '@/core/version.ts'; import type { LatestExchangeRateResponse } from '@/models/exchange_rate.ts'; +import { parseDateTimeFromUnixTime } from '@/lib/datetime.ts'; import { getMapProvider } from '@/lib/server_settings.ts'; import { getMapWebsite } from '@/lib/map/index.ts'; import { getLicense, getThirdPartyLicenses } from '@/lib/licenses.ts'; @@ -16,7 +17,7 @@ import { formatDisplayVersion, getClientDisplayVersion, getClientBuildTime } fro import { clearBrowserCaches } from '@/lib/ui/common.ts'; export function useAboutPageBase() { - const { tt, formatUnixTimeToLongDateTime } = useI18n(); + const { tt, formatDateTimeToLongDateTime } = useI18n(); const systemsStore = useSystemsStore(); const exchangeRatesStore = useExchangeRatesStore(); @@ -41,7 +42,8 @@ export function useAboutPageBase() { return time; } - return formatUnixTimeToLongDateTime(parseInt(time)); + const buildDateTime = parseDateTimeFromUnixTime(parseInt(time)); + return formatDateTimeToLongDateTime(buildDateTime); }); const exchangeRatesData = computed(() => exchangeRatesStore.latestExchangeRates.data); diff --git a/src/views/base/ExchangeRatesPageBase.ts b/src/views/base/ExchangeRatesPageBase.ts index 641d69c2..ad08d0cf 100644 --- a/src/views/base/ExchangeRatesPageBase.ts +++ b/src/views/base/ExchangeRatesPageBase.ts @@ -12,9 +12,10 @@ import type { } from '@/models/exchange_rate.ts'; import { getExchangedAmountByRate } from '@/lib/numeral.ts'; +import { parseDateTimeFromUnixTime } from '@/lib/datetime.ts'; export function useExchangeRatesPageBase() { - const { getAllDisplayExchangeRates, formatUnixTimeToLongDate, parseAmountFromWesternArabicNumerals } = useI18n(); + const { getAllDisplayExchangeRates, formatDateTimeToLongDate, parseAmountFromWesternArabicNumerals } = useI18n(); const userStore = useUserStore(); const exchangeRatesStore = useExchangeRatesStore(); @@ -27,8 +28,12 @@ export function useExchangeRatesPageBase() { const isUserCustomExchangeRates = computed(() => exchangeRatesStore.isUserCustomExchangeRates); const exchangeRatesDataUpdateTime = computed(() => { - const exchangeRatesLastUpdateTime = exchangeRatesStore.exchangeRatesLastUpdateTime; - return exchangeRatesLastUpdateTime ? formatUnixTimeToLongDate(exchangeRatesLastUpdateTime) : ''; + if (!exchangeRatesStore.exchangeRatesLastUpdateTime) { + return ''; + } + + const exchangeRatesLastUpdateTime = parseDateTimeFromUnixTime(exchangeRatesStore.exchangeRatesLastUpdateTime); + return formatDateTimeToLongDate(exchangeRatesLastUpdateTime); }); const availableExchangeRates = computed(() => { diff --git a/src/views/base/HomePageBase.ts b/src/views/base/HomePageBase.ts index a0eb660c..216e0c1f 100644 --- a/src/views/base/HomePageBase.ts +++ b/src/views/base/HomePageBase.ts @@ -17,12 +17,14 @@ import type { TransactionOverviewResponseItem } from '@/models/transaction.ts'; +import { parseDateTimeFromUnixTime } from '@/lib/datetime.ts'; + export function useHomePageBase() { const { - formatUnixTimeToLongDate, - formatUnixTimeToLongMonthDay, - formatUnixTimeToGregorianLikeLongYear, - formatUnixTimeToGregorianLikeLongMonth, + formatDateTimeToLongDate, + formatDateTimeToLongMonthDay, + formatDateTimeToGregorianLikeLongYear, + formatDateTimeToGregorianLikeLongMonth, formatAmountToLocalizedNumeralsWithCurrency } = useI18n(); @@ -57,19 +59,19 @@ export function useHomePageBase() { const displayDateRange = computed(() => { return { today: { - displayTime: formatUnixTimeToLongDate(overviewStore.transactionDataRange.today.startTime), + displayTime: formatDateTimeToLongDate(parseDateTimeFromUnixTime(overviewStore.transactionDataRange.today.startTime)), }, thisWeek: { - startTime: formatUnixTimeToLongMonthDay(overviewStore.transactionDataRange.thisWeek.startTime), - endTime: formatUnixTimeToLongMonthDay(overviewStore.transactionDataRange.thisWeek.endTime) + startTime: formatDateTimeToLongMonthDay(parseDateTimeFromUnixTime(overviewStore.transactionDataRange.thisWeek.startTime)), + endTime: formatDateTimeToLongMonthDay(parseDateTimeFromUnixTime(overviewStore.transactionDataRange.thisWeek.endTime)) }, thisMonth: { - displayTime: formatUnixTimeToGregorianLikeLongMonth(overviewStore.transactionDataRange.thisMonth.startTime), - startTime: formatUnixTimeToLongMonthDay(overviewStore.transactionDataRange.thisMonth.startTime), - endTime: formatUnixTimeToLongMonthDay(overviewStore.transactionDataRange.thisMonth.endTime) + displayTime: formatDateTimeToGregorianLikeLongMonth(parseDateTimeFromUnixTime(overviewStore.transactionDataRange.thisMonth.startTime)), + startTime: formatDateTimeToLongMonthDay(parseDateTimeFromUnixTime(overviewStore.transactionDataRange.thisMonth.startTime)), + endTime: formatDateTimeToLongMonthDay(parseDateTimeFromUnixTime(overviewStore.transactionDataRange.thisMonth.endTime)) }, thisYear: { - displayTime: formatUnixTimeToGregorianLikeLongYear(overviewStore.transactionDataRange.thisYear.startTime) + displayTime: formatDateTimeToGregorianLikeLongYear(parseDateTimeFromUnixTime(overviewStore.transactionDataRange.thisYear.startTime)) } }; }); diff --git a/src/views/base/accounts/AccountEditPageBase.ts b/src/views/base/accounts/AccountEditPageBase.ts index ca6ee590..3c3f1dc3 100644 --- a/src/views/base/accounts/AccountEditPageBase.ts +++ b/src/views/base/accounts/AccountEditPageBase.ts @@ -9,7 +9,13 @@ import { AccountCategory, AccountType } from '@/core/account.ts'; import type { LocalizedAccountCategory } from '@/core/account.ts'; import { Account } from '@/models/account.ts'; -import { getCurrentUnixTime } from '@/lib/datetime.ts'; +import { isDefined } from '@/lib/common.ts'; +import { + getTimezoneOffsetMinutes, + getSameDateTimeWithCurrentTimezone, + parseDateTimeFromUnixTimeWithBrowserTimezone, + getCurrentUnixTime +} from '@/lib/datetime.ts'; export interface DayAndDisplayName { readonly day: number; @@ -25,7 +31,7 @@ export function useAccountEditPageBase() { const clientSessionId = ref(''); const loading = ref(false); const submitting = ref(false); - const account = ref(Account.createNewAccount(userStore.currentUserDefaultCurrency, getCurrentUnixTime())); + const account = ref(Account.createNewAccount(userStore.currentUserDefaultCurrency, getCurrentUnixTimeForNewAccount())); const subAccounts = ref([]); const title = computed(() => { @@ -89,6 +95,18 @@ export function useAccountEditPageBase() { const isAccountSupportCreditCardStatementDate = computed(() => account.value && account.value.category === AccountCategory.CreditCard.type); + function getCurrentUnixTimeForNewAccount(): number { + return getSameDateTimeWithCurrentTimezone(parseDateTimeFromUnixTimeWithBrowserTimezone(getCurrentUnixTime())).getUnixTime(); + } + + function getDefaultTimezoneOffsetMinutes(account: Account): number { + if (!account.balanceTime) { + return 0; + } + + return getTimezoneOffsetMinutes(account.balanceTime); + } + function getAccountCreditCardStatementDate(statementDate?: number): string | null { for (const item of allAvailableMonthDays.value) { if (item.day === statementDate) { @@ -99,6 +117,23 @@ export function useAccountEditPageBase() { return null; } + function updateAccountBalanceTime(account: Account, balanceTime: number): void { + if (!isDefined(account.balanceTime)) { + account.balanceTime = balanceTime; + return; + } + + const oldUtcOffset = getTimezoneOffsetMinutes(account.balanceTime); + const newUtcOffset = getTimezoneOffsetMinutes(balanceTime); + + if (oldUtcOffset === newUtcOffset) { + account.balanceTime = balanceTime; + return; + } + + account.balanceTime = balanceTime - (newUtcOffset - oldUtcOffset) * 60; + } + function getInputEmptyProblemMessage(account: Account, isSubAccount: boolean): string | null { if (!isSubAccount && !account.category) { return 'Account category cannot be blank'; @@ -122,7 +157,7 @@ export function useAccountEditPageBase() { return false; } - const subAccount = account.value.createNewSubAccount(userStore.currentUserDefaultCurrency, getCurrentUnixTime()); + const subAccount = account.value.createNewSubAccount(userStore.currentUserDefaultCurrency, getCurrentUnixTimeForNewAccount()); subAccounts.value.push(subAccount); return true; } @@ -133,7 +168,7 @@ export function useAccountEditPageBase() { if (newAccount.subAccounts && newAccount.subAccounts.length > 0) { for (const oldSubAccount of newAccount.subAccounts) { - const subAccount: Account = account.value.createNewSubAccount(userStore.currentUserDefaultCurrency, getCurrentUnixTime()); + const subAccount: Account = account.value.createNewSubAccount(userStore.currentUserDefaultCurrency, getCurrentUnixTimeForNewAccount()); subAccount.fillFrom(oldSubAccount); subAccounts.value.push(subAccount); @@ -163,7 +198,10 @@ export function useAccountEditPageBase() { allAvailableMonthDays, isAccountSupportCreditCardStatementDate, // functions + getCurrentUnixTimeForNewAccount, + getDefaultTimezoneOffsetMinutes, getAccountCreditCardStatementDate, + updateAccountBalanceTime, isNewAccount, addSubAccount, setAccount diff --git a/src/views/base/accounts/ReconciliationStatementPageBase.ts b/src/views/base/accounts/ReconciliationStatementPageBase.ts index ac2fede2..cce929c3 100644 --- a/src/views/base/accounts/ReconciliationStatementPageBase.ts +++ b/src/views/base/accounts/ReconciliationStatementPageBase.ts @@ -2,7 +2,6 @@ import { ref, computed } from 'vue'; import { useI18n } from '@/locales/helpers.ts'; -import { useSettingsStore } from '@/stores/setting.ts'; import { useUserStore } from '@/stores/user.ts'; import { useAccountsStore } from '@/stores/account.ts'; import { useTransactionCategoriesStore } from '@/stores/transactionCategory.ts'; @@ -25,7 +24,8 @@ import { replaceAll } from '@/lib/common.ts'; import { getUtcOffsetByUtcOffsetMinutes, getTimezoneOffsetMinutes, - parseDateTimeFromUnixTime + parseDateTimeFromUnixTime, + parseDateTimeFromUnixTimeWithTimezoneOffset } from '@/lib/datetime.ts'; export function useReconciliationStatementPageBase() { @@ -33,15 +33,14 @@ export function useReconciliationStatementPageBase() { tt, getAllAccountBalanceTrendChartTypes, getAllStatisticsDateAggregationTypesWithShortName, - formatUnixTimeToLongDateTime, - formatUnixTimeToLongDate, - formatUnixTimeToShortTime, - formatUnixTimeToGregorianDefaultDateTime, + formatDateTimeToLongDateTime, + formatDateTimeToLongDate, + formatDateTimeToShortTime, + formatDateTimeToGregorianDefaultDateTime, formatAmountToWesternArabicNumeralsWithoutDigitGrouping, formatAmountToLocalizedNumeralsWithCurrency } = useI18n(); - const settingsStore = useSettingsStore(); const userStore = useUserStore(); const accountsStore = useAccountsStore(); const transactionCategoriesStore = useTransactionCategoriesStore(); @@ -53,7 +52,6 @@ export function useReconciliationStatementPageBase() { const firstDayOfWeek = computed(() => userStore.currentUserFirstDayOfWeek); const fiscalYearStart = computed(() => userStore.currentUserFiscalYearStart); - const currentTimezoneOffsetMinutes = computed(() => getTimezoneOffsetMinutes(settingsStore.appSettings.timeZone)); const defaultCurrency = computed(() => userStore.currentUserDefaultCurrency); const allChartTypes = computed(() => getAllAccountBalanceTrendChartTypes()); @@ -79,11 +77,13 @@ export function useReconciliationStatementPageBase() { const allCategoriesMap = computed>(() => transactionCategoriesStore.allTransactionCategoriesMap); const displayStartDateTime = computed(() => { - return formatUnixTimeToLongDateTime(startTime.value); + const dateTime = parseDateTimeFromUnixTime(startTime.value); + return formatDateTimeToLongDateTime(dateTime); }); const displayEndDateTime = computed(() => { - return formatUnixTimeToLongDateTime(endTime.value); + const dateTime = parseDateTimeFromUnixTime(endTime.value); + return formatDateTimeToLongDateTime(dateTime); }); const displayTotalInflows = computed(() => { @@ -160,15 +160,22 @@ export function useReconciliationStatementPageBase() { } function getDisplayDateTime(transaction: TransactionReconciliationStatementResponseItemWithInfo): string { - return formatUnixTimeToLongDateTime(transaction.time, transaction.utcOffset, currentTimezoneOffsetMinutes.value); + const dateTime = parseDateTimeFromUnixTimeWithTimezoneOffset(transaction.time, transaction.utcOffset); + return formatDateTimeToLongDateTime(dateTime); } function getDisplayDate(transaction: TransactionReconciliationStatementResponseItemWithInfo): string { - return formatUnixTimeToLongDate(transaction.time, transaction.utcOffset, currentTimezoneOffsetMinutes.value); + const dateTime = parseDateTimeFromUnixTimeWithTimezoneOffset(transaction.time, transaction.utcOffset); + return formatDateTimeToLongDate(dateTime); } function getDisplayTime(transaction: TransactionReconciliationStatementResponseItemWithInfo): string { - return formatUnixTimeToShortTime(transaction.time, transaction.utcOffset, currentTimezoneOffsetMinutes.value); + const dateTime = parseDateTimeFromUnixTimeWithTimezoneOffset(transaction.time, transaction.utcOffset); + return formatDateTimeToShortTime(dateTime); + } + + function isSameAsDefaultTimezoneOffsetMinutes(transaction: TransactionReconciliationStatementResponseItemWithInfo): boolean { + return transaction.utcOffset === getTimezoneOffsetMinutes(transaction.time); } function getDisplayTimezone(transaction: TransactionReconciliationStatementResponseItemWithInfo): string { @@ -227,7 +234,7 @@ export function useReconciliationStatementPageBase() { const transactions = reconciliationStatements.value?.transactions ?? []; const rows = transactions.map(transaction => { - const transactionTime = parseDateTimeFromUnixTime(transaction.time, transaction.utcOffset, currentTimezoneOffsetMinutes.value).getUnixTime(); + const transactionTime = parseDateTimeFromUnixTimeWithTimezoneOffset(transaction.time, transaction.utcOffset); const type = getDisplayTransactionType(transaction); let categoryName = transaction.categoryName; let displayAmount = formatAmountToWesternArabicNumeralsWithoutDigitGrouping(transaction.sourceAmount); @@ -260,7 +267,7 @@ export function useReconciliationStatementPageBase() { } return [ - formatUnixTimeToGregorianDefaultDateTime(transactionTime), + formatDateTimeToGregorianDefaultDateTime(transactionTime), type, categoryName, displayAmount, @@ -282,7 +289,6 @@ export function useReconciliationStatementPageBase() { // computed states firstDayOfWeek, fiscalYearStart, - currentTimezoneOffsetMinutes, defaultCurrency, allChartTypes, allDateAggregationTypes, @@ -303,6 +309,7 @@ export function useReconciliationStatementPageBase() { getDisplayDateTime, getDisplayDate, getDisplayTime, + isSameAsDefaultTimezoneOffsetMinutes, getDisplayTimezone, getDisplaySourceAmount, getDisplayDestinationAmount, diff --git a/src/views/base/settings/AppSettingsPageBase.ts b/src/views/base/settings/AppSettingsPageBase.ts index f7118472..ac3a03e1 100644 --- a/src/views/base/settings/AppSettingsPageBase.ts +++ b/src/views/base/settings/AppSettingsPageBase.ts @@ -15,6 +15,7 @@ import { CategoryType } from '@/core/category.ts'; import type { Account } from '@/models/account.ts'; import { isObjectEmpty } from '@/lib/common.ts'; +import { getCurrentUnixTime } from '@/lib/datetime.ts'; export function useAppSettingPageBase() { const { tt, getAllTimezones, getAllTimezoneTypesUsedForStatistics, getAllCurrencySortingTypes, setTimeZone } = useI18n(); @@ -37,7 +38,7 @@ export function useAppSettingPageBase() { ]; }); - const allTimezones = computed(() => getAllTimezones(true)); + const allTimezones = computed(() => getAllTimezones(getCurrentUnixTime(), true)); const allTimezoneTypesUsedForStatistics = computed(() => getAllTimezoneTypesUsedForStatistics()); const allCurrencySortingTypes = computed(() => getAllCurrencySortingTypes()); diff --git a/src/views/base/statistics/StatisticsTransactionPageBase.ts b/src/views/base/statistics/StatisticsTransactionPageBase.ts index 07ab10f2..211eae86 100644 --- a/src/views/base/statistics/StatisticsTransactionPageBase.ts +++ b/src/views/base/statistics/StatisticsTransactionPageBase.ts @@ -28,7 +28,11 @@ import type { } from '@/models/transaction.ts'; import { limitText, findNameByType, findDisplayNameByType } from '@/lib/common.ts'; -import { getYearMonthFirstUnixTime, getYearMonthLastUnixTime } from '@/lib/datetime.ts'; +import { + parseDateTimeFromUnixTime, + getYearMonthFirstUnixTime, + getYearMonthLastUnixTime +} from '@/lib/datetime.ts'; import { getDisplayColor, getCategoryDisplayColor, getAccountDisplayColor } from '@/lib/color.ts'; export function useStatisticsTransactionPageBase() { @@ -37,8 +41,8 @@ export function useStatisticsTransactionPageBase() { getAllDateRanges, getAllStatisticsSortingTypes, getAllStatisticsDateAggregationTypes, - formatUnixTimeToLongDateTime, - formatUnixTimeToGregorianLikeLongYearMonth, + formatDateTimeToLongDateTime, + formatDateTimeToGregorianLikeLongYearMonth, formatDateRange, formatAmountToLocalizedNumeralsWithCurrency } = useI18n(); @@ -88,11 +92,11 @@ export function useStatisticsTransactionPageBase() { const queryStartTime = computed(() => { if (analysisType.value === StatisticsAnalysisType.CategoricalAnalysis) { - return formatUnixTimeToLongDateTime(query.value.categoricalChartStartTime); + return formatDateTimeToLongDateTime(parseDateTimeFromUnixTime(query.value.categoricalChartStartTime)); } else if (analysisType.value === StatisticsAnalysisType.TrendAnalysis) { - return formatUnixTimeToGregorianLikeLongYearMonth(getYearMonthFirstUnixTime(query.value.trendChartStartYearMonth)); + return formatDateTimeToGregorianLikeLongYearMonth(parseDateTimeFromUnixTime(getYearMonthFirstUnixTime(query.value.trendChartStartYearMonth))); } else if (analysisType.value === StatisticsAnalysisType.AssetTrends) { - return formatUnixTimeToLongDateTime(query.value.assetTrendsChartStartTime); + return formatDateTimeToLongDateTime(parseDateTimeFromUnixTime(query.value.assetTrendsChartStartTime)); } else { return ''; } @@ -100,11 +104,11 @@ export function useStatisticsTransactionPageBase() { const queryEndTime = computed(() => { if (analysisType.value === StatisticsAnalysisType.CategoricalAnalysis) { - return formatUnixTimeToLongDateTime(query.value.categoricalChartEndTime); + return formatDateTimeToLongDateTime(parseDateTimeFromUnixTime(query.value.categoricalChartEndTime)); } else if (analysisType.value === StatisticsAnalysisType.TrendAnalysis) { - return formatUnixTimeToGregorianLikeLongYearMonth(getYearMonthLastUnixTime(query.value.trendChartEndYearMonth)); + return formatDateTimeToGregorianLikeLongYearMonth(parseDateTimeFromUnixTime(getYearMonthLastUnixTime(query.value.trendChartEndYearMonth))); } else if (analysisType.value === StatisticsAnalysisType.AssetTrends) { - return formatUnixTimeToLongDateTime(query.value.assetTrendsChartEndTime); + return formatDateTimeToLongDateTime(parseDateTimeFromUnixTime(query.value.assetTrendsChartEndTime)); } else { return ''; } diff --git a/src/views/base/transactions/TransactionEditPageBase.ts b/src/views/base/transactions/TransactionEditPageBase.ts index ff179e87..c83ae8ea 100644 --- a/src/views/base/transactions/TransactionEditPageBase.ts +++ b/src/views/base/transactions/TransactionEditPageBase.ts @@ -36,6 +36,8 @@ import { import { getUtcOffsetByUtcOffsetMinutes, getTimezoneOffsetMinutes, + getSameDateTimeWithCurrentTimezone, + parseDateTimeFromUnixTimeWithBrowserTimezone, getCurrentUnixTime } from '@/lib/datetime.ts'; @@ -92,14 +94,14 @@ export function useTransactionEditPageBase(type: TransactionEditPageType, initMo const transaction = ref(createNewTransactionModel(transactionDefaultType)); const numeralSystem = computed(() => getCurrentNumeralSystemType()); - const currentTimezoneOffsetMinutes = computed(() => getTimezoneOffsetMinutes(settingsStore.appSettings.timeZone)); + const currentTimezoneOffsetMinutes = computed(() => getTimezoneOffsetMinutes(transaction.value.time)); const showAccountBalance = computed(() => settingsStore.appSettings.showAccountBalance); const defaultCurrency = computed(() => userStore.currentUserDefaultCurrency); const defaultAccountId = computed(() => userStore.currentUserDefaultAccountId); const firstDayOfWeek = computed(() => userStore.currentUserFirstDayOfWeek); const coordinateDisplayType = computed(() => userStore.currentUserCoordinateDisplayType); - const allTimezones = computed(() => getAllTimezones(true)); + const allTimezones = computed(() => getAllTimezones(transaction.value.time, true)); const allAccounts = computed(() => accountsStore.allPlainAccounts); const allVisibleAccounts = computed(() => accountsStore.allVisiblePlainAccounts); const allAccountsMap = computed>(() => accountsStore.allAccountsMap); @@ -274,7 +276,7 @@ export function useTransactionEditPageBase(type: TransactionEditPageType, initMo }); const transactionTimezoneTimeDifference = computed(() => { - return getTimezoneDifferenceDisplayText(transaction.value.utcOffset); + return getTimezoneDifferenceDisplayText(transaction.value.time, transaction.value.utcOffset); }); const geoLocationStatusInfo = computed(() => { @@ -331,8 +333,12 @@ export function useTransactionEditPageBase(type: TransactionEditPageType, initMo return !!inputEmptyProblemMessage.value; }); + function getCurrentUnixTimeForNewTransaction(): number { + return getSameDateTimeWithCurrentTimezone(parseDateTimeFromUnixTimeWithBrowserTimezone(getCurrentUnixTime())).getUnixTime(); + } + function createNewTransactionModel(transactionType?: number): Transaction | TransactionTemplate { - const now: number = getCurrentUnixTime(); + const now: number = getCurrentUnixTimeForNewTransaction(); const currentTimezone: string = settingsStore.appSettings.timeZone; let defaultType: TransactionType = TransactionType.Expense; @@ -343,7 +349,7 @@ export function useTransactionEditPageBase(type: TransactionEditPageType, initMo defaultType = TransactionType.Transfer; } - let newTransaction: Transaction | TransactionTemplate = Transaction.createNewTransaction(defaultType, now, currentTimezone, getTimezoneOffsetMinutes(currentTimezone)); + let newTransaction: Transaction | TransactionTemplate = Transaction.createNewTransaction(defaultType, now, currentTimezone, getTimezoneOffsetMinutes(now, currentTimezone)); if (type === TransactionEditPageType.Template) { newTransaction = TransactionTemplate.createNewTransactionTemplate(newTransaction); @@ -352,6 +358,25 @@ export function useTransactionEditPageBase(type: TransactionEditPageType, initMo return newTransaction; } + function updateTransactionTime(newTime: number): void { + transaction.value.time = newTime; + updateTransactionTimezone(transaction.value.timeZone ?? ''); + } + + function updateTransactionTimezone(timezoneName: string): void { + const oldUtcOffset = transaction.value.utcOffset; + + for (const timezone of allTimezones.value) { + if (timezone.name === timezoneName) { + transaction.value.timeZone = timezone.name; + transaction.value.utcOffset = timezone.utcOffsetMinutes; + break; + } + } + + transaction.value.time = transaction.value.time - (transaction.value.utcOffset - oldUtcOffset) * 60; + } + function swapTransactionData(swapAccount: boolean, swapAmount: boolean): void { if (swapAccount) { const oldSourceAccountId = transaction.value.sourceAccountId; @@ -396,15 +421,6 @@ export function useTransactionEditPageBase(type: TransactionEditPageType, initMo } }); - watch(() => transaction.value.timeZone, (newValue) => { - for (const timezone of allTimezones.value) { - if (timezone.name === newValue) { - transaction.value.utcOffset = timezone.utcOffsetMinutes; - break; - } - } - }); - return { // constants isSupportGeoLocation, @@ -460,6 +476,8 @@ export function useTransactionEditPageBase(type: TransactionEditPageType, initMo inputIsEmpty, // functions createNewTransactionModel, + updateTransactionTime, + updateTransactionTimezone, swapTransactionData, getDisplayAmount, getTransactionPictureUrl diff --git a/src/views/base/transactions/TransactionListPageBase.ts b/src/views/base/transactions/TransactionListPageBase.ts index 2de5c062..a9deeda2 100644 --- a/src/views/base/transactions/TransactionListPageBase.ts +++ b/src/views/base/transactions/TransactionListPageBase.ts @@ -23,13 +23,12 @@ import { type Transaction, TransactionTagFilter } from '@/models/transaction.ts' import { getUtcOffsetByUtcOffsetMinutes, - getTimezoneOffset, getTimezoneOffsetMinutes, - getBrowserTimezoneOffsetMinutes, getLocalDatetimeFromUnixTime, - getDummyUnixTimeForLocalUsage, + getSameDateTimeWithBrowserTimezone, getCurrentDateTime, parseDateTimeFromUnixTime, + parseDateTimeFromUnixTimeWithTimezoneOffset, getYearMonthFirstUnixTime, isDateRangeMatchOneMonth } from '@/lib/datetime.ts'; @@ -76,10 +75,10 @@ export function useTransactionListPageBase() { tt, getAllDateRanges, getCurrentNumeralSystemType, - formatUnixTimeToLongDateTime, - formatUnixTimeToLongDate, - formatUnixTimeToShortTime, - formatUnixTimeToGregorianLikeLongYearMonth, + formatDateTimeToLongDateTime, + formatDateTimeToLongDate, + formatDateTimeToShortTime, + formatDateTimeToGregorianLikeLongYearMonth, formatDateRange, formatAmountToLocalizedNumeralsWithCurrency } = useI18n(); @@ -98,7 +97,6 @@ export function useTransactionListPageBase() { const currentCalendarDate = ref(''); const numeralSystem = computed(() => getCurrentNumeralSystemType()); - const currentTimezoneOffsetMinutes = computed(() => getTimezoneOffsetMinutes(settingsStore.appSettings.timeZone)); const firstDayOfWeek = computed(() => userStore.currentUserFirstDayOfWeek); const fiscalYearStart = computed(() => userStore.currentUserFiscalYearStart); const defaultCurrency = computed(() => getUnifiedSelectedAccountsCurrencyOrDefaultCurrency(allAccountsMap.value, queryAllFilterAccountIds.value, userStore.currentUserDefaultCurrency)); @@ -161,8 +159,8 @@ export function useTransactionListPageBase() { return formatDateRange(query.value.dateType, query.value.minTime, query.value.maxTime); }); - const queryMinTime = computed(() => formatUnixTimeToLongDateTime(query.value.minTime)); - const queryMaxTime = computed(() => formatUnixTimeToLongDateTime(query.value.maxTime)); + const queryMinTime = computed(() => formatDateTimeToLongDateTime(parseDateTimeFromUnixTime(query.value.minTime))); + const queryMaxTime = computed(() => formatDateTimeToLongDateTime(parseDateTimeFromUnixTime(query.value.maxTime))); const queryMonthlyData = computed(() => isDateRangeMatchOneMonth(query.value.minTime, query.value.maxTime)); const queryMonth = computed(() => { if (!query.value.minTime || !query.value.maxTime) { @@ -235,8 +233,8 @@ export function useTransactionListPageBase() { return displayAmount.join(' ~ '); }); - const transactionCalendarMinDate = computed(() => getLocalDatetimeFromUnixTime(getDummyUnixTimeForLocalUsage(query.value.minTime, getTimezoneOffsetMinutes(), getBrowserTimezoneOffsetMinutes()))); - const transactionCalendarMaxDate = computed(() => getLocalDatetimeFromUnixTime(getDummyUnixTimeForLocalUsage(query.value.maxTime, getTimezoneOffsetMinutes(), getBrowserTimezoneOffsetMinutes()))); + const transactionCalendarMinDate = computed(() => getLocalDatetimeFromUnixTime(getSameDateTimeWithBrowserTimezone(parseDateTimeFromUnixTime(query.value.minTime)).getUnixTime())); + const transactionCalendarMaxDate = computed(() => getLocalDatetimeFromUnixTime(getSameDateTimeWithBrowserTimezone(parseDateTimeFromUnixTime(query.value.maxTime)).getUnixTime())); const currentMonthTransactionData = computed(() => { const allTransactions = transactionsStore.transactions; @@ -284,6 +282,10 @@ export function useTransactionListPageBase() { return false; } + function isSameAsDefaultTimezoneOffsetMinutes(transaction: Transaction): boolean { + return transaction.utcOffset === getTimezoneOffsetMinutes(transaction.time); + } + function formatAmount(amount: number, hideAmount: boolean, currencyCode: string): string { if (hideAmount) { return formatAmountToLocalizedNumeralsWithCurrency(DISPLAY_HIDDEN_AMOUNT, currencyCode); @@ -293,15 +295,18 @@ export function useTransactionListPageBase() { } function getDisplayTime(transaction: Transaction): string { - return formatUnixTimeToShortTime(transaction.time, transaction.utcOffset, currentTimezoneOffsetMinutes.value); + const dateTime = parseDateTimeFromUnixTimeWithTimezoneOffset(transaction.time, transaction.utcOffset); + return formatDateTimeToShortTime(dateTime); } function getDisplayLongDate(transaction: Transaction): string { - return formatUnixTimeToLongDate(transaction.time, transaction.utcOffset, currentTimezoneOffsetMinutes.value); + const dateTime = parseDateTimeFromUnixTimeWithTimezoneOffset(transaction.time, transaction.utcOffset); + return formatDateTimeToLongDate(dateTime); } function getDisplayLongYearMonth(transactionMonthList: TransactionMonthList): string { - return formatUnixTimeToGregorianLikeLongYearMonth(getYearMonthFirstUnixTime(transactionMonthList.yearDashMonth)); + const yearMonthDateTime = parseDateTimeFromUnixTime(getYearMonthFirstUnixTime(transactionMonthList.yearDashMonth)); + return formatDateTimeToGregorianLikeLongYearMonth(yearMonthDateTime); } function getDisplayTimezone(transaction: Transaction): string { @@ -310,8 +315,10 @@ export function useTransactionListPageBase() { } function getDisplayTimeInDefaultTimezone(transaction: Transaction): string { - const utcOffset = numeralSystem.value.replaceWesternArabicDigitsToLocalizedDigits(getTimezoneOffset(settingsStore.appSettings.timeZone)); - return `${formatUnixTimeToLongDateTime(transaction.time)} (UTC${utcOffset})`; + const timezoneOffsetMinutes = getTimezoneOffsetMinutes(transaction.time); + const dateTime = parseDateTimeFromUnixTimeWithTimezoneOffset(transaction.time, timezoneOffsetMinutes); + const utcOffset = numeralSystem.value.replaceWesternArabicDigitsToLocalizedDigits(getUtcOffsetByUtcOffsetMinutes(timezoneOffsetMinutes)); + return `${formatDateTimeToLongDateTime(dateTime)} (UTC${utcOffset})`; } function getDisplayAmount(transaction: Transaction): string { @@ -372,7 +379,6 @@ export function useTransactionListPageBase() { customMaxDatetime, currentCalendarDate, // computed states - currentTimezoneOffsetMinutes, firstDayOfWeek, fiscalYearStart, defaultCurrency, @@ -410,6 +416,7 @@ export function useTransactionListPageBase() { canAddTransaction, // functions hasSubCategoryInQuery, + isSameAsDefaultTimezoneOffsetMinutes, getDisplayTime, getDisplayLongDate, getDisplayLongYearMonth, diff --git a/src/views/desktop/HomePage.vue b/src/views/desktop/HomePage.vue index e32e9b96..6cad51ff 100644 --- a/src/views/desktop/HomePage.vue +++ b/src/views/desktop/HomePage.vue @@ -202,12 +202,10 @@ import { useAccountsStore } from '@/stores/account.ts'; import { useTransactionCategoriesStore } from '@/stores/transactionCategory.ts'; import { useOverviewStore } from '@/stores/overview.ts'; -import { entries } from '@/core/base.ts'; import { type NumeralSystem } from '@/core/numeral.ts'; import { DateRange } from '@/core/datetime.ts'; import { ThemeType } from '@/core/theme.ts'; import { - type TransactionAmountsRequestType, type TransactionMonthlyIncomeAndExpenseData, LATEST_12MONTHS_TRANSACTION_AMOUNTS_REQUEST_TYPES } from '@/models/transaction.ts'; @@ -280,8 +278,7 @@ const monthlyIncomeAndExpenseData = computed @@ -210,7 +212,6 @@ import { ALL_ACCOUNT_COLORS } from '@/consts/color.ts'; import { Account } from '@/models/account.ts'; import { isNumber } from '@/lib/common.ts'; -import { getCurrentUnixTime } from '@/lib/datetime.ts'; import { generateRandomUUID } from '@/lib/misc.ts'; import { @@ -242,6 +243,9 @@ const { allAccountTypes, allAvailableMonthDays, isAccountSupportCreditCardStatementDate, + getCurrentUnixTimeForNewAccount, + getDefaultTimezoneOffsetMinutes, + updateAccountBalanceTime, isNewAccount, addSubAccount, setAccount @@ -275,7 +279,7 @@ const accountAmountTitle = computed(() => { const isAccountModified = computed(() => { if (!editAccountId.value) { - return !account.value.equals(Account.createNewAccount(userStore.currentUserDefaultCurrency, account.value.balanceTime ?? getCurrentUnixTime())); + return !account.value.equals(Account.createNewAccount(userStore.currentUserDefaultCurrency, account.value.balanceTime ?? getCurrentUnixTimeForNewAccount())); } else { return true; } @@ -289,7 +293,7 @@ function open(options?: { id?: string, currentAccount?: Account, category?: numb loading.value = true; submitting.value = false; - const newAccount = Account.createNewAccount(userStore.currentUserDefaultCurrency, getCurrentUnixTime()); + const newAccount = Account.createNewAccount(userStore.currentUserDefaultCurrency, getCurrentUnixTimeForNewAccount()); account.value.fillFrom(newAccount); subAccounts.value = []; currentAccountIndex.value = -1; diff --git a/src/views/desktop/accounts/list/dialogs/ReconciliationStatementDialog.vue b/src/views/desktop/accounts/list/dialogs/ReconciliationStatementDialog.vue index a136d327..8464cd9e 100644 --- a/src/views/desktop/accounts/list/dialogs/ReconciliationStatementDialog.vue +++ b/src/views/desktop/accounts/list/dialogs/ReconciliationStatementDialog.vue @@ -149,7 +149,7 @@ + @update:model-value="updateTransactionTime"> @@ -323,8 +325,9 @@ :filter-placeholder="tt('Timezone')" :filter-no-items-text="tt('No results')" :items="allTimezones" + :model-value="transaction.timeZone" v-model:show="showTimezonePopup" - v-model="transaction.timeZone"> + @update:model-value="updateTransactionTimezone"> @@ -512,10 +515,9 @@ import type { TransactionPictureInfoBasicResponse } from '@/models/transaction_p import { Transaction } from '@/models/transaction.ts'; import { - getActualUnixTimeForStore, - getBrowserTimezoneOffsetMinutes, getTimezoneOffset, - getTimezoneOffsetMinutes + getTimezoneOffsetMinutes, + parseDateTimeFromUnixTimeWithTimezoneOffset } from '@/lib/datetime.ts'; import { formatCoordinate } from '@/lib/coordinate.ts'; import { generateRandomUUID } from '@/lib/misc.ts'; @@ -536,8 +538,8 @@ const { tt, getMultiMonthdayShortNames, getMultiWeekdayLongNames, - formatUnixTimeToLongDate, - formatUnixTimeToLongTime, + formatDateTimeToLongDate, + formatDateTimeToLongTime, formatGregorianTextualYearMonthDayToLongDate, parseAmountFromLocalizedNumerals } = useI18n(); @@ -589,6 +591,8 @@ const { geoLocationStatusInfo, inputEmptyProblemMessage, inputIsEmpty, + updateTransactionTime, + updateTransactionTimezone, swapTransactionData, getDisplayAmount, getTransactionPictureUrl @@ -655,19 +659,23 @@ const destinationAmountClass = computed>(() => { const transactionDisplayDate = computed(() => { if (mode.value !== TransactionEditPageMode.View || !showTimeInDefaultTimezone.value) { - return formatUnixTimeToLongDate(getActualUnixTimeForStore(transaction.value.time, getTimezoneOffsetMinutes(), getBrowserTimezoneOffsetMinutes())); + const dateTime = parseDateTimeFromUnixTimeWithTimezoneOffset(transaction.value.time, transaction.value.utcOffset); + return formatDateTimeToLongDate(dateTime); } - return formatUnixTimeToLongDate(getActualUnixTimeForStore(transaction.value.time, transaction.value.utcOffset, getBrowserTimezoneOffsetMinutes())); + const dateTime = parseDateTimeFromUnixTimeWithTimezoneOffset(transaction.value.time, getTimezoneOffsetMinutes(transaction.value.time)); + return formatDateTimeToLongDate(dateTime); }); const transactionDisplayTime = computed(() => { if (mode.value !== TransactionEditPageMode.View || !showTimeInDefaultTimezone.value) { - return formatUnixTimeToLongTime(getActualUnixTimeForStore(transaction.value.time, getTimezoneOffsetMinutes(), getBrowserTimezoneOffsetMinutes())); + const dateTime = parseDateTimeFromUnixTimeWithTimezoneOffset(transaction.value.time, transaction.value.utcOffset); + return formatDateTimeToLongTime(dateTime); } - const utcOffset = numeralSystem.value.replaceWesternArabicDigitsToLocalizedDigits(getTimezoneOffset(settingsStore.appSettings.timeZone)); - return `${formatUnixTimeToLongTime(getActualUnixTimeForStore(transaction.value.time, transaction.value.utcOffset, getBrowserTimezoneOffsetMinutes()))} (UTC${utcOffset})`; + const dateTime = parseDateTimeFromUnixTimeWithTimezoneOffset(transaction.value.time, getTimezoneOffsetMinutes(transaction.value.time)); + const utcOffset = numeralSystem.value.replaceWesternArabicDigitsToLocalizedDigits(getTimezoneOffset(transaction.value.time)); + return `${formatDateTimeToLongTime(dateTime)} (UTC${utcOffset})`; }); const transactionDisplayTimezoneName = computed(() => { @@ -949,7 +957,6 @@ function init(): void { tagIds: query['tagIds'], comment: query['comment'] }, - pageTypeAndMode.type === TransactionEditPageType.Transaction && (mode.value === TransactionEditPageMode.Edit || mode.value === TransactionEditPageMode.View), pageTypeAndMode.type === TransactionEditPageType.Transaction && (mode.value === TransactionEditPageMode.Edit || mode.value === TransactionEditPageMode.View) ); diff --git a/src/views/mobile/transactions/ListPage.vue b/src/views/mobile/transactions/ListPage.vue index d9682486..fdc91ea5 100644 --- a/src/views/mobile/transactions/ListPage.vue +++ b/src/views/mobile/transactions/ListPage.vue @@ -205,7 +205,7 @@