Compare commits

..

189 Commits

Author SHA1 Message Date
MaysWind c1a728c391 add default value for newly added columns 2026-01-17 23:43:32 +08:00
MaysWind 46ff0ecd3b place the account at the end of the account category after changing account category 2026-01-17 23:32:13 +08:00
MaysWind 8db69f64c8 place the transaction category at the end of the primary category after changing primary category 2026-01-17 23:31:48 +08:00
MaysWind 8447dd7ae6 support pasting amount on number pad sheet for non-ios device 2026-01-17 22:16:30 +08:00
MaysWind 543cc5f656 disable buttons in navigation bar during initial page load 2026-01-17 20:52:48 +08:00
MaysWind 9664bac47f update the title of the change display order dialog/page 2026-01-17 20:12:43 +08:00
MaysWind 42ae323568 support adding / renaming / deleting / changing display order for tag group on mobile version 2026-01-17 20:04:07 +08:00
MaysWind a357fb8136 automatically switch to the newly added tag group 2026-01-17 19:24:09 +08:00
MaysWind 3b487ca0d9 fix the currently displayed group is incorrect after deleting a tag group 2026-01-17 19:02:44 +08:00
MaysWind 91e98f3126 make delete tag group button disabled when tag group has tags 2026-01-17 18:55:32 +08:00
MaysWind 7ecacaeb05 support moving tags on mobile version 2026-01-17 18:48:45 +08:00
MaysWind 598ae9fa06 show add button in default group 2026-01-17 16:05:04 +08:00
MaysWind 0803a5930f add new contributor 2026-01-17 14:29:46 +08:00
MaysWind 9aaf3284c0 remove unnecessary type assertions 2026-01-17 14:29:36 +08:00
GaryOu b27f9c12de refactor: use isDefined util for account ID check in TransactionEditPageBase.ts 2026-01-17 14:24:07 +08:00
GaryOu a730ebab8f Refactor transfer amount calculation to handle account changes
- Refactor `setTransactionSuitableDestinationAmount` in transactions store to handle account ID changes and avoid logic duplication.
- Recalculate transfer-in amount when changing accounts, while preserving manual edits by verifying previous exchange rate relationships.
- Clean up redundant calculation logic in `TransactionEditPageBase.ts`.
2026-01-17 14:24:07 +08:00
GaryOu f7d0e2279a Watch for account changes and recalculate destination amount for transfers on the Add Transfer Transaction dialog 2026-01-17 14:24:07 +08:00
MaysWind e41dd1c1f8 add new contributor 2026-01-17 10:25:02 +08:00
MaysWind 98274ab864 Merge branch 'main' of https://github.com/mayswind/EasyBookkeeping 2026-01-17 10:23:18 +08:00
lucdsouza a3261acc82 Fix: missing hyphen in 'utf-8' encoding. Error when importing OFX files due to unknown encoding. Fixes issue #48. 2026-01-17 10:13:29 +08:00
MaysWind 7d9cfc4ced support transaction tag group 2026-01-17 00:52:02 +08:00
MaysWind b556efa510 adjust the interaction for displaying and reordering all explorers on the Insights Explorer page 2026-01-17 00:45:09 +08:00
MaysWind 4b72bfd76d fix the changes are cleared after changing date range under "New Explorer" 2026-01-16 23:55:50 +08:00
MaysWind 0f532094ca update the initial styling of the pie chart 2026-01-16 23:30:28 +08:00
MaysWind 7e48cca4ab fix the non-amount numbers in charts are not formatted using localized number formatting 2026-01-16 23:29:26 +08:00
MaysWind 98aa535193 fix the year–quarter format date is not formatted using localized number formatting 2026-01-16 23:29:15 +08:00
MaysWind 48ef9acc19 support the username returned by Synology DSM SSO Server during OIDC authentication (#449) 2026-01-16 21:59:05 +08:00
MaysWind e304f4d3fa display all data in statistics & analysis and hide percentages with values below zero 2026-01-16 01:26:06 +08:00
MaysWind 83a34ae322 allow the either username or email is empty which returns from oauth 2.0 provider, but require both to be present when automatically registering a new user 2026-01-16 00:01:48 +08:00
MaysWind 43a6d1be0f initialize the http transport only once 2026-01-15 23:38:52 +08:00
MaysWind 89fb8a099e add a unified logging handler to the http client 2026-01-15 23:29:48 +08:00
MaysWind 853b0d430e update the supported currencies based on the exchange rate data source 2026-01-15 22:38:53 +08:00
Andrej Kralj 88b63d0222 Updated Slovenian translation 2026-01-14 20:12:45 +08:00
MaysWind 618ad4cac2 support importing Alipay transaction statements with transactions in the pending goods receipt confirmation status (#441) 2026-01-11 14:57:37 +08:00
MaysWind 9b4dd5600a show insights explorer count on data management page 2026-01-11 14:24:38 +08:00
MaysWind ca959fb9ce automatically focus after opening the dialog and support confirming with the enter key 2026-01-11 14:01:14 +08:00
MaysWind ee9b281919 insights explorer supports axis chart 2026-01-11 02:53:37 +08:00
MaysWind 1a0630846d refactor the trends chart component and extract a reusable axis chart component 2026-01-11 00:11:36 +08:00
MaysWind 9585cbc8a9 fix the explorer is not selected when opening a hidden explorer on the insights explorer page 2026-01-10 01:00:04 +08:00
MaysWind 19c0ca8191 add new contributor 2026-01-10 00:33:15 +08:00
Dmitry Shemin 3b0b95ac4a fix: trim trailling spaces in username 2026-01-10 00:32:03 +08:00
MaysWind 1691c320cc update json schema description of mcp tool 2026-01-10 00:16:04 +08:00
thehijacker caf88a9488 Updated Slovenian translation 2026-01-09 23:07:00 +08:00
MaysWind b295b99d3d update the supported currencies based on the exchange rate data source 2026-01-09 00:37:11 +08:00
MaysWind 3cf1276fa7 add all explorers dialog and show confirm dialog when restoring to last saved explorer 2026-01-09 00:33:31 +08:00
MaysWind 5ae763273a format code 2026-01-08 23:41:33 +08:00
MaysWind e39965e7b5 add restore to last saved for insights explorer 2026-01-08 01:29:54 +08:00
MaysWind af36fe9212 highlight the save button when the explorer has been updated 2026-01-08 01:20:30 +08:00
MaysWind 6eb7fa27f6 support configuring the data source of the data table in insights explorer 2026-01-08 00:43:27 +08:00
MaysWind 0dd0597c3b code refactor 2026-01-08 00:05:07 +08:00
MaysWind f0a74a6108 save the number of transactions per page in database 2026-01-07 23:56:31 +08:00
MaysWind 6829eddde5 display different dialog titles when saving a new explorer and renaming an explorer 2026-01-07 23:47:14 +08:00
MaysWind 1c596c4a15 support hiding and unhiding explorers 2026-01-07 23:39:07 +08:00
MaysWind ab88b0bf44 support drag-and-drop to change query display orders 2026-01-07 23:08:30 +08:00
MaysWind d462d0164c save insights explorer to database 2026-01-07 01:04:54 +08:00
MaysWind d4d1342c70 update the supported currencies based on the exchange rate data source 2026-01-05 23:58:16 +08:00
MaysWind a157c1961a fix the incorrect transaction text item 2026-01-05 23:18:17 +08:00
MaysWind 9a037ace5a remember last selected file type in import transaction dialog (#412) 2026-01-05 00:52:57 +08:00
MaysWind c64b4502cb support canceling the sorting operation on mobile version 2026-01-04 23:35:00 +08:00
MaysWind dc41bf8e10 replace the button labels in the navigation bar with a unified icons 2026-01-04 23:29:27 +08:00
MaysWind 0ce66d9070 support changing account category order 2026-01-04 22:50:13 +08:00
MaysWind 6e369f39a4 support setting account categories hidden which has no accounts 2026-01-04 14:10:03 +08:00
MaysWind fb25f589fb add clear all filters in import dialog (#416) 2026-01-04 11:02:09 +08:00
MaysWind 8651755d7a in the import dialog's data review table, keep the selection checkboxes and action button columns fixed in place 2026-01-04 10:31:52 +08:00
MaysWind 277da30339 update the supported currencies based on the exchange rate data source 2026-01-04 01:36:14 +08:00
MaysWind 2fb509beb2 support opening transaction view dialog in insights explorer page 2026-01-04 01:22:23 +08:00
MaysWind 6634d5b791 show transaction tags in insights explorer page 2026-01-04 01:21:33 +08:00
MaysWind 41739d97e7 show transaction date time in current timezone when hover over the transaction time 2026-01-04 00:39:47 +08:00
MaysWind 43bc04012d support setting timezone type in reconciliation statement dialog / page 2026-01-04 00:36:00 +08:00
MaysWind 43154832b6 support filtering geographic location and pictures in insights explorer 2026-01-03 22:42:58 +08:00
MaysWind 91a00cb5b3 support custom chart sorting order 2026-01-03 21:33:06 +08:00
MaysWind 526d7e50ec move the "Timezone Used for Date Range" option from insights explorer settings into each exploration 2026-01-03 20:46:42 +08:00
MaysWind cc0996e0d2 update name to insights explorer 2026-01-03 16:42:02 +08:00
Andrej Kralj 8be5e8aa1d Update Slovenian language
Sorry. One more change. Makes more sense in UI.
2026-01-02 09:57:42 +08:00
MaysWind 022dd3303b adjust the display order of the third party dependency home page url and license url 2026-01-02 00:55:44 +08:00
MaysWind 2865635013 update the supported currencies based on the exchange rate data source 2026-01-02 00:25:53 +08:00
MaysWind c276f261f9 show documentation in the iframe by default 2026-01-02 00:10:08 +08:00
MaysWind ee7e98bb00 show license type of third party dependency on about page 2026-01-01 23:55:35 +08:00
MaysWind 554ce37475 code refactor 2026-01-01 23:55:34 +08:00
MaysWind 1938d972ff bump year 2026-01-01 23:55:34 +08:00
Andrej Kralj 630859bc25 Update Slovenian translation
Fix for some typos.
2026-01-01 23:55:15 +08:00
MaysWind 8ea8a9fe2a add time-based categories "Transaction Day of Week", "Transaction Day of Month", "Transaction Month of Year" and "Transaction Quarter of Year" in insights & explore 2025-12-31 00:38:36 +08:00
MaysWind f5e4d82efc in insights & explore, time-based category supports calculated based on the transaction's time zone 2025-12-31 00:00:04 +08:00
MaysWind 958515b9e0 add new translation contributor 2025-12-30 22:52:07 +08:00
MaysWind 5131e3d6e3 update translation 2025-12-30 22:50:56 +08:00
Andrej Kralj b5a18c86dc Added Slovenian translation
Added Slovenian translation
2025-12-30 20:13:03 +08:00
MaysWind 2f3e26dbe5 revise ambiguous content 2025-12-30 00:55:22 +08:00
MaysWind 3313ccf051 add contributors to the about page 2025-12-30 00:28:20 +08:00
MaysWind 2ada077b38 update translation contributor 2025-12-30 00:10:21 +08:00
MaysWind 31c36f0edf fix the median amount was calculated incorrectly in account reconciliation statements 2025-12-29 00:11:33 +08:00
MaysWind e74d290016 add chart tab to insights & explore page 2025-12-28 23:58:38 +08:00
MaysWind 28337ae228 make the query name input field automatically adjust its width to match the text length 2025-12-27 23:54:20 +08:00
MaysWind e252378898 update query area style 2025-12-26 00:46:38 +08:00
MaysWind 1cc0cd7ae6 if X-Timezone-Name header is provided, always calculate the UTC offset based on the specified time 2025-12-26 00:19:16 +08:00
MaysWind 088e9a339d upgrade golang to 1.25.5, node.js to 24.12.0 and alpine base image to 3.23.2 2025-12-26 00:07:50 +08:00
MaysWind b009c7b6e5 prefer the value of X-Timezone-Name 2025-12-25 09:48:24 +08:00
MaysWind 6bb69b0c27 fix daylight saving time is not calculated correctly when checking whether a transaction can be edited 2025-12-25 00:59:08 +08:00
MaysWind 842683da25 update button style 2025-12-25 00:35:04 +08:00
MaysWind d39816bb9f support using parseDateTime function and IANA time zone names when importing DSV files using custom script 2025-12-25 00:24:26 +08:00
MaysWind e856aefd7b support date time with YYYY.MM.DD HH:mm:ss / MM.DD.YYYY HH:mm:ss / DD.MM.YYYY HH:mm:ss format when importing delimiter-separated values file / data 2025-12-24 23:13:14 +08:00
MaysWind f54c4998ef support IANA time zone names when importing DSV files using column mapping 2025-12-24 23:10:09 +08:00
MaysWind 59a138d417 change the file that reference third-party library 2025-12-24 09:26:54 +08:00
MaysWind 0dc2825e5d support renaming queries, duplicating queries, and displaying query expressions separately for each query 2025-12-24 01:32:15 +08:00
MaysWind 76af5d946a use the daylight saving time zone as default time zone rather than the current standard time zone during the DST 2025-12-24 00:33:47 +08:00
MaysWind c35cbbda15 automatically adjust table column widths based on their content 2025-12-21 14:10:38 +08:00
MaysWind ece58b60ec fix the month names were displayed incorrectly in the monthly income and expense trends chart when daylight saving time was involved (#392) 2025-12-21 02:35:25 +08:00
MaysWind d95e34a597 fix the dates in Statistics & Analysis page does not be processed for daylight saving time 2025-12-21 02:35:11 +08:00
MaysWind a09d7b57f9 automatically scroll to the selected item when opening the language selection drop down list menu 2025-12-21 02:34:57 +08:00
MaysWind a535fbcef1 use the same code for page scrolling on both the desktop and mobile versions 2025-12-21 02:34:35 +08:00
MaysWind 931d5e8395 show ellipsis when the time zone text is too long 2025-12-20 22:15:45 +08:00
MaysWind b37450db15 update style of "OK" button in the dialog 2025-12-20 22:12:18 +08:00
MaysWind 2dd7fd30de update style for long content 2025-12-20 00:53:37 +08:00
MaysWind fb55cd1b33 do not switch expression when there are no conditions 2025-12-20 00:40:58 +08:00
MaysWind e9b4392163 add insights & explore page 2025-12-18 00:55:01 +08:00
MaysWind 861e4c036b remove redundant code 2025-12-14 17:47:35 +08:00
MaysWind e825323bb0 add search box in filter account page / dialog 2025-12-14 17:41:50 +08:00
MaysWind aebd65449b fix the incorrect text color of the time zone in transaction view dialog on the desktop version 2025-12-14 01:41:21 +08:00
MaysWind 0a8f62741a remove redundant code 2025-12-14 01:06:40 +08:00
MaysWind b1cefa5a34 add search box in transaction category page / dialog 2025-12-14 01:05:42 +08:00
MaysWind a12038e40c fix the filter dropdown menu not display the selected items after selecting multiple hidden transaction categories or accounts 2025-12-14 01:03:51 +08:00
MaysWind b2fab42170 reduce dialog margins and make the action buttons always at the bottom of the dialog 2025-12-13 21:04:43 +08:00
MaysWind e9c3001c28 add search box in tag filter page / dialog (#382) 2025-12-13 01:16:51 +08:00
MaysWind 44039438e0 upgrade third party dependencies 2025-12-12 23:24:05 +08:00
MaysWind 372ea29edd hide search bar by clicking search icon 2025-12-12 14:30:25 +08:00
MaysWind 1eb958a21b show more user-friendly messages when some features are disabled 2025-12-12 13:42:46 +08:00
MaysWind 89dd306bb4 use i18n resource item to replace ambiguous configuration item 2025-12-12 12:30:56 +08:00
MaysWind c170cb42e6 add new translation contributor 2025-12-12 12:10:06 +08:00
MaysWind 78f3beaf2f update translation and default locale settings 2025-12-12 12:05:39 +08:00
aydnykn 482d025c90 Add files via upload 2025-12-12 10:57:48 +08:00
aydnykn 11c943efef Add files via upload 2025-12-12 10:57:48 +08:00
aydnykn 6debea6dbb Register Turkish language to index.ts 2025-12-12 10:57:48 +08:00
aydnykn a7a8b9a2fb Add Turkish localization file for the app 2025-12-12 10:57:48 +08:00
Albert Brugués 47cc046e60 improved spanish translations 2025-12-09 23:15:59 +08:00
MaysWind 5a9f4ec3b4 update style 2025-12-06 20:30:46 +08:00
MaysWind 70aa19c623 upgrade framework7 to 9.0.2 2025-12-06 20:30:39 +08:00
MaysWind d2771f6fa9 add new translation contributor 2025-12-06 01:19:30 +08:00
MaysWind 5e4637c6ad update locale default settings 2025-12-06 01:15:39 +08:00
Darshanbm05 a9a7d28082 added kannada language tranlation 2025-12-06 00:50:59 +08:00
Darshanbm05 ffce01c612 Add full Kannada locale src/locales/kn.json (copied from ko.json) to provide 2398-line placeholder translations 2025-12-06 00:50:59 +08:00
Darshanbm05 7a3ec9468f Add Kannada (ಕನ್ನಡ) language translation support
- Add frontend Kannada translations (src/locales/kn.json)
- Add backend Kannada locale text items (pkg/locales/kn.go)
- Update frontend language configuration (src/locales/index.ts)
- Update backend language registry (pkg/locales/all_locales.go)

Language code: kn (Kannada, ISO 639-1)
Display name: ಕನ್ನಡ
Text direction: ltr (left-to-right)
2025-12-06 00:50:59 +08:00
MaysWind 5850e0e298 update sheet height 2025-12-06 00:44:27 +08:00
MaysWind fdf6548cc9 hide the search bar by default on the mobile transaction list page 2025-12-06 00:44:20 +08:00
MaysWind ee8aa2bb8e use popover-close property to close popover 2025-12-05 23:00:08 +08:00
MaysWind eccea273e6 make the styling consistent across all pages of the mobile version 2025-12-05 00:21:07 +08:00
MaysWind e143c8f098 automatically detect file encoding when importing delimiter-separated values (DSV) file 2025-12-03 23:56:13 +08:00
MaysWind 81226c3bb2 add new translation contributor 2025-12-02 23:52:49 +08:00
Albert Brugués c3db8cee2d Spanish translations changed to be familiar 2025-12-02 23:49:58 +08:00
Albert Brugués 9061fc3188 Spanish translations changed to title case 2025-12-02 23:49:58 +08:00
Albert Brugués b9539c4aba Fixed mistakes in Spanish translations 2025-12-02 23:49:58 +08:00
Albert Brugués deabe178df Added missing Spanish translations 2025-12-02 23:49:58 +08:00
MaysWind a5320cf929 development server supports proxying mcp, avatar and transaction image requests 2025-12-02 23:46:33 +08:00
MaysWind 79842d9171 fix cannot access Alibaba Cloud OSS using minio object storage type (#230) 2025-12-02 23:27:24 +08:00
MaysWind 3daff44155 remove outdated code 2025-12-02 00:30:52 +08:00
MaysWind cd2cce4268 update style 2025-12-02 00:30:36 +08:00
MaysWind ad132d5637 fix input field placeholders overlap after upgrading vuetify to version 3.11.0 2025-12-01 01:32:17 +08:00
MaysWind c8b3daa915 make the styling consistent across all pages of the mobile version 2025-12-01 00:55:22 +08:00
MaysWind 96561ec2be upgrade framework7 to 9.0 2025-11-30 03:22:53 +08:00
MaysWind 608411feab upgrade third party dependencies 2025-11-29 22:02:19 +08:00
MaysWind 516e3a5613 upgrade third party dependencies 2025-11-29 19:00:23 +08:00
MaysWind 2431152cec support batch converting amounts to positive / negative values in import dialog 2025-11-27 01:43:01 +08:00
MaysWind 17f604b6aa support filtering transactions by amount in import transaction dialog 2025-11-27 01:25:34 +08:00
MaysWind 3fe51dce63 fix the divider line was positioned incorrectly 2025-11-27 00:01:07 +08:00
MaysWind 2c454f001e support importing amounts that use non-breaking space (NBSP), narrow no-break space (NNBSP) or figure space as digit grouping symbol when importing delimiter-separated values file / data (#361) 2025-11-26 23:57:54 +08:00
MaysWind 4781cb34eb support dates with YYYY.MM.DD / MM.DD.YYYY / DD.MM.YYYY format when importing delimiter-separated values file / data (#361) 2025-11-26 23:31:08 +08:00
MaysWind 9faea14e36 support import delimiter-separated values file / data with UTF-16 with BOM encoding (#361) 2025-11-26 23:30:35 +08:00
MaysWind bd704a8c15 update function name 2025-11-25 01:25:56 +08:00
MaysWind bb9a19bcb2 import member, project and merchant fields as tags when importing feidee mymoney export file 2025-11-25 01:21:44 +08:00
MaysWind 9ff1334584 import payee field as tags when importing a QIF file (#356) 2025-11-25 00:56:28 +08:00
MaysWind de27c8e6c5 fix no results were shown when previewing all results while importing transactions with user custom script 2025-11-24 22:38:00 +08:00
MaysWind ba278e47ff the preview count dropdown menu always shows the "10" option 2025-11-24 22:37:24 +08:00
MaysWind 7d70859107 modify style 2025-11-24 22:04:20 +08:00
MaysWind 6430a52027 tag filter supports selecting both included and excluded tags simultaneously 2025-11-24 02:21:03 +08:00
MaysWind 45be96cf68 adjust column widths to fit their content 2025-11-23 19:06:37 +08:00
MaysWind 837a62a534 paste amount on number pad sheet for ios 2025-11-23 02:32:12 +08:00
MaysWind 707283fd66 hide paste amount menu item for ios 2025-11-23 01:57:29 +08:00
MaysWind 10b9c09192 insert the pasted content after the cursor when pasting numbers or amounts 2025-11-23 01:44:40 +08:00
MaysWind ed8c5c96ac set allowed number range 2025-11-23 01:44:34 +08:00
MaysWind 3ba91c590e highlight the current row 2025-11-23 00:49:43 +08:00
MaysWind 44dc45de51 transaction reconciliation statement supports sorting by account name and category name on desktop version 2025-11-23 00:33:54 +08:00
MaysWind 83bd68e7f4 use the number system configured in the user's settings for all numeric values in the token generation dialog 2025-11-22 23:49:23 +08:00
MaysWind dafbc115c4 support pasting amount from clipboard on mobile version 2025-11-22 23:29:09 +08:00
MaysWind 5de1e32cd8 update text 2025-11-22 20:04:27 +08:00
MaysWind 8ae5c1ea99 limit the maximum height of the date range menu 2025-11-19 00:53:41 +08:00
MaysWind 29651f674a code refactor 2025-11-19 00:00:20 +08:00
MaysWind b1dff5ef51 add error log 2025-11-17 23:26:58 +08:00
MaysWind bb0971ea17 modify log content 2025-11-17 23:18:11 +08:00
MaysWind 8a020b666c fix the incorrect email verify page, reset password page, and OAuth 2.0 callback page url when accessing ezBookkeeping through a subpath (#348) 2025-11-17 00:38:04 +08:00
MaysWind 8b34750426 fix the incorrect display type name of transaction categories 2025-11-14 00:02:57 +08:00
MaysWind 32cb2b2354 bump version to 1.3.0 2025-11-09 23:29:33 +08:00
393 changed files with 31763 additions and 7824 deletions
@@ -3,13 +3,13 @@ name: Build backend file for windows
inputs:
go-version:
required: false
default: "1.25.3"
default: "1.25.5"
mingw-version:
required: false
default: "14.2.0"
default: "15.2.0"
mingw-revison:
required: false
default: "v12-rev2"
default: "v13-rev0"
release-build:
required: false
type: string
+3 -3
View File
@@ -1,5 +1,5 @@
# Build backend binary file
FROM golang:1.25.3-alpine3.22 AS be-builder
FROM golang:1.25.5-alpine3.23 AS be-builder
ARG RELEASE_BUILD
ARG BUILD_PIPELINE
ARG BUILD_UNIXTIME
@@ -19,7 +19,7 @@ RUN apk add git gcc g++ libc-dev
RUN ./build.sh backend
# Build frontend files
FROM --platform=$BUILDPLATFORM node:24.10.0-alpine3.22 AS fe-builder
FROM --platform=$BUILDPLATFORM node:24.12.0-alpine3.23 AS fe-builder
ARG RELEASE_BUILD
ARG BUILD_PIPELINE
ARG BUILD_UNIXTIME
@@ -35,7 +35,7 @@ RUN apk add git
RUN ./build.sh frontend
# Package docker image
FROM alpine:3.22.2
FROM alpine:3.23.2
LABEL maintainer="MaysWind <i@mayswind.net>"
RUN addgroup -S -g 1000 ezbookkeeping && adduser -S -G ezbookkeeping -u 1000 ezbookkeeping
RUN apk --no-cache add tzdata
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2020-2025 MaysWind (i@mayswind.net)
Copyright (c) 2020-2026 MaysWind (i@mayswind.net)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+5 -2
View File
@@ -129,15 +129,18 @@ Currently available translations:
| --- | --- | --- |
| de | Deutsch | [@chrgm](https://github.com/chrgm) |
| en | English | / |
| es | Español | [@Miguelonlonlon](https://github.com/Miguelonlonlon) |
| es | Español | [@Miguelonlonlon](https://github.com/Miguelonlonlon), [@abrugues](https://github.com/abrugues) |
| fr | Français | [@brieucdlf](https://github.com/brieucdlf) |
| it | Italiano | [@waron97](https://github.com/waron97) |
| ja | 日本語 | [@tkymmm](https://github.com/tkymmm) |
| kn | ಕನ್ನಡ | [@Darshanbm05](https://github.com/Darshanbm05) |
| ko | 한국어 | [@overworks](https://github.com/overworks) |
| nl | Nederlands | [@automagic](https://github.com/automagics) |
| nl | Nederlands | [@automagics](https://github.com/automagics) |
| pt-BR | Português (Brasil) | [@thecodergus](https://github.com/thecodergus) |
| ru | Русский | [@artegoser](https://github.com/artegoser) |
| sl | Slovenščina | [@thehijacker](https://github.com/thehijacker) |
| th | ไทย | [@natthavat28](https://github.com/natthavat28) |
| tr | Türkçe | [@aydnykn](https://github.com/aydnykn) |
| uk | Українська | [@nktlitvinenko](https://github.com/nktlitvinenko) |
| vi | Tiếng Việt | [@f97](https://github.com/f97) |
| zh-Hans | 中文 (简体) | / |
+16
View File
@@ -101,6 +101,14 @@ func updateAllDatabaseTablesStructure(c *core.CliContext) error {
log.BootInfof(c, "[database.updateAllDatabaseTablesStructure] transaction category table maintained successfully")
err = datastore.Container.UserDataStore.SyncStructs(new(models.TransactionTagGroup))
if err != nil {
return err
}
log.BootInfof(c, "[database.updateAllDatabaseTablesStructure] transaction tag group table maintained successfully")
err = datastore.Container.UserDataStore.SyncStructs(new(models.TransactionTag))
if err != nil {
@@ -157,5 +165,13 @@ func updateAllDatabaseTablesStructure(c *core.CliContext) error {
log.BootInfof(c, "[database.updateAllDatabaseTablesStructure] user external auth table maintained successfully")
err = datastore.Container.UserDataStore.SyncStructs(new(models.InsightsExplorer))
if err != nil {
return err
}
log.BootInfof(c, "[database.updateAllDatabaseTablesStructure] insights explorer table maintained successfully")
return nil
}
+19
View File
@@ -116,6 +116,7 @@ func startWebServer(c *core.CliContext) error {
_ = v.RegisterValidation("validCurrency", validators.ValidCurrency)
_ = v.RegisterValidation("validHexRGBColor", validators.ValidHexRGBColor)
_ = v.RegisterValidation("validAmountFilter", validators.ValidAmountFilter)
_ = v.RegisterValidation("validTagFilter", validators.ValidTagFilter)
_ = v.RegisterValidation("validFiscalYearStart", validators.ValidateFiscalYearStart)
}
@@ -382,6 +383,7 @@ func startWebServer(c *core.CliContext) error {
apiV1Route.GET("/transactions/count.json", bindApi(api.Transactions.TransactionCountHandler))
apiV1Route.GET("/transactions/list.json", bindApi(api.Transactions.TransactionListHandler))
apiV1Route.GET("/transactions/list/by_month.json", bindApi(api.Transactions.TransactionMonthListHandler))
apiV1Route.GET("/transactions/list/all.json", bindApi(api.Transactions.TransactionListAllHandler))
apiV1Route.GET("/transactions/reconciliation_statements.json", bindApi(api.Transactions.TransactionReconciliationStatementHandler))
apiV1Route.GET("/transactions/statistics.json", bindApi(api.Transactions.TransactionStatisticsHandler))
apiV1Route.GET("/transactions/statistics/trends.json", bindApi(api.Transactions.TransactionStatisticsTrendsHandler))
@@ -416,6 +418,14 @@ func startWebServer(c *core.CliContext) error {
apiV1Route.POST("/transaction/categories/move.json", bindApi(api.TransactionCategories.CategoryMoveHandler))
apiV1Route.POST("/transaction/categories/delete.json", bindApi(api.TransactionCategories.CategoryDeleteHandler))
// Transaction Tag Groups
apiV1Route.GET("/transaction/tags/groups/list.json", bindApi(api.TransactionTagGroups.TagGroupListHandler))
apiV1Route.GET("/transaction/tags/groups/get.json", bindApi(api.TransactionTagGroups.TagGroupGetHandler))
apiV1Route.POST("/transaction/tags/groups/add.json", bindApi(api.TransactionTagGroups.TagGroupCreateHandler))
apiV1Route.POST("/transaction/tags/groups/modify.json", bindApi(api.TransactionTagGroups.TagGroupModifyHandler))
apiV1Route.POST("/transaction/tags/groups/move.json", bindApi(api.TransactionTagGroups.TagGroupMoveHandler))
apiV1Route.POST("/transaction/tags/groups/delete.json", bindApi(api.TransactionTagGroups.TagGroupDeleteHandler))
// Transaction Tags
apiV1Route.GET("/transaction/tags/list.json", bindApi(api.TransactionTags.TagListHandler))
apiV1Route.GET("/transaction/tags/get.json", bindApi(api.TransactionTags.TagGetHandler))
@@ -435,6 +445,15 @@ func startWebServer(c *core.CliContext) error {
apiV1Route.POST("/transaction/templates/move.json", bindApi(api.TransactionTemplates.TemplateMoveHandler))
apiV1Route.POST("/transaction/templates/delete.json", bindApi(api.TransactionTemplates.TemplateDeleteHandler))
// Insights Explorers
apiV1Route.GET("/insights/explorers/list.json", bindApi(api.InsightsExplorers.InsightsExplorerListHandler))
apiV1Route.GET("/insights/explorers/get.json", bindApi(api.InsightsExplorers.InsightsExplorerGetHandler))
apiV1Route.POST("/insights/explorers/add.json", bindApi(api.InsightsExplorers.InsightsExplorerCreateHandler))
apiV1Route.POST("/insights/explorers/modify.json", bindApi(api.InsightsExplorers.InsightsExplorerModifyHandler))
apiV1Route.POST("/insights/explorers/hide.json", bindApi(api.InsightsExplorers.InsightsExplorerHideHandler))
apiV1Route.POST("/insights/explorers/move.json", bindApi(api.InsightsExplorers.InsightsExplorerMoveHandler))
apiV1Route.POST("/insights/explorers/delete.json", bindApi(api.InsightsExplorers.InsightsExplorerDeleteHandler))
// Large Language Models
if config.ReceiptImageRecognitionLLMConfig != nil && config.ReceiptImageRecognitionLLMConfig.LLMProvider != "" {
if config.TransactionFromAIImageRecognition {
-3
View File
@@ -1,7 +1,4 @@
[global]
# Application instance name
app_name = ezBookkeeping
# Either "production", "development"
mode = production
+65
View File
@@ -0,0 +1,65 @@
{
"code": [
"jiangshengwu",
"vigdail",
"f97",
"Miguelonlonlon",
"seb26",
"nktlitvinenko",
"lvdou-bing",
"dshemin",
"lucdsouza",
"OuIChien"
],
"translators": {
"de": [
"chrgm"
],
"en": [],
"es": [
"Miguelonlonlon",
"abrugues"
],
"fr": [
"brieucdlf"
],
"it": [
"waron97"
],
"ja": [
"tkymmm"
],
"kn": [
"Darshanbm05"
],
"ko": [
"overworks"
],
"nl": [
"automagics"
],
"pt-BR": [
"thecodergus"
],
"ru": [
"artegoser"
],
"sl": [
"thehijacker"
],
"th": [
"natthavat28"
],
"tr": [
"aydnykn"
],
"uk": [
"nktlitvinenko"
],
"vi": [
"f97"
],
"zh-Hans": [],
"zh-Hant": []
}
}
+14 -13
View File
@@ -4,30 +4,30 @@ go 1.25
require (
github.com/boombuler/barcode v1.1.0
github.com/coreos/go-oidc/v3 v3.16.0
github.com/coreos/go-oidc/v3 v3.17.0
github.com/extrame/xls v0.0.2-0.20200426124601-4a6cf263071b
github.com/gin-contrib/cache v1.4.1
github.com/gin-contrib/gzip v1.2.5
github.com/gin-gonic/gin v1.11.0
github.com/go-co-op/gocron/v2 v2.17.0
github.com/go-co-op/gocron/v2 v2.18.2
github.com/go-playground/validator/v10 v10.28.0
github.com/go-sql-driver/mysql v1.9.3
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/invopop/jsonschema v0.13.0
github.com/lib/pq v1.10.9
github.com/mattn/go-sqlite3 v1.14.32
github.com/minio/minio-go/v7 v7.0.95
github.com/minio/minio-go/v7 v7.0.97
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/pquerna/otp v1.5.0
github.com/sirupsen/logrus v1.9.3
github.com/stretchr/testify v1.11.1
github.com/urfave/cli/v3 v3.5.0
github.com/urfave/cli/v3 v3.6.1
github.com/wk8/go-ordered-map/v2 v2.1.8
github.com/xuri/excelize/v2 v2.10.0
golang.org/x/crypto v0.43.0
golang.org/x/net v0.46.0
golang.org/x/oauth2 v0.32.0
golang.org/x/text v0.30.0
golang.org/x/crypto v0.46.0
golang.org/x/net v0.48.0
golang.org/x/oauth2 v0.34.0
golang.org/x/text v0.32.0
gopkg.in/ini.v1 v1.67.0
gopkg.in/mail.v2 v2.3.1
xorm.io/builder v0.3.13
@@ -67,11 +67,12 @@ require (
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/klauspost/crc32 v1.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/memcachier/mc/v3 v3.0.3 // indirect
github.com/minio/crc64nvme v1.0.2 // indirect
github.com/minio/crc64nvme v1.1.0 // indirect
github.com/minio/md5-simd v1.1.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
@@ -98,10 +99,10 @@ require (
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // indirect
golang.org/x/arch v0.22.0 // indirect
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect
golang.org/x/mod v0.29.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.37.0 // indirect
golang.org/x/tools v0.38.0 // indirect
golang.org/x/mod v0.30.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/tools v0.39.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
+28 -26
View File
@@ -28,8 +28,8 @@ github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLI
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/coreos/go-oidc/v3 v3.16.0 h1:qRQUCFstKpXwmEjDQTIbyY/5jF00+asXzSkmkoa/mow=
github.com/coreos/go-oidc/v3 v3.16.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc=
github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -53,8 +53,8 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
github.com/go-co-op/gocron/v2 v2.17.0 h1:e/oj6fcAM8vOOKZxv2Cgfmjo+s8AXC46po5ZPtaSea4=
github.com/go-co-op/gocron/v2 v2.17.0/go.mod h1:Zii6he+Zfgy5W9B+JKk/KwejFOW0kZTFvHtwIpR4aBI=
github.com/go-co-op/gocron/v2 v2.18.2 h1:+5VU41FUXPWSPKLXZQ/77SGzUiPCcakU0v7ENc2H20Q=
github.com/go-co-op/gocron/v2 v2.18.2/go.mod h1:Zii6he+Zfgy5W9B+JKk/KwejFOW0kZTFvHtwIpR4aBI=
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
@@ -96,6 +96,8 @@ github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa02
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM=
github.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
@@ -112,12 +114,12 @@ github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuE
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/memcachier/mc/v3 v3.0.3 h1:qii+lDiPKi36O4Xg+HVKwHu6Oq+Gt17b+uEiA0Drwv4=
github.com/memcachier/mc/v3 v3.0.3/go.mod h1:GzjocBahcXPxt2cmqzknrgqCOmMxiSzhVKPOe90Tpug=
github.com/minio/crc64nvme v1.0.2 h1:6uO1UxGAD+kwqWWp7mBFsi5gAse66C4NXO8cmcVculg=
github.com/minio/crc64nvme v1.0.2/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
github.com/minio/crc64nvme v1.1.0 h1:e/tAguZ+4cw32D+IO/8GSf5UVr9y+3eJcxZI2WOO/7Q=
github.com/minio/crc64nvme v1.1.0/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
github.com/minio/minio-go/v7 v7.0.95 h1:ywOUPg+PebTMTzn9VDsoFJy32ZuARN9zhB+K3IYEvYU=
github.com/minio/minio-go/v7 v7.0.95/go.mod h1:wOOX3uxS334vImCNRVyIDdXX9OsXDm89ToynKgqUKlo=
github.com/minio/minio-go/v7 v7.0.97 h1:lqhREPyfgHTB/ciX8k2r8k0D93WaFqxbJX36UZq5occ=
github.com/minio/minio-go/v7 v7.0.97/go.mod h1:re5VXuo0pwEtoNLsNuSr0RrLfT/MBtohwdaSmPPSRSk=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -177,8 +179,8 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/urfave/cli/v3 v3.5.0 h1:qCuFMmdayTF3zmjG8TSsoBzrDqszNrklYg2x3g4MSgw=
github.com/urfave/cli/v3 v3.5.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
github.com/urfave/cli/v3 v3.6.1 h1:j8Qq8NyUawj/7rTYdBGrxcH7A/j7/G8Q5LhWEW4G3Mo=
github.com/urfave/cli/v3 v3.6.1/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
@@ -191,29 +193,29 @@ github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/T
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
+979 -999
View File
File diff suppressed because it is too large Load Diff
+24 -23
View File
@@ -1,6 +1,6 @@
{
"name": "ezbookkeeping",
"version": "1.2.0",
"version": "1.3.0",
"private": true,
"repository": {
"type": "git",
@@ -20,62 +20,63 @@
},
"dependencies": {
"@mdi/js": "^7.4.47",
"@vuepic/vue-datepicker": "^11.0.3",
"axios": "^1.12.2",
"@vuepic/vue-datepicker": "^12.1.0",
"axios": "^1.13.2",
"cbor-js": "^0.1.0",
"chardet": "^2.1.1",
"clipboard": "^2.0.11",
"crypto-js": "^4.2.0",
"dom7": "^4.0.6",
"echarts": "^6.0.0",
"framework7": "^8.3.4",
"framework7": "^9.0.2",
"framework7-icons": "^5.0.5",
"framework7-vue": "^8.3.4",
"framework7-vue": "^9.0.2",
"jalaali-js": "^1.2.8",
"leaflet": "^1.9.4",
"line-awesome": "^1.3.0",
"moment": "^2.30.1",
"moment-timezone": "^0.6.0",
"pinia": "^3.0.3",
"pinia": "^3.0.4",
"register-service-worker": "^1.7.2",
"skeleton-elements": "^4.0.1",
"swiper": "^10.2.0",
"swiper": "^12.0.3",
"ua-parser-js": "^1.0.39",
"vue": "^3.5.22",
"vue": "^3.5.25",
"vue-echarts": "^8.0.1",
"vue-i18n": "^11.1.12",
"vue-router": "^4.6.3",
"vue-i18n": "^11.2.2",
"vue-router": "^4.6.4",
"vue3-perfect-scrollbar": "^2.0.0",
"vuedraggable": "^4.1.0",
"vuetify": "^3.10.7"
"vuetify": "^3.11.3"
},
"devDependencies": {
"@jest/globals": "^30.2.0",
"@tsconfig/node24": "^24.0.1",
"@tsconfig/node24": "^24.0.3",
"@types/cbor-js": "^0.1.1",
"@types/crypto-js": "^4.2.2",
"@types/git-rev-sync": "^2.0.2",
"@types/jalaali-js": "^1.2.0",
"@types/jest": "^30.0.0",
"@types/node": "^24.9.1",
"@types/node": "^24.1.0",
"@types/ua-parser-js": "^0.7.39",
"@vitejs/plugin-vue": "^6.0.1",
"@vitejs/plugin-vue": "^6.0.3",
"@vue/eslint-config-typescript": "^14.6.0",
"@vue/tsconfig": "^0.8.1",
"cross-env": "^10.1.0",
"eslint": "^9.38.0",
"eslint-plugin-vue": "^10.5.1",
"eslint": "^9.39.1",
"eslint-plugin-vue": "^10.6.2",
"git-rev-sync": "^3.0.2",
"jest": "^30.2.0",
"postcss-preset-env": "^10.4.0",
"sass": "^1.93.2",
"ts-jest": "^29.4.5",
"postcss-preset-env": "^10.5.0",
"sass": "^1.96.0",
"ts-jest": "^29.4.6",
"ts-node": "^10.9.2",
"typescript": "^5.9.3",
"vite": "^7.1.12",
"vite-plugin-checker": "^0.11.0",
"vite-plugin-pwa": "^1.1.0",
"vite": "^7.2.7",
"vite-plugin-checker": "^0.12.0",
"vite-plugin-pwa": "^1.2.0",
"vite-plugin-vuetify": "^2.1.2",
"vue-tsc": "^3.1.2"
"vue-tsc": "^3.1.8"
},
"browserslist": [
"last 5 Chrome versions",
+27 -16
View File
@@ -150,10 +150,10 @@ func (a *AccountsApi) AccountCreateHandler(c *core.WebContext) (any, *errs.Error
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
utcOffset, err := c.GetClientTimezoneOffset()
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.GetClientTimezoneOffset()
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
}
@@ -437,6 +437,17 @@ func (a *AccountsApi) AccountModifyHandler(c *core.WebContext) (any, *errs.Error
toUpdateAccount := a.getToUpdateAccount(uid, &accountModifyReq, mainAccount, false)
if toUpdateAccount != nil {
if toUpdateAccount.Category != mainAccount.Category {
maxOrderId, err := a.accounts.GetMaxDisplayOrder(c, uid, toUpdateAccount.Category)
if err != nil {
log.Errorf(c, "[accounts.AccountModifyHandler] failed to get max display order for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
toUpdateAccount.DisplayOrder = maxOrderId + 1
}
anythingUpdate = true
toUpdateAccounts = append(toUpdateAccounts, toUpdateAccount)
}
@@ -521,7 +532,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())
@@ -542,7 +553,6 @@ func (a *AccountsApi) AccountModifyHandler(c *core.WebContext) (any, *errs.Error
account.Type = oldAccount.Type
account.ParentAccountId = oldAccount.ParentAccountId
account.DisplayOrder = oldAccount.DisplayOrder
account.Currency = oldAccount.Currency
account.Balance = oldAccount.Balance
@@ -762,15 +772,16 @@ func (a *AccountsApi) getToUpdateAccount(uid int64, accountModifyReq *models.Acc
}
newAccount := &models.Account{
AccountId: oldAccount.AccountId,
Uid: uid,
Name: accountModifyReq.Name,
Category: accountModifyReq.Category,
Icon: accountModifyReq.Icon,
Color: accountModifyReq.Color,
Comment: accountModifyReq.Comment,
Extend: newAccountExtend,
Hidden: accountModifyReq.Hidden,
AccountId: oldAccount.AccountId,
Uid: uid,
Name: accountModifyReq.Name,
DisplayOrder: oldAccount.DisplayOrder,
Category: accountModifyReq.Category,
Icon: accountModifyReq.Icon,
Color: accountModifyReq.Color,
Comment: accountModifyReq.Comment,
Extend: newAccountExtend,
Hidden: accountModifyReq.Hidden,
}
if newAccount.Name != oldAccount.Name ||
+47 -23
View File
@@ -28,9 +28,11 @@ type DataManagementsApi struct {
transactions *services.TransactionService
categories *services.TransactionCategoryService
tags *services.TransactionTagService
tagGroups *services.TransactionTagGroupService
pictures *services.TransactionPictureService
templates *services.TransactionTemplateService
userCustomExchangeRates *services.UserCustomExchangeRatesService
insightsExploreres *services.InsightsExplorerService
}
// Initialize a data management api singleton instance
@@ -45,9 +47,11 @@ var (
transactions: services.Transactions,
categories: services.TransactionCategories,
tags: services.TransactionTags,
tagGroups: services.TransactionTagGroups,
pictures: services.TransactionPictures,
templates: services.TransactionTemplates,
userCustomExchangeRates: services.UserCustomExchangeRates,
insightsExploreres: services.InsightsExplorers,
}
)
@@ -99,6 +103,13 @@ func (a *DataManagementsApi) DataStatisticsHandler(c *core.WebContext) (any, *er
return nil, errs.ErrOperationFailed
}
totalInsightsExplorerCount, err := a.insightsExploreres.GetTotalInsightsExplorersCountByUid(c, uid)
if err != nil {
log.Errorf(c, "[data_managements.DataStatisticsHandler] failed to get total insights explorer count for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.ErrOperationFailed
}
totalTransactionTemplateCount, err := a.templates.GetTotalNormalTemplateCountByUid(c, uid)
if err != nil {
@@ -119,6 +130,7 @@ func (a *DataManagementsApi) DataStatisticsHandler(c *core.WebContext) (any, *er
TotalTransactionTagCount: totalTransactionTagCount,
TotalTransactionCount: totalTransactionCount,
TotalTransactionPictureCount: totalTransactionPictureCount,
TotalInsightsExplorerCount: totalInsightsExplorerCount,
TotalTransactionTemplateCount: totalTransactionTemplateCount,
TotalScheduledTransactionCount: totalScheduledTransactionCount,
}
@@ -183,6 +195,13 @@ func (a *DataManagementsApi) ClearAllDataHandler(c *core.WebContext) (any, *errs
return nil, errs.Or(err, errs.ErrOperationFailed)
}
err = a.tagGroups.DeleteAllTagGroups(c, uid)
if err != nil {
log.Errorf(c, "[data_managements.ClearAllDataHandler] failed to delete all transaction tag groups, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
err = a.userCustomExchangeRates.DeleteAllCustomExchangeRates(c, uid)
if err != nil {
@@ -190,6 +209,13 @@ func (a *DataManagementsApi) ClearAllDataHandler(c *core.WebContext) (any, *errs
return nil, errs.Or(err, errs.ErrOperationFailed)
}
err = a.insightsExploreres.DeleteAllInsightsExplorers(c, uid)
if err != nil {
log.Errorf(c, "[data_managements.ClearAllDataHandler] failed to delete all insights explorers, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
log.Infof(c, "[data_managements.ClearAllDataHandler] user \"uid:%d\" has cleared all data", uid)
return true, nil
}
@@ -298,17 +324,15 @@ func (a *DataManagementsApi) getExportedFileContent(c *core.WebContext, fileType
err := c.ShouldBindQuery(&exportTransactionDataReq)
if err != nil {
log.Warnf(c, "[data_managements.ExportDataHandler] parse request failed, because %s", err.Error())
log.Warnf(c, "[data_managements.getExportedFileContent] parse request failed, because %s", err.Error())
return nil, "", errs.NewIncompleteOrIncorrectSubmissionError(err)
}
timezone := time.Local
utcOffset, err := c.GetClientTimezoneOffset()
clientTimezone, err := c.GetClientTimezone()
if err != nil {
log.Warnf(c, "[data_managements.ExportDataHandler] cannot get client timezone offset, because %s", err.Error())
} else {
timezone = time.FixedZone("Client Timezone", int(utcOffset)*60)
log.Warnf(c, "[data_managements.getExportedFileContent] cannot get client timezone, because %s", err.Error())
clientTimezone = time.Local
}
uid := c.GetCurrentUid()
@@ -316,7 +340,7 @@ func (a *DataManagementsApi) getExportedFileContent(c *core.WebContext, fileType
if err != nil {
if !errs.IsCustomError(err) {
log.Warnf(c, "[data_managements.ExportDataHandler] failed to get user for user \"uid:%d\", because %s", uid, err.Error())
log.Warnf(c, "[data_managements.getExportedFileContent] failed to get user for user \"uid:%d\", because %s", uid, err.Error())
}
return nil, "", errs.ErrUserNotFound
@@ -329,28 +353,28 @@ func (a *DataManagementsApi) getExportedFileContent(c *core.WebContext, fileType
accounts, err := a.accounts.GetAllAccountsByUid(c, uid)
if err != nil {
log.Errorf(c, "[data_managements.ExportDataHandler] failed to get all accounts for user \"uid:%d\", because %s", uid, err.Error())
log.Errorf(c, "[data_managements.getExportedFileContent] failed to get all accounts for user \"uid:%d\", because %s", uid, err.Error())
return nil, "", errs.ErrOperationFailed
}
categories, err := a.categories.GetAllCategoriesByUid(c, uid, 0, -1)
if err != nil {
log.Errorf(c, "[data_managements.ExportDataHandler] failed to get categories for user \"uid:%d\", because %s", uid, err.Error())
log.Errorf(c, "[data_managements.getExportedFileContent] failed to get categories for user \"uid:%d\", because %s", uid, err.Error())
return nil, "", errs.ErrOperationFailed
}
tags, err := a.tags.GetAllTagsByUid(c, uid)
if err != nil {
log.Errorf(c, "[data_managements.ExportDataHandler] failed to get tags for user \"uid:%d\", because %s", uid, err.Error())
log.Errorf(c, "[data_managements.getExportedFileContent] failed to get tags for user \"uid:%d\", because %s", uid, err.Error())
return nil, "", errs.ErrOperationFailed
}
tagIndexes, err := a.tags.GetAllTagIdsMapOfAllTransactions(c, uid)
if err != nil {
log.Errorf(c, "[data_managements.ExportDataHandler] failed to get tag index for user \"uid:%d\", because %s", uid, err.Error())
log.Errorf(c, "[data_managements.getExportedFileContent] failed to get tag index for user \"uid:%d\", because %s", uid, err.Error())
return nil, "", errs.ErrOperationFailed
}
@@ -361,25 +385,25 @@ func (a *DataManagementsApi) getExportedFileContent(c *core.WebContext, fileType
allAccountIds, err := a.accounts.GetAccountOrSubAccountIds(c, exportTransactionDataReq.AccountIds, uid)
if err != nil {
log.Warnf(c, "[data_managements.ExportDataHandler] get account error, because %s", err.Error())
log.Warnf(c, "[data_managements.getExportedFileContent] get account error, because %s", err.Error())
return nil, "", errs.Or(err, errs.ErrOperationFailed)
}
allCategoryIds, err := a.categories.GetCategoryOrSubCategoryIds(c, exportTransactionDataReq.CategoryIds, uid)
if err != nil {
log.Warnf(c, "[data_managements.ExportDataHandler] get transaction category error, because %s", err.Error())
log.Warnf(c, "[data_managements.getExportedFileContent] get transaction category error, because %s", err.Error())
return nil, "", errs.Or(err, errs.ErrOperationFailed)
}
var allTagIds []int64
noTags := exportTransactionDataReq.TagIds == "none"
noTags := exportTransactionDataReq.TagFilter == models.TransactionNoTagFilterValue
var tagFilters []*models.TransactionTagFilter
if !noTags {
allTagIds, err = a.tags.GetTagIds(exportTransactionDataReq.TagIds)
tagFilters, err = models.ParseTransactionTagFilter(exportTransactionDataReq.TagFilter)
if err != nil {
log.Warnf(c, "[data_managements.ExportDataHandler] get transaction tag ids error, because %s", err.Error())
log.Warnf(c, "[data_managements.getExportedFileContent] parse transaction tag filters error, because %s", err.Error())
return nil, "", errs.Or(err, errs.ErrOperationFailed)
}
}
@@ -395,10 +419,10 @@ func (a *DataManagementsApi) getExportedFileContent(c *core.WebContext, fileType
minTransactionTime = utils.GetMinTransactionTimeFromUnixTime(exportTransactionDataReq.MinTime)
}
allTransactions, err := a.transactions.GetAllSpecifiedTransactions(c, uid, maxTransactionTime, minTransactionTime, exportTransactionDataReq.Type, allCategoryIds, allAccountIds, allTagIds, noTags, exportTransactionDataReq.TagFilterType, exportTransactionDataReq.AmountFilter, exportTransactionDataReq.Keyword, pageCountForDataExport, true)
allTransactions, err := a.transactions.GetAllSpecifiedTransactions(c, uid, maxTransactionTime, minTransactionTime, exportTransactionDataReq.Type, allCategoryIds, allAccountIds, tagFilters, noTags, exportTransactionDataReq.AmountFilter, exportTransactionDataReq.Keyword, pageCountForDataExport, true)
if err != nil {
log.Errorf(c, "[data_managements.ExportDataHandler] failed to all transactions user \"uid:%d\", because %s", uid, err.Error())
log.Errorf(c, "[data_managements.getExportedFileContent] failed to all transactions user \"uid:%d\", because %s", uid, err.Error())
return nil, "", errs.ErrOperationFailed
}
@@ -411,17 +435,17 @@ func (a *DataManagementsApi) getExportedFileContent(c *core.WebContext, fileType
result, err := dataExporter.ToExportedContent(c, uid, allTransactions, accountMap, categoryMap, tagMap, tagIndexes)
if err != nil {
log.Errorf(c, "[data_managements.ExportDataHandler] failed to get csv format exported data for \"uid:%d\", because %s", uid, err.Error())
log.Errorf(c, "[data_managements.getExportedFileContent] failed to get exported data for \"uid:%d\", because %s", uid, err.Error())
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)
+274
View File
@@ -0,0 +1,274 @@
package api
import (
"encoding/json"
"sort"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/services"
)
// InsightsExplorersApi represents insights explorers api
type InsightsExplorersApi struct {
insightsExploreres *services.InsightsExplorerService
}
// Initialize a insights explorers api singleton instance
var (
InsightsExplorers = &InsightsExplorersApi{
insightsExploreres: services.InsightsExplorers,
}
)
// InsightsExplorerListHandler returns insights explorer list of current user
func (a *InsightsExplorersApi) InsightsExplorerListHandler(c *core.WebContext) (any, *errs.Error) {
uid := c.GetCurrentUid()
explorers, err := a.insightsExploreres.GetAllInsightsExplorerNamesByUid(c, uid)
if err != nil {
log.Errorf(c, "[explorers.InsightsExplorerListHandler] failed to get insights explorers for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
explorerResps := make(models.InsightsExplorerInfoResponseSlice, len(explorers))
for i := 0; i < len(explorers); i++ {
explorerResps[i], err = explorers[i].ToInsightsExplorerInfoResponse()
if err != nil {
log.Errorf(c, "[explorers.InsightsExplorerListHandler] failed to get insights explorer response for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.ErrInsightsExplorerDataInvalid
}
}
sort.Sort(explorerResps)
return explorerResps, nil
}
// InsightsExplorerGetHandler returns one specific insights explorer of current user
func (a *InsightsExplorersApi) InsightsExplorerGetHandler(c *core.WebContext) (any, *errs.Error) {
var explorerGetReq models.InsightsExplorerGetRequest
err := c.ShouldBindQuery(&explorerGetReq)
if err != nil {
log.Warnf(c, "[explorers.InsightsExplorerGetHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
uid := c.GetCurrentUid()
explorer, err := a.insightsExploreres.GetInsightsExplorerByExplorerId(c, uid, explorerGetReq.Id)
if err != nil {
log.Errorf(c, "[explorers.InsightsExplorerGetHandler] failed to get insights explorer \"id:%d\" for user \"uid:%d\", because %s", explorerGetReq.Id, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
explorerResp, err := explorer.ToInsightsExplorerInfoResponse()
if err != nil {
log.Errorf(c, "[explorers.InsightsExplorerGetHandler] failed to get insights explorer response for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.ErrInsightsExplorerDataInvalid
}
return explorerResp, nil
}
// InsightsExplorerCreateHandler saves a new insights explorer by request parameters for current user
func (a *InsightsExplorersApi) InsightsExplorerCreateHandler(c *core.WebContext) (any, *errs.Error) {
var explorerCreateReq models.InsightsExplorerCreateRequest
err := c.ShouldBindJSON(&explorerCreateReq)
if err != nil {
log.Warnf(c, "[explorers.InsightsExplorerCreateHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
uid := c.GetCurrentUid()
maxOrderId, err := a.insightsExploreres.GetMaxDisplayOrder(c, uid)
if err != nil {
log.Errorf(c, "[explorers.InsightsExplorerCreateHandler] failed to get max display order for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
explorer, err := a.createNewInsightsExplorerModel(uid, &explorerCreateReq, maxOrderId+1)
if err != nil {
log.Errorf(c, "[explorers.InsightsExplorerCreateHandler] failed to parse insights explorer data for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.ErrInsightsExplorerDataInvalid
}
err = a.insightsExploreres.CreateInsightsExplorer(c, explorer)
if err != nil {
log.Errorf(c, "[explorers.InsightsExplorerCreateHandler] failed to create insights explorer \"id:%d\" for user \"uid:%d\", because %s", explorer.ExplorerId, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
log.Infof(c, "[explorers.InsightsExplorerCreateHandler] user \"uid:%d\" has created a new insights explorer \"id:%d\" successfully", uid, explorer.ExplorerId)
explorerResp, err := explorer.ToInsightsExplorerInfoResponse()
if err != nil {
log.Errorf(c, "[explorers.InsightsExplorerCreateHandler] failed to get insights explorer response for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.ErrInsightsExplorerDataInvalid
}
return explorerResp, nil
}
// InsightsExplorerModifyHandler saves an existed insights explorer by request parameters for current user
func (a *InsightsExplorersApi) InsightsExplorerModifyHandler(c *core.WebContext) (any, *errs.Error) {
var explorerModifyReq models.InsightsExplorerModifyRequest
err := c.ShouldBindJSON(&explorerModifyReq)
if err != nil {
log.Warnf(c, "[explorers.InsightsExplorerModifyHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
uid := c.GetCurrentUid()
explorer, err := a.insightsExploreres.GetInsightsExplorerByExplorerId(c, uid, explorerModifyReq.Id)
if err != nil {
log.Errorf(c, "[explorers.InsightsExplorerModifyHandler] failed to get insights explorer \"id:%d\" for user \"uid:%d\", because %s", explorerModifyReq.Id, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
newData, err := json.Marshal(explorerModifyReq.Data)
if err != nil {
log.Errorf(c, "[explorers.InsightsExplorerModifyHandler] failed to parse insights explorer data for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.ErrInsightsExplorerDataInvalid
}
newExplorer := &models.InsightsExplorer{
ExplorerId: explorer.ExplorerId,
Uid: uid,
Name: explorerModifyReq.Name,
Data: string(newData),
}
if newExplorer.Name == explorer.Name && newExplorer.Data == explorer.Data {
return nil, errs.ErrNothingWillBeUpdated
}
err = a.insightsExploreres.ModifyInsightsExplorer(c, newExplorer)
if err != nil {
log.Errorf(c, "[explorers.InsightsExplorerModifyHandler] failed to update insights explorer \"id:%d\" for user \"uid:%d\", because %s", explorerModifyReq.Id, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
log.Infof(c, "[explorers.InsightsExplorerModifyHandler] user \"uid:%d\" has updated insights explorer \"id:%d\" successfully", uid, explorerModifyReq.Id)
explorer.Name = newExplorer.Name
explorer.Data = newExplorer.Data
explorerResp, err := explorer.ToInsightsExplorerInfoResponse()
if err != nil {
log.Errorf(c, "[explorers.InsightsExplorerModifyHandler] failed to get insights explorer response for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.ErrInsightsExplorerDataInvalid
}
return explorerResp, nil
}
// InsightsExplorerHideHandler hides a insights explorer by request parameters for current user
func (a *InsightsExplorersApi) InsightsExplorerHideHandler(c *core.WebContext) (any, *errs.Error) {
var explorerHideReq models.InsightsExplorerHideRequest
err := c.ShouldBindJSON(&explorerHideReq)
if err != nil {
log.Warnf(c, "[explorers.InsightsExplorerHideHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
uid := c.GetCurrentUid()
err = a.insightsExploreres.HideInsightsExplorer(c, uid, []int64{explorerHideReq.Id}, explorerHideReq.Hidden)
if err != nil {
log.Errorf(c, "[explorers.InsightsExplorerHideHandler] failed to hide insights explorer \"id:%d\" for user \"uid:%d\", because %s", explorerHideReq.Id, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
log.Infof(c, "[explorers.InsightsExplorerHideHandler] user \"uid:%d\" has hidden insights explorer \"id:%d\"", uid, explorerHideReq.Id)
return true, nil
}
// InsightsExplorerMoveHandler moves display order of existed insights explorers by request parameters for current user
func (a *InsightsExplorersApi) InsightsExplorerMoveHandler(c *core.WebContext) (any, *errs.Error) {
var explorerMoveReq models.InsightsExplorerMoveRequest
err := c.ShouldBindJSON(&explorerMoveReq)
if err != nil {
log.Warnf(c, "[explorers.InsightsExplorerMoveHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
uid := c.GetCurrentUid()
explorers := make([]*models.InsightsExplorer, len(explorerMoveReq.NewDisplayOrders))
for i := 0; i < len(explorerMoveReq.NewDisplayOrders); i++ {
newDisplayOrder := explorerMoveReq.NewDisplayOrders[i]
explorer := &models.InsightsExplorer{
Uid: uid,
ExplorerId: newDisplayOrder.Id,
DisplayOrder: newDisplayOrder.DisplayOrder,
}
explorers[i] = explorer
}
err = a.insightsExploreres.ModifyInsightsExplorerDisplayOrders(c, uid, explorers)
if err != nil {
log.Errorf(c, "[explorers.InsightsExplorerMoveHandler] failed to move insights explorers for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
log.Infof(c, "[explorers.InsightsExplorerMoveHandler] user \"uid:%d\" has moved insights explorers", uid)
return true, nil
}
// InsightsExplorerDeleteHandler deletes an existed insights explorer by request parameters for current user
func (a *InsightsExplorersApi) InsightsExplorerDeleteHandler(c *core.WebContext) (any, *errs.Error) {
var explorerDeleteReq models.InsightsExplorerDeleteRequest
err := c.ShouldBindJSON(&explorerDeleteReq)
if err != nil {
log.Warnf(c, "[explorers.InsightsExplorerDeleteHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
uid := c.GetCurrentUid()
err = a.insightsExploreres.DeleteInsightsExplorer(c, uid, explorerDeleteReq.Id)
if err != nil {
log.Errorf(c, "[explorers.InsightsExplorerDeleteHandler] failed to delete insights explorer \"id:%d\" for user \"uid:%d\", because %s", explorerDeleteReq.Id, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
log.Infof(c, "[explorers.InsightsExplorerDeleteHandler] user \"uid:%d\" has deleted insights explorer \"id:%d\"", uid, explorerDeleteReq.Id)
return true, nil
}
func (a *InsightsExplorersApi) createNewInsightsExplorerModel(uid int64, explorerCreateReq *models.InsightsExplorerCreateRequest, order int32) (*models.InsightsExplorer, error) {
data, err := json.Marshal(explorerCreateReq.Data)
if err != nil {
return nil, err
}
return &models.InsightsExplorer{
Uid: uid,
Name: explorerCreateReq.Name,
Data: string(data),
DisplayOrder: order,
}, nil
}
+9 -7
View File
@@ -47,14 +47,13 @@ func (a *LargeLanguageModelsApi) RecognizeReceiptImageHandler(c *core.WebContext
return nil, errs.ErrLargeLanguageModelProviderNotEnabled
}
utcOffset, err := c.GetClientTimezoneOffset()
clientTimezone, err := c.GetClientTimezone()
if err != nil {
log.Warnf(c, "[large_language_models.RecognizeReceiptImageHandler] cannot get client timezone offset, because %s", err.Error())
log.Warnf(c, "[large_language_models.RecognizeReceiptImageHandler] cannot get client timezone, because %s", err.Error())
return nil, errs.ErrClientTimezoneOffsetInvalid
}
timezone := time.FixedZone("Client Timezone", int(utcOffset)*60)
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(c, uid)
@@ -192,11 +191,12 @@ func (a *LargeLanguageModelsApi) RecognizeReceiptImageHandler(c *core.WebContext
systemPrompt, err := templates.GetTemplate(templates.SYSTEM_PROMPT_RECEIPT_IMAGE_RECOGNITION)
if err != nil {
log.Errorf(c, "[large_language_models.RecognizeReceiptImageHandler] failed to get system prompt template for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
systemPromptParams := map[string]any{
"CurrentDateTime": utils.FormatUnixTimeToLongDateTime(time.Now().Unix(), timezone),
"CurrentDateTime": utils.FormatUnixTimeToLongDateTime(time.Now().Unix(), clientTimezone),
"AllExpenseCategoryNames": strings.Join(expenseCategoryNames, "\n"),
"AllIncomeCategoryNames": strings.Join(incomeCategoryNames, "\n"),
"AllTransferCategoryNames": strings.Join(transferCategoryNames, "\n"),
@@ -208,6 +208,7 @@ func (a *LargeLanguageModelsApi) RecognizeReceiptImageHandler(c *core.WebContext
err = systemPrompt.Execute(&bodyBuffer, systemPromptParams)
if err != nil {
log.Errorf(c, "[large_language_models.RecognizeReceiptImageHandler] failed to get final system prompt from template for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
@@ -222,6 +223,7 @@ func (a *LargeLanguageModelsApi) RecognizeReceiptImageHandler(c *core.WebContext
llmResponse, err := llm.Container.GetJsonResponseByReceiptImageRecognitionModel(c, c.GetCurrentUid(), a.CurrentConfig(), llmRequest)
if err != nil {
log.Errorf(c, "[large_language_models.RecognizeReceiptImageHandler] failed to get llm response user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
@@ -236,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,
}
@@ -288,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)
+20 -4
View File
@@ -5,11 +5,12 @@ import (
"net/http/httputil"
"net/url"
"strings"
"sync"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/httpclient"
"github.com/mayswind/ezbookkeeping/pkg/settings"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
const openStreetMapTileImageUrlFormat = "https://tile.openstreetmap.org/{z}/{x}/{y}.png" // https://tile.openstreetmap.org/{z}/{x}/{y}.png
@@ -25,6 +26,8 @@ const tianDiTuMapAnnotationUrlFormat = "https://t0.tianditu.gov.cn/cva_w/wmts?SE
// MapImageProxy represents map image proxy
type MapImageProxy struct {
ApiUsingConfig
mutex sync.Mutex
transport *http.Transport
}
// Initialize a map image proxy singleton instance
@@ -36,6 +39,18 @@ var (
}
)
func (p *MapImageProxy) initializeHttpTransport() {
p.mutex.Lock()
defer p.mutex.Unlock()
if p.transport != nil {
return
}
p.transport = http.DefaultTransport.(*http.Transport).Clone()
httpclient.SetProxyUrl(p.transport, p.CurrentConfig().MapProxy)
}
// MapTileImageProxyHandler returns map tile image
func (p *MapImageProxy) MapTileImageProxyHandler(c *core.WebContext) (*httputil.ReverseProxy, *errs.Error) {
return p.mapImageProxyHandler(c, func(c *core.WebContext, mapProvider string) (string, *errs.Error) {
@@ -109,8 +124,9 @@ func (p *MapImageProxy) mapImageProxyHandler(c *core.WebContext, fn func(c *core
return nil, err
}
transport := http.DefaultTransport.(*http.Transport).Clone()
utils.SetProxyUrl(transport, p.CurrentConfig().MapProxy)
if p.transport == nil {
p.initializeHttpTransport()
}
director := func(req *http.Request) {
imageRawUrl := targetUrl
@@ -126,7 +142,7 @@ func (p *MapImageProxy) mapImageProxyHandler(c *core.WebContext, fn func(c *core
}
return &httputil.ReverseProxy{
Transport: transport,
Transport: p.transport,
Director: director,
}, nil
}
+2 -2
View File
@@ -13,7 +13,7 @@ import (
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
const mcpServerName = "ezBookkeeping-mcp"
const mcpServerName = core.ApplicationName + "-mcp"
// ModelContextProtocolAPI represents model context protocol api
type ModelContextProtocolAPI struct {
@@ -102,7 +102,7 @@ func (a *ModelContextProtocolAPI) InitializeHandler(c *core.WebContext, jsonRPCR
},
ServerInfo: &mcp.MCPImplementation{
Name: mcpServerName,
Title: a.CurrentConfig().AppName,
Title: core.ApplicationName,
Version: settings.Version,
},
}
+28 -9
View File
@@ -20,10 +20,10 @@ import (
"github.com/mayswind/ezbookkeeping/pkg/validators"
)
const oauth2CallbackPageUrlSuccessFormat = "%sdesktop/#/oauth2_callback?platform=%s&provider=%s&token=%s"
const oauth2CallbackPageUrlNeedVerifyFormat = "%sdesktop/#/oauth2_callback?platform=%s&provider=%s&userName=%s&token=%s"
const oauth2CallbackPageUrlFailedFormat = "%sdesktop/#/oauth2_callback?errorCode=%d&errorMessage=%s"
const oauth2CallbackPageUrlErrorMessageFormat = "%sdesktop/#/oauth2_callback?errorMessage=%s"
const oauth2CallbackPageUrlSuccessFormat = "%sdesktop#/oauth2_callback?platform=%s&provider=%s&token=%s"
const oauth2CallbackPageUrlNeedVerifyFormat = "%sdesktop#/oauth2_callback?platform=%s&provider=%s&userName=%s&token=%s"
const oauth2CallbackPageUrlFailedFormat = "%sdesktop#/oauth2_callback?errorCode=%d&errorMessage=%s"
const oauth2CallbackPageUrlErrorMessageFormat = "%sdesktop#/oauth2_callback?errorMessage=%s"
// OAuth2AuthenticationApi represents OAuth 2.0 authorization api
type OAuth2AuthenticationApi struct {
@@ -208,9 +208,20 @@ func (a *OAuth2AuthenticationApi) CallbackHandler(c *core.WebContext) (string, *
return a.redirectToFailedCallbackPage(c, errs.ErrCannotRetrieveUserInfo)
}
if oauth2UserInfo.UserName == "" || oauth2UserInfo.Email == "" {
log.Errorf(c, "[oauth2_authentications.CallbackHandler] invalid oauth 2.0 user info, userName: %s, email: %s", oauth2UserInfo.UserName, oauth2UserInfo.Email)
return a.redirectToFailedCallbackPage(c, errs.ErrCannotRetrieveUserInfo)
log.Infof(c, "[oauth2_authentications.CallbackHandler] oauth 2.0 user info, userName: %s, email: %s", oauth2UserInfo.UserName, oauth2UserInfo.Email)
if oauth2UserInfo.UserName == "" && oauth2UserInfo.Email == "" {
return a.redirectToFailedCallbackPage(c, errs.ErrOAuth2UserNameAndEmailEmpty)
}
if a.CurrentConfig().OAuth2UserIdentifier == settings.OAuth2UserIdentifierEmail && oauth2UserInfo.Email == "" {
log.Errorf(c, "[oauth2_authentications.CallbackHandler] invalid oauth 2.0 user info, email is empty")
return a.redirectToFailedCallbackPage(c, errs.ErrOAuth2EmailEmpty)
}
if a.CurrentConfig().OAuth2UserIdentifier == settings.OAuth2UserIdentifierUsername && oauth2UserInfo.UserName == "" {
log.Errorf(c, "[oauth2_authentications.CallbackHandler] invalid oauth 2.0 user info, userName is empty")
return a.redirectToFailedCallbackPage(c, errs.ErrOAuth2UserNameEmpty)
}
userExternalAuthType := oauth2.GetExternalUserAuthType()
@@ -221,7 +232,7 @@ func (a *OAuth2AuthenticationApi) CallbackHandler(c *core.WebContext) (string, *
} else if a.CurrentConfig().OAuth2UserIdentifier == settings.OAuth2UserIdentifierUsername {
userExternalAuth, err = a.userExternalAuths.GetUserExternalAuthByExternalUserName(c, oauth2UserInfo.UserName, userExternalAuthType)
} else {
userExternalAuth, err = a.userExternalAuths.GetUserExternalAuthByExternalEmail(c, oauth2UserInfo.Email, userExternalAuthType)
return a.redirectToFailedCallbackPage(c, errs.ErrNotSupported)
}
if err != nil && !errors.Is(err, errs.ErrUserExternalAuthNotFound) {
@@ -257,7 +268,7 @@ func (a *OAuth2AuthenticationApi) CallbackHandler(c *core.WebContext) (string, *
} else if a.CurrentConfig().OAuth2UserIdentifier == settings.OAuth2UserIdentifierUsername {
user, err = a.users.GetUserByUsername(c, oauth2UserInfo.UserName)
} else {
user, err = a.users.GetUserByEmail(c, oauth2UserInfo.Email)
err = errs.ErrNotSupported
}
if err != nil && !errors.Is(err, errs.ErrUserNotFound) {
@@ -267,6 +278,14 @@ func (a *OAuth2AuthenticationApi) CallbackHandler(c *core.WebContext) (string, *
}
if user == nil && a.CurrentConfig().EnableUserRegister && a.CurrentConfig().OAuth2AutoRegister {
if oauth2UserInfo.UserName == "" {
return a.redirectToFailedCallbackPage(c, errs.ErrOAuth2UserNameEmptyCannotRegister)
}
if oauth2UserInfo.Email == "" {
return a.redirectToFailedCallbackPage(c, errs.ErrOAuth2EmailEmptyCannotRegister)
}
userName := strings.TrimSpace(oauth2UserInfo.UserName)
email := strings.TrimSpace(oauth2UserInfo.Email)
nickName := strings.TrimSpace(oauth2UserInfo.NickName)
+10 -1
View File
@@ -214,6 +214,7 @@ func (a *TransactionCategoriesApi) CategoryModifyHandler(c *core.WebContext) (an
Uid: uid,
ParentCategoryId: categoryModifyReq.ParentId,
Name: categoryModifyReq.Name,
DisplayOrder: category.DisplayOrder,
Icon: categoryModifyReq.Icon,
Color: categoryModifyReq.Color,
Comment: categoryModifyReq.Comment,
@@ -259,6 +260,15 @@ func (a *TransactionCategoriesApi) CategoryModifyHandler(c *core.WebContext) (an
if toPrimaryCategory.ParentCategoryId != models.LevelOneTransactionCategoryParentId {
return nil, errs.Or(err, errs.ErrNotAllowUseSecondaryTransactionAsPrimaryCategory)
}
maxOrderId, err := a.categories.GetMaxSubCategoryDisplayOrder(c, uid, category.Type, newCategory.ParentCategoryId)
if err != nil {
log.Errorf(c, "[transaction_categories.CategoryModifyHandler] failed to get max display order for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
newCategory.DisplayOrder = maxOrderId + 1
}
err = a.categories.ModifyCategory(c, newCategory)
@@ -271,7 +281,6 @@ func (a *TransactionCategoriesApi) CategoryModifyHandler(c *core.WebContext) (an
log.Infof(c, "[transaction_categories.CategoryModifyHandler] user \"uid:%d\" has updated category \"id:%d\" successfully", uid, categoryModifyReq.Id)
newCategory.Type = category.Type
newCategory.DisplayOrder = category.DisplayOrder
categoryResp := newCategory.ToTransactionCategoryInfoResponse()
return categoryResp, nil
+210
View File
@@ -0,0 +1,210 @@
package api
import (
"sort"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/services"
)
// TransactionTagGroupsApi represents transaction tag group api
type TransactionTagGroupsApi struct {
tagGroups *services.TransactionTagGroupService
}
// Initialize a transaction tag group api singleton instance
var (
TransactionTagGroups = &TransactionTagGroupsApi{
tagGroups: services.TransactionTagGroups,
}
)
// TagGroupListHandler returns transaction tag group list of current user
func (a *TransactionTagGroupsApi) TagGroupListHandler(c *core.WebContext) (any, *errs.Error) {
uid := c.GetCurrentUid()
tagGroups, err := a.tagGroups.GetAllTagGroupsByUid(c, uid)
if err != nil {
log.Errorf(c, "[transaction_tag_groups.TagGroupListHandler] failed to get tag groups for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
tagGroupResps := make(models.TransactionTagGroupInfoResponseSlice, len(tagGroups))
for i := 0; i < len(tagGroups); i++ {
tagGroupResps[i] = tagGroups[i].ToTransactionTagGroupInfoResponse()
}
sort.Sort(tagGroupResps)
return tagGroupResps, nil
}
// TagGroupGetHandler returns one specific transaction tag group of current user
func (a *TransactionTagGroupsApi) TagGroupGetHandler(c *core.WebContext) (any, *errs.Error) {
var tagGroupGetReq models.TransactionTagGroupGetRequest
err := c.ShouldBindQuery(&tagGroupGetReq)
if err != nil {
log.Warnf(c, "[transaction_tag_groups.TagGroupGetHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
uid := c.GetCurrentUid()
tagGroup, err := a.tagGroups.GetTagGroupByTagGroupId(c, uid, tagGroupGetReq.Id)
if err != nil {
log.Errorf(c, "[transaction_tag_groups.TagGroupGetHandler] failed to get tag group \"id:%d\" for user \"uid:%d\", because %s", tagGroupGetReq.Id, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
tagGroupResp := tagGroup.ToTransactionTagGroupInfoResponse()
return tagGroupResp, nil
}
// TagGroupCreateHandler saves a new transaction tag group by request parameters for current user
func (a *TransactionTagGroupsApi) TagGroupCreateHandler(c *core.WebContext) (any, *errs.Error) {
var tagGroupCreateReq models.TransactionTagGroupCreateRequest
err := c.ShouldBindJSON(&tagGroupCreateReq)
if err != nil {
log.Warnf(c, "[transaction_tag_groups.TagGroupCreateHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
uid := c.GetCurrentUid()
maxOrderId, err := a.tagGroups.GetMaxDisplayOrder(c, uid)
if err != nil {
log.Errorf(c, "[transaction_tag_groups.TagGroupCreateHandler] failed to get max display order for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
tagGroup := a.createNewTagGroupModel(uid, &tagGroupCreateReq, maxOrderId+1)
err = a.tagGroups.CreateTagGroup(c, tagGroup)
if err != nil {
log.Errorf(c, "[transaction_tag_groups.TagGroupCreateHandler] failed to create tag group \"id:%d\" for user \"uid:%d\", because %s", tagGroup.TagGroupId, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
log.Infof(c, "[transaction_tag_groups.TagGroupCreateHandler] user \"uid:%d\" has created a new tag group \"id:%d\" successfully", uid, tagGroup.TagGroupId)
tagGroupResp := tagGroup.ToTransactionTagGroupInfoResponse()
return tagGroupResp, nil
}
// TagGroupModifyHandler saves an existed transaction tag group by request parameters for current user
func (a *TransactionTagGroupsApi) TagGroupModifyHandler(c *core.WebContext) (any, *errs.Error) {
var tagGroupModifyReq models.TransactionTagGroupModifyRequest
err := c.ShouldBindJSON(&tagGroupModifyReq)
if err != nil {
log.Warnf(c, "[transaction_tag_groups.TagGroupModifyHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
uid := c.GetCurrentUid()
tagGroup, err := a.tagGroups.GetTagGroupByTagGroupId(c, uid, tagGroupModifyReq.Id)
if err != nil {
log.Errorf(c, "[transaction_tag_groups.TagGroupModifyHandler] failed to get tag group \"id:%d\" for user \"uid:%d\", because %s", tagGroupModifyReq.Id, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
newTagGroup := &models.TransactionTagGroup{
TagGroupId: tagGroup.TagGroupId,
Uid: uid,
Name: tagGroupModifyReq.Name,
}
if newTagGroup.Name == tagGroup.Name {
return nil, errs.ErrNothingWillBeUpdated
}
err = a.tagGroups.ModifyTagGroup(c, newTagGroup)
if err != nil {
log.Errorf(c, "[transaction_tag_groups.TagGroupModifyHandler] failed to update tag group \"id:%d\" for user \"uid:%d\", because %s", tagGroupModifyReq.Id, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
log.Infof(c, "[transaction_tag_groups.TagGroupModifyHandler] user \"uid:%d\" has updated tag group \"id:%d\" successfully", uid, tagGroupModifyReq.Id)
tagGroup.Name = newTagGroup.Name
tagGroupResp := tagGroup.ToTransactionTagGroupInfoResponse()
return tagGroupResp, nil
}
// TagGroupMoveHandler moves display order of existed transaction tag groups by request parameters for current user
func (a *TransactionTagGroupsApi) TagGroupMoveHandler(c *core.WebContext) (any, *errs.Error) {
var tagGroupMoveReq models.TransactionTagGroupMoveRequest
err := c.ShouldBindJSON(&tagGroupMoveReq)
if err != nil {
log.Warnf(c, "[transaction_tag_groups.TagGroupMoveHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
uid := c.GetCurrentUid()
tagGroups := make([]*models.TransactionTagGroup, len(tagGroupMoveReq.NewDisplayOrders))
for i := 0; i < len(tagGroupMoveReq.NewDisplayOrders); i++ {
newDisplayOrder := tagGroupMoveReq.NewDisplayOrders[i]
tagGroup := &models.TransactionTagGroup{
Uid: uid,
TagGroupId: newDisplayOrder.Id,
DisplayOrder: newDisplayOrder.DisplayOrder,
}
tagGroups[i] = tagGroup
}
err = a.tagGroups.ModifyTagGroupDisplayOrders(c, uid, tagGroups)
if err != nil {
log.Errorf(c, "[transaction_tag_groups.TagGroupMoveHandler] failed to move tag groups for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
log.Infof(c, "[transaction_tag_groups.TagGroupMoveHandler] user \"uid:%d\" has moved tag groups", uid)
return true, nil
}
// TagGroupDeleteHandler deletes an existed transaction tag group by request parameters for current user
func (a *TransactionTagGroupsApi) TagGroupDeleteHandler(c *core.WebContext) (any, *errs.Error) {
var tagGroupDeleteReq models.TransactionTagGroupDeleteRequest
err := c.ShouldBindJSON(&tagGroupDeleteReq)
if err != nil {
log.Warnf(c, "[transaction_tag_groups.TagGroupDeleteHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
uid := c.GetCurrentUid()
err = a.tagGroups.DeleteTagGroup(c, uid, tagGroupDeleteReq.Id)
if err != nil {
log.Errorf(c, "[transaction_tag_groups.TagGroupDeleteHandler] failed to delete tag group \"id:%d\" for user \"uid:%d\", because %s", tagGroupDeleteReq.Id, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
log.Infof(c, "[transaction_tag_groups.TagGroupDeleteHandler] user \"uid:%d\" has deleted tag group \"id:%d\"", uid, tagGroupDeleteReq.Id)
return true, nil
}
func (a *TransactionTagGroupsApi) createNewTagGroupModel(uid int64, tagGroupCreateReq *models.TransactionTagGroupCreateRequest, order int32) *models.TransactionTagGroup {
return &models.TransactionTagGroup{
Uid: uid,
Name: tagGroupCreateReq.Name,
DisplayOrder: order,
}
}
+33 -7
View File
@@ -78,7 +78,7 @@ func (a *TransactionTagsApi) TagCreateHandler(c *core.WebContext) (any, *errs.Er
uid := c.GetCurrentUid()
maxOrderId, err := a.tags.GetMaxDisplayOrder(c, uid)
maxOrderId, err := a.tags.GetMaxDisplayOrder(c, uid, tagCreateReq.GroupId)
if err != nil {
log.Errorf(c, "[transaction_tags.TagCreateHandler] failed to get max display order for user \"uid:%d\", because %s", uid, err.Error())
@@ -111,9 +111,16 @@ func (a *TransactionTagsApi) TagCreateBatchHandler(c *core.WebContext) (any, *er
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
for i := 0; i < len(tagCreateBatchReq.Tags); i++ {
if tagCreateBatchReq.Tags[i].GroupId != tagCreateBatchReq.GroupId {
log.Warnf(c, "[transaction_tags.TagCreateBatchHandler] the group id \"%d\" of tag#%d is inconsistent with the batch group id \"%d\"", tagCreateBatchReq.Tags[i].GroupId, i, tagCreateBatchReq.GroupId)
return nil, errs.ErrTransactionTagGroupIdInvalid
}
}
uid := c.GetCurrentUid()
maxOrderId, err := a.tags.GetMaxDisplayOrder(c, uid)
maxOrderId, err := a.tags.GetMaxDisplayOrder(c, uid, tagCreateBatchReq.GroupId)
if err != nil {
log.Errorf(c, "[transaction_tags.TagCreateBatchHandler] failed to get max display order for user \"uid:%d\", because %s", uid, err.Error())
@@ -161,16 +168,31 @@ func (a *TransactionTagsApi) TagModifyHandler(c *core.WebContext) (any, *errs.Er
}
newTag := &models.TransactionTag{
TagId: tag.TagId,
Uid: uid,
Name: tagModifyReq.Name,
TagId: tag.TagId,
Uid: uid,
Name: tagModifyReq.Name,
TagGroupId: tagModifyReq.GroupId,
DisplayOrder: tag.DisplayOrder,
}
if newTag.Name == tag.Name {
tagNameChanged := newTag.Name != tag.Name
if !tagNameChanged && newTag.TagGroupId == tag.TagGroupId {
return nil, errs.ErrNothingWillBeUpdated
}
err = a.tags.ModifyTag(c, newTag)
if newTag.TagGroupId != tag.TagGroupId {
maxOrderId, err := a.tags.GetMaxDisplayOrder(c, uid, newTag.TagGroupId)
if err != nil {
log.Errorf(c, "[transaction_tags.TagModifyHandler] failed to get max display order for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
newTag.DisplayOrder = maxOrderId + 1
}
err = a.tags.ModifyTag(c, newTag, tagNameChanged)
if err != nil {
log.Errorf(c, "[transaction_tags.TagModifyHandler] failed to update tag \"id:%d\" for user \"uid:%d\", because %s", tagModifyReq.Id, uid, err.Error())
@@ -180,6 +202,8 @@ func (a *TransactionTagsApi) TagModifyHandler(c *core.WebContext) (any, *errs.Er
log.Infof(c, "[transaction_tags.TagModifyHandler] user \"uid:%d\" has updated tag \"id:%d\" successfully", uid, tagModifyReq.Id)
tag.Name = newTag.Name
tag.TagGroupId = newTag.TagGroupId
tag.DisplayOrder = newTag.DisplayOrder
tagResp := tag.ToTransactionTagInfoResponse()
return tagResp, nil
@@ -268,6 +292,7 @@ func (a *TransactionTagsApi) createNewTagModel(uid int64, tagCreateReq *models.T
return &models.TransactionTag{
Uid: uid,
Name: tagCreateReq.Name,
TagGroupId: tagCreateReq.GroupId,
DisplayOrder: order,
}
}
@@ -278,6 +303,7 @@ func (a *TransactionTagsApi) createNewTagModels(uid int64, tagCreateBatchReq *mo
for i := 0; i < len(tagCreateBatchReq.Tags); i++ {
tagCreateReq := tagCreateBatchReq.Tags[i]
tag := a.createNewTagModel(uid, tagCreateReq, order+int32(i))
tag.TagGroupId = tagCreateBatchReq.GroupId
tags[i] = tag
}
+174 -60
View File
@@ -4,8 +4,10 @@ import (
"encoding/json"
"fmt"
"io"
"math"
"sort"
"strings"
"time"
orderedmap "github.com/wk8/go-ordered-map/v2"
@@ -83,19 +85,19 @@ func (a *TransactionsApi) TransactionCountHandler(c *core.WebContext) (any, *err
return nil, errs.Or(err, errs.ErrOperationFailed)
}
var allTagIds []int64
noTags := transactionCountReq.TagIds == "none"
noTags := transactionCountReq.TagFilter == models.TransactionNoTagFilterValue
var tagFilters []*models.TransactionTagFilter
if !noTags {
allTagIds, err = a.transactionTags.GetTagIds(transactionCountReq.TagIds)
tagFilters, err = models.ParseTransactionTagFilter(transactionCountReq.TagFilter)
if err != nil {
log.Warnf(c, "[transactions.TransactionCountHandler] get transaction tag ids error, because %s", err.Error())
log.Warnf(c, "[transactions.TransactionCountHandler] parse transaction filters error, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
}
totalCount, err := a.transactions.GetTransactionCount(c, uid, transactionCountReq.MaxTime, transactionCountReq.MinTime, transactionCountReq.Type, allCategoryIds, allAccountIds, allTagIds, noTags, transactionCountReq.TagFilterType, transactionCountReq.AmountFilter, transactionCountReq.Keyword)
totalCount, err := a.transactions.GetTransactionCount(c, uid, transactionCountReq.MaxTime, transactionCountReq.MinTime, transactionCountReq.Type, allCategoryIds, allAccountIds, tagFilters, noTags, transactionCountReq.AmountFilter, transactionCountReq.Keyword)
if err != nil {
log.Errorf(c, "[transactions.TransactionCountHandler] failed to get transaction count for user \"uid:%d\", because %s", uid, err.Error())
@@ -119,10 +121,10 @@ func (a *TransactionsApi) TransactionListHandler(c *core.WebContext) (any, *errs
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
utcOffset, err := c.GetClientTimezoneOffset()
clientTimezone, err := c.GetClientTimezone()
if err != nil {
log.Warnf(c, "[transactions.TransactionListHandler] cannot get client timezone offset, because %s", err.Error())
log.Warnf(c, "[transactions.TransactionListHandler] cannot get client timezone, because %s", err.Error())
return nil, errs.ErrClientTimezoneOffsetInvalid
}
@@ -151,14 +153,14 @@ func (a *TransactionsApi) TransactionListHandler(c *core.WebContext) (any, *errs
return nil, errs.Or(err, errs.ErrOperationFailed)
}
var allTagIds []int64
noTags := transactionListReq.TagIds == "none"
noTags := transactionListReq.TagFilter == models.TransactionNoTagFilterValue
var tagFilters []*models.TransactionTagFilter
if !noTags {
allTagIds, err = a.transactionTags.GetTagIds(transactionListReq.TagIds)
tagFilters, err = models.ParseTransactionTagFilter(transactionListReq.TagFilter)
if err != nil {
log.Warnf(c, "[transactions.TransactionListHandler] get transaction tag ids error, because %s", err.Error())
log.Warnf(c, "[transactions.TransactionListHandler] parse transaction tag filters error, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
}
@@ -166,7 +168,7 @@ func (a *TransactionsApi) TransactionListHandler(c *core.WebContext) (any, *errs
var totalCount int64
if transactionListReq.WithCount {
totalCount, err = a.transactions.GetTransactionCount(c, uid, transactionListReq.MaxTime, transactionListReq.MinTime, transactionListReq.Type, allCategoryIds, allAccountIds, allTagIds, noTags, transactionListReq.TagFilterType, transactionListReq.AmountFilter, transactionListReq.Keyword)
totalCount, err = a.transactions.GetTransactionCount(c, uid, transactionListReq.MaxTime, transactionListReq.MinTime, transactionListReq.Type, allCategoryIds, allAccountIds, tagFilters, noTags, transactionListReq.AmountFilter, transactionListReq.Keyword)
if err != nil {
log.Errorf(c, "[transactions.TransactionListHandler] failed to get transaction count for user \"uid:%d\", because %s", uid, err.Error())
@@ -174,7 +176,7 @@ func (a *TransactionsApi) TransactionListHandler(c *core.WebContext) (any, *errs
}
}
transactions, err := a.transactions.GetTransactionsByMaxTime(c, uid, transactionListReq.MaxTime, transactionListReq.MinTime, transactionListReq.Type, allCategoryIds, allAccountIds, allTagIds, noTags, transactionListReq.TagFilterType, transactionListReq.AmountFilter, transactionListReq.Keyword, transactionListReq.Page, transactionListReq.Count, true, true)
transactions, err := a.transactions.GetTransactionsByMaxTime(c, uid, transactionListReq.MaxTime, transactionListReq.MinTime, transactionListReq.Type, allCategoryIds, allAccountIds, tagFilters, noTags, transactionListReq.AmountFilter, transactionListReq.Keyword, transactionListReq.Page, transactionListReq.Count, true, true)
if err != nil {
log.Errorf(c, "[transactions.TransactionListHandler] failed to get transactions earlier than \"%d\" for user \"uid:%d\", because %s", transactionListReq.MaxTime, uid, err.Error())
@@ -190,7 +192,7 @@ func (a *TransactionsApi) TransactionListHandler(c *core.WebContext) (any, *errs
transactions = transactions[:transactionListReq.Count]
}
transactionResult, err := a.getTransactionResponseListResult(c, user, transactions, utcOffset, transactionListReq.WithPictures, transactionListReq.TrimAccount, transactionListReq.TrimCategory, transactionListReq.TrimTag)
transactionResult, err := a.getTransactionResponseListResult(c, user, transactions, clientTimezone, transactionListReq.WithPictures, transactionListReq.TrimAccount, transactionListReq.TrimCategory, transactionListReq.TrimTag)
if err != nil {
log.Errorf(c, "[transactions.TransactionListHandler] failed to assemble transaction result for user \"uid:%d\", because %s", uid, err.Error())
@@ -222,10 +224,10 @@ func (a *TransactionsApi) TransactionMonthListHandler(c *core.WebContext) (any,
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
utcOffset, err := c.GetClientTimezoneOffset()
clientTimezone, err := c.GetClientTimezone()
if err != nil {
log.Warnf(c, "[transactions.TransactionMonthListHandler] cannot get client timezone offset, because %s", err.Error())
log.Warnf(c, "[transactions.TransactionMonthListHandler] cannot get client timezone, because %s", err.Error())
return nil, errs.ErrClientTimezoneOffsetInvalid
}
@@ -254,26 +256,26 @@ func (a *TransactionsApi) TransactionMonthListHandler(c *core.WebContext) (any,
return nil, errs.Or(err, errs.ErrOperationFailed)
}
var allTagIds []int64
noTags := transactionListReq.TagIds == "none"
noTags := transactionListReq.TagFilter == models.TransactionNoTagFilterValue
var tagFilters []*models.TransactionTagFilter
if !noTags {
allTagIds, err = a.transactionTags.GetTagIds(transactionListReq.TagIds)
tagFilters, err = models.ParseTransactionTagFilter(transactionListReq.TagFilter)
if err != nil {
log.Warnf(c, "[transactions.TransactionMonthListHandler] get transaction tag ids error, because %s", err.Error())
log.Warnf(c, "[transactions.TransactionMonthListHandler] parse transaction tag filters error, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
}
transactions, err := a.transactions.GetTransactionsInMonthByPage(c, uid, transactionListReq.Year, transactionListReq.Month, transactionListReq.Type, allCategoryIds, allAccountIds, allTagIds, noTags, transactionListReq.TagFilterType, transactionListReq.AmountFilter, transactionListReq.Keyword)
transactions, err := a.transactions.GetTransactionsInMonthByPage(c, uid, transactionListReq.Year, transactionListReq.Month, transactionListReq.Type, allCategoryIds, allAccountIds, tagFilters, noTags, transactionListReq.AmountFilter, transactionListReq.Keyword)
if err != nil {
log.Errorf(c, "[transactions.TransactionMonthListHandler] failed to get transactions in month \"%d-%d\" for user \"uid:%d\", because %s", transactionListReq.Year, transactionListReq.Month, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
transactionResult, err := a.getTransactionResponseListResult(c, user, transactions, utcOffset, transactionListReq.WithPictures, transactionListReq.TrimAccount, transactionListReq.TrimCategory, transactionListReq.TrimTag)
transactionResult, err := a.getTransactionResponseListResult(c, user, transactions, clientTimezone, transactionListReq.WithPictures, transactionListReq.TrimAccount, transactionListReq.TrimCategory, transactionListReq.TrimTag)
if err != nil {
log.Errorf(c, "[transactions.TransactionMonthListHandler] failed to assemble transaction result for user \"uid:%d\", because %s", uid, err.Error())
@@ -288,6 +290,88 @@ func (a *TransactionsApi) TransactionMonthListHandler(c *core.WebContext) (any,
return transactionResps, nil
}
// TransactionListAllHandler returns all transaction list of current user
func (a *TransactionsApi) TransactionListAllHandler(c *core.WebContext) (any, *errs.Error) {
var transactionAllListReq models.TransactionAllListRequest
err := c.ShouldBindQuery(&transactionAllListReq)
if err != nil {
log.Warnf(c, "[transactions.TransactionListAllHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
clientTimezone, err := c.GetClientTimezone()
if err != nil {
log.Warnf(c, "[transactions.TransactionListAllHandler] cannot get client timezone, because %s", err.Error())
return nil, errs.ErrClientTimezoneOffsetInvalid
}
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(c, uid)
if err != nil {
if !errs.IsCustomError(err) {
log.Errorf(c, "[transactions.TransactionListAllHandler] failed to get user, because %s", err.Error())
}
return nil, errs.ErrUserNotFound
}
allAccountIds, err := a.accounts.GetAccountOrSubAccountIds(c, transactionAllListReq.AccountIds, uid)
if err != nil {
log.Warnf(c, "[transactions.TransactionListAllHandler] get account error, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
allCategoryIds, err := a.transactionCategories.GetCategoryOrSubCategoryIds(c, transactionAllListReq.CategoryIds, uid)
if err != nil {
log.Warnf(c, "[transactions.TransactionListAllHandler] get transaction category error, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
noTags := transactionAllListReq.TagFilter == models.TransactionNoTagFilterValue
var tagFilters []*models.TransactionTagFilter
if !noTags {
tagFilters, err = models.ParseTransactionTagFilter(transactionAllListReq.TagFilter)
if err != nil {
log.Warnf(c, "[transactions.TransactionListAllHandler] parse transaction tag filters error, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
}
maxTransactionTime := int64(math.MaxInt64)
minTransactionTime := int64(0)
if transactionAllListReq.EndTime > 0 {
maxTransactionTime = utils.GetMaxTransactionTimeFromUnixTime(transactionAllListReq.EndTime)
}
if transactionAllListReq.StartTime > 0 {
minTransactionTime = utils.GetMinTransactionTimeFromUnixTime(transactionAllListReq.StartTime)
}
allTransactions, err := a.transactions.GetAllSpecifiedTransactions(c, uid, maxTransactionTime, minTransactionTime, transactionAllListReq.Type, allCategoryIds, allAccountIds, tagFilters, noTags, transactionAllListReq.AmountFilter, transactionAllListReq.Keyword, pageCountForDataExport, true)
if err != nil {
log.Errorf(c, "[transactions.TransactionListAllHandler] failed to get all transactions for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
transactionResult, err := a.getTransactionResponseListResult(c, user, allTransactions, clientTimezone, transactionAllListReq.WithPictures, transactionAllListReq.TrimAccount, transactionAllListReq.TrimCategory, transactionAllListReq.TrimTag)
if err != nil {
log.Errorf(c, "[transactions.TransactionListAllHandler] failed to assemble transaction result for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
return transactionResult, nil
}
// TransactionReconciliationStatementHandler returns transaction reconciliation statement list of current user
func (a *TransactionsApi) TransactionReconciliationStatementHandler(c *core.WebContext) (any, *errs.Error) {
var reconciliationStatementRequest models.TransactionReconciliationStatementRequest
@@ -298,10 +382,10 @@ func (a *TransactionsApi) TransactionReconciliationStatementHandler(c *core.WebC
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
utcOffset, err := c.GetClientTimezoneOffset()
clientTimezone, err := c.GetClientTimezone()
if err != nil {
log.Warnf(c, "[transactions.TransactionReconciliationStatementHandler] cannot get client timezone offset, because %s", err.Error())
log.Warnf(c, "[transactions.TransactionReconciliationStatementHandler] cannot get client timezone, because %s", err.Error())
return nil, errs.ErrClientTimezoneOffsetInvalid
}
@@ -357,7 +441,7 @@ func (a *TransactionsApi) TransactionReconciliationStatementHandler(c *core.WebC
transactionAccountBalanceMap[transactionWithBalance.RelatedId] = transactionWithBalance
}
transactionResult, err := a.getTransactionResponseListResult(c, user, transactions, utcOffset, false, true, true, true)
transactionResult, err := a.getTransactionResponseListResult(c, user, transactions, clientTimezone, false, true, true, true)
if err != nil {
log.Errorf(c, "[transactions.TransactionReconciliationStatementHandler] failed to assemble transaction result for user \"uid:%d\", because %s", uid, err.Error())
@@ -406,27 +490,27 @@ func (a *TransactionsApi) TransactionStatisticsHandler(c *core.WebContext) (any,
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
utcOffset, err := c.GetClientTimezoneOffset()
clientTimezone, err := c.GetClientTimezone()
if err != nil {
log.Warnf(c, "[transactions.TransactionStatisticsHandler] cannot get client timezone offset, because %s", err.Error())
log.Warnf(c, "[transactions.TransactionStatisticsHandler] cannot get client timezone, because %s", err.Error())
return nil, errs.ErrClientTimezoneOffsetInvalid
}
var allTagIds []int64
noTags := statisticReq.TagIds == "none"
noTags := statisticReq.TagFilter == models.TransactionNoTagFilterValue
var tagFilters []*models.TransactionTagFilter
if !noTags {
allTagIds, err = a.transactionTags.GetTagIds(statisticReq.TagIds)
tagFilters, err = models.ParseTransactionTagFilter(statisticReq.TagFilter)
if err != nil {
log.Warnf(c, "[transactions.TransactionStatisticsHandler] get transaction tag ids error, because %s", err.Error())
log.Warnf(c, "[transactions.TransactionStatisticsHandler] parse transaction tag filters error, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
}
uid := c.GetCurrentUid()
totalAmounts, err := a.transactions.GetAccountsAndCategoriesTotalInflowAndOutflow(c, uid, statisticReq.StartTime, statisticReq.EndTime, allTagIds, noTags, statisticReq.TagFilterType, statisticReq.Keyword, utcOffset, statisticReq.UseTransactionTimezone)
totalAmounts, err := a.transactions.GetAccountsAndCategoriesTotalInflowAndOutflow(c, uid, statisticReq.StartTime, statisticReq.EndTime, tagFilters, noTags, statisticReq.Keyword, clientTimezone, statisticReq.UseTransactionTimezone)
if err != nil {
log.Errorf(c, "[transactions.TransactionStatisticsHandler] failed to get accounts and categories total income and expense for user \"uid:%d\", because %s", uid, err.Error())
@@ -467,10 +551,10 @@ func (a *TransactionsApi) TransactionStatisticsTrendsHandler(c *core.WebContext)
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
utcOffset, err := c.GetClientTimezoneOffset()
clientTimezone, err := c.GetClientTimezone()
if err != nil {
log.Warnf(c, "[transactions.TransactionStatisticsTrendsHandler] cannot get client timezone offset, because %s", err.Error())
log.Warnf(c, "[transactions.TransactionStatisticsTrendsHandler] cannot get client timezone, because %s", err.Error())
return nil, errs.ErrClientTimezoneOffsetInvalid
}
@@ -481,20 +565,20 @@ func (a *TransactionsApi) TransactionStatisticsTrendsHandler(c *core.WebContext)
return nil, errs.Or(err, errs.ErrOperationFailed)
}
var allTagIds []int64
noTags := statisticTrendsReq.TagIds == "none"
noTags := statisticTrendsReq.TagFilter == models.TransactionNoTagFilterValue
var tagFilters []*models.TransactionTagFilter
if !noTags {
allTagIds, err = a.transactionTags.GetTagIds(statisticTrendsReq.TagIds)
tagFilters, err = models.ParseTransactionTagFilter(statisticTrendsReq.TagFilter)
if err != nil {
log.Warnf(c, "[transactions.TransactionStatisticsTrendsHandler] get transaction tag ids error, because %s", err.Error())
log.Warnf(c, "[transactions.TransactionStatisticsTrendsHandler] parse transaction tag filters error, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
}
uid := c.GetCurrentUid()
allMonthlyTotalAmounts, err := a.transactions.GetAccountsAndCategoriesMonthlyInflowAndOutflow(c, uid, startYear, startMonth, endYear, endMonth, allTagIds, noTags, statisticTrendsReq.TagFilterType, statisticTrendsReq.Keyword, utcOffset, statisticTrendsReq.UseTransactionTimezone)
allMonthlyTotalAmounts, err := a.transactions.GetAccountsAndCategoriesMonthlyInflowAndOutflow(c, uid, startYear, startMonth, endYear, endMonth, tagFilters, noTags, statisticTrendsReq.Keyword, clientTimezone, statisticTrendsReq.UseTransactionTimezone)
if err != nil {
log.Errorf(c, "[transactions.TransactionStatisticsTrendsHandler] failed to get accounts and categories total income and expense for user \"uid:%d\", because %s", uid, err.Error())
@@ -542,10 +626,10 @@ func (a *TransactionsApi) TransactionStatisticsAssetTrendsHandler(c *core.WebCon
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
utcOffset, err := c.GetClientTimezoneOffset()
clientTimezone, err := c.GetClientTimezone()
if err != nil {
log.Warnf(c, "[transactions.TransactionStatisticsAssetTrendsHandler] cannot get client timezone offset, because %s", err.Error())
log.Warnf(c, "[transactions.TransactionStatisticsAssetTrendsHandler] cannot get client timezone, because %s", err.Error())
return nil, errs.ErrClientTimezoneOffsetInvalid
}
@@ -563,7 +647,7 @@ func (a *TransactionsApi) TransactionStatisticsAssetTrendsHandler(c *core.WebCon
minTransactionTime = utils.GetMinTransactionTimeFromUnixTime(statisticAssetTrendsReq.StartTime)
}
accountDailyBalances, err := a.transactions.GetAllAccountsDailyOpeningAndClosingBalance(c, uid, maxTransactionTime, minTransactionTime, utcOffset)
accountDailyBalances, err := a.transactions.GetAllAccountsDailyOpeningAndClosingBalance(c, uid, maxTransactionTime, minTransactionTime, clientTimezone)
if err != nil {
log.Errorf(c, "[transactions.TransactionStatisticsAssetTrendsHandler] failed to get transactions from \"%d\" to \"%d\" for user \"uid:%d\", because %s", statisticAssetTrendsReq.StartTime, statisticAssetTrendsReq.EndTime, uid, err.Error())
@@ -643,10 +727,10 @@ func (a *TransactionsApi) TransactionAmountsHandler(c *core.WebContext) (any, *e
}
}
utcOffset, err := c.GetClientTimezoneOffset()
clientTimezone, err := c.GetClientTimezone()
if err != nil {
log.Warnf(c, "[transactions.TransactionAmountsHandler] cannot get client timezone offset, because %s", err.Error())
log.Warnf(c, "[transactions.TransactionAmountsHandler] cannot get client timezone, because %s", err.Error())
return nil, errs.ErrClientTimezoneOffsetInvalid
}
@@ -665,7 +749,7 @@ func (a *TransactionsApi) TransactionAmountsHandler(c *core.WebContext) (any, *e
for i := 0; i < len(requestItems); i++ {
requestItem := requestItems[i]
incomeAmounts, expenseAmounts, err := a.transactions.GetAccountsTotalIncomeAndExpense(c, uid, requestItem.StartTime, requestItem.EndTime, excludeAccountIds, excludeCategoryIds, utcOffset, transactionAmountsReq.UseTransactionTimezone)
incomeAmounts, expenseAmounts, err := a.transactions.GetAccountsTotalIncomeAndExpense(c, uid, requestItem.StartTime, requestItem.EndTime, excludeAccountIds, excludeCategoryIds, clientTimezone, transactionAmountsReq.UseTransactionTimezone)
if err != nil {
log.Errorf(c, "[transactions.TransactionAmountsHandler] failed to get transaction amounts item for user \"uid:%d\", because %s", uid, err.Error())
@@ -746,10 +830,10 @@ func (a *TransactionsApi) TransactionGetHandler(c *core.WebContext) (any, *errs.
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
utcOffset, err := c.GetClientTimezoneOffset()
clientTimezone, err := c.GetClientTimezone()
if err != nil {
log.Warnf(c, "[transactions.TransactionGetHandler] cannot get client timezone offset, because %s", err.Error())
log.Warnf(c, "[transactions.TransactionGetHandler] cannot get client timezone, because %s", err.Error())
return nil, errs.ErrClientTimezoneOffsetInvalid
}
@@ -835,7 +919,7 @@ func (a *TransactionsApi) TransactionGetHandler(c *core.WebContext) (any, *errs.
}
}
transactionEditable := transaction.IsEditable(user, utcOffset, accountMap[transaction.AccountId], accountMap[transaction.RelatedAccountId])
transactionEditable := transaction.IsEditable(user, clientTimezone, accountMap[transaction.AccountId], accountMap[transaction.RelatedAccountId])
transactionTagIds := allTransactionTagIds[transaction.TransactionId]
transactionResp := transaction.ToTransactionInfoResponse(transactionTagIds, transactionEditable)
@@ -876,6 +960,13 @@ func (a *TransactionsApi) TransactionCreateHandler(c *core.WebContext) (any, *er
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
clientTimezone, err := c.GetClientTimezone()
if err != nil {
log.Warnf(c, "[transactions.TransactionCreateHandler] cannot get client timezone, because %s", err.Error())
return nil, errs.ErrClientTimezoneOffsetInvalid
}
tagIds, err := utils.StringArrayToInt64Array(transactionCreateReq.TagIds)
if err != nil {
@@ -933,7 +1024,7 @@ func (a *TransactionsApi) TransactionCreateHandler(c *core.WebContext) (any, *er
}
transaction := a.createNewTransactionModel(uid, &transactionCreateReq, c.ClientIP())
transactionEditable := user.CanEditTransactionByTransactionTime(transaction.TransactionTime, transactionCreateReq.UtcOffset)
transactionEditable := user.CanEditTransactionByTransactionTime(transaction.TransactionTime, clientTimezone)
if !transactionEditable {
return nil, errs.ErrCannotCreateTransactionWithThisTransactionTime
@@ -1006,6 +1097,13 @@ func (a *TransactionsApi) TransactionModifyHandler(c *core.WebContext) (any, *er
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
clientTimezone, err := c.GetClientTimezone()
if err != nil {
log.Warnf(c, "[transactions.TransactionModifyHandler] cannot get client timezone, because %s", err.Error())
return nil, errs.ErrClientTimezoneOffsetInvalid
}
tagIds, err := utils.StringArrayToInt64Array(transactionModifyReq.TagIds)
if err != nil {
@@ -1119,8 +1217,8 @@ func (a *TransactionsApi) TransactionModifyHandler(c *core.WebContext) (any, *er
return nil, errs.ErrNothingWillBeUpdated
}
transactionEditable := user.CanEditTransactionByTransactionTime(transaction.TransactionTime, transaction.TimezoneUtcOffset)
newTransactionEditable := user.CanEditTransactionByTransactionTime(newTransaction.TransactionTime, transactionModifyReq.UtcOffset)
transactionEditable := user.CanEditTransactionByTransactionTime(transaction.TransactionTime, clientTimezone)
newTransactionEditable := user.CanEditTransactionByTransactionTime(newTransaction.TransactionTime, clientTimezone)
if !transactionEditable || !newTransactionEditable {
return nil, errs.ErrCannotModifyTransactionWithThisTransactionTime
@@ -1256,10 +1354,10 @@ func (a *TransactionsApi) TransactionDeleteHandler(c *core.WebContext) (any, *er
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
utcOffset, err := c.GetClientTimezoneOffset()
clientTimezone, err := c.GetClientTimezone()
if err != nil {
log.Warnf(c, "[transactions.TransactionDeleteHandler] cannot get client timezone offset, because %s", err.Error())
log.Warnf(c, "[transactions.TransactionDeleteHandler] cannot get client timezone, because %s", err.Error())
return nil, errs.ErrClientTimezoneOffsetInvalid
}
@@ -1286,7 +1384,7 @@ func (a *TransactionsApi) TransactionDeleteHandler(c *core.WebContext) (any, *er
return nil, errs.ErrTransactionTypeInvalid
}
transactionEditable := user.CanEditTransactionByTransactionTime(transaction.TransactionTime, utcOffset)
transactionEditable := user.CanEditTransactionByTransactionTime(transaction.TransactionTime, clientTimezone)
if !transactionEditable {
return nil, errs.ErrCannotDeleteTransactionWithThisTransactionTime
@@ -1390,10 +1488,10 @@ func (a *TransactionsApi) TransactionParseImportFileHandler(c *core.WebContext)
return nil, errs.ErrParameterInvalid
}
utcOffset, err := c.GetClientTimezoneOffset()
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
}
@@ -1405,6 +1503,15 @@ func (a *TransactionsApi) TransactionParseImportFileHandler(c *core.WebContext)
fileType := fileTypes[0]
textualOptions := form.Value["options"]
textualOption := ""
if len(textualOptions) > 0 {
textualOption = textualOptions[0]
}
additionalOptions := converter.ParseImporterOptions(textualOption)
var dataImporter converter.TransactionDataImporter
if converters.IsCustomDelimiterSeparatedValuesFileType(fileType) {
@@ -1581,7 +1688,7 @@ func (a *TransactionsApi) TransactionParseImportFileHandler(c *core.WebContext)
tagMap := a.transactionTags.GetVisibleTagNameMapByList(tags)
parsedTransactions, _, _, _, _, _, err := dataImporter.ParseImportedData(c, user, fileData, utcOffset, 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())
@@ -1612,6 +1719,13 @@ func (a *TransactionsApi) TransactionImportHandler(c *core.WebContext) (any, *er
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
clientTimezone, err := c.GetClientTimezone()
if err != nil {
log.Warnf(c, "[transactions.TransactionImportHandler] cannot get client timezone, because %s", err.Error())
return nil, errs.ErrClientTimezoneOffsetInvalid
}
uid := c.GetCurrentUid()
if a.CurrentConfig().EnableDuplicateSubmissionsCheck && transactionImportReq.ClientSessionId != "" {
@@ -1697,7 +1811,7 @@ func (a *TransactionsApi) TransactionImportHandler(c *core.WebContext) (any, *er
for i := 0; i < len(transactionImportReq.Transactions); i++ {
transactionCreateReq := transactionImportReq.Transactions[i]
transaction := a.createNewTransactionModel(uid, transactionCreateReq, c.ClientIP())
transactionEditable := user.CanEditTransactionByTransactionTime(transaction.TransactionTime, transactionCreateReq.UtcOffset)
transactionEditable := user.CanEditTransactionByTransactionTime(transaction.TransactionTime, clientTimezone)
if !transactionEditable {
return nil, errs.ErrCannotCreateTransactionWithThisTransactionTime
@@ -1814,7 +1928,7 @@ func (a *TransactionsApi) getTransactionTagInfoResponses(tagIds []int64, allTran
return allTags
}
func (a *TransactionsApi) getTransactionResponseListResult(c *core.WebContext, user *models.User, transactions []*models.Transaction, utcOffset int16, withPictures bool, trimAccount bool, trimCategory bool, trimTag bool) (models.TransactionInfoResponseSlice, error) {
func (a *TransactionsApi) getTransactionResponseListResult(c *core.WebContext, user *models.User, transactions []*models.Transaction, clientTimezone *time.Location, withPictures bool, trimAccount bool, trimCategory bool, trimTag bool) (models.TransactionInfoResponseSlice, error) {
uid := user.Uid
transactionIds := make([]int64, len(transactions))
accountIds := make([]int64, 0, len(transactions)*2)
@@ -1893,7 +2007,7 @@ func (a *TransactionsApi) getTransactionResponseListResult(c *core.WebContext, u
transaction = a.transactions.GetRelatedTransferTransaction(transaction)
}
transactionEditable := transaction.IsEditable(user, utcOffset, allAccounts[transaction.AccountId], allAccounts[transaction.RelatedAccountId])
transactionEditable := transaction.IsEditable(user, clientTimezone, allAccounts[transaction.AccountId], allAccounts[transaction.RelatedAccountId])
transactionTagIds := allTransactionTagIds[transaction.TransactionId]
result[i] = transaction.ToTransactionInfoResponse(transactionTagIds, transactionEditable)
+1 -1
View File
@@ -85,7 +85,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorEnableRequestHandler(c *core.WebCo
return nil, errs.ErrNotPermittedToPerformThisAction
}
key, err := a.twoFactorAuthorizations.GenerateTwoFactorSecret(c, user)
key, err := a.twoFactorAuthorizations.GenerateTwoFactorSecret(c, user, c.GetClientLocale())
if err != nil {
log.Errorf(c, "[twofactor_authorizations.TwoFactorEnableRequestHandler] failed to generate two-factor secret, because %s", err.Error())
+2 -2
View File
@@ -13,8 +13,8 @@ import (
"github.com/mayswind/ezbookkeeping/pkg/auth/oauth2/provider/oidc"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/httpclient"
"github.com/mayswind/ezbookkeeping/pkg/settings"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
// OAuth2Container contains the current OAuth 2.0 authentication provider
@@ -67,7 +67,7 @@ func InitializeOAuth2Provider(config *settings.Config) error {
Container.current = oauth2Provider
Container.usePKCE = config.OAuth2UsePKCE
Container.oauth2HttpClient = utils.NewHttpClient(config.OAuth2RequestTimeout, config.OAuth2Proxy, config.OAuth2SkipTLSVerify, settings.GetUserAgent())
Container.oauth2HttpClient = httpclient.NewHttpClient(config.OAuth2RequestTimeout, config.OAuth2Proxy, config.OAuth2SkipTLSVerify, settings.GetUserAgent(), config.EnableDebugLog)
Container.externalUserAuthType = externalUserAuthType
return nil
@@ -10,6 +10,7 @@ import (
"github.com/mayswind/ezbookkeeping/pkg/auth/oauth2/provider"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/httpclient"
"github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/settings"
)
@@ -59,6 +60,11 @@ func (p *CommonOAuth2Provider) GetUserInfo(c core.Context, oauth2Token *oauth2.T
}
oauth2Client := oauth2.NewClient(c, oauth2.StaticTokenSource(oauth2Token))
req = req.WithContext(httpclient.CustomHttpResponseLog(c, func(data []byte) {
log.Debugf(c, "[common_oauth2_provider.GetUserInfo] response is %s", data)
}))
resp, err := oauth2Client.Do(req)
if err != nil {
@@ -69,8 +75,6 @@ func (p *CommonOAuth2Provider) GetUserInfo(c core.Context, oauth2Token *oauth2.T
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
log.Debugf(c, "[common_oauth2_provider.GetUserInfo] response is %s", body)
if resp.StatusCode != 200 {
log.Errorf(c, "[common_oauth2_provider.GetUserInfo] failed to get user info response, because response code is %d", resp.StatusCode)
return nil, errs.ErrFailedToRequestRemoteApi
@@ -11,6 +11,7 @@ import (
"github.com/mayswind/ezbookkeeping/pkg/auth/oauth2/provider"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/httpclient"
"github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/settings"
)
@@ -61,6 +62,11 @@ func (p *GithubOAuth2Provider) GetUserInfo(c core.Context, oauth2Token *oauth2.T
}
oauth2Client := oauth2.NewClient(c, oauth2.StaticTokenSource(oauth2Token))
req = req.WithContext(httpclient.CustomHttpResponseLog(c, func(data []byte) {
log.Debugf(c, "[github_oauth2_provider.GetUserInfo] user profile response is %s", data)
}))
resp, err := oauth2Client.Do(req)
if err != nil {
@@ -71,8 +77,6 @@ func (p *GithubOAuth2Provider) GetUserInfo(c core.Context, oauth2Token *oauth2.T
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
log.Debugf(c, "[github_oauth2_provider.GetUserInfo] user profile response is %s", body)
if resp.StatusCode != 200 {
log.Errorf(c, "[github_oauth2_provider.GetUserInfo] failed to get user info response, because response code is %d", resp.StatusCode)
return nil, errs.ErrFailedToRequestRemoteApi
@@ -92,6 +96,10 @@ func (p *GithubOAuth2Provider) GetUserInfo(c core.Context, oauth2Token *oauth2.T
return nil, errs.ErrFailedToRequestRemoteApi
}
req = req.WithContext(httpclient.CustomHttpResponseLog(c, func(data []byte) {
log.Debugf(c, "[github_oauth2_provider.GetUserInfo] user emails response is %s", data)
}))
resp, err = oauth2Client.Do(req)
if err != nil {
@@ -102,8 +110,6 @@ func (p *GithubOAuth2Provider) GetUserInfo(c core.Context, oauth2Token *oauth2.T
defer resp.Body.Close()
body, err = io.ReadAll(resp.Body)
log.Debugf(c, "[github_oauth2_provider.GetUserInfo] user emails response is %s", body)
if resp.StatusCode != 200 {
log.Errorf(c, "[github_oauth2_provider.GetUserInfo] failed to get user emails response, because response code is %d", resp.StatusCode)
return nil, errs.ErrFailedToRequestRemoteApi
@@ -11,6 +11,7 @@ import (
"github.com/mayswind/ezbookkeeping/pkg/auth/oauth2/provider"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/httpclient"
"github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/settings"
)
@@ -18,6 +19,7 @@ import (
// OIDCClaims represents OIDC claims
type OIDCClaims struct {
PreferredUserName string `json:"preferred_username"`
UserName string `json:"username"`
Name string `json:"name"`
Email string `json:"email"`
}
@@ -92,7 +94,9 @@ func (p *OIDCProvider) GetUserInfo(c core.Context, oauth2Token *oauth2.Token) (*
nickName := claims.Name
if userName == "" || email == "" || nickName == "" {
userInfo, err := p.oidcProvider.UserInfo(c, oauth2.StaticTokenSource(oauth2Token))
userInfo, err := p.oidcProvider.UserInfo(httpclient.CustomHttpResponseLog(c, func(data []byte) {
log.Debugf(c, "[oidc_provider.GetUserInfo] response is %s", data)
}), oauth2.StaticTokenSource(oauth2Token))
if err != nil {
log.Errorf(c, "[oidc_provider.GetUserInfo] failed to get user info, because %s", err.Error())
@@ -110,6 +114,10 @@ func (p *OIDCProvider) GetUserInfo(c core.Context, oauth2Token *oauth2.Token) (*
userName = claims.PreferredUserName
}
if userName == "" {
userName = claims.UserName
}
if email == "" {
email = claims.Email
}
+2 -1
View File
@@ -5,6 +5,7 @@ import (
"time"
"github.com/mayswind/ezbookkeeping/pkg/converters"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/log"
@@ -818,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), 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())
@@ -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, 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, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezone, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
}
@@ -8,6 +8,7 @@ import (
"golang.org/x/text/encoding/simplifiedchinese"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/models"
@@ -15,7 +16,7 @@ import (
)
func TestAlipayCsvFileImporterParseImportedData_MinimumValidData(t *testing.T) {
converter := AlipayWebTransactionDataCsvFileImporter
importer := AlipayWebTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -35,7 +36,7 @@ func TestAlipayCsvFileImporterParseImportedData_MinimumValidData(t *testing.T) {
"------------------------------------------------------------------------------------\n")
assert.Nil(t, err)
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := converter.ParseImportedData(context, user, []byte(data), 0, 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))
@@ -94,7 +95,7 @@ func TestAlipayCsvFileImporterParseImportedData_MinimumValidData(t *testing.T) {
}
func TestAlipayCsvFileImporterParseImportedData_ParseRefundTransaction(t *testing.T) {
converter := AlipayWebTransactionDataCsvFileImporter
importer := AlipayWebTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -112,7 +113,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseRefundTransaction(t *testin
"------------------------------------------------------------------------------------\n")
assert.Nil(t, err)
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(data1), 0, 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)
@@ -132,7 +133,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseRefundTransaction(t *testin
"------------------------------------------------------------------------------------\n")
assert.Nil(t, err)
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data2), 0, 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)
@@ -144,7 +145,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseRefundTransaction(t *testin
}
func TestAlipayCsvFileImporterParseImportedData_ParseInvestmentRefundTransaction(t *testing.T) {
converter := AlipayAppTransactionDataCsvFileImporter
importer := AlipayAppTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -163,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 := converter.ParseImportedData(context, user, []byte(data1), 0, 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))
@@ -184,7 +185,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseInvestmentRefundTransaction
}
func TestAlipayCsvFileImporterParseImportedData_ParseInvalidTime(t *testing.T) {
converter := AlipayWebTransactionDataCsvFileImporter
importer := AlipayWebTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -201,7 +202,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseInvalidTime(t *testing.T) {
"------------------------------------------------------------------------------------\n")
assert.Nil(t, err)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data1), 0, 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" +
@@ -213,12 +214,12 @@ func TestAlipayCsvFileImporterParseImportedData_ParseInvalidTime(t *testing.T) {
"------------------------------------------------------------------------------------\n")
assert.Nil(t, err)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data2), 0, 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)
}
func TestAlipayCsvFileImporterParseImportedData_ParseInvalidType(t *testing.T) {
converter := AlipayWebTransactionDataCsvFileImporter
importer := AlipayWebTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -235,12 +236,12 @@ func TestAlipayCsvFileImporterParseImportedData_ParseInvalidType(t *testing.T) {
"------------------------------------------------------------------------------------\n")
assert.Nil(t, err)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data), 0, 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)
}
func TestAlipayCsvFileImporterParseImportedData_ParseAccountName(t *testing.T) {
converter := AlipayWebTransactionDataCsvFileImporter
importer := AlipayWebTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -258,7 +259,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseAccountName(t *testing.T) {
"------------------------------------------------------------------------------------\n")
assert.Nil(t, err)
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(data1), 0, 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))
@@ -274,7 +275,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseAccountName(t *testing.T) {
"------------------------------------------------------------------------------------\n")
assert.Nil(t, err)
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data2), 0, 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))
@@ -290,7 +291,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseAccountName(t *testing.T) {
"------------------------------------------------------------------------------------\n")
assert.Nil(t, err)
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data3), 0, 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))
@@ -307,7 +308,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseAccountName(t *testing.T) {
"------------------------------------------------------------------------------------\n")
assert.Nil(t, err)
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data4), 0, 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))
@@ -324,7 +325,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseAccountName(t *testing.T) {
"------------------------------------------------------------------------------------\n")
assert.Nil(t, err)
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data5), 0, 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))
@@ -341,7 +342,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseAccountName(t *testing.T) {
"------------------------------------------------------------------------------------\n")
assert.Nil(t, err)
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data6), 0, 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))
@@ -358,7 +359,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseAccountName(t *testing.T) {
"------------------------------------------------------------------------------------\n")
assert.Nil(t, err)
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data7), 0, 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))
@@ -367,7 +368,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseAccountName(t *testing.T) {
}
func TestAlipayCsvFileImporterParseImportedData_ParseCategory(t *testing.T) {
converter := AlipayAppTransactionDataCsvFileImporter
importer := AlipayAppTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -388,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 := converter.ParseImportedData(context, user, []byte(data1), 0, 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))
@@ -407,7 +408,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseCategory(t *testing.T) {
}
func TestAlipayCsvFileImporterParseImportedData_ParseRelatedAccount(t *testing.T) {
converter := AlipayAppTransactionDataCsvFileImporter
importer := AlipayAppTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -434,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 := converter.ParseImportedData(context, user, []byte(data1), 0, 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))
@@ -529,7 +530,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseRelatedAccount(t *testing.T
}
func TestAlipayCsvFileImporterParseImportedData_ParseDescription(t *testing.T) {
converter := AlipayWebTransactionDataCsvFileImporter
importer := AlipayWebTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -546,7 +547,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseDescription(t *testing.T) {
"------------------------------------------------------------------------------------\n")
assert.Nil(t, err)
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(data1), 0, 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))
@@ -561,7 +562,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseDescription(t *testing.T) {
"------------------------------------------------------------------------------------\n")
assert.Nil(t, err)
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data2), 0, 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))
@@ -569,7 +570,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseDescription(t *testing.T) {
}
func TestAlipayCsvFileImporterParseImportedData_SkipClosedIncomeOrTransferTransaction(t *testing.T) {
converter := AlipayWebTransactionDataCsvFileImporter
importer := AlipayWebTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -586,12 +587,12 @@ func TestAlipayCsvFileImporterParseImportedData_SkipClosedIncomeOrTransferTransa
"2024-09-01 23:59:59 ,充值-普通充值 ,0.05 ,不计收支 ,交易关闭 ,\n" +
"------------------------------------------------------------------------------------\n")
assert.Nil(t, err)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data), 0, 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)
}
func TestAlipayCsvFileImporterParseImportedData_SkipUnknownProductTransferTransaction(t *testing.T) {
converter := AlipayWebTransactionDataCsvFileImporter
importer := AlipayWebTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -607,12 +608,12 @@ func TestAlipayCsvFileImporterParseImportedData_SkipUnknownProductTransferTransa
"2024-09-01 23:59:59 ,xxxx ,0.05 ,不计收支 ,交易成功 ,\n" +
"------------------------------------------------------------------------------------\n")
assert.Nil(t, err)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data), 0, 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)
}
func TestAlipayCsvFileImporterParseImportedData_SkipUnknownStatusTransaction(t *testing.T) {
converter := AlipayWebTransactionDataCsvFileImporter
importer := AlipayWebTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -628,12 +629,12 @@ func TestAlipayCsvFileImporterParseImportedData_SkipUnknownStatusTransaction(t *
"2024-09-01 01:23:45 ,xxxx ,0.12 ,收入 ,xxxx ,\n" +
"------------------------------------------------------------------------------------\n")
assert.Nil(t, err)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data), 0, 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)
}
func TestAlipayCsvFileImporterParseImportedData_MissingFileHeader(t *testing.T) {
converter := AlipayWebTransactionDataCsvFileImporter
importer := AlipayWebTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -647,15 +648,15 @@ func TestAlipayCsvFileImporterParseImportedData_MissingFileHeader(t *testing.T)
"------------------------------------------------------------------------------------\n")
assert.Nil(t, err)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data), 0, 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 = converter.ParseImportedData(context, user, []byte(""), 0, 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)
}
func TestAlipayCsvFileImporterParseImportedData_MissingRequiredColumn(t *testing.T) {
converter := AlipayWebTransactionDataCsvFileImporter
importer := AlipayWebTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -671,7 +672,7 @@ func TestAlipayCsvFileImporterParseImportedData_MissingRequiredColumn(t *testing
"金额(元),收/支 ,交易状态 ,\n" +
"0.12 ,收入 ,交易成功 ,\n" +
"------------------------------------------------------------------------------------\n")
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data1), 0, 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
@@ -682,7 +683,7 @@ func TestAlipayCsvFileImporterParseImportedData_MissingRequiredColumn(t *testing
"交易创建时间 ,收/支 ,交易状态 ,\n" +
"2024-09-01 12:34:56 ,收入 ,交易成功 ,\n" +
"------------------------------------------------------------------------------------\n")
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data2), 0, 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
@@ -693,7 +694,7 @@ func TestAlipayCsvFileImporterParseImportedData_MissingRequiredColumn(t *testing
"交易创建时间 ,金额(元),收/支 ,\n" +
"2024-09-01 12:34:56 ,0.12 ,收入 ,\n" +
"------------------------------------------------------------------------------------\n")
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data3), 0, 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
@@ -704,12 +705,12 @@ func TestAlipayCsvFileImporterParseImportedData_MissingRequiredColumn(t *testing
"交易创建时间 ,金额(元),交易状态 ,\n" +
"2024-09-01 12:34:56 ,0.12 ,交易成功 ,\n" +
"------------------------------------------------------------------------------------\n")
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data4), 0, 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)
}
func TestAlipayCsvFileImporterParseImportedData_NoTransactionData(t *testing.T) {
converter := AlipayWebTransactionDataCsvFileImporter
importer := AlipayWebTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -723,6 +724,6 @@ func TestAlipayCsvFileImporterParseImportedData_NoTransactionData(t *testing.T)
"---------------------------------交易记录明细列表------------------------------------\n" +
"交易创建时间 ,金额(元),收/支 ,交易状态 ,\n" +
"------------------------------------------------------------------------------------\n")
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data1), 0, 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)
}
@@ -13,6 +13,7 @@ import (
const alipayTransactionDataStatusSuccessName = "交易成功"
const alipayTransactionDataStatusPaymentSuccessName = "支付成功"
const alipayTransactionDataStatusPendingGoodsReceiptConfirmationName = "等待确认收货"
const alipayTransactionDataStatusRepaymentSuccessName = "还款成功"
const alipayTransactionDataStatusClosedName = "交易关闭"
const alipayTransactionDataStatusRefundSuccessName = "退款成功"
@@ -46,6 +47,7 @@ func (p *alipayTransactionDataRowParser) Parse(ctx core.Context, user *models.Us
if dataRow.GetData(p.columns.statusColumnName) != alipayTransactionDataStatusSuccessName &&
dataRow.GetData(p.columns.statusColumnName) != alipayTransactionDataStatusPaymentSuccessName &&
dataRow.GetData(p.columns.statusColumnName) != alipayTransactionDataStatusPendingGoodsReceiptConfirmationName &&
dataRow.GetData(p.columns.statusColumnName) != alipayTransactionDataStatusRepaymentSuccessName &&
dataRow.GetData(p.columns.statusColumnName) != alipayTransactionDataStatusClosedName &&
dataRow.GetData(p.columns.statusColumnName) != alipayTransactionDataStatusRefundSuccessName &&
@@ -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, 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, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezone, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
}
@@ -2,9 +2,11 @@ package beancount
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/models"
@@ -12,7 +14,7 @@ import (
)
func TestBeancountTransactionDataFileParseImportedData_MinimumValidData(t *testing.T) {
converter := BeancountTransactionDataImporter
importer := BeancountTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -20,7 +22,7 @@ func TestBeancountTransactionDataFileParseImportedData_MinimumValidData(t *testi
DefaultCurrency: "CNY",
}
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := converter.ParseImportedData(context, user, []byte(
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := importer.ParseImportedData(context, user, []byte(
"2024-09-01 *\n"+
" Equity:Opening-Balances -123.45 CNY\n"+
" Assets:TestAccount 123.45 CNY\n"+
@@ -32,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, nil, nil, nil, nil, nil)
" Assets:TestAccount2 0.05 CNY\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err)
@@ -91,7 +93,7 @@ func TestBeancountTransactionDataFileParseImportedData_MinimumValidData(t *testi
}
func TestBeancountTransactionDataFileParseImportedData_MinimumValidData2(t *testing.T) {
converter := BeancountTransactionDataImporter
importer := BeancountTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -99,7 +101,7 @@ func TestBeancountTransactionDataFileParseImportedData_MinimumValidData2(t *test
DefaultCurrency: "CNY",
}
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := converter.ParseImportedData(context, user, []byte(
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := importer.ParseImportedData(context, user, []byte(
"2024-09-01 *\n"+
" Assets:TestAccount 123.45 CNY\n"+
" Equity:Opening-Balances -123.45 CNY\n"+
@@ -111,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, nil, nil, nil, nil, nil)
" Assets:TestAccount -0.05 CNY\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err)
@@ -170,7 +172,7 @@ func TestBeancountTransactionDataFileParseImportedData_MinimumValidData2(t *test
}
func TestBeancountTransactionDataFileParseImportedData_ParseInvalidTime(t *testing.T) {
converter := BeancountTransactionDataImporter
importer := BeancountTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -178,15 +180,15 @@ func TestBeancountTransactionDataFileParseImportedData_ParseInvalidTime(t *testi
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"2024/09/01 *\n"+
" Equity:Opening-Balances -123.45 CNY\n"+
" Assets:TestAccount 123.45 CNY\n"), 0, 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)
}
func TestBeancountTransactionDataFileParseImportedData_ParseValidCurrency(t *testing.T) {
converter := BeancountTransactionDataImporter
importer := BeancountTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -194,10 +196,10 @@ func TestBeancountTransactionDataFileParseImportedData_ParseValidCurrency(t *tes
DefaultCurrency: "CNY",
}
allNewTransactions, allNewAccounts, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
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, nil, nil, nil, nil, nil)
" Assets:TestAccount2 0.84 CNY\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err)
@@ -222,7 +224,7 @@ func TestBeancountTransactionDataFileParseImportedData_ParseValidCurrency(t *tes
}
func TestBeancountTransactionDataFileParseImportedData_ParseInvalidAmount(t *testing.T) {
converter := BeancountTransactionDataImporter
importer := BeancountTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -230,21 +232,21 @@ func TestBeancountTransactionDataFileParseImportedData_ParseInvalidAmount(t *tes
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"2024-09-01 *\n"+
" Equity:Opening-Balances -abc CNY\n"+
" Assets:TestAccount abc CNY\n"), 0, 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 = converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"2024-09-01 *\n"+
" Equity:Opening-Balances -1/0 CNY\n"+
" Assets:TestAccount 1/0 CNY\n"), 0, 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)
}
func TestBeancountTransactionDataFileParseImportedData_ParseDescription(t *testing.T) {
converter := BeancountTransactionDataImporter
importer := BeancountTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -252,13 +254,13 @@ func TestBeancountTransactionDataFileParseImportedData_ParseDescription(t *testi
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"2024-09-01 * \"foo bar\t#test\n\"\n"+
" Equity:Opening-Balances -123.45 CNY\n"+
" 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, nil, nil, nil, nil, nil)
" Assets:TestAccount 0.12 CNY\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err)
@@ -269,7 +271,7 @@ func TestBeancountTransactionDataFileParseImportedData_ParseDescription(t *testi
}
func TestBeancountTransactionDataFileParseImportedData_InvalidTransaction(t *testing.T) {
converter := BeancountTransactionDataImporter
importer := BeancountTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -277,33 +279,33 @@ func TestBeancountTransactionDataFileParseImportedData_InvalidTransaction(t *tes
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, 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, 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 = converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, 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, 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 = converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, 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, 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 = converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, 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, 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)
}
func TestBeancountTransactionDataFileParseImportedData_NotSupportedToParseSplitTransaction(t *testing.T) {
converter := BeancountTransactionDataImporter
importer := BeancountTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -311,16 +313,16 @@ func TestBeancountTransactionDataFileParseImportedData_NotSupportedToParseSplitT
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"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, 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)
}
func TestBeancountTransactionDataFileParseImportedData_MissingTransactionRequiredData(t *testing.T) {
converter := BeancountTransactionDataImporter
importer := BeancountTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -329,30 +331,30 @@ func TestBeancountTransactionDataFileParseImportedData_MissingTransactionRequire
}
// Missing Transaction Time
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"* \"narration\"\n"+
" Equity:Opening-Balances -123.45 CNY\n"+
" Assets:TestAccount 123.45 CNY\n"), 0, 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 = converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"2024-09-01 * \"narration\"\n"+
" Equity:Opening-Balances -123.45 CNY\n"+
" 123.45 CNY\n"), 0, 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 = converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"2024-09-01 * \"narration\"\n"+
" Equity:Opening-Balances\n"+
" Assets:TestAccount\n"), 0, 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 = converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"2024-09-01 * \"narration\"\n"+
" Equity:Opening-Balances -123.45\n"+
" Assets:TestAccount 123.45\n"), 0, 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)
}
@@ -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
@@ -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, 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, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezone, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
}
@@ -2,9 +2,11 @@ package camt
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/models"
@@ -12,7 +14,7 @@ import (
)
func TestCamt053TransactionDataFileParseImportedData_MinimumValidData(t *testing.T) {
converter := Camt053TransactionDataImporter
importer := Camt053TransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -20,7 +22,7 @@ func TestCamt053TransactionDataFileParseImportedData_MinimumValidData(t *testing
DefaultCurrency: "CNY",
}
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := converter.ParseImportedData(context, user, []byte(
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := importer.ParseImportedData(context, user, []byte(
`<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt>
@@ -64,7 +66,7 @@ func TestCamt053TransactionDataFileParseImportedData_MinimumValidData(t *testing
</Ntry>
</Stmt>
</BkToCstmrStmt>
</Document>`), 0, nil, nil, nil, nil, nil)
</Document>`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err)
@@ -115,7 +117,7 @@ func TestCamt053TransactionDataFileParseImportedData_MinimumValidData(t *testing
}
func TestCamt053TransactionDataFileParseImportedData_ParseValidTransactionTime(t *testing.T) {
converter := Camt053TransactionDataImporter
importer := Camt053TransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -123,7 +125,7 @@ func TestCamt053TransactionDataFileParseImportedData_ParseValidTransactionTime(t
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
`<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt>
@@ -157,7 +159,7 @@ func TestCamt053TransactionDataFileParseImportedData_ParseValidTransactionTime(t
</Ntry>
</Stmt>
</BkToCstmrStmt>
</Document>`), 0, nil, nil, nil, nil, nil)
</Document>`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 3, len(allNewTransactions))
@@ -168,7 +170,7 @@ func TestCamt053TransactionDataFileParseImportedData_ParseValidTransactionTime(t
}
func TestCamt053TransactionDataFileParseImportedData_ParseInvalidTransactionTime(t *testing.T) {
converter := Camt053TransactionDataImporter
importer := Camt053TransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -176,7 +178,7 @@ func TestCamt053TransactionDataFileParseImportedData_ParseInvalidTransactionTime
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
`<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt>
@@ -196,10 +198,10 @@ func TestCamt053TransactionDataFileParseImportedData_ParseInvalidTransactionTime
</Ntry>
</Stmt>
</BkToCstmrStmt>
</Document>`), 0, nil, nil, nil, nil, nil)
</Document>`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
`<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt>
@@ -219,10 +221,10 @@ func TestCamt053TransactionDataFileParseImportedData_ParseInvalidTransactionTime
</Ntry>
</Stmt>
</BkToCstmrStmt>
</Document>`), 0, nil, nil, nil, nil, nil)
</Document>`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
`<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt>
@@ -242,10 +244,10 @@ func TestCamt053TransactionDataFileParseImportedData_ParseInvalidTransactionTime
</Ntry>
</Stmt>
</BkToCstmrStmt>
</Document>`), 0, nil, nil, nil, nil, nil)
</Document>`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
`<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt>
@@ -265,12 +267,12 @@ func TestCamt053TransactionDataFileParseImportedData_ParseInvalidTransactionTime
</Ntry>
</Stmt>
</BkToCstmrStmt>
</Document>`), 0, nil, nil, nil, nil, nil)
</Document>`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
}
func TestCamt053TransactionDataFileParseImportedData_ParseTransactionValidAmountAndCurrency(t *testing.T) {
converter := Camt053TransactionDataImporter
importer := Camt053TransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -278,7 +280,7 @@ func TestCamt053TransactionDataFileParseImportedData_ParseTransactionValidAmount
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
`<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt>
@@ -314,7 +316,7 @@ func TestCamt053TransactionDataFileParseImportedData_ParseTransactionValidAmount
</Ntry>
</Stmt>
</BkToCstmrStmt>
</Document>`), 0, nil, nil, nil, nil, nil)
</Document>`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 2, len(allNewTransactions))
@@ -323,7 +325,7 @@ func TestCamt053TransactionDataFileParseImportedData_ParseTransactionValidAmount
assert.Equal(t, "USD", allNewTransactions[1].OriginalSourceAccountCurrency)
assert.Equal(t, int64(10023), allNewTransactions[1].Amount)
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
`<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt>
@@ -365,7 +367,7 @@ func TestCamt053TransactionDataFileParseImportedData_ParseTransactionValidAmount
</Ntry>
</Stmt>
</BkToCstmrStmt>
</Document>`), 0, nil, nil, nil, nil, nil)
</Document>`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 2, len(allNewTransactions))
@@ -374,7 +376,7 @@ func TestCamt053TransactionDataFileParseImportedData_ParseTransactionValidAmount
assert.Equal(t, "USD", allNewTransactions[1].OriginalSourceAccountCurrency)
assert.Equal(t, int64(9999), allNewTransactions[1].Amount)
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
`<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt>
@@ -403,14 +405,14 @@ func TestCamt053TransactionDataFileParseImportedData_ParseTransactionValidAmount
</Ntry>
</Stmt>
</BkToCstmrStmt>
</Document>`), 0, nil, nil, nil, nil, nil)
</Document>`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, "USD", allNewTransactions[0].OriginalSourceAccountCurrency)
assert.Equal(t, int64(12345), allNewTransactions[0].Amount)
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
`<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt>
@@ -430,7 +432,7 @@ func TestCamt053TransactionDataFileParseImportedData_ParseTransactionValidAmount
</Ntry>
</Stmt>
</BkToCstmrStmt>
</Document>`), 0, nil, nil, nil, nil, nil)
</Document>`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
@@ -439,7 +441,7 @@ func TestCamt053TransactionDataFileParseImportedData_ParseTransactionValidAmount
}
func TestCamt053TransactionDataFileParseImportedData_ParseTransactionInvalidAmountAndCurrency(t *testing.T) {
converter := Camt053TransactionDataImporter
importer := Camt053TransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -447,7 +449,7 @@ func TestCamt053TransactionDataFileParseImportedData_ParseTransactionInvalidAmou
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
`<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt>
@@ -467,10 +469,10 @@ func TestCamt053TransactionDataFileParseImportedData_ParseTransactionInvalidAmou
</Ntry>
</Stmt>
</BkToCstmrStmt>
</Document>`), 0, nil, nil, nil, nil, nil)
</Document>`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
`<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt>
@@ -498,10 +500,10 @@ func TestCamt053TransactionDataFileParseImportedData_ParseTransactionInvalidAmou
</Ntry>
</Stmt>
</BkToCstmrStmt>
</Document>`), 0, nil, nil, nil, nil, nil)
</Document>`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
`<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt>
@@ -529,12 +531,12 @@ func TestCamt053TransactionDataFileParseImportedData_ParseTransactionInvalidAmou
</Ntry>
</Stmt>
</BkToCstmrStmt>
</Document>`), 0, nil, nil, nil, nil, nil)
</Document>`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
}
func TestCamt053TransactionDataFileParseImportedData_ParseDescription(t *testing.T) {
converter := Camt053TransactionDataImporter
importer := Camt053TransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -542,7 +544,7 @@ func TestCamt053TransactionDataFileParseImportedData_ParseDescription(t *testing
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
`<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt>
@@ -572,13 +574,13 @@ func TestCamt053TransactionDataFileParseImportedData_ParseDescription(t *testing
</Ntry>
</Stmt>
</BkToCstmrStmt>
</Document>`), 0, nil, nil, nil, nil, nil)
</Document>`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, "Test Transaction", allNewTransactions[0].Comment)
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
`<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt>
@@ -607,13 +609,13 @@ func TestCamt053TransactionDataFileParseImportedData_ParseDescription(t *testing
</Ntry>
</Stmt>
</BkToCstmrStmt>
</Document>`), 0, nil, nil, nil, nil, nil)
</Document>`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, "Test Line 1\nTest Line 2", allNewTransactions[0].Comment)
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
`<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt>
@@ -634,7 +636,7 @@ func TestCamt053TransactionDataFileParseImportedData_ParseDescription(t *testing
</Ntry>
</Stmt>
</BkToCstmrStmt>
</Document>`), 0, nil, nil, nil, nil, nil)
</Document>`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
@@ -642,7 +644,7 @@ func TestCamt053TransactionDataFileParseImportedData_ParseDescription(t *testing
}
func TestCamt053TransactionDataFileParseImportedData_MissingAccountNode(t *testing.T) {
converter := Camt053TransactionDataImporter
importer := Camt053TransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -650,7 +652,7 @@ func TestCamt053TransactionDataFileParseImportedData_MissingAccountNode(t *testi
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
`<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt>
@@ -664,12 +666,12 @@ func TestCamt053TransactionDataFileParseImportedData_MissingAccountNode(t *testi
</Ntry>
</Stmt>
</BkToCstmrStmt>
</Document>`), 0, nil, nil, nil, nil, nil)
</Document>`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrMissingAccountData.Message)
}
func TestCamt053TransactionDataFileParseImportedData_MissingTransactionRequiredNode(t *testing.T) {
converter := Camt053TransactionDataImporter
importer := Camt053TransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -677,7 +679,7 @@ func TestCamt053TransactionDataFileParseImportedData_MissingTransactionRequiredN
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
`<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt>
@@ -694,10 +696,10 @@ func TestCamt053TransactionDataFileParseImportedData_MissingTransactionRequiredN
</Ntry>
</Stmt>
</BkToCstmrStmt>
</Document>`), 0, nil, nil, nil, nil, nil)
</Document>`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrMissingTransactionTime.Message)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
`<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt>
@@ -716,10 +718,10 @@ func TestCamt053TransactionDataFileParseImportedData_MissingTransactionRequiredN
</Ntry>
</Stmt>
</BkToCstmrStmt>
</Document>`), 0, nil, nil, nil, nil, nil)
</Document>`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrTransactionTypeInvalid.Message)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
`<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt>
@@ -738,10 +740,10 @@ func TestCamt053TransactionDataFileParseImportedData_MissingTransactionRequiredN
</Ntry>
</Stmt>
</BkToCstmrStmt>
</Document>`), 0, nil, nil, nil, nil, nil)
</Document>`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
`<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt>
@@ -760,6 +762,6 @@ func TestCamt053TransactionDataFileParseImportedData_MissingTransactionRequiredN
</Ntry>
</Stmt>
</BkToCstmrStmt>
</Document>`), 0, nil, nil, nil, nil, nil)
</Document>`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message)
}
@@ -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)
@@ -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, 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())
@@ -303,6 +304,7 @@ func (c *DataTableTransactionDataImporter) ParseImportedData(ctx core.Context, u
var tagIds []string
var tagNames []string
tagNamesMap := make(map[string]bool)
if dataTable.HasColumn(datatable.TRANSACTION_DATA_TABLE_TAGS) {
var tagNameItems []string
@@ -320,19 +322,39 @@ func (c *DataTableTransactionDataImporter) ParseImportedData(ctx core.Context, u
continue
}
tag, exists := tagMap[tagName]
allNewTags, tagIds, tagNames = c.addTag(user, tagName, tagNamesMap, tagMap, allNewTags, tagIds, tagNames)
}
}
if !exists {
tag = c.createNewTransactionTagModel(user.Uid, tagName)
allNewTags = append(allNewTags, tag)
tagMap[tagName] = tag
}
if dataTable.HasColumn(datatable.TRANSACTION_DATA_TABLE_PAYEE) && additionalOptions.IsPayeeAsTag() {
payee := dataRow.GetData(datatable.TRANSACTION_DATA_TABLE_PAYEE)
if tag != nil {
tagIds = append(tagIds, utils.Int64ToString(tag.TagId))
}
if payee != "" {
allNewTags, tagIds, tagNames = c.addTag(user, payee, tagNamesMap, tagMap, allNewTags, tagIds, tagNames)
}
}
tagNames = append(tagNames, tagName)
if dataTable.HasColumn(datatable.TRANSACTION_DATA_TABLE_MEMBER) && additionalOptions.IsMemberAsTag() {
member := dataRow.GetData(datatable.TRANSACTION_DATA_TABLE_MEMBER)
if member != "" {
allNewTags, tagIds, tagNames = c.addTag(user, member, tagNamesMap, tagMap, allNewTags, tagIds, tagNames)
}
}
if dataTable.HasColumn(datatable.TRANSACTION_DATA_TABLE_PROJECT) && additionalOptions.IsProjectAsTag() {
project := dataRow.GetData(datatable.TRANSACTION_DATA_TABLE_PROJECT)
if project != "" {
allNewTags, tagIds, tagNames = c.addTag(user, project, tagNamesMap, tagMap, allNewTags, tagIds, tagNames)
}
}
if dataTable.HasColumn(datatable.TRANSACTION_DATA_TABLE_MERCHANT) && additionalOptions.IsMerchantAsTag() {
merchant := dataRow.GetData(datatable.TRANSACTION_DATA_TABLE_MERCHANT)
if merchant != "" {
allNewTags, tagIds, tagNames = c.addTag(user, merchant, tagNamesMap, tagMap, allNewTags, tagIds, tagNames)
}
}
@@ -342,13 +364,17 @@ func (c *DataTableTransactionDataImporter) ParseImportedData(ctx core.Context, u
description = dataRow.GetData(datatable.TRANSACTION_DATA_TABLE_DESCRIPTION)
}
if description == "" && additionalOptions.IsPayeeAsDescription() && dataTable.HasColumn(datatable.TRANSACTION_DATA_TABLE_PAYEE) {
description = dataRow.GetData(datatable.TRANSACTION_DATA_TABLE_PAYEE)
}
transaction := &models.ImportTransaction{
Transaction: &models.Transaction{
Uid: user.Uid,
Type: transactionDbType,
CategoryId: categoryId,
TransactionTime: utils.GetMinTransactionTimeFromUnixTime(transactionTime.Unix()),
TimezoneUtcOffset: timezoneOffset,
TimezoneUtcOffset: utils.GetTimezoneOffsetMinutes(transactionTime.Unix(), timezone),
AccountId: account.AccountId,
Amount: amount,
HideAmount: false,
@@ -459,6 +485,27 @@ func (c *DataTableTransactionDataImporter) getTransactionCategory(categories map
return subCategory, exists
}
func (c *DataTableTransactionDataImporter) addTag(user *models.User, tagName string, tagNamesMap map[string]bool, tagMap map[string]*models.TransactionTag, allNewTags []*models.TransactionTag, tagIds []string, tagNames []string) ([]*models.TransactionTag, []string, []string) {
if tagName != "" && !tagNamesMap[tagName] {
tag, exists := tagMap[tagName]
if !exists {
tag = c.createNewTransactionTagModel(user.Uid, tagName)
allNewTags = append(allNewTags, tag)
tagMap[tagName] = tag
}
if tag != nil {
tagIds = append(tagIds, utils.Int64ToString(tag.TagId))
}
tagNames = append(tagNames, tagName)
tagNamesMap[tagName] = true
}
return allNewTags, tagIds, tagNames
}
func (c *DataTableTransactionDataImporter) createNewAccountModel(uid int64, accountName string, currency string) *models.Account {
return &models.Account{
Uid: uid,
@@ -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, 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
@@ -0,0 +1,118 @@
package converter
import "strings"
// TransactionDataImporterOptions defines the options for transaction data importer
type TransactionDataImporterOptions struct {
payeeAsTag bool
payeeAsDescription bool
memberAsTag bool
projectAsTag bool
merchantAsTag bool
}
// DefaultImporterOptions provides the default options for transaction data importer
var DefaultImporterOptions = TransactionDataImporterOptions{
payeeAsTag: false,
payeeAsDescription: false,
memberAsTag: false,
projectAsTag: false,
merchantAsTag: false,
}
// IsPayeeAsTag returns whether to import payee as tag
func (o TransactionDataImporterOptions) IsPayeeAsTag() bool {
return o.payeeAsTag
}
// IsPayeeAsDescription returns whether to import payee as description
func (o TransactionDataImporterOptions) IsPayeeAsDescription() bool {
return o.payeeAsDescription
}
// IsMemberAsTag returns whether to import member as tag
func (o TransactionDataImporterOptions) IsMemberAsTag() bool {
return o.memberAsTag
}
// IsProjectAsTag returns whether to import project as tag
func (o TransactionDataImporterOptions) IsProjectAsTag() bool {
return o.projectAsTag
}
// IsMerchantAsTag returns whether to import merchant as tag
func (o TransactionDataImporterOptions) IsMerchantAsTag() bool {
return o.merchantAsTag
}
// WithPayeeAsTag sets the option to import payee as tag
func (o TransactionDataImporterOptions) WithPayeeAsTag() TransactionDataImporterOptions {
cloned := o.Clone()
cloned.payeeAsTag = true
return cloned
}
// WithPayeeAsDescription sets the option to import payee as description
func (o TransactionDataImporterOptions) WithPayeeAsDescription() TransactionDataImporterOptions {
cloned := o.Clone()
cloned.payeeAsDescription = true
return cloned
}
// WithMemberAsTag sets the option to import member as tag
func (o TransactionDataImporterOptions) WithMemberAsTag() TransactionDataImporterOptions {
cloned := o.Clone()
cloned.memberAsTag = true
return cloned
}
// WithProjectAsTag sets the option to import project as tag
func (o TransactionDataImporterOptions) WithProjectAsTag() TransactionDataImporterOptions {
cloned := o.Clone()
cloned.projectAsTag = true
return cloned
}
// WithMerchantAsTag sets the option to import merchant as tag
func (o TransactionDataImporterOptions) WithMerchantAsTag() TransactionDataImporterOptions {
cloned := o.Clone()
cloned.merchantAsTag = true
return cloned
}
// Clone creates a copy of the options instance
func (o TransactionDataImporterOptions) Clone() TransactionDataImporterOptions {
return TransactionDataImporterOptions{
payeeAsTag: o.payeeAsTag,
payeeAsDescription: o.payeeAsDescription,
memberAsTag: o.memberAsTag,
projectAsTag: o.projectAsTag,
merchantAsTag: o.merchantAsTag,
}
}
// ParseImporterOptions parses the textual options to the instance
func ParseImporterOptions(s string) TransactionDataImporterOptions {
options := TransactionDataImporterOptions{}
if s == "" {
return options
}
for _, option := range strings.Split(s, ",") {
switch option {
case "payeeAsTag":
options.payeeAsTag = true
case "payeeAsDescription":
options.payeeAsDescription = true
case "memberAsTag":
options.memberAsTag = true
case "projectAsTag":
options.projectAsTag = true
case "merchantAsTag":
options.merchantAsTag = true
}
}
return options
}
@@ -0,0 +1,110 @@
package converter
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestParseImporterOptions(t *testing.T) {
actualValue := ParseImporterOptions("payeeAsTag,memberAsTag")
expectedValue := TransactionDataImporterOptions{
payeeAsTag: true,
memberAsTag: true,
projectAsTag: false,
merchantAsTag: false,
}
assert.Equal(t, expectedValue, actualValue)
assert.Equal(t, true, actualValue.IsPayeeAsTag())
assert.Equal(t, true, actualValue.IsMemberAsTag())
assert.Equal(t, false, actualValue.IsProjectAsTag())
assert.Equal(t, false, actualValue.IsMerchantAsTag())
actualValue = ParseImporterOptions("")
expectedValue = TransactionDataImporterOptions{
payeeAsTag: false,
memberAsTag: false,
projectAsTag: false,
merchantAsTag: false,
}
assert.Equal(t, expectedValue, actualValue)
assert.Equal(t, false, actualValue.IsPayeeAsTag())
assert.Equal(t, false, actualValue.IsMemberAsTag())
assert.Equal(t, false, actualValue.IsProjectAsTag())
assert.Equal(t, false, actualValue.IsMerchantAsTag())
}
func TestParseImporterOptions_WithAllOptions(t *testing.T) {
actualValue := ParseImporterOptions("payeeAsTag,payeeAsDescription,memberAsTag,projectAsTag,merchantAsTag")
expectedValue := TransactionDataImporterOptions{
payeeAsTag: true,
payeeAsDescription: true,
memberAsTag: true,
projectAsTag: true,
merchantAsTag: true,
}
assert.Equal(t, expectedValue, actualValue)
assert.Equal(t, true, actualValue.IsPayeeAsTag())
assert.Equal(t, true, actualValue.IsPayeeAsDescription())
assert.Equal(t, true, actualValue.IsMemberAsTag())
assert.Equal(t, true, actualValue.IsProjectAsTag())
assert.Equal(t, true, actualValue.IsMerchantAsTag())
}
func TestParseImporterOptions_WithInvalidOptions(t *testing.T) {
actualValue := ParseImporterOptions("invalidOption,payeeAsTag,memberAsTag")
expectedValue := TransactionDataImporterOptions{
payeeAsTag: true,
memberAsTag: true,
projectAsTag: false,
merchantAsTag: false,
}
assert.Equal(t, expectedValue, actualValue)
assert.Equal(t, true, actualValue.IsPayeeAsTag())
assert.Equal(t, true, actualValue.IsMemberAsTag())
assert.Equal(t, false, actualValue.IsProjectAsTag())
assert.Equal(t, false, actualValue.IsMerchantAsTag())
actualValue = ParseImporterOptions("invalidOption")
expectedValue = TransactionDataImporterOptions{
payeeAsTag: false,
memberAsTag: false,
projectAsTag: false,
merchantAsTag: false,
}
assert.Equal(t, expectedValue, actualValue)
assert.Equal(t, false, actualValue.IsPayeeAsTag())
assert.Equal(t, false, actualValue.IsMemberAsTag())
assert.Equal(t, false, actualValue.IsProjectAsTag())
assert.Equal(t, false, actualValue.IsMerchantAsTag())
}
func TestParseImporterOptions_Clone(t *testing.T) {
original := TransactionDataImporterOptions{
payeeAsTag: true,
payeeAsDescription: false,
memberAsTag: false,
projectAsTag: true,
merchantAsTag: false,
}
cloned := original.Clone()
assert.Equal(t, original, cloned)
// Modify cloned options and verify original options are not affected
cloned.payeeAsTag = false
cloned.payeeAsDescription = true
cloned.memberAsTag = true
assert.Equal(t, true, original.payeeAsTag)
assert.Equal(t, false, original.payeeAsDescription)
assert.Equal(t, false, original.memberAsTag)
assert.Equal(t, true, original.projectAsTag)
assert.Equal(t, false, original.merchantAsTag)
assert.Equal(t, false, cloned.payeeAsTag)
assert.Equal(t, true, cloned.payeeAsDescription)
assert.Equal(t, true, cloned.memberAsTag)
assert.Equal(t, true, cloned.projectAsTag)
assert.Equal(t, false, cloned.merchantAsTag)
}
@@ -72,6 +72,10 @@ const (
TRANSACTION_DATA_TABLE_GEOGRAPHIC_LOCATION TransactionDataTableColumn = 12
TRANSACTION_DATA_TABLE_TAGS TransactionDataTableColumn = 13
TRANSACTION_DATA_TABLE_DESCRIPTION TransactionDataTableColumn = 14
TRANSACTION_DATA_TABLE_PAYEE TransactionDataTableColumn = 101
TRANSACTION_DATA_TABLE_MEMBER TransactionDataTableColumn = 102
TRANSACTION_DATA_TABLE_PROJECT TransactionDataTableColumn = 103
TRANSACTION_DATA_TABLE_MERCHANT TransactionDataTableColumn = 104
)
// TRANSACTION_DATA_TABLE_TIMEZONE_NOT_AVAILABLE represents the constant for timezone not available
@@ -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, 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, 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
@@ -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, 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, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezone, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
}
@@ -2,9 +2,11 @@ package _default
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/models"
@@ -12,7 +14,7 @@ import (
)
func TestDefaultTransactionDataCSVFileConverterToExportedContent(t *testing.T) {
converter := DefaultTransactionDataCSVFileConverter
exporter := DefaultTransactionDataCSVFileConverter
context := core.NewNullContext()
transactions := make([]*models.Transaction, 3)
@@ -119,14 +121,14 @@ func TestDefaultTransactionDataCSVFileConverterToExportedContent(t *testing.T) {
"2024-09-01 12:34:56,+08:00,Income,Test Category,Test Sub Category,Test Account,CNY,123.45,,,,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,-0.10,,,,,Test Tag,Foo#Bar\n" +
"2024-09-01 12:34:56,-05:00,Transfer,Test Category3,Test Sub Category3,Test Account,CNY,123.45,Test Account2,USD,17.35,,Test Tag2,T\te s t test\n"
actualContent, err := converter.ToExportedContent(context, 123, transactions, accountMap, categoryMap, tagMap, allTagIndexes)
actualContent, err := exporter.ToExportedContent(context, 123, transactions, accountMap, categoryMap, tagMap, allTagIndexes)
assert.Nil(t, err)
assert.Equal(t, expectedContent, string(actualContent))
}
func TestDefaultTransactionDataCSVFileConverterParseImportedData_MinimumValidData(t *testing.T) {
converter := DefaultTransactionDataCSVFileConverter
importer := DefaultTransactionDataCSVFileConverter
context := core.NewNullContext()
user := &models.User{
@@ -134,11 +136,11 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_MinimumValidDat
DefaultCurrency: "CNY",
}
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := converter.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := importer.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
"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, 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)
@@ -197,7 +199,7 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_MinimumValidDat
}
func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseInvalidTime(t *testing.T) {
converter := DefaultTransactionDataCSVFileConverter
importer := DefaultTransactionDataCSVFileConverter
context := core.NewNullContext()
user := &models.User{
@@ -205,17 +207,17 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseInvalidTim
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := converter.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, nil, nil, nil, nil, nil)
_, _, _, _, _, _, 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,,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
_, _, _, _, _, _, err = converter.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, nil, nil, nil, nil, nil)
_, _, _, _, _, _, 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,,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
}
func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseInvalidType(t *testing.T) {
converter := DefaultTransactionDataCSVFileConverter
importer := DefaultTransactionDataCSVFileConverter
context := core.NewNullContext()
user := &models.User{
@@ -223,13 +225,13 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseInvalidTyp
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := converter.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, nil, nil, nil, nil, nil)
_, _, _, _, _, _, 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,,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrTransactionTypeInvalid.Message)
}
func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseValidTimezone(t *testing.T) {
converter := DefaultTransactionDataCSVFileConverter
importer := DefaultTransactionDataCSVFileConverter
context := core.NewNullContext()
user := &models.User{
@@ -237,27 +239,27 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseValidTimez
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := converter.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, nil, nil, nil, nil, nil)
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,,"), 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 = converter.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, nil, nil, nil, nil, nil)
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,,"), 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 = converter.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, nil, nil, nil, nil, nil)
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,,"), 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))
}
func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseInvalidTimezone(t *testing.T) {
converter := DefaultTransactionDataCSVFileConverter
importer := DefaultTransactionDataCSVFileConverter
context := core.NewNullContext()
user := &models.User{
@@ -265,13 +267,13 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseInvalidTim
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := converter.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, nil, nil, nil, nil, nil)
_, _, _, _, _, _, 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,,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrTransactionTimeZoneInvalid.Message)
}
func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseValidAccountCurrency(t *testing.T) {
converter := DefaultTransactionDataCSVFileConverter
importer := DefaultTransactionDataCSVFileConverter
context := core.NewNullContext()
user := &models.User{
@@ -279,9 +281,9 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseValidAccou
DefaultCurrency: "CNY",
}
allNewTransactions, allNewAccounts, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount\n"+
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, 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)
@@ -298,7 +300,7 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseValidAccou
}
func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseInvalidAccountCurrency(t *testing.T) {
converter := DefaultTransactionDataCSVFileConverter
importer := DefaultTransactionDataCSVFileConverter
context := core.NewNullContext()
user := &models.User{
@@ -306,19 +308,19 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseInvalidAcc
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount\n"+
_, _, _, _, _, _, 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, 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 = converter.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount\n"+
_, _, _, _, _, _, 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, 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)
}
func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseNotSupportedCurrency(t *testing.T) {
converter := DefaultTransactionDataCSVFileConverter
importer := DefaultTransactionDataCSVFileConverter
context := core.NewNullContext()
user := &models.User{
@@ -326,17 +328,17 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseNotSupport
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := converter.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, nil, nil, nil, nil, nil)
_, _, _, _, _, _, 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,,,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message)
_, _, _, _, _, _, err = converter.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, nil, nil, nil, nil, nil)
_, _, _, _, _, _, 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"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message)
}
func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseInvalidAmount(t *testing.T) {
converter := DefaultTransactionDataCSVFileConverter
importer := DefaultTransactionDataCSVFileConverter
context := core.NewNullContext()
user := &models.User{
@@ -344,17 +346,17 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseInvalidAmo
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := converter.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, nil, nil, nil, nil, nil)
_, _, _, _, _, _, 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,,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
_, _, _, _, _, _, err = converter.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, nil, nil, nil, nil, nil)
_, _, _, _, _, _, 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"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
}
func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseNoAmount2(t *testing.T) {
converter := DefaultTransactionDataCSVFileConverter
importer := DefaultTransactionDataCSVFileConverter
context := core.NewNullContext()
user := &models.User{
@@ -362,15 +364,15 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseNoAmount2(
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := converter.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, nil, nil, nil, nil, nil)
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,"), 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 = converter.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, nil, nil, nil, nil, nil)
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"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, int64(12345), allNewTransactions[0].Amount)
@@ -378,7 +380,7 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseNoAmount2(
}
func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseValidGeographicLocation(t *testing.T) {
converter := DefaultTransactionDataCSVFileConverter
importer := DefaultTransactionDataCSVFileConverter
context := core.NewNullContext()
user := &models.User{
@@ -386,8 +388,8 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseValidGeogr
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := converter.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, nil, nil, nil, nil, nil)
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"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
@@ -396,7 +398,7 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseValidGeogr
}
func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseInvalidGeographicLocation(t *testing.T) {
converter := DefaultTransactionDataCSVFileConverter
importer := DefaultTransactionDataCSVFileConverter
context := core.NewNullContext()
user := &models.User{
@@ -404,24 +406,24 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseInvalidGeo
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := converter.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, nil, nil, nil, nil, nil)
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"), 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 = converter.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, nil, nil, nil, nil, nil)
_, _, _, _, _, _, 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"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrGeographicLocationInvalid.Message)
_, _, _, _, _, _, err = converter.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, nil, nil, nil, nil, nil)
_, _, _, _, _, _, 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 "), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrGeographicLocationInvalid.Message)
}
func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseTag(t *testing.T) {
converter := DefaultTransactionDataCSVFileConverter
importer := DefaultTransactionDataCSVFileConverter
context := core.NewNullContext()
user := &models.User{
@@ -429,8 +431,8 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseTag(t *tes
DefaultCurrency: "CNY",
}
_, _, _, _, _, allNewTags, err := converter.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, nil, nil, nil, nil, nil)
_, _, _, _, _, 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;;"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err)
@@ -450,7 +452,7 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseTag(t *tes
}
func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseDescription(t *testing.T) {
converter := DefaultTransactionDataCSVFileConverter
importer := DefaultTransactionDataCSVFileConverter
context := core.NewNullContext()
user := &models.User{
@@ -458,8 +460,8 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseDescriptio
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := converter.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, nil, nil, nil, nil, nil)
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"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
@@ -467,7 +469,7 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseDescriptio
}
func TestDefaultTransactionDataCSVFileConverterParseImportedData_MissingFileHeader(t *testing.T) {
converter := DefaultTransactionDataCSVFileConverter
importer := DefaultTransactionDataCSVFileConverter
context := core.NewNullContext()
user := &models.User{
@@ -475,12 +477,12 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_MissingFileHead
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(""), 0, 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)
}
func TestDefaultTransactionDataCSVFileConverterParseImportedData_MissingRequiredColumn(t *testing.T) {
converter := DefaultTransactionDataCSVFileConverter
importer := DefaultTransactionDataCSVFileConverter
context := core.NewNullContext()
user := &models.User{
@@ -489,32 +491,32 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_MissingRequired
}
// Missing Time Column
_, _, _, _, _, _, err := converter.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, nil, nil, nil, nil, nil)
_, _, _, _, _, _, 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,,,,,,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
// Missing Type Column
_, _, _, _, _, _, err = converter.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, nil, nil, nil, nil, nil)
_, _, _, _, _, _, 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,,,,,,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
// Missing Sub Category Column
_, _, _, _, _, _, err = converter.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, nil, nil, nil, nil, nil)
_, _, _, _, _, _, 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,,,,,,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
// Missing Account Name Column
_, _, _, _, _, _, err = converter.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, nil, nil, nil, nil, nil)
_, _, _, _, _, _, 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,,,,,,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
// Missing Amount Column
_, _, _, _, _, _, err = converter.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, nil, nil, nil, nil, nil)
_, _, _, _, _, _, 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,,,,,,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
// Missing Account2 Name Column
_, _, _, _, _, _, err = converter.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, nil, nil, nil, nil, nil)
_, _, _, _, _, _, 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,,,,,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
}
@@ -5,6 +5,7 @@ import (
"encoding/csv"
"io"
"strings"
"time"
"golang.org/x/text/encoding"
"golang.org/x/text/encoding/charmap"
@@ -33,8 +34,10 @@ var supportedFileTypeSeparators = map[string]rune{
var supportedFileEncodings = map[string]encoding.Encoding{
"utf-8": unicode.UTF8, // UTF-8
"utf-8-bom": unicode.UTF8BOM, // UTF-8 with BOM
"utf-16le": unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM), // UTF-16 Little Endian
"utf-16be": unicode.UTF16(unicode.BigEndian, unicode.IgnoreBOM), // UTF-16 Big Endian
"utf-16le": unicode.UTF16(unicode.LittleEndian, unicode.UseBOM), // UTF-16 Little Endian
"utf-16be": unicode.UTF16(unicode.BigEndian, unicode.UseBOM), // UTF-16 Big Endian
"utf-16le-bom": unicode.UTF16(unicode.LittleEndian, unicode.ExpectBOM), // UTF-16 Little Endian with BOM
"utf-16be-bom": unicode.UTF16(unicode.BigEndian, unicode.ExpectBOM), // UTF-16 Big Endian with BOM
"cp437": charmap.CodePage437, // OEM United States (CP-437)
"cp863": charmap.CodePage863, // OEM Canadian French (CP-863)
"cp037": charmap.CodePage037, // IBM EBCDIC US/Canada (CP-037)
@@ -146,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, 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 {
@@ -157,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, 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
@@ -2,9 +2,11 @@ package dsv
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
@@ -22,12 +24,12 @@ func TestIsDelimiterSeparatedValuesFileType(t *testing.T) {
}
func TestCustomTransactionDataDsvFileParser_ParseDsvFileLines(t *testing.T) {
converter, err := CreateNewCustomTransactionDataDsvFileParser("custom_csv", "utf-8")
importer, err := CreateNewCustomTransactionDataDsvFileParser("custom_csv", "utf-8")
assert.Nil(t, err)
context := core.NewNullContext()
allLines, err := converter.ParseDsvFileLines(context, []byte(
allLines, err := importer.ParseDsvFileLines(context, []byte(
"2024-09-01 00:00:00,B,123.45\n"+
"2024-09-01 01:23:45,I,0.12\n"))
assert.Nil(t, err)
@@ -44,10 +46,10 @@ func TestCustomTransactionDataDsvFileParser_ParseDsvFileLines(t *testing.T) {
assert.Equal(t, "I", allLines[1][1])
assert.Equal(t, "0.12", allLines[1][2])
converter, err = CreateNewCustomTransactionDataDsvFileParser("custom_tsv", "utf-8")
importer, err = CreateNewCustomTransactionDataDsvFileParser("custom_tsv", "utf-8")
assert.Nil(t, err)
allLines, err = converter.ParseDsvFileLines(context, []byte(
allLines, err = importer.ParseDsvFileLines(context, []byte(
"2024-09-01 12:34:56\tE\t1.00\n"+
"2024-09-01 23:59:59\tT\t0.05"))
assert.Nil(t, err)
@@ -77,7 +79,7 @@ func TestCustomTransactionDataDsvFileImporter_MinimumValidData(t *testing.T) {
"E": models.TRANSACTION_TYPE_EXPENSE,
"T": models.TRANSACTION_TYPE_TRANSFER,
}
converter, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", ".", "", "", "", "", "")
importer, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", ".", "", "", "", "", "")
assert.Nil(t, err)
context := core.NewNullContext()
@@ -87,11 +89,11 @@ func TestCustomTransactionDataDsvFileImporter_MinimumValidData(t *testing.T) {
DefaultCurrency: "CNY",
}
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := converter.ParseImportedData(context, user, []byte(
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := importer.ParseImportedData(context, user, []byte(
"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, 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)
@@ -168,7 +170,7 @@ func TestCustomTransactionDataDsvFileImporter_WithAllSupportedColumns(t *testing
"Expense": models.TRANSACTION_TYPE_EXPENSE,
"Transfer": models.TRANSACTION_TYPE_TRANSFER,
}
converter, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, true, "YYYY-MM-DD HH:mm:ss", "", ".", "", " ", "lonlat", ";")
importer, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, true, "YYYY-MM-DD HH:mm:ss", "", ".", "", " ", "lonlat", ";")
assert.Nil(t, err)
context := core.NewNullContext()
@@ -178,12 +180,12 @@ func TestCustomTransactionDataDsvFileImporter_WithAllSupportedColumns(t *testing
DefaultCurrency: "CNY",
}
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := converter.ParseImportedData(context, user, []byte(
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := importer.ParseImportedData(context, user, []byte(
"\"Time\",\"Timezone\",\"Type\",\"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\",\"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, 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)
@@ -261,7 +263,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidTime(t *testing.T) {
transactionTypeMapping := map[string]models.TransactionType{
"E": models.TRANSACTION_TYPE_EXPENSE,
}
converter, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ".", "", "", "", "")
importer, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ".", "", "", "", "")
assert.Nil(t, err)
context := core.NewNullContext()
@@ -271,12 +273,12 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidTime(t *testing.T) {
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
"2024-09-01T12:34:56,E,123.45"), 0, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"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 = converter.ParseImportedData(context, user, []byte(
"09/01/2024 12:34:56,E,123.45"), 0, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"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)
}
@@ -292,7 +294,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseTransactionWithoutType(t *tes
"E": models.TRANSACTION_TYPE_EXPENSE,
"T": models.TRANSACTION_TYPE_TRANSFER,
}
converter, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ".", "", "", "", "")
importer, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ".", "", "", "", "")
assert.Nil(t, err)
context := core.NewNullContext()
@@ -302,8 +304,8 @@ func TestCustomTransactionDataDsvFileImporter_ParseTransactionWithoutType(t *tes
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,A,123.45"), 0, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"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)
}
@@ -316,7 +318,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidType(t *testing.T) {
transactionTypeMapping := map[string]models.TransactionType{
"B": 0,
}
converter, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ".", "", "", "", "")
importer, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ".", "", "", "", "")
assert.Nil(t, err)
context := core.NewNullContext()
@@ -326,8 +328,8 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidType(t *testing.T) {
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,B,123.45"), 0, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"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)
}
@@ -340,7 +342,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseTimeWithTimezone(t *testing.T
transactionTypeMapping := map[string]models.TransactionType{
"E": models.TRANSACTION_TYPE_EXPENSE,
}
converter, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ssZ", "", ".", "", "", "", "")
importer, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ssZ", "", ".", "", "", "", "")
assert.Nil(t, err)
context := core.NewNullContext()
@@ -350,20 +352,20 @@ func TestCustomTransactionDataDsvFileImporter_ParseTimeWithTimezone(t *testing.T
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56-10:00,E,123.45"), 0, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"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 = converter.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56+00:00,E,123.45"), 0, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"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 = converter.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56+12:45,E,123.45"), 0, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"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))
@@ -378,7 +380,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseTimeWithTimezone2(t *testing.
transactionTypeMapping := map[string]models.TransactionType{
"E": models.TRANSACTION_TYPE_EXPENSE,
}
converter, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ssZZ", "", ".", "", "", "", "")
importer, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ssZZ", "", ".", "", "", "", "")
assert.Nil(t, err)
context := core.NewNullContext()
@@ -388,20 +390,20 @@ func TestCustomTransactionDataDsvFileImporter_ParseTimeWithTimezone2(t *testing.
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56-1000,E,123.45"), 0, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"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 = converter.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56+0000,E,123.45"), 0, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"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 = converter.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56+1245,E,123.45"), 0, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"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))
@@ -417,7 +419,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseValidTimezone(t *testing.T) {
transactionTypeMapping := map[string]models.TransactionType{
"E": models.TRANSACTION_TYPE_EXPENSE,
}
converter, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ".", "", "", "", "")
importer, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ".", "", "", "", "")
assert.Nil(t, err)
context := core.NewNullContext()
@@ -427,20 +429,20 @@ func TestCustomTransactionDataDsvFileImporter_ParseValidTimezone(t *testing.T) {
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,-10:00,E,123.45"), 0, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"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 = converter.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,+00:00,E,123.45"), 0, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"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 = converter.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,+12:45,E,123.45"), 0, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"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))
@@ -456,7 +458,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseValidTimezone2(t *testing.T)
transactionTypeMapping := map[string]models.TransactionType{
"E": models.TRANSACTION_TYPE_EXPENSE,
}
converter, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "ZZ", ".", "", "", "", "")
importer, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "ZZ", ".", "", "", "", "")
assert.Nil(t, err)
context := core.NewNullContext()
@@ -466,20 +468,20 @@ func TestCustomTransactionDataDsvFileImporter_ParseValidTimezone2(t *testing.T)
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,-1000,E,123.45"), 0, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"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 = converter.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,+0000,E,123.45"), 0, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"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 = converter.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,+1245,E,123.45"), 0, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"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))
@@ -495,7 +497,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidTimezoneFormat(t *test
transactionTypeMapping := map[string]models.TransactionType{
"E": models.TRANSACTION_TYPE_EXPENSE,
}
converter, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "z", ".", "", "", "", "")
importer, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "z", ".", "", "", "", "")
assert.Nil(t, err)
context := core.NewNullContext()
@@ -505,8 +507,8 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidTimezoneFormat(t *test
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,CST,E,123.45"), 0, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"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)
}
@@ -520,7 +522,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidTimezone(t *testing.T)
transactionTypeMapping := map[string]models.TransactionType{
"E": models.TRANSACTION_TYPE_EXPENSE,
}
converter, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ".", "", "", "", "")
importer, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ".", "", "", "", "")
assert.Nil(t, err)
context := core.NewNullContext()
@@ -530,12 +532,12 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidTimezone(t *testing.T)
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,Asia/Shanghai,E,123.45"), 0, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"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 = converter.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,-0700,E,123.45"), 0, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"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)
}
@@ -549,7 +551,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidTimezone2(t *testing.T
transactionTypeMapping := map[string]models.TransactionType{
"E": models.TRANSACTION_TYPE_EXPENSE,
}
converter, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "ZZ", ".", "", "", "", "")
importer, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "ZZ", ".", "", "", "", "")
assert.Nil(t, err)
context := core.NewNullContext()
@@ -559,12 +561,12 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidTimezone2(t *testing.T
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,Asia/Shanghai,E,123.45"), 0, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"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 = converter.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,0700,E,123.45"), 0, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"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)
}
@@ -577,7 +579,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseAmountWithCustomFormat(t *tes
transactionTypeMapping := map[string]models.TransactionType{
"E": models.TRANSACTION_TYPE_EXPENSE,
}
converter, err := CreateNewCustomTransactionDataDsvFileImporter("custom_tsv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ",", ".", "", "", "")
importer, err := CreateNewCustomTransactionDataDsvFileImporter("custom_tsv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ",", ".", "", "", "")
assert.Nil(t, err)
context := core.NewNullContext()
@@ -587,8 +589,8 @@ func TestCustomTransactionDataDsvFileImporter_ParseAmountWithCustomFormat(t *tes
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56\tE\t1.234,56"), 0, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"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)
@@ -603,7 +605,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidAmountWithCustomFormat
transactionTypeMapping := map[string]models.TransactionType{
"E": models.TRANSACTION_TYPE_EXPENSE,
}
converter, err := CreateNewCustomTransactionDataDsvFileImporter("custom_tsv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ".", ",", "", "", "")
importer, err := CreateNewCustomTransactionDataDsvFileImporter("custom_tsv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ".", ",", "", "", "")
assert.Nil(t, err)
context := core.NewNullContext()
@@ -613,8 +615,8 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidAmountWithCustomFormat
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56\tE\t1.234,56"), 0, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"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)
}
@@ -627,7 +629,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidAmountWithCustomFormat
transactionTypeMapping := map[string]models.TransactionType{
"E": models.TRANSACTION_TYPE_EXPENSE,
}
converter, err := CreateNewCustomTransactionDataDsvFileImporter("custom_tsv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ",", "", "", "", "")
importer, err := CreateNewCustomTransactionDataDsvFileImporter("custom_tsv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ",", "", "", "", "")
assert.Nil(t, err)
context := core.NewNullContext()
@@ -637,8 +639,8 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidAmountWithCustomFormat
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56\tE\t1.234,56"), 0, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"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)
}
@@ -655,7 +657,7 @@ func TestCustomTransactionDataDsvFileImporter_ParsePrimaryCategory(t *testing.T)
"E": models.TRANSACTION_TYPE_EXPENSE,
"T": models.TRANSACTION_TYPE_TRANSFER,
}
converter, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ".", "", "", "", "")
importer, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ".", "", "", "", "")
assert.Nil(t, err)
context := core.NewNullContext()
@@ -665,11 +667,11 @@ func TestCustomTransactionDataDsvFileImporter_ParsePrimaryCategory(t *testing.T)
DefaultCurrency: "CNY",
}
allNewTransactions, _, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, _, err := converter.ParseImportedData(context, user, []byte(
allNewTransactions, _, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, _, err := importer.ParseImportedData(context, user, []byte(
"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, 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)
@@ -724,7 +726,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseValidAccountCurrency(t *testi
"B": models.TRANSACTION_TYPE_MODIFY_BALANCE,
"T": models.TRANSACTION_TYPE_TRANSFER,
}
converter, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ".", "", "", "", "")
importer, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ".", "", "", "", "")
assert.Nil(t, err)
context := core.NewNullContext()
@@ -734,9 +736,9 @@ func TestCustomTransactionDataDsvFileImporter_ParseValidAccountCurrency(t *testi
DefaultCurrency: "CNY",
}
allNewTransactions, allNewAccounts, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
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, 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)
@@ -767,7 +769,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidAccountCurrency(t *tes
"B": models.TRANSACTION_TYPE_MODIFY_BALANCE,
"T": models.TRANSACTION_TYPE_TRANSFER,
}
converter, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ".", "", "", "", "")
importer, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ".", "", "", "", "")
assert.Nil(t, err)
context := core.NewNullContext()
@@ -777,14 +779,14 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidAccountCurrency(t *tes
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, 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, 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 = converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, 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, 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)
}
@@ -803,7 +805,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseNotSupportedCurrency(t *testi
"B": models.TRANSACTION_TYPE_MODIFY_BALANCE,
"T": models.TRANSACTION_TYPE_TRANSFER,
}
converter, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ".", "", "", "", "")
importer, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ".", "", "", "", "")
assert.Nil(t, err)
context := core.NewNullContext()
@@ -813,12 +815,12 @@ func TestCustomTransactionDataDsvFileImporter_ParseNotSupportedCurrency(t *testi
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
"2024-09-01 01:23:45,B,Test Account,XXX,123.45,,,"), 0, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"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 = converter.ParseImportedData(context, user, []byte(
"2024-09-01 01:23:45,T,Test Account,USD,123.45,Test Account2,XXX,123.45"), 0, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"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)
}
@@ -835,7 +837,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseValidAmount(t *testing.T) {
"E": models.TRANSACTION_TYPE_EXPENSE,
"T": models.TRANSACTION_TYPE_TRANSFER,
}
converter, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ".", "", "", "", "")
importer, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ".", "", "", "", "")
assert.Nil(t, err)
context := core.NewNullContext()
@@ -845,11 +847,11 @@ func TestCustomTransactionDataDsvFileImporter_ParseValidAmount(t *testing.T) {
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"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, 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)
@@ -873,6 +875,62 @@ func TestCustomTransactionDataDsvFileImporter_ParseValidAmount(t *testing.T) {
assert.Equal(t, int64(35), allNewTransactions[3].RelatedAccountAmount)
}
func TestCustomTransactionDataDsvFileImporter_ParseAmountWithSpaceDigitGroupingSymbol(t *testing.T) {
columnIndexMapping := map[datatable.TransactionDataTableColumn]int{
datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME: 0,
datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE: 1,
datatable.TRANSACTION_DATA_TABLE_AMOUNT: 2,
}
transactionTypeMapping := map[string]models.TransactionType{
"E": models.TRANSACTION_TYPE_EXPENSE,
}
importer, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ".", " ", "", "", "")
assert.Nil(t, err)
context := core.NewNullContext()
user := &models.User{
Uid: 1234567890,
DefaultCurrency: "CNY",
}
// normal space
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"2024-09-01 00:00:00,E,1 234,\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, int64(123400), allNewTransactions[0].Amount)
// no-break space (NBSP)
allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"2024-09-01 00:00:00,E,1 234,\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, int64(123400), allNewTransactions[0].Amount)
// narrow no-break space (NNBSP)
allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"2024-09-01 00:00:00,E,1234,\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, int64(123400), allNewTransactions[0].Amount)
// figure space
allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"2024-09-01 00:00:00,E,1234,\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, int64(123400), allNewTransactions[0].Amount)
}
func TestCustomTransactionDataDsvFileImporter_ParseInvalidAmount(t *testing.T) {
columnIndexMapping := map[datatable.TransactionDataTableColumn]int{
datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME: 0,
@@ -886,7 +944,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidAmount(t *testing.T) {
"E": models.TRANSACTION_TYPE_EXPENSE,
"T": models.TRANSACTION_TYPE_TRANSFER,
}
converter, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ".", "", "", "", "")
importer, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ".", "", "", "", "")
assert.Nil(t, err)
context := core.NewNullContext()
@@ -896,12 +954,12 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidAmount(t *testing.T) {
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,E,Test Account,123 45,,"), 0, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"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 = converter.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,T,Test Account,123.45,Test Account2,123 45"), 0, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"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)
}
@@ -917,7 +975,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseNoAmount2(t *testing.T) {
"E": models.TRANSACTION_TYPE_EXPENSE,
"T": models.TRANSACTION_TYPE_TRANSFER,
}
converter, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ".", "", "", "", "")
importer, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ".", "", "", "", "")
assert.Nil(t, err)
context := core.NewNullContext()
@@ -927,15 +985,15 @@ func TestCustomTransactionDataDsvFileImporter_ParseNoAmount2(t *testing.T) {
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,E,Test Account,123.45,"), 0, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"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 = converter.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,T,Test Account,123.45,Test Account2"), 0, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"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)
@@ -952,7 +1010,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseValidGeographicLocation(t *te
transactionTypeMapping := map[string]models.TransactionType{
"E": models.TRANSACTION_TYPE_EXPENSE,
}
converter, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ".", "", ";", "lonlat", "")
importer, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ".", "", ";", "lonlat", "")
assert.Nil(t, err)
context := core.NewNullContext()
@@ -962,8 +1020,8 @@ func TestCustomTransactionDataDsvFileImporter_ParseValidGeographicLocation(t *te
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,E,123.45,123.45;45.56"), 0, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"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))
@@ -981,7 +1039,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidGeographicLocation(t *
transactionTypeMapping := map[string]models.TransactionType{
"E": models.TRANSACTION_TYPE_EXPENSE,
}
converter, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ".", "", " ", "lonlat", "")
importer, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ".", "", " ", "lonlat", "")
assert.Nil(t, err)
context := core.NewNullContext()
@@ -991,15 +1049,15 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidGeographicLocation(t *
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,E,123.45,,,1"), 0, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"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 = converter.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,E,123.45,a b"), 0, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"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)
}
@@ -1013,7 +1071,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseTag(t *testing.T) {
transactionTypeMapping := map[string]models.TransactionType{
"E": models.TRANSACTION_TYPE_EXPENSE,
}
converter, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ".", "", "", "", ";")
importer, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ".", "", "", "", ";")
assert.Nil(t, err)
context := core.NewNullContext()
@@ -1023,8 +1081,8 @@ func TestCustomTransactionDataDsvFileImporter_ParseTag(t *testing.T) {
DefaultCurrency: "CNY",
}
_, _, _, _, _, allNewTags, err := converter.ParseImportedData(context, user, []byte(
"2024-09-01 00:00:00,E,123.45,foo;;bar.;#test;hello\tworld;;"), 0, nil, nil, nil, nil, nil)
_, _, _, _, _, allNewTags, err := importer.ParseImportedData(context, user, []byte(
"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)
@@ -1053,7 +1111,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseTagWithoutSeparator(t *testin
transactionTypeMapping := map[string]models.TransactionType{
"E": models.TRANSACTION_TYPE_EXPENSE,
}
converter, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ".", "", "", "", "")
importer, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ".", "", "", "", "")
assert.Nil(t, err)
context := core.NewNullContext()
@@ -1063,8 +1121,8 @@ func TestCustomTransactionDataDsvFileImporter_ParseTagWithoutSeparator(t *testin
DefaultCurrency: "CNY",
}
_, _, _, _, _, allNewTags, err := converter.ParseImportedData(context, user, []byte(
"2024-09-01 00:00:00,E,123.45,foo;;bar.;#test;hello\tworld;;"), 0, nil, nil, nil, nil, nil)
_, _, _, _, _, allNewTags, err := importer.ParseImportedData(context, user, []byte(
"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)
@@ -1084,7 +1142,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseDescription(t *testing.T) {
transactionTypeMapping := map[string]models.TransactionType{
"T": models.TRANSACTION_TYPE_TRANSFER,
}
converter, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ".", "", "", "", "")
importer, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ".", "", "", "", "")
assert.Nil(t, err)
context := core.NewNullContext()
@@ -1094,8 +1152,8 @@ func TestCustomTransactionDataDsvFileImporter_ParseDescription(t *testing.T) {
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,T,123.45,foo bar\t#test"), 0, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"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))
@@ -107,6 +107,7 @@ func (t *customPlainTextDataRowIterator) Next(ctx core.Context, user *models.Use
func (t *customPlainTextDataRowIterator) parseTransaction(ctx core.Context, user *models.User, row datatable.BasicDataTableRow) (map[datatable.TransactionDataTableColumn]string, bool, error) {
rowData := make(map[datatable.TransactionDataTableColumn]string, len(t.transactionDataTable.columnIndexMapping))
var transactionTime *time.Time = nil
for column, columnIndex := range t.transactionDataTable.columnIndexMapping {
if columnIndex < 0 || columnIndex >= row.ColumnCount() {
@@ -144,10 +145,11 @@ func (t *customPlainTextDataRowIterator) parseTransaction(ctx core.Context, user
return nil, false, errs.ErrTransactionTimeInvalid
}
transactionTime = &dateTime
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())
}
}
@@ -164,6 +166,19 @@ func (t *customPlainTextDataRowIterator) parseTransaction(ctx core.Context, user
timezone = timezone[:3] + ":" + timezone[3:]
rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE] = timezone
} else if t.transactionDataTable.timezoneFormat == "zzz" { // IANA Timezone Name
timezoneName := rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE]
timezone, err := time.LoadLocation(timezoneName)
if err != nil {
return nil, false, errs.ErrTransactionTimeZoneInvalid
}
if transactionTime == nil {
return nil, false, errs.ErrTransactionTimeInvalid
}
rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE] = utils.FormatTimezoneOffset(transactionTime.Unix(), timezone)
} else {
return nil, false, errs.ErrImportFileTransactionTimezoneFormatInvalid
}
@@ -215,6 +230,12 @@ func (t *customPlainTextDataRowIterator) parseTransaction(ctx core.Context, user
func (t *customPlainTextDataRowIterator) parseAmount(ctx core.Context, amountValue string) (string, error) {
if t.transactionDataTable.amountDigitGroupingSymbol != "" {
amountValue = strings.ReplaceAll(amountValue, t.transactionDataTable.amountDigitGroupingSymbol, "")
if t.transactionDataTable.amountDigitGroupingSymbol == " " {
amountValue = strings.ReplaceAll(amountValue, "\u00A0", "") // No-Break Space (NBSP)
amountValue = strings.ReplaceAll(amountValue, "\u202F", "") // Narrow No-Break Space (NNBSP)
amountValue = strings.ReplaceAll(amountValue, "\u2007", "") // Figure Space
}
}
if t.transactionDataTable.amountDecimalSeparator != "" && t.transactionDataTable.amountDecimalSeparator != "." {
@@ -3,6 +3,7 @@ package feidee
import (
"bytes"
"strings"
"time"
"golang.org/x/text/encoding/unicode"
"golang.org/x/text/transform"
@@ -27,6 +28,9 @@ const feideeMymoneyAppTransactionAccountCurrencyColumnName = "账户币种"
const feideeMymoneyAppTransactionAmountColumnName = "金额"
const feideeMymoneyAppTransactionDescriptionColumnName = "备注"
const feideeMymoneyAppTransactionRelatedIdColumnName = "关联Id"
const feideeMymoneyAppTransactionMemberColumnName = "成员"
const feideeMymoneyAppTransactionProjectColumnName = "项目"
const feideeMymoneyAppTransactionMerchantColumnName = "商家"
const feideeMymoneyAppTransactionTypeModifyBalanceText = "余额变更"
const feideeMymoneyAppTransactionTypeModifyOutstandingBalanceText = "负债变更"
@@ -44,6 +48,9 @@ var feideeMymoneyAppDataColumnNameMapping = map[datatable.TransactionDataTableCo
datatable.TRANSACTION_DATA_TABLE_ACCOUNT_CURRENCY: feideeMymoneyAppTransactionAccountCurrencyColumnName,
datatable.TRANSACTION_DATA_TABLE_AMOUNT: feideeMymoneyAppTransactionAmountColumnName,
datatable.TRANSACTION_DATA_TABLE_DESCRIPTION: feideeMymoneyAppTransactionDescriptionColumnName,
datatable.TRANSACTION_DATA_TABLE_MEMBER: feideeMymoneyAppTransactionMemberColumnName,
datatable.TRANSACTION_DATA_TABLE_PROJECT: feideeMymoneyAppTransactionProjectColumnName,
datatable.TRANSACTION_DATA_TABLE_MERCHANT: feideeMymoneyAppTransactionMerchantColumnName,
}
// feideeMymoneyAppTransactionDataCsvFileImporter defines the structure of feidee mymoney app csv importer for transaction data
@@ -55,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, 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))
@@ -91,7 +98,7 @@ func (c *feideeMymoneyAppTransactionDataCsvFileImporter) ParseImportedData(ctx c
dataTableImporter := converter.CreateNewSimpleImporterWithTypeNameMapping(feideeMymoneyTransactionTypeNameMapping)
return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezoneOffset, 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) {
@@ -123,6 +130,18 @@ func (c *feideeMymoneyAppTransactionDataCsvFileImporter) createNewFeideeMymoneyA
newColumns = append(newColumns, datatable.TRANSACTION_DATA_TABLE_DESCRIPTION)
}
if commonDataTable.HasColumn(feideeMymoneyAppTransactionMemberColumnName) {
newColumns = append(newColumns, datatable.TRANSACTION_DATA_TABLE_MEMBER)
}
if commonDataTable.HasColumn(feideeMymoneyAppTransactionProjectColumnName) {
newColumns = append(newColumns, datatable.TRANSACTION_DATA_TABLE_PROJECT)
}
if commonDataTable.HasColumn(feideeMymoneyAppTransactionMerchantColumnName) {
newColumns = append(newColumns, datatable.TRANSACTION_DATA_TABLE_MERCHANT)
}
transactionRowParser := createFeideeMymoneyTransactionDataRowParser()
transactionDataTable := datatable.CreateNewWritableTransactionDataTableWithRowParser(newColumns, transactionRowParser)
transferTransactionsMap := make(map[string]map[datatable.TransactionDataTableColumn]string, 0)
@@ -6,6 +6,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/models"
@@ -13,7 +14,7 @@ import (
)
func TestFeideeMymoneyCsvFileImporterParseImportedData_MinimumValidData(t *testing.T) {
converter := FeideeMymoneyAppTransactionDataCsvFileImporter
importer := FeideeMymoneyAppTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -21,7 +22,7 @@ func TestFeideeMymoneyCsvFileImporterParseImportedData_MinimumValidData(t *testi
DefaultCurrency: "CNY",
}
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, 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"+
@@ -30,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, 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)
@@ -110,7 +111,7 @@ func TestFeideeMymoneyCsvFileImporterParseImportedData_MinimumValidData(t *testi
}
func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseOutstandingBalanceModification(t *testing.T) {
converter := FeideeMymoneyAppTransactionDataCsvFileImporter
importer := FeideeMymoneyAppTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -118,10 +119,10 @@ func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseOutstandingBalanceMo
DefaultCurrency: "CNY",
}
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, _, _, err := converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
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, 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)
@@ -160,7 +161,7 @@ func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseOutstandingBalanceMo
}
func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseInvalidTime(t *testing.T) {
converter := FeideeMymoneyAppTransactionDataCsvFileImporter
importer := FeideeMymoneyAppTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -168,19 +169,19 @@ func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseInvalidTime(t *testi
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
"\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+
"\"收入\",\"2024-09-01T12:34:56\",\"Test Category\",\"Test Account\",\"0.12\",\"\",\"\""), 0, 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 = converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
_, _, _, _, _, _, 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, 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)
}
func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseInvalidType(t *testing.T) {
converter := FeideeMymoneyAppTransactionDataCsvFileImporter
importer := FeideeMymoneyAppTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -188,14 +189,14 @@ func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseInvalidType(t *testi
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
_, _, _, _, _, _, 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, 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)
}
func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseValidAccountCurrency(t *testing.T) {
converter := FeideeMymoneyAppTransactionDataCsvFileImporter
importer := FeideeMymoneyAppTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -203,11 +204,11 @@ func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseValidAccountCurrency
DefaultCurrency: "CNY",
}
allNewTransactions, allNewAccounts, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
allNewTransactions, allNewAccounts, _, _, _, _, 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 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, 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)
@@ -224,7 +225,7 @@ func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseValidAccountCurrency
}
func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseInvalidAccountCurrency(t *testing.T) {
converter := FeideeMymoneyAppTransactionDataCsvFileImporter
importer := FeideeMymoneyAppTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -232,23 +233,23 @@ func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseInvalidAccountCurren
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
_, _, _, _, _, _, 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 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, 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 = converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
_, _, _, _, _, _, 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, 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)
}
func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseNotSupportedCurrency(t *testing.T) {
converter := FeideeMymoneyAppTransactionDataCsvFileImporter
importer := FeideeMymoneyAppTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -256,26 +257,26 @@ func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseNotSupportedCurrency
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
"\"交易类型\",\"日期\",\"子类别\",\"账户\",\"账户币种\",\"金额\",\"备注\",\"关联Id\"\n"+
"\"余额变更\",\"2024-09-01 01:23:45\",\"\",\"Test Account\",\"XXX\",\"123.45\",\"\",\"\""), 0, 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 = converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
_, _, _, _, _, _, 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, 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 = converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
_, _, _, _, _, _, 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, 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)
}
func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseInvalidAmount(t *testing.T) {
converter := FeideeMymoneyAppTransactionDataCsvFileImporter
importer := FeideeMymoneyAppTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -283,31 +284,31 @@ func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseInvalidAmount(t *tes
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
"\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+
"\"余额变更\",\"2024-09-01 01:23:45\",\"\",\"Test Account\",\"123 45\",\"\",\"\""), 0, 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 = converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
"\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+
"\"负债变更\",\"2024-09-01 01:23:45\",\"\",\"Test Account\",\"123 45\",\"\",\"\""), 0, 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 = converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
_, _, _, _, _, _, 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, 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 = converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
_, _, _, _, _, _, 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, 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)
}
func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseDescription(t *testing.T) {
converter := FeideeMymoneyAppTransactionDataCsvFileImporter
importer := FeideeMymoneyAppTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -315,18 +316,18 @@ func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseDescription(t *testi
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
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, 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))
assert.Equal(t, "Test\nA new line break", allNewTransactions[0].Comment)
}
func TestFeideeMymoneyCsvFileImporterParseImportedData_InvalidRelatedId(t *testing.T) {
converter := FeideeMymoneyAppTransactionDataCsvFileImporter
func TestFeideeMymoneyCsvFileImporterParseImportedData_WithAdditionalOptions(t *testing.T) {
importer := FeideeMymoneyAppTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -334,41 +335,70 @@ func TestFeideeMymoneyCsvFileImporterParseImportedData_InvalidRelatedId(t *testi
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
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\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, 0, len(allNewTransactions[0].OriginalTagNames))
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\""), time.UTC, converter.DefaultImporterOptions.WithMemberAsTag().WithProjectAsTag().WithMerchantAsTag(), nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, 3, len(allNewTransactions[0].OriginalTagNames))
assert.Contains(t, allNewTransactions[0].OriginalTagNames, "test1")
assert.Contains(t, allNewTransactions[0].OriginalTagNames, "test2")
assert.Contains(t, allNewTransactions[0].OriginalTagNames, "test3")
}
func TestFeideeMymoneyCsvFileImporterParseImportedData_InvalidRelatedId(t *testing.T) {
importer := FeideeMymoneyAppTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
Uid: 1234567890,
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, 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, 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 = converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
_, _, _, _, _, _, 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, 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 = converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
_, _, _, _, _, _, 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, 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)
}
func TestFeideeMymoneyCsvFileImporterParseImportedData_MissingFileHeader(t *testing.T) {
converter := FeideeMymoneyAppTransactionDataCsvFileImporter
importer := FeideeMymoneyAppTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
Uid: 1,
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+
"\"余额变更\",\"2024-09-01 00:00:00\",\"\",\"Test Account\",\"123.45\",\"\",\"\"\n"), 0, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+
"\"余额变更\",\"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 = converter.ParseImportedData(context, user, []byte(""), 0, 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)
}
func TestFeideeMymoneyCsvFileImporterParseImportedData_MissingRequiredColumn(t *testing.T) {
converter := FeideeMymoneyAppTransactionDataCsvFileImporter
importer := FeideeMymoneyAppTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -377,38 +407,38 @@ func TestFeideeMymoneyCsvFileImporterParseImportedData_MissingRequiredColumn(t *
}
// Missing Time Column
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
"\"交易类型\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+
"\"余额变更\",\"\",\"Test Account\",\"123.45\",\"\",\"\"\n"), 0, 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 = converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
_, _, _, _, _, _, 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, 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 = converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
"\"交易类型\",\"日期\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+
"\"余额变更\",\"2024-09-01 00:00:00\",\"Test Account\",\"123.45\",\"\",\"\"\n"), 0, 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 = converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
"\"交易类型\",\"日期\",\"子类别\",\"金额\",\"备注\",\"关联Id\"\n"+
"\"余额变更\",\"2024-09-01 00:00:00\",\"\",\"123.45\",\"\",\"\"\n"), 0, 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 = converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
"\"交易类型\",\"日期\",\"子类别\",\"账户\",\"备注\",\"关联Id\"\n"+
"\"余额变更\",\"2024-09-01 00:00:00\",\"\",\"Test Account\",\"\",\"\"\n"), 0, 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 = converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
"\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\"\n"+
"\"余额变更\",\"2024-09-01 00:00:00\",\"\",\"Test Account\",\"123.45\",\"\"\n"), 0, 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)
}
@@ -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"
@@ -18,6 +20,9 @@ var feideeMymoneyElecloudDataColumnNameMapping = map[datatable.TransactionDataTa
datatable.TRANSACTION_DATA_TABLE_AMOUNT: "金额",
datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME: "账户2",
datatable.TRANSACTION_DATA_TABLE_DESCRIPTION: "备注",
datatable.TRANSACTION_DATA_TABLE_MEMBER: "成员",
datatable.TRANSACTION_DATA_TABLE_PROJECT: "项目",
datatable.TRANSACTION_DATA_TABLE_MERCHANT: "商家",
}
// feideeMymoneyElecloudTransactionDataXlsxFileImporter defines the structure of feidee mymoney (elecloud) xlsx importer for transaction data
@@ -31,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, 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 {
@@ -42,5 +47,5 @@ func (c *feideeMymoneyElecloudTransactionDataXlsxFileImporter) ParseImportedData
transactionDataTable := datatable.CreateNewTransactionDataTableFromBasicDataTableWithRowParser(dataTable, feideeMymoneyElecloudDataColumnNameMapping, transactionRowParser)
dataTableImporter := converter.CreateNewSimpleImporter(feideeMymoneyElecloudTransactionTypeNameMapping)
return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezoneOffset, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezone, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
}
@@ -7,13 +7,14 @@ import (
"github.com/stretchr/testify/assert"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
func TestFeideeMymoneyElecloudTransactionDataXlsxImporterParseImportedData_MinimumValidData(t *testing.T) {
converter := FeideeMymoneyElecloudTransactionDataXlsxFileImporter
importer := FeideeMymoneyElecloudTransactionDataXlsxFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -24,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 := converter.ParseImportedData(context, user, testdata, 0, 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))
@@ -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"
@@ -17,6 +19,9 @@ var feideeMymoneyWebDataColumnNameMapping = map[datatable.TransactionDataTableCo
datatable.TRANSACTION_DATA_TABLE_AMOUNT: "金额",
datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME: "账户2",
datatable.TRANSACTION_DATA_TABLE_DESCRIPTION: "备注",
datatable.TRANSACTION_DATA_TABLE_MEMBER: "成员",
datatable.TRANSACTION_DATA_TABLE_PROJECT: "项目",
datatable.TRANSACTION_DATA_TABLE_MERCHANT: "商家",
}
// feideeMymoneyWebTransactionDataXlsFileImporter defines the structure of feidee mymoney (web) xls importer for transaction data
@@ -30,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, 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 {
@@ -41,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, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezone, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
}
@@ -7,13 +7,14 @@ import (
"github.com/stretchr/testify/assert"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
func TestFeideeMymoneyTransactionDataXlsImporterParseImportedData_MinimumValidData(t *testing.T) {
converter := FeideeMymoneyWebTransactionDataXlsFileImporter
importer := FeideeMymoneyWebTransactionDataXlsFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -24,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 := converter.ParseImportedData(context, user, testdata, 0, 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))
@@ -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, 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, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezone, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
}
@@ -2,17 +2,19 @@ package fireflyIII
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
func TestFireFlyIIICsvFileConverterParseImportedData_MinimumValidData(t *testing.T) {
converter := FireflyIIITransactionDataCsvFileImporter
func TestFireFlyIIICsvFileimporterParseImportedData_MinimumValidData(t *testing.T) {
importer := FireflyIIITransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -20,11 +22,11 @@ func TestFireFlyIIICsvFileConverterParseImportedData_MinimumValidData(t *testing
DefaultCurrency: "CNY",
}
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := converter.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name,category\n"+
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := importer.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name,category\n"+
"\"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, 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)
@@ -82,8 +84,8 @@ func TestFireFlyIIICsvFileConverterParseImportedData_MinimumValidData(t *testing
assert.Equal(t, "Test Category3", allNewSubTransferCategories[0].Name)
}
func TestFireFlyIIICsvFileConverterParseImportedData_ParseInvalidTime(t *testing.T) {
converter := FireflyIIITransactionDataCsvFileImporter
func TestFireFlyIIICsvFileimporterParseImportedData_ParseInvalidTime(t *testing.T) {
importer := FireflyIIITransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -91,17 +93,17 @@ func TestFireFlyIIICsvFileConverterParseImportedData_ParseInvalidTime(t *testing
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := converter.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, nil, nil, nil, nil, nil)
_, _, _, _, _, _, 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\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
_, _, _, _, _, _, err = converter.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, nil, nil, nil, nil, nil)
_, _, _, _, _, _, 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\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
}
func TestFireFlyIIICsvFileConverterParseImportedData_ParseInvalidType(t *testing.T) {
converter := FireflyIIITransactionDataCsvFileImporter
func TestFireFlyIIICsvFileimporterParseImportedData_ParseInvalidType(t *testing.T) {
importer := FireflyIIITransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -109,13 +111,13 @@ func TestFireFlyIIICsvFileConverterParseImportedData_ParseInvalidType(t *testing
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := converter.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, nil, nil, nil, nil, nil)
_, _, _, _, _, _, 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\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrTransactionTypeInvalid.Message)
}
func TestFireFlyIIICsvFileConverterParseImportedData_ParseAccountNameAsCategoryName(t *testing.T) {
converter := FireflyIIITransactionDataCsvFileImporter
func TestFireFlyIIICsvFileimporterParseImportedData_ParseAccountNameAsCategoryName(t *testing.T) {
importer := FireflyIIITransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -123,23 +125,23 @@ func TestFireFlyIIICsvFileConverterParseImportedData_ParseAccountNameAsCategoryN
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := converter.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, nil, nil, nil, nil, nil)
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\",\"\""), 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 = converter.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, nil, nil, nil, nil, nil)
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\",\"\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, "A revenue account", allNewTransactions[0].OriginalCategoryName)
}
func TestFireFlyIIICsvFileConverterParseImportedData_ParseValidTimezone(t *testing.T) {
converter := FireflyIIITransactionDataCsvFileImporter
func TestFireFlyIIICsvFileimporterParseImportedData_ParseValidTimezone(t *testing.T) {
importer := FireflyIIITransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -147,27 +149,27 @@ func TestFireFlyIIICsvFileConverterParseImportedData_ParseValidTimezone(t *testi
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := converter.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, nil, nil, nil, nil, nil)
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\""), 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 = converter.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, nil, nil, nil, nil, nil)
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\""), 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 = converter.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, nil, nil, nil, nil, nil)
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\""), 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))
}
func TestFireFlyIIICsvFileConverterParseImportedData_ParseValidAccountCurrency(t *testing.T) {
converter := FireflyIIITransactionDataCsvFileImporter
func TestFireFlyIIICsvFileimporterParseImportedData_ParseValidAccountCurrency(t *testing.T) {
importer := FireflyIIITransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -175,9 +177,9 @@ func TestFireFlyIIICsvFileConverterParseImportedData_ParseValidAccountCurrency(t
DefaultCurrency: "CNY",
}
allNewTransactions, allNewAccounts, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("type,amount,foreign_amount,date,currency_code,foreign_currency_code,source_name,destination_name,category\n"+
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, 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)
@@ -193,8 +195,8 @@ func TestFireFlyIIICsvFileConverterParseImportedData_ParseValidAccountCurrency(t
assert.Equal(t, "EUR", allNewAccounts[1].Currency)
}
func TestFireFlyIIICsvFileConverterParseImportedData_ParseValidForeignAmountAndCurrency(t *testing.T) {
converter := FireflyIIITransactionDataCsvFileImporter
func TestFireFlyIIICsvFileimporterParseImportedData_ParseValidForeignAmountAndCurrency(t *testing.T) {
importer := FireflyIIITransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -202,8 +204,8 @@ func TestFireFlyIIICsvFileConverterParseImportedData_ParseValidForeignAmountAndC
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := converter.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, nil, nil, nil, nil, nil)
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\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
@@ -213,8 +215,8 @@ func TestFireFlyIIICsvFileConverterParseImportedData_ParseValidForeignAmountAndC
assert.Equal(t, "USD", allNewTransactions[0].OriginalSourceAccountCurrency)
assert.Equal(t, "EUR", allNewTransactions[0].OriginalDestinationAccountCurrency)
allNewTransactions, _, _, _, _, _, err = converter.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, nil, nil, nil, nil, nil)
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\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
@@ -223,8 +225,8 @@ func TestFireFlyIIICsvFileConverterParseImportedData_ParseValidForeignAmountAndC
assert.Equal(t, "USD", allNewTransactions[0].OriginalSourceAccountCurrency)
assert.Equal(t, "EUR", allNewTransactions[0].OriginalDestinationAccountCurrency)
allNewTransactions, _, _, _, _, _, err = converter.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, nil, nil, nil, nil, nil)
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\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
@@ -232,8 +234,8 @@ func TestFireFlyIIICsvFileConverterParseImportedData_ParseValidForeignAmountAndC
assert.Equal(t, "USD", allNewTransactions[0].OriginalDestinationAccountCurrency)
}
func TestFireFlyIIICsvFileConverterParseImportedData_ParseInvalidAccountCurrency(t *testing.T) {
converter := FireflyIIITransactionDataCsvFileImporter
func TestFireFlyIIICsvFileimporterParseImportedData_ParseInvalidAccountCurrency(t *testing.T) {
importer := FireflyIIITransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -241,19 +243,19 @@ func TestFireFlyIIICsvFileConverterParseImportedData_ParseInvalidAccountCurrency
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("type,amount,foreign_amount,date,currency_code,foreign_currency_code,source_name,destination_name,category\n"+
_, _, _, _, _, _, 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, 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 = converter.ParseImportedData(context, user, []byte("type,amount,foreign_amount,date,currency_code,foreign_currency_code,source_name,destination_name,category\n"+
_, _, _, _, _, _, 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, 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)
}
func TestFireFlyIIICsvFileConverterParseImportedData_ParseNotSupportedCurrency(t *testing.T) {
converter := FireflyIIITransactionDataCsvFileImporter
func TestFireFlyIIICsvFileimporterParseImportedData_ParseNotSupportedCurrency(t *testing.T) {
importer := FireflyIIITransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -261,17 +263,17 @@ func TestFireFlyIIICsvFileConverterParseImportedData_ParseNotSupportedCurrency(t
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := converter.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, nil, nil, nil, nil, nil)
_, _, _, _, _, _, 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"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message)
_, _, _, _, _, _, err = converter.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, nil, nil, nil, nil, nil)
_, _, _, _, _, _, 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\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message)
}
func TestFireFlyIIICsvFileConverterParseImportedData_ParseInvalidAmount(t *testing.T) {
converter := FireflyIIITransactionDataCsvFileImporter
func TestFireFlyIIICsvFileimporterParseImportedData_ParseInvalidAmount(t *testing.T) {
importer := FireflyIIITransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -279,17 +281,17 @@ func TestFireFlyIIICsvFileConverterParseImportedData_ParseInvalidAmount(t *testi
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := converter.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, nil, nil, nil, nil, nil)
_, _, _, _, _, _, 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"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
_, _, _, _, _, _, err = converter.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, nil, nil, nil, nil, nil)
_, _, _, _, _, _, 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\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
}
func TestFireFlyIIICsvFileConverterParseImportedData_ParseDescription(t *testing.T) {
converter := FireflyIIITransactionDataCsvFileImporter
func TestFireFlyIIICsvFileimporterParseImportedData_ParseDescription(t *testing.T) {
importer := FireflyIIITransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -297,16 +299,16 @@ func TestFireFlyIIICsvFileConverterParseImportedData_ParseDescription(t *testing
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := converter.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, nil, nil, nil, nil, nil)
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"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, "foo bar\t#test", allNewTransactions[0].Comment)
}
func TestFireFlyIIICsvFileConverterParseImportedData_ParseTags(t *testing.T) {
converter := FireflyIIITransactionDataCsvFileImporter
func TestFireFlyIIICsvFileimporterParseImportedData_ParseTags(t *testing.T) {
importer := FireflyIIITransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -314,8 +316,8 @@ func TestFireFlyIIICsvFileConverterParseImportedData_ParseTags(t *testing.T) {
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, allNewTags, err := converter.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, nil, nil, nil, nil, nil)
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"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
@@ -328,8 +330,8 @@ func TestFireFlyIIICsvFileConverterParseImportedData_ParseTags(t *testing.T) {
assert.Equal(t, "tag3", allNewTags[2].Name)
}
func TestFireFlyIIICsvFileConverterParseImportedData_MissingFileHeader(t *testing.T) {
converter := FireflyIIITransactionDataCsvFileImporter
func TestFireFlyIIICsvFileimporterParseImportedData_MissingFileHeader(t *testing.T) {
importer := FireflyIIITransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -337,12 +339,12 @@ func TestFireFlyIIICsvFileConverterParseImportedData_MissingFileHeader(t *testin
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(""), 0, 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)
}
func TestFireFlyIIICsvFileConverterParseImportedData_MissingRequiredColumn(t *testing.T) {
converter := FireflyIIITransactionDataCsvFileImporter
func TestFireFlyIIICsvFileimporterParseImportedData_MissingRequiredColumn(t *testing.T) {
importer := FireflyIIITransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -351,32 +353,32 @@ func TestFireFlyIIICsvFileConverterParseImportedData_MissingRequiredColumn(t *te
}
// Missing Time Column
_, _, _, _, _, _, err := converter.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, nil, nil, nil, nil, nil)
_, _, _, _, _, _, 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"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
// Missing Type Column
_, _, _, _, _, _, err = converter.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, nil, nil, nil, nil, nil)
_, _, _, _, _, _, 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"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
// Missing Sub Category Column
_, _, _, _, _, _, err = converter.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, nil, nil, nil, nil, nil)
_, _, _, _, _, _, 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"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
// Missing Account Name Column
_, _, _, _, _, _, err = converter.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, nil, nil, nil, nil, nil)
_, _, _, _, _, _, 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"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
// Missing Amount Column
_, _, _, _, _, _, err = converter.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, nil, nil, nil, nil, nil)
_, _, _, _, _, _, 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"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
// Missing Account2 Name Column
_, _, _, _, _, _, err = converter.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, nil, nil, nil, nil, nil)
_, _, _, _, _, _, 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"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
}
@@ -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
@@ -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, 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, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezone, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
}
@@ -4,9 +4,11 @@ import (
"bytes"
"compress/gzip"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/models"
@@ -185,7 +187,7 @@ const gnucashCommonValidDataCaseFooter = "</gnc:book>\n" +
"</gnc-v2>\n"
func TestGnuCashTransactionDatabaseFileParseImportedData_MinimumValidData(t *testing.T) {
converter := GnuCashTransactionDataImporter
importer := GnuCashTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -193,14 +195,14 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_MinimumValidData(t *tes
DefaultCurrency: "CNY",
}
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := converter.ParseImportedData(context, user, []byte(gnucashMinimumValidDataCase), 0, 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)
}
func TestGnuCashTransactionDatabaseFileParseImportedData_GzippedMinimumValidData(t *testing.T) {
converter := GnuCashTransactionDataImporter
importer := GnuCashTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -217,14 +219,14 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_GzippedMinimumValidData
assert.Nil(t, err)
gzippedData := buffer.Bytes()
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := converter.ParseImportedData(context, user, gzippedData, 0, 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)
}
func TestGnuCashTransactionDatabaseFileParseImportedData_MinimumValidDataWithReversedSplitOrder(t *testing.T) {
converter := GnuCashTransactionDataImporter
importer := GnuCashTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -232,7 +234,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_MinimumValidDataWithRev
DefaultCurrency: "CNY",
}
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := converter.ParseImportedData(context, user, []byte("<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n"+
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := importer.ParseImportedData(context, user, []byte("<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n"+
"<gnc-v2\n"+
" xmlns:gnc=\"http://www.gnucash.org/XML/gnc\"\n"+
" xmlns:act=\"http://www.gnucash.org/XML/act\"\n"+
@@ -356,14 +358,14 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_MinimumValidDataWithRev
" </trn:splits>\n"+
"</gnc:transaction>\n"+
"</gnc:book>\n"+
"</gnc-v2>\n"), 0, nil, nil, nil, nil, nil)
"</gnc-v2>\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err)
checkParsedMinimumValidData(t, allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags)
}
func TestGnuCashTransactionDatabaseFileParseImportedData_ParseInvalidTime(t *testing.T) {
converter := GnuCashTransactionDataImporter
importer := GnuCashTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -371,7 +373,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseInvalidTime(t *tes
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
gnucashCommonValidDataCaseHeader+
"<gnc:transaction version=\"2.0.0\">\n"+
" <trn:date-posted>\n"+
@@ -388,10 +390,10 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseInvalidTime(t *tes
" </trn:split>\n"+
" </trn:splits>\n"+
"</gnc:transaction>\n"+
gnucashCommonValidDataCaseFooter), 0, nil, nil, nil, nil, nil)
gnucashCommonValidDataCaseFooter), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
gnucashCommonValidDataCaseHeader+
"<gnc:transaction version=\"2.0.0\">\n"+
" <trn:date-posted>\n"+
@@ -408,12 +410,12 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseInvalidTime(t *tes
" </trn:split>\n"+
" </trn:splits>\n"+
"</gnc:transaction>\n"+
gnucashCommonValidDataCaseFooter), 0, nil, nil, nil, nil, nil)
gnucashCommonValidDataCaseFooter), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
}
func TestGnuCashTransactionDatabaseFileParseImportedData_ParseValidTimezone(t *testing.T) {
converter := GnuCashTransactionDataImporter
importer := GnuCashTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -421,7 +423,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseValidTimezone(t *t
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
gnucashCommonValidDataCaseHeader+
"<gnc:transaction version=\"2.0.0\">\n"+
" <trn:date-posted>\n"+
@@ -438,12 +440,12 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseValidTimezone(t *t
" </trn:split>\n"+
" </trn:splits>\n"+
"</gnc:transaction>\n"+
gnucashCommonValidDataCaseFooter), 0, 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))
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
gnucashCommonValidDataCaseHeader+
"<gnc:transaction version=\"2.0.0\">\n"+
" <trn:date-posted>\n"+
@@ -460,14 +462,14 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseValidTimezone(t *t
" </trn:split>\n"+
" </trn:splits>\n"+
"</gnc:transaction>\n"+
gnucashCommonValidDataCaseFooter), 0, 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))
}
func TestGnuCashTransactionDatabaseFileParseImportedData_ParseValidAccountCurrency(t *testing.T) {
converter := GnuCashTransactionDataImporter
importer := GnuCashTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -475,7 +477,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseValidAccountCurren
DefaultCurrency: "CNY",
}
allNewTransactions, allNewAccounts, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n"+
allNewTransactions, allNewAccounts, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n"+
"<gnc-v2\n"+
" xmlns:gnc=\"http://www.gnucash.org/XML/gnc\"\n"+
" xmlns:act=\"http://www.gnucash.org/XML/act\"\n"+
@@ -557,7 +559,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseValidAccountCurren
" </trn:splits>\n"+
"</gnc:transaction>\n"+
"</gnc:book>\n"+
"</gnc-v2>\n"), 0, nil, nil, nil, nil, nil)
"</gnc-v2>\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err)
@@ -574,7 +576,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseValidAccountCurren
}
func TestGnuCashTransactionDatabaseFileParseImportedData_ParseValidAmount(t *testing.T) {
converter := GnuCashTransactionDataImporter
importer := GnuCashTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -582,7 +584,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseValidAmount(t *tes
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
gnucashCommonValidDataCaseHeader+
"<gnc:transaction version=\"2.0.0\">\n"+
" <trn:date-posted>\n"+
@@ -599,13 +601,13 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseValidAmount(t *tes
" </trn:split>\n"+
" </trn:splits>\n"+
"</gnc:transaction>\n"+
gnucashCommonValidDataCaseFooter), 0, 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(1234500), allNewTransactions[0].Amount)
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
gnucashCommonValidDataCaseHeader+
"<gnc:transaction version=\"2.0.0\">\n"+
" <trn:date-posted>\n"+
@@ -622,7 +624,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseValidAmount(t *tes
" </trn:split>\n"+
" </trn:splits>\n"+
"</gnc:transaction>\n"+
gnucashCommonValidDataCaseFooter), 0, 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))
@@ -630,7 +632,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseValidAmount(t *tes
}
func TestGnuCashTransactionDatabaseFileParseImportedData_ParseInvalidAmount(t *testing.T) {
converter := GnuCashTransactionDataImporter
importer := GnuCashTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -638,7 +640,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseInvalidAmount(t *t
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
gnucashCommonValidDataCaseHeader+
"<gnc:transaction version=\"2.0.0\">\n"+
" <trn:date-posted>\n"+
@@ -655,10 +657,10 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseInvalidAmount(t *t
" </trn:split>\n"+
" </trn:splits>\n"+
"</gnc:transaction>\n"+
gnucashCommonValidDataCaseFooter), 0, nil, nil, nil, nil, nil)
gnucashCommonValidDataCaseFooter), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
gnucashCommonValidDataCaseHeader+
"<gnc:transaction version=\"2.0.0\">\n"+
" <trn:date-posted>\n"+
@@ -675,10 +677,10 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseInvalidAmount(t *t
" </trn:split>\n"+
" </trn:splits>\n"+
"</gnc:transaction>\n"+
gnucashCommonValidDataCaseFooter), 0, nil, nil, nil, nil, nil)
gnucashCommonValidDataCaseFooter), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
gnucashCommonValidDataCaseHeader+
"<gnc:transaction version=\"2.0.0\">\n"+
" <trn:date-posted>\n"+
@@ -695,12 +697,12 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseInvalidAmount(t *t
" </trn:split>\n"+
" </trn:splits>\n"+
"</gnc:transaction>\n"+
gnucashCommonValidDataCaseFooter), 0, nil, nil, nil, nil, nil)
gnucashCommonValidDataCaseFooter), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
}
func TestGnuCashTransactionDatabaseFileParseImportedData_ParseDescription(t *testing.T) {
converter := GnuCashTransactionDataImporter
importer := GnuCashTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -708,7 +710,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseDescription(t *tes
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
gnucashCommonValidDataCaseHeader+
"<gnc:transaction version=\"2.0.0\">\n"+
" <trn:date-posted>\n"+
@@ -726,7 +728,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseDescription(t *tes
" </trn:split>\n"+
" </trn:splits>\n"+
"</gnc:transaction>\n"+
gnucashCommonValidDataCaseFooter), 0, 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))
@@ -734,7 +736,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseDescription(t *tes
}
func TestGnuCashTransactionDatabaseFileParseImportedData_SkipZeroAmountTransaction(t *testing.T) {
converter := GnuCashTransactionDataImporter
importer := GnuCashTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -742,7 +744,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_SkipZeroAmountTransacti
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
gnucashCommonValidDataCaseHeader+
"<gnc:transaction version=\"2.0.0\">\n"+
" <trn:date-posted>\n"+
@@ -755,12 +757,12 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_SkipZeroAmountTransacti
" </trn:split>\n"+
" </trn:splits>\n"+
"</gnc:transaction>\n"+
gnucashCommonValidDataCaseFooter), 0, nil, nil, nil, nil, nil)
gnucashCommonValidDataCaseFooter), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrNotFoundTransactionDataInFile.Message)
}
func TestGnuCashTransactionDatabaseFileParseImportedData_NotSupportedToParseSplitTransaction(t *testing.T) {
converter := GnuCashTransactionDataImporter
importer := GnuCashTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -768,7 +770,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_NotSupportedToParseSpli
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
gnucashCommonValidDataCaseHeader+
"<gnc:account version=\"2.0.0\">\n"+
" <act:name>Test Category2</act:name>\n"+
@@ -805,12 +807,12 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_NotSupportedToParseSpli
" </trn:split>\n"+
" </trn:splits>\n"+
"</gnc:transaction>\n"+
gnucashCommonValidDataCaseFooter), 0, nil, nil, nil, nil, nil)
gnucashCommonValidDataCaseFooter), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrNotSupportedSplitTransactions.Message)
}
func TestGnuCashTransactionDatabaseFileParseImportedData_MissingAccountRequiredNode(t *testing.T) {
converter := GnuCashTransactionDataImporter
importer := GnuCashTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -819,7 +821,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_MissingAccountRequiredN
}
// Missing Account Currency Node
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n"+
"<gnc-v2\n"+
" xmlns:gnc=\"http://www.gnucash.org/XML/gnc\"\n"+
@@ -872,12 +874,12 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_MissingAccountRequiredN
" </trn:split>\n"+
" </trn:splits>\n"+
"</gnc:transaction>\n"+
gnucashCommonValidDataCaseFooter), 0, nil, nil, nil, nil, nil)
gnucashCommonValidDataCaseFooter), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message)
}
func TestGnuCashTransactionDatabaseFileParseImportedData_MissingTransactionRequiredNode(t *testing.T) {
converter := GnuCashTransactionDataImporter
importer := GnuCashTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -886,7 +888,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_MissingTransactionRequi
}
// Missing Transaction Time Node
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
gnucashCommonValidDataCaseHeader+
"<gnc:transaction version=\"2.0.0\">\n"+
" <trn:splits>\n"+
@@ -900,22 +902,22 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_MissingTransactionRequi
" </trn:split>\n"+
" </trn:splits>\n"+
"</gnc:transaction>\n"+
gnucashCommonValidDataCaseFooter), 0, 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
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
gnucashCommonValidDataCaseHeader+
"<gnc:transaction version=\"2.0.0\">\n"+
" <trn:date-posted>\n"+
" <ts:date>2024-09-01 00:00:00 +0000</ts:date>\n"+
" </trn:date-posted>\n"+
"</gnc:transaction>\n"+
gnucashCommonValidDataCaseFooter), 0, 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
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
gnucashCommonValidDataCaseHeader+
"<gnc:transaction version=\"2.0.0\">\n"+
" <trn:date-posted>\n"+
@@ -930,11 +932,11 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_MissingTransactionRequi
" </trn:split>\n"+
" </trn:splits>\n"+
"</gnc:transaction>\n"+
gnucashCommonValidDataCaseFooter), 0, 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
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
gnucashCommonValidDataCaseHeader+
"<gnc:transaction version=\"2.0.0\">\n"+
" <trn:date-posted>\n"+
@@ -950,7 +952,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_MissingTransactionRequi
" </trn:split>\n"+
" </trn:splits>\n"+
"</gnc:transaction>\n"+
gnucashCommonValidDataCaseFooter), 0, nil, nil, nil, nil, nil)
gnucashCommonValidDataCaseFooter), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrMissingAccountData.Message)
}
@@ -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]
@@ -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, 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, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezone, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
}
@@ -2,9 +2,11 @@ package iif
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/models"
@@ -12,7 +14,7 @@ import (
)
func TestIIFTransactionDataFileParseImportedData_MinimumValidData(t *testing.T) {
converter := IifTransactionDataFileImporter
importer := IifTransactionDataFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -20,7 +22,7 @@ func TestIIFTransactionDataFileParseImportedData_MinimumValidData(t *testing.T)
DefaultCurrency: "CNY",
}
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := converter.ParseImportedData(context, user, []byte(
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := importer.ParseImportedData(context, user, []byte(
"!ACCNT\tNAME\tACCNTTYPE\n"+
"ACCNT\tTest Account\tBANK\n"+
"ACCNT\tTest Account2\tBANK\n"+
@@ -49,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, nil, nil, nil, nil, nil)
"ENDTRNS\t\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err)
@@ -132,7 +134,7 @@ func TestIIFTransactionDataFileParseImportedData_MinimumValidData(t *testing.T)
}
func TestIIFTransactionDataFileParseImportedData_MinimumValidDataWithoutAccountData(t *testing.T) {
converter := IifTransactionDataFileImporter
importer := IifTransactionDataFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -140,13 +142,13 @@ func TestIIFTransactionDataFileParseImportedData_MinimumValidDataWithoutAccountD
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!SPL\tDATE\tACCNT\tAMOUNT\n"+
"!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, nil, nil, nil, nil, nil)
"ENDTRNS\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err)
@@ -162,7 +164,7 @@ func TestIIFTransactionDataFileParseImportedData_MinimumValidDataWithoutAccountD
}
func TestIIFTransactionDataFileParseImportedData_MultipleDataset(t *testing.T) {
converter := IifTransactionDataFileImporter
importer := IifTransactionDataFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -170,7 +172,7 @@ func TestIIFTransactionDataFileParseImportedData_MultipleDataset(t *testing.T) {
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"!ACCNT\tNAME\tACCNTTYPE\n"+
"ACCNT\tTest Account3\tBANK\n"+
"ACCNT\tTest Account4\tBANK\n"+
@@ -202,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, nil, nil, nil, nil, nil)
"ACCNT\t\tTest Category2\tEXP\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err)
@@ -247,7 +249,7 @@ func TestIIFTransactionDataFileParseImportedData_MultipleDataset(t *testing.T) {
}
func TestIIFTransactionDataFileParseImportedData_ParseCategoryAndSubCategory(t *testing.T) {
converter := IifTransactionDataFileImporter
importer := IifTransactionDataFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -255,7 +257,7 @@ func TestIIFTransactionDataFileParseImportedData_ParseCategoryAndSubCategory(t *
DefaultCurrency: "CNY",
}
allNewTransactions, _, allNewSubExpenseCategories, allNewSubIncomeCategories, _, _, err := converter.ParseImportedData(context, user, []byte(
allNewTransactions, _, allNewSubExpenseCategories, allNewSubIncomeCategories, _, _, err := importer.ParseImportedData(context, user, []byte(
"!ACCNT\tNAME\tACCNTTYPE\n"+
"ACCNT\tTest Parent Category:Test Category\tINC\n"+
"ACCNT\tTest Parent Category2:Test Category2\tEXP\n"+
@@ -267,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, nil, nil, nil, nil, nil)
"ENDTRNS\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err)
@@ -299,7 +301,7 @@ func TestIIFTransactionDataFileParseImportedData_ParseCategoryAndSubCategory(t *
}
func TestIIFTransactionDataFileParseImportedData_ParseYearMonthDayFormatTime(t *testing.T) {
converter := IifTransactionDataFileImporter
importer := IifTransactionDataFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -307,7 +309,7 @@ func TestIIFTransactionDataFileParseImportedData_ParseYearMonthDayFormatTime(t *
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!SPL\tDATE\tACCNT\tAMOUNT\n"+
"!ENDTRNS\t\t\t\n"+
@@ -322,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, nil, nil, nil, nil, nil)
"ENDTRNS\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err)
@@ -334,7 +336,7 @@ func TestIIFTransactionDataFileParseImportedData_ParseYearMonthDayFormatTime(t *
}
func TestIIFTransactionDataFileParseImportedData_ParseShortMonthDayYearFormatTime(t *testing.T) {
converter := IifTransactionDataFileImporter
importer := IifTransactionDataFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -342,7 +344,7 @@ func TestIIFTransactionDataFileParseImportedData_ParseShortMonthDayYearFormatTim
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!SPL\tDATE\tACCNT\tAMOUNT\n"+
"!ENDTRNS\t\t\t\n"+
@@ -354,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, nil, nil, nil, nil, nil)
"ENDTRNS\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err)
@@ -365,7 +367,7 @@ func TestIIFTransactionDataFileParseImportedData_ParseShortMonthDayYearFormatTim
}
func TestIIFTransactionDataFileParseImportedData_ParseShortMonthDayTwoDigitsYearFormatTime(t *testing.T) {
converter := IifTransactionDataFileImporter
importer := IifTransactionDataFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -373,7 +375,7 @@ func TestIIFTransactionDataFileParseImportedData_ParseShortMonthDayTwoDigitsYear
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!SPL\tDATE\tACCNT\tAMOUNT\n"+
"!ENDTRNS\t\t\t\n"+
@@ -385,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, nil, nil, nil, nil, nil)
"ENDTRNS\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err)
@@ -396,7 +398,7 @@ func TestIIFTransactionDataFileParseImportedData_ParseShortMonthDayTwoDigitsYear
}
func TestIIFTransactionDataFileParseImportedData_ParseInvalidTime(t *testing.T) {
converter := IifTransactionDataFileImporter
importer := IifTransactionDataFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -404,36 +406,36 @@ func TestIIFTransactionDataFileParseImportedData_ParseInvalidTime(t *testing.T)
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!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"+
"ENDTRNS\t\t\t\n"), 0, 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 = converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!SPL\tDATE\tACCNT\tAMOUNT\n"+
"!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, 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 = converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!SPL\tDATE\tACCNT\tAMOUNT\n"+
"!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, 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)
}
func TestIIFTransactionDataFileParseImportedData_ParseAmountWithThousandsSeparator(t *testing.T) {
converter := IifTransactionDataFileImporter
importer := IifTransactionDataFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -441,13 +443,13 @@ func TestIIFTransactionDataFileParseImportedData_ParseAmountWithThousandsSeparat
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!SPL\tDATE\tACCNT\tAMOUNT\n"+
"!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, nil, nil, nil, nil, nil)
"ENDTRNS\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err)
@@ -456,7 +458,7 @@ func TestIIFTransactionDataFileParseImportedData_ParseAmountWithThousandsSeparat
}
func TestIIFTransactionDataFileParseImportedData_ParseInvalidAmount(t *testing.T) {
converter := IifTransactionDataFileImporter
importer := IifTransactionDataFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -464,27 +466,27 @@ func TestIIFTransactionDataFileParseImportedData_ParseInvalidAmount(t *testing.T
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!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"+
"ENDTRNS\t\t\t\n"), 0, 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 = converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!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"+
"ENDTRNS\t\t\t\n"), 0, 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)
}
func TestIIFTransactionDataFileParseImportedData_ParseDescription(t *testing.T) {
converter := IifTransactionDataFileImporter
importer := IifTransactionDataFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -492,25 +494,25 @@ func TestIIFTransactionDataFileParseImportedData_ParseDescription(t *testing.T)
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tNAME\tAMOUNT\tMEMO\n"+
"!SPL\tDATE\tACCNT\tNAME\tAMOUNT\tMEMO\n"+
"!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, 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))
assert.Equal(t, "foo bar\t#test", allNewTransactions[0].Comment)
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tNAME\tAMOUNT\tMEMO\n"+
"!SPL\tDATE\tACCNT\tNAME\tAMOUNT\tMEMO\n"+
"!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, 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))
@@ -518,7 +520,7 @@ func TestIIFTransactionDataFileParseImportedData_ParseDescription(t *testing.T)
}
func TestIIFTransactionDataFileParseImportedData_ParseSplitTransaction(t *testing.T) {
converter := IifTransactionDataFileImporter
importer := IifTransactionDataFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -526,7 +528,7 @@ func TestIIFTransactionDataFileParseImportedData_ParseSplitTransaction(t *testin
DefaultCurrency: "CNY",
}
allNewTransactions, allNewAccounts, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
allNewTransactions, allNewAccounts, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"!ACCNT\tNAME\tACCNTTYPE\n"+
"ACCNT\tTest Category\tINC\n"+
"ACCNT\tTest Category2\tEXP\n"+
@@ -552,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, nil, nil, nil, nil, nil)
"ENDTRNS\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err)
@@ -651,7 +653,7 @@ func TestIIFTransactionDataFileParseImportedData_ParseSplitTransaction(t *testin
}
func TestIIFTransactionDataFileParseImportedData_ParseSplitTransactionDescription(t *testing.T) {
converter := IifTransactionDataFileImporter
importer := IifTransactionDataFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -659,21 +661,21 @@ func TestIIFTransactionDataFileParseImportedData_ParseSplitTransactionDescriptio
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tNAME\tAMOUNT\tMEMO\n"+
"!SPL\tDATE\tACCNT\tNAME\tAMOUNT\tMEMO\n"+
"!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-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, 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))
assert.Equal(t, "foo\ttest#bar", allNewTransactions[0].Comment)
assert.Equal(t, "foo bar\t#test", allNewTransactions[1].Comment)
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tNAME\tAMOUNT\tMEMO\n"+
"!SPL\tDATE\tACCNT\tNAME\tAMOUNT\tMEMO\n"+
"!ENDTRNS\t\t\t\t\t\n"+
@@ -681,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, 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))
@@ -691,7 +693,7 @@ func TestIIFTransactionDataFileParseImportedData_ParseSplitTransactionDescriptio
}
func TestIIFTransactionDataFileParseImportedData_NotSupportedSplitTransaction(t *testing.T) {
converter := IifTransactionDataFileImporter
importer := IifTransactionDataFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -700,52 +702,52 @@ func TestIIFTransactionDataFileParseImportedData_NotSupportedSplitTransaction(t
}
// Opening balance transaction
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"!TRNS\tTRNSTYPE\tDATE\tACCNT\tAMOUNT\n"+
"!SPL\tTRNSTYPE\tDATE\tACCNT\tAMOUNT\n"+
"!ENDTRNS\t\t\t\t\n"+
"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, 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
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!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-100.00\n"+
"SPL\t09/01/2024\tTest Account3\t-23.45\n"+
"ENDTRNS\t\t\t\n"), 0, 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
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!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-100 00\n"+
"SPL\t09/01/2024\tTest Account3\t-23.45\n"+
"ENDTRNS\t\t\t\n"), 0, 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
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!SPL\tDATE\tACCNT\tAMOUNT\n"+
"!ENDTRNS\t\t\t\n"+
"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, 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)
}
func TestIIFTransactionDataFileParseImportedData_InvalidDataLines(t *testing.T) {
converter := IifTransactionDataFileImporter
importer := IifTransactionDataFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -754,75 +756,75 @@ func TestIIFTransactionDataFileParseImportedData_InvalidDataLines(t *testing.T)
}
//Missing Transaction Line
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!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, 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
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!SPL\tDATE\tACCNT\tAMOUNT\n"+
"!ENDTRNS\t\t\t\n"+
"ENDTRNS\t\t\t\n"), 0, 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
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!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, 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
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!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, 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)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!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"+
"!ACCNT\tNAME\tACCNTTYPE\n"+
"ACCNT\tTest Account\tBANK\n"), 0, 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
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!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"+
"TEST\t\t\t\n"+
"ENDTRNS\t\t\t\n"), 0, 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
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!SPL\tDATE\tACCNT\tAMOUNT\n"+
"!ENDTRNS\t\t\t\n"+
"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, 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
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\t\n"+
"!SPL\tDATE\tACCNT\tAMOUNT\t\n"+
"!ENDTRNS\t\t\t\n"+
@@ -830,12 +832,12 @@ 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, 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)
}
func TestIIFTransactionDataFileParseImportedData_InvalidHeaderLines(t *testing.T) {
converter := IifTransactionDataFileImporter
importer := IifTransactionDataFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -844,49 +846,49 @@ func TestIIFTransactionDataFileParseImportedData_InvalidHeaderLines(t *testing.T
}
// Missing All Sample Lines
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, 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, 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 = converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"!SPL\tDATE\tACCNT\tAMOUNT\n"+
"!ENDTRNS\t\t\t\n"), 0, 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 = converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!ENDTRNS\t\t\t\n"), 0, 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 = converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!SPL\tDATE\tACCNT\tAMOUNT\n"), 0, 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)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!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, 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 = converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!TEST\tDATE\tACCNT\tAMOUNT\n"+
"!ENDTRNS\t\t\t\n"), 0, 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)
}
func TestIIFTransactionDataFileParseImportedData_MissingRequiredColumn(t *testing.T) {
converter := IifTransactionDataFileImporter
importer := IifTransactionDataFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -895,32 +897,32 @@ func TestIIFTransactionDataFileParseImportedData_MissingRequiredColumn(t *testin
}
// Missing Date Column
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"!TRNS\tACCNT\tAMOUNT\t\n"+
"!SPL\tACCNT\tAMOUNT\t\n"+
"!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, 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
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tAMOUNT\t\n"+
"!SPL\tDATE\tAMOUNT\t\n"+
"!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, 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
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\t\n"+
"!SPL\tDATE\tACCNT\t\n"+
"!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, 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)
}
@@ -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, 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, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezone, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
}
@@ -6,6 +6,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/models"
@@ -13,7 +14,7 @@ import (
)
func TestJDComFinanceCsvFileImporterParseImportedData_MinimumValidData(t *testing.T) {
converter := JDComFinanceTransactionDataCsvFileImporter
importer := JDComFinanceTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -30,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 := converter.ParseImportedData(context, user, []byte(data), 0, 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))
@@ -93,7 +94,7 @@ func TestJDComFinanceCsvFileImporterParseImportedData_MinimumValidData(t *testin
}
func TestJDComFinanceCsvFileImporterParseImportedData_ParseRefundTransaction(t *testing.T) {
converter := JDComFinanceTransactionDataCsvFileImporter
importer := JDComFinanceTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -110,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 := converter.ParseImportedData(context, user, []byte(data1), 0, 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)
@@ -139,7 +140,7 @@ func TestJDComFinanceCsvFileImporterParseImportedData_ParseRefundTransaction(t *
}
func TestJDComFinanceCsvFileImporterParseImportedData_ParseInvalidTime(t *testing.T) {
converter := JDComFinanceTransactionDataCsvFileImporter
importer := JDComFinanceTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -153,7 +154,7 @@ func TestJDComFinanceCsvFileImporterParseImportedData_ParseInvalidTime(t *testin
"\n" +
"交易时间,商户名称,交易说明,金额,收/付款方式,交易状态,收/支\n" +
"2025-09-01T01:23:45,xxx,xxx,0.12,银行卡,交易成功,支出\n"
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(data1), 0, 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" +
@@ -162,12 +163,12 @@ func TestJDComFinanceCsvFileImporterParseImportedData_ParseInvalidTime(t *testin
"\n" +
"交易时间,商户名称,交易说明,金额,收/付款方式,交易状态,收/支\n" +
"09/01/2025 01:23:45,xxx,xxx,0.12,银行卡,交易成功,支出\n"
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data2), 0, 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)
}
func TestJDComFinanceCsvFileImporterParseImportedData_ParseInvalidType(t *testing.T) {
converter := JDComFinanceTransactionDataCsvFileImporter
importer := JDComFinanceTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -181,12 +182,12 @@ func TestJDComFinanceCsvFileImporterParseImportedData_ParseInvalidType(t *testin
"\n" +
"交易时间,商户名称,交易说明,金额,收/付款方式,交易状态,收/支\n" +
"2025-09-01 01:23:45,xxx,xxx,0.12,银行卡,交易成功,转账\n"
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(data), 0, 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)
}
func TestJDComFinanceCsvFileImporterParseImportedData_ParseInvalidAmount(t *testing.T) {
converter := JDComFinanceTransactionDataCsvFileImporter
importer := JDComFinanceTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -200,12 +201,12 @@ func TestJDComFinanceCsvFileImporterParseImportedData_ParseInvalidAmount(t *test
"\n" +
"交易时间,商户名称,交易说明,金额,收/付款方式,交易状态,收/支\n" +
"2025-09-01 01:23:45,xxx,xxx,¥0.12,银行卡,交易成功,支出\n"
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(data), 0, 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)
}
func TestJDComFinanceCsvFileImporterParseImportedData_ParseAccountName(t *testing.T) {
converter := JDComFinanceTransactionDataCsvFileImporter
importer := JDComFinanceTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -220,7 +221,7 @@ func TestJDComFinanceCsvFileImporterParseImportedData_ParseAccountName(t *testin
"\n" +
"交易时间,商户名称,交易说明,金额,收/付款方式,交易状态,收/支,交易分类\n" +
"2025-09-01 01:23:45,xxx,京东钱包余额充值,0.05,银行卡,交易成功,不计收支,余额\n"
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(data1), 0, 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))
@@ -235,7 +236,7 @@ func TestJDComFinanceCsvFileImporterParseImportedData_ParseAccountName(t *testin
"\n" +
"交易时间,商户名称,交易说明,金额,收/付款方式,交易状态,收/支,交易分类\n" +
"2025-09-01 01:23:45,xxx,京东余额提现,0.05,银行卡,交易成功,不计收支,余额\n"
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data2), 0, 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))
@@ -252,7 +253,7 @@ func TestJDComFinanceCsvFileImporterParseImportedData_ParseAccountName(t *testin
"2025-09-01 01:23:45,xxx,京东小金库-转入,0.05,余额,交易成功,不计收支,小金库\n"
assert.Nil(t, err)
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data3), 0, 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))
@@ -269,7 +270,7 @@ func TestJDComFinanceCsvFileImporterParseImportedData_ParseAccountName(t *testin
"2025-09-01 01:23:45,xxx,京东小金库-转出,0.05,余额,交易成功,不计收支,小金库\n"
assert.Nil(t, err)
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data4), 0, 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))
@@ -286,7 +287,7 @@ func TestJDComFinanceCsvFileImporterParseImportedData_ParseAccountName(t *testin
"2025-09-01 01:23:45,xxx,价保退款,0.05,银行卡,交易成功,不计收支,其他\n"
assert.Nil(t, err)
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data5), 0, 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))
@@ -302,7 +303,7 @@ func TestJDComFinanceCsvFileImporterParseImportedData_ParseAccountName(t *testin
"2025-09-01 01:23:45,xxx,白条主动还款,0.05,银行卡,交易成功,不计收支,白条\n"
assert.Nil(t, err)
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data6), 0, 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))
@@ -312,7 +313,7 @@ func TestJDComFinanceCsvFileImporterParseImportedData_ParseAccountName(t *testin
}
func TestJDComFinanceCsvFileImporterParseImportedData_ParseDescription(t *testing.T) {
converter := JDComFinanceTransactionDataCsvFileImporter
importer := JDComFinanceTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -326,7 +327,7 @@ func TestJDComFinanceCsvFileImporterParseImportedData_ParseDescription(t *testin
"\n" +
"交易时间,商户名称,交易说明,金额,收/付款方式,交易状态,收/支\n" +
"2025-09-01 01:23:45,xxx,,0.12,银行卡,交易成功,支出\n"
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(data1), 0, 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))
@@ -338,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 = converter.ParseImportedData(context, user, []byte(data2), 0, 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)
@@ -348,13 +349,13 @@ func TestJDComFinanceCsvFileImporterParseImportedData_ParseDescription(t *testin
"\n" +
"交易时间,商户名称,交易说明,交易说明,金额,收/付款方式,交易状态,收/支,备注\n" +
"2025-09-01 01:23:45,xxx,xxx,Test,0.12,银行卡,交易成功,支出,\n"
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data3), 0, 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)
}
func TestJDComFinanceCsvFileImporterParseImportedData_SkipUnknownStatusTransaction(t *testing.T) {
converter := JDComFinanceTransactionDataCsvFileImporter
importer := JDComFinanceTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -368,12 +369,12 @@ func TestJDComFinanceCsvFileImporterParseImportedData_SkipUnknownStatusTransacti
"\n" +
"交易时间,商户名称,交易说明,金额,收/付款方式,交易状态,收/支\n" +
"2025-09-01 01:23:45,xxx,xxx,0.12,银行卡,xxxx,支出\n"
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(data), 0, 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)
}
func TestJDComFinanceCsvFileImporterParseImportedData_SkipUnknownMemoTransferTransaction(t *testing.T) {
converter := JDComFinanceTransactionDataCsvFileImporter
importer := JDComFinanceTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -387,12 +388,12 @@ func TestJDComFinanceCsvFileImporterParseImportedData_SkipUnknownMemoTransferTra
"\n" +
"交易时间,商户名称,交易说明,金额,收/付款方式,交易状态,收/支\n" +
"2025-09-01 01:23:45,xxx,xxx,0.12,银行卡,交易成功,不计收支\n"
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(data), 0, 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)
}
func TestJDComFinanceCsvFileImporterParseImportedData_MissingFileHeader(t *testing.T) {
converter := JDComFinanceTransactionDataCsvFileImporter
importer := JDComFinanceTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -402,15 +403,15 @@ func TestJDComFinanceCsvFileImporterParseImportedData_MissingFileHeader(t *testi
data := "交易时间,商户名称,交易说明,金额,收/付款方式,交易状态,收/支\n" +
"2025-09-01 01:23:45,xxx,xxx,0.12,银行卡,交易成功,支出\n"
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(data), 0, 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 = converter.ParseImportedData(context, user, []byte(""), 0, 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)
}
func TestJDComFinanceCsvFileImporterParseImportedData_MissingRequiredColumn(t *testing.T) {
converter := JDComFinanceTransactionDataCsvFileImporter
importer := JDComFinanceTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -425,7 +426,7 @@ func TestJDComFinanceCsvFileImporterParseImportedData_MissingRequiredColumn(t *t
"\n" +
"商户名称,交易说明,金额,收/付款方式,交易状态,收/支\n" +
"xxx,xxx,0.12,银行卡,交易成功,支出\n"
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(data1), 0, 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
@@ -435,7 +436,7 @@ func TestJDComFinanceCsvFileImporterParseImportedData_MissingRequiredColumn(t *t
"\n" +
"交易时间,交易说明,金额,收/付款方式,交易状态,收/支\n" +
"2025-09-01 01:23:45,xxx,0.12,银行卡,交易成功,支出\n"
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data2), 0, 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
@@ -445,7 +446,7 @@ func TestJDComFinanceCsvFileImporterParseImportedData_MissingRequiredColumn(t *t
"\n" +
"交易时间,商户名称,金额,收/付款方式,交易状态,收/支\n" +
"2025-09-01 01:23:45,xxx,0.12,银行卡,交易成功,支出\n"
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data3), 0, 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
@@ -455,7 +456,7 @@ func TestJDComFinanceCsvFileImporterParseImportedData_MissingRequiredColumn(t *t
"\n" +
"交易时间,商户名称,交易说明,收/付款方式,交易状态,收/支\n" +
"2025-09-01 01:23:45,xxx,xxx,银行卡,交易成功,支出\n"
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data4), 0, 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
@@ -465,7 +466,7 @@ func TestJDComFinanceCsvFileImporterParseImportedData_MissingRequiredColumn(t *t
"\n" +
"交易时间,商户名称,交易说明,金额,交易状态,收/支\n" +
"2025-09-01 01:23:45,xxx,xxx,0.12,交易成功,支出\n"
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data5), 0, 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
@@ -475,7 +476,7 @@ func TestJDComFinanceCsvFileImporterParseImportedData_MissingRequiredColumn(t *t
"\n" +
"交易时间,商户名称,交易说明,金额,收/付款方式,收/支\n" +
"2025-09-01 01:23:45,xxx,xxx,0.12,银行卡,支出\n"
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data6), 0, 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
@@ -485,12 +486,12 @@ func TestJDComFinanceCsvFileImporterParseImportedData_MissingRequiredColumn(t *t
"\n" +
"交易时间,商户名称,交易说明,金额,收/付款方式,交易状态\n" +
"2025-09-01 01:23:45,xxx,xxx,0.12,银行卡,交易成功\n"
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data7), 0, 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)
}
func TestJDComFinanceCsvFileImporterParseImportedData_NoTransactionData(t *testing.T) {
converter := JDComFinanceTransactionDataCsvFileImporter
importer := JDComFinanceTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -503,6 +504,6 @@ func TestJDComFinanceCsvFileImporterParseImportedData_NoTransactionData(t *testi
"日期区间:2025-01-01 至 2025-09-01\n" +
"\n" +
"交易时间,商户名称,交易说明,金额,收/付款方式,交易状态,收/支\n"
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(data), 0, 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)
}
@@ -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, 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, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezone, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
}
@@ -2,9 +2,11 @@ package mt
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/models"
@@ -12,7 +14,7 @@ import (
)
func TestMT940TransactionDataFileParseImportedData_MinimumValidData(t *testing.T) {
converter := MT940TransactionDataFileImporter
importer := MT940TransactionDataFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -20,7 +22,7 @@ func TestMT940TransactionDataFileParseImportedData_MinimumValidData(t *testing.T
DefaultCurrency: "CNY",
}
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := converter.ParseImportedData(context, user, []byte(
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := importer.ParseImportedData(context, user, []byte(
`{1:F01TESTBANK123456789}{2:I940TESTBANK}{4:
:20:123456789
:25:12345678
@@ -31,7 +33,7 @@ func TestMT940TransactionDataFileParseImportedData_MinimumValidData(t *testing.T
:61:2506020603D234,56NTRFFOOBAR
:86:Transaction 2
:62F:C250601CNY123,45
-}`), 0, nil, nil, nil, nil, nil)
-}`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err)
@@ -70,7 +72,7 @@ func TestMT940TransactionDataFileParseImportedData_MinimumValidData(t *testing.T
}
func TestMT940TransactionDataFileParseImportedData_ParseTransactionValidAmountAndCurrency(t *testing.T) {
converter := MT940TransactionDataFileImporter
importer := MT940TransactionDataFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -78,7 +80,7 @@ func TestMT940TransactionDataFileParseImportedData_ParseTransactionValidAmountAn
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
`{1:F01TESTBANK123456789}{2:I940TESTBANK}{4:
:20:123456789
:25:12345678
@@ -91,7 +93,7 @@ func TestMT940TransactionDataFileParseImportedData_ParseTransactionValidAmountAn
:61:250603C1,NTRFTEST
:86:Transaction 3
:62F:C250601CNY123,45
-}`), 0, nil, nil, nil, nil, nil)
-}`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 3, len(allNewTransactions))
@@ -101,7 +103,7 @@ func TestMT940TransactionDataFileParseImportedData_ParseTransactionValidAmountAn
}
func TestMT940TransactionDataFileParseImportedData_ParseTransactionInvalidAmountAndCurrency(t *testing.T) {
converter := MT940TransactionDataFileImporter
importer := MT940TransactionDataFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -109,7 +111,7 @@ func TestMT940TransactionDataFileParseImportedData_ParseTransactionInvalidAmount
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
`{1:F01TESTBANK123456789}{2:I940TESTBANK}{4:
:20:123456789
:25:12345678
@@ -117,10 +119,10 @@ func TestMT940TransactionDataFileParseImportedData_ParseTransactionInvalidAmount
:60F:C250601CNY123,45
:61:2506010602C123 45NTRFTEST
:62F:C250601CNY123,45
-}`), 0, nil, nil, nil, nil, nil)
-}`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrInvalidMT940File.Message)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
`{1:F01TESTBANK123456789}{2:I940TESTBANK}{4:
:20:123456789
:25:12345678
@@ -128,12 +130,12 @@ func TestMT940TransactionDataFileParseImportedData_ParseTransactionInvalidAmount
:60F:C250601CNY123,45
:61:2506010602C12.45NTRFTEST
:62F:C250601CNY123,45
-}`), 0, nil, nil, nil, nil, nil)
-}`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrInvalidMT940File.Message)
}
func TestMT940TransactionDataFileParseImportedData_ParseTransactionType(t *testing.T) {
converter := MT940TransactionDataFileImporter
importer := MT940TransactionDataFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -141,7 +143,7 @@ func TestMT940TransactionDataFileParseImportedData_ParseTransactionType(t *testi
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
`{1:F01TESTBANK123456789}{2:I940TESTBANK}{4:
:20:123456789
:25:12345678
@@ -156,7 +158,7 @@ func TestMT940TransactionDataFileParseImportedData_ParseTransactionType(t *testi
:61:250604RD123,45NTRFTEST
:86:Transaction 4
:62F:C250601CNY123,45
-}`), 0, nil, nil, nil, nil, nil)
-}`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 4, len(allNewTransactions))
@@ -167,7 +169,7 @@ func TestMT940TransactionDataFileParseImportedData_ParseTransactionType(t *testi
}
func TestMT940TransactionDataFileParseImportedData_ParseDescription(t *testing.T) {
converter := MT940TransactionDataFileImporter
importer := MT940TransactionDataFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -175,7 +177,7 @@ func TestMT940TransactionDataFileParseImportedData_ParseDescription(t *testing.T
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
`{1:F01TESTBANK123456789}{2:I940TESTBANK}{4:
:20:123456789
:25:12345678
@@ -186,7 +188,7 @@ func TestMT940TransactionDataFileParseImportedData_ParseDescription(t *testing.T
Part 2
Part 3
:62F:C250601CNY123,45
-}`), 0, nil, nil, nil, nil, nil)
-}`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
@@ -194,7 +196,7 @@ func TestMT940TransactionDataFileParseImportedData_ParseDescription(t *testing.T
}
func TestMT940TransactionDataFileParseImportedData_MissingRequiredField(t *testing.T) {
converter := MT940TransactionDataFileImporter
importer := MT940TransactionDataFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -203,12 +205,12 @@ func TestMT940TransactionDataFileParseImportedData_MissingRequiredField(t *testi
}
// Missing opening balance and closing balance
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
`{1:F01TESTBANK123456789}{2:I940TESTBANK}{4:
:20:123456789
:28C:123/1
:61:250601C123,45NTRFTEST
:86:Transaction 1
-}`), 0, nil, nil, nil, nil, nil)
-}`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message)
}
+1 -1
View File
@@ -23,7 +23,7 @@ import (
const ofx1USAsciiEncoding = "usascii"
const ofx1UnicodeEncoding = "unicode"
const ofx1UTF8Encoding = "utf8" // non-standard ofx 1.x encoding, used by some banks (https://github.com/mayswind/ezbookkeeping/issues/48)
const ofx1UTF8Encoding = "utf-8" // non-standard ofx 1.x encoding, used by some banks (https://github.com/mayswind/ezbookkeeping/issues/48)
const ofx1SGMLDataFormat = "OFXSGML"
var ofx2HeaderPattern = regexp.MustCompile("<\\?OFX( +[A-Z]+=\"[^=]*\")* *\\?>")
@@ -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, 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, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezone, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
}
@@ -2,9 +2,11 @@ package ofx
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/models"
@@ -12,7 +14,7 @@ import (
)
func TestOFXTransactionDataFileParseImportedData_MinimumValidData(t *testing.T) {
converter := OFXTransactionDataImporter
importer := OFXTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -20,7 +22,7 @@ func TestOFXTransactionDataFileParseImportedData_MinimumValidData(t *testing.T)
DefaultCurrency: "CNY",
}
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := converter.ParseImportedData(context, user, []byte(
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := importer.ParseImportedData(context, user, []byte(
"<OFX>\n"+
" <BANKMSGSRSV1>\n"+
" <STMTTRNRS>\n"+
@@ -76,7 +78,7 @@ func TestOFXTransactionDataFileParseImportedData_MinimumValidData(t *testing.T)
" </CCSTMTRS>\n"+
" </CCSTMTTRNRS>\n"+
" </CREDITCARDMSGSRSV1>\n"+
"</OFX>"), 0, nil, nil, nil, nil, nil)
"</OFX>"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err)
@@ -160,7 +162,7 @@ func TestOFXTransactionDataFileParseImportedData_MinimumValidData(t *testing.T)
}
func TestOFXTransactionDataFileParseImportedData_ParseAccountTo(t *testing.T) {
converter := OFXTransactionDataImporter
importer := OFXTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -168,7 +170,7 @@ func TestOFXTransactionDataFileParseImportedData_ParseAccountTo(t *testing.T) {
DefaultCurrency: "CNY",
}
allNewTransactions, allNewAccounts, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
allNewTransactions, allNewAccounts, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"<OFX>\n"+
" <BANKMSGSRSV1>\n"+
" <STMTTRNRS>\n"+
@@ -210,7 +212,7 @@ func TestOFXTransactionDataFileParseImportedData_ParseAccountTo(t *testing.T) {
" </CCSTMTRS>\n"+
" </CCSTMTTRNRS>\n"+
" </CREDITCARDMSGSRSV1>\n"+
"</OFX>"), 0, nil, nil, nil, nil, nil)
"</OFX>"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err)
@@ -243,7 +245,7 @@ func TestOFXTransactionDataFileParseImportedData_ParseAccountTo(t *testing.T) {
}
func TestOFXTransactionDataFileParseImportedData_ParseValidTransactionTime(t *testing.T) {
converter := OFXTransactionDataImporter
importer := OFXTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -251,7 +253,7 @@ func TestOFXTransactionDataFileParseImportedData_ParseValidTransactionTime(t *te
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"<OFX>\n"+
" <BANKMSGSRSV1>\n"+
" <STMTTRNRS>\n"+
@@ -295,7 +297,7 @@ func TestOFXTransactionDataFileParseImportedData_ParseValidTransactionTime(t *te
" </STMTRS>\n"+
" </STMTTRNRS>\n"+
" </BANKMSGSRSV1>\n"+
"</OFX>"), 0, nil, nil, nil, nil, nil)
"</OFX>"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err)
@@ -310,7 +312,7 @@ func TestOFXTransactionDataFileParseImportedData_ParseValidTransactionTime(t *te
}
func TestOFXTransactionDataFileParseImportedData_ParseInvalidTransactionTime(t *testing.T) {
converter := OFXTransactionDataImporter
importer := OFXTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -318,7 +320,7 @@ func TestOFXTransactionDataFileParseImportedData_ParseInvalidTransactionTime(t *
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"<OFX>\n"+
" <BANKMSGSRSV1>\n"+
" <STMTTRNRS>\n"+
@@ -337,10 +339,10 @@ func TestOFXTransactionDataFileParseImportedData_ParseInvalidTransactionTime(t *
" </STMTRS>\n"+
" </STMTTRNRS>\n"+
" </BANKMSGSRSV1>\n"+
"</OFX>"), 0, nil, nil, nil, nil, nil)
"</OFX>"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"<OFX>\n"+
" <BANKMSGSRSV1>\n"+
" <STMTTRNRS>\n"+
@@ -359,10 +361,10 @@ func TestOFXTransactionDataFileParseImportedData_ParseInvalidTransactionTime(t *
" </STMTRS>\n"+
" </STMTTRNRS>\n"+
" </BANKMSGSRSV1>\n"+
"</OFX>"), 0, nil, nil, nil, nil, nil)
"</OFX>"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"<OFX>\n"+
" <BANKMSGSRSV1>\n"+
" <STMTTRNRS>\n"+
@@ -381,10 +383,10 @@ func TestOFXTransactionDataFileParseImportedData_ParseInvalidTransactionTime(t *
" </STMTRS>\n"+
" </STMTTRNRS>\n"+
" </BANKMSGSRSV1>\n"+
"</OFX>"), 0, nil, nil, nil, nil, nil)
"</OFX>"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"<OFX>\n"+
" <BANKMSGSRSV1>\n"+
" <STMTTRNRS>\n"+
@@ -403,12 +405,12 @@ func TestOFXTransactionDataFileParseImportedData_ParseInvalidTransactionTime(t *
" </STMTRS>\n"+
" </STMTTRNRS>\n"+
" </BANKMSGSRSV1>\n"+
"</OFX>"), 0, nil, nil, nil, nil, nil)
"</OFX>"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
}
func TestOFXTransactionDataFileParseImportedData_ParseAmount_CommaAsDecimalPoint(t *testing.T) {
converter := OFXTransactionDataImporter
importer := OFXTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -416,7 +418,7 @@ func TestOFXTransactionDataFileParseImportedData_ParseAmount_CommaAsDecimalPoint
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"<OFX>\n"+
" <BANKMSGSRSV1>\n"+
" <STMTTRNRS>\n"+
@@ -435,7 +437,7 @@ func TestOFXTransactionDataFileParseImportedData_ParseAmount_CommaAsDecimalPoint
" </STMTRS>\n"+
" </STMTTRNRS>\n"+
" </BANKMSGSRSV1>\n"+
"</OFX>"), 0, nil, nil, nil, nil, nil)
"</OFX>"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err)
@@ -444,7 +446,7 @@ func TestOFXTransactionDataFileParseImportedData_ParseAmount_CommaAsDecimalPoint
}
func TestOFXTransactionDataFileParseImportedData_ParseInvalidAmount(t *testing.T) {
converter := OFXTransactionDataImporter
importer := OFXTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -452,7 +454,7 @@ func TestOFXTransactionDataFileParseImportedData_ParseInvalidAmount(t *testing.T
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"<OFX>\n"+
" <BANKMSGSRSV1>\n"+
" <STMTTRNRS>\n"+
@@ -471,12 +473,12 @@ func TestOFXTransactionDataFileParseImportedData_ParseInvalidAmount(t *testing.T
" </STMTRS>\n"+
" </STMTTRNRS>\n"+
" </BANKMSGSRSV1>\n"+
"</OFX>"), 0, nil, nil, nil, nil, nil)
"</OFX>"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
}
func TestOFXTransactionDataFileParseImportedData_ParseTransactionCurrency(t *testing.T) {
converter := OFXTransactionDataImporter
importer := OFXTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -484,7 +486,7 @@ func TestOFXTransactionDataFileParseImportedData_ParseTransactionCurrency(t *tes
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"<OFX>\n"+
" <BANKMSGSRSV1>\n"+
" <STMTTRNRS>\n"+
@@ -504,7 +506,7 @@ func TestOFXTransactionDataFileParseImportedData_ParseTransactionCurrency(t *tes
" </STMTRS>\n"+
" </STMTTRNRS>\n"+
" </BANKMSGSRSV1>\n"+
"</OFX>"), 0, nil, nil, nil, nil, nil)
"</OFX>"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
@@ -512,7 +514,7 @@ func TestOFXTransactionDataFileParseImportedData_ParseTransactionCurrency(t *tes
}
func TestOFXTransactionDataFileParseImportedData_ParseDescription(t *testing.T) {
converter := OFXTransactionDataImporter
importer := OFXTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -520,7 +522,7 @@ func TestOFXTransactionDataFileParseImportedData_ParseDescription(t *testing.T)
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"<OFX>\n"+
" <BANKMSGSRSV1>\n"+
" <STMTTRNRS>\n"+
@@ -541,13 +543,13 @@ func TestOFXTransactionDataFileParseImportedData_ParseDescription(t *testing.T)
" </STMTRS>\n"+
" </STMTTRNRS>\n"+
" </BANKMSGSRSV1>\n"+
"</OFX>"), 0, nil, nil, nil, nil, nil)
"</OFX>"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, "foo bar\t#test", allNewTransactions[0].Comment)
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"<OFX>\n"+
" <BANKMSGSRSV1>\n"+
" <STMTTRNRS>\n"+
@@ -567,13 +569,13 @@ func TestOFXTransactionDataFileParseImportedData_ParseDescription(t *testing.T)
" </STMTRS>\n"+
" </STMTTRNRS>\n"+
" </BANKMSGSRSV1>\n"+
"</OFX>"), 0, nil, nil, nil, nil, nil)
"</OFX>"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, "Test", allNewTransactions[0].Comment)
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"<OFX>\n"+
" <BANKMSGSRSV1>\n"+
" <STMTTRNRS>\n"+
@@ -595,7 +597,7 @@ func TestOFXTransactionDataFileParseImportedData_ParseDescription(t *testing.T)
" </STMTRS>\n"+
" </STMTTRNRS>\n"+
" </BANKMSGSRSV1>\n"+
"</OFX>"), 0, nil, nil, nil, nil, nil)
"</OFX>"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
@@ -603,7 +605,7 @@ func TestOFXTransactionDataFileParseImportedData_ParseDescription(t *testing.T)
}
func TestOFXTransactionDataFileParseImportedData_MissingAccountFromNode(t *testing.T) {
converter := OFXTransactionDataImporter
importer := OFXTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -612,7 +614,7 @@ func TestOFXTransactionDataFileParseImportedData_MissingAccountFromNode(t *testi
}
// Missing Posted Date Node
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"<OFX>\n"+
" <BANKMSGSRSV1>\n"+
" <STMTTRNRS>\n"+
@@ -628,12 +630,12 @@ func TestOFXTransactionDataFileParseImportedData_MissingAccountFromNode(t *testi
" </STMTRS>\n"+
" </STMTTRNRS>\n"+
" </BANKMSGSRSV1>\n"+
"</OFX>"), 0, nil, nil, nil, nil, nil)
"</OFX>"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrMissingAccountData.Message)
}
func TestOFXTransactionDataFileParseImportedData_MissingCurrencyNode(t *testing.T) {
converter := OFXTransactionDataImporter
importer := OFXTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -642,7 +644,7 @@ func TestOFXTransactionDataFileParseImportedData_MissingCurrencyNode(t *testing.
}
// Missing Default Currency Node
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"<OFX>\n"+
" <BANKMSGSRSV1>\n"+
" <STMTTRNRS>\n"+
@@ -660,12 +662,12 @@ func TestOFXTransactionDataFileParseImportedData_MissingCurrencyNode(t *testing.
" </STMTRS>\n"+
" </STMTTRNRS>\n"+
" </BANKMSGSRSV1>\n"+
"</OFX>"), 0, nil, nil, nil, nil, nil)
"</OFX>"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message)
}
func TestOFXTransactionDataFileParseImportedData_MissingTransactionRequiredNode(t *testing.T) {
converter := OFXTransactionDataImporter
importer := OFXTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -674,7 +676,7 @@ func TestOFXTransactionDataFileParseImportedData_MissingTransactionRequiredNode(
}
// Missing Posted Date Node
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"<OFX>\n"+
" <BANKMSGSRSV1>\n"+
" <STMTTRNRS>\n"+
@@ -692,11 +694,11 @@ func TestOFXTransactionDataFileParseImportedData_MissingTransactionRequiredNode(
" </STMTRS>\n"+
" </STMTTRNRS>\n"+
" </BANKMSGSRSV1>\n"+
"</OFX>"), 0, nil, nil, nil, nil, nil)
"</OFX>"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrMissingTransactionTime.Message)
// Missing Transaction Type Node
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"<OFX>\n"+
" <BANKMSGSRSV1>\n"+
" <STMTTRNRS>\n"+
@@ -714,11 +716,11 @@ func TestOFXTransactionDataFileParseImportedData_MissingTransactionRequiredNode(
" </STMTRS>\n"+
" </STMTTRNRS>\n"+
" </BANKMSGSRSV1>\n"+
"</OFX>"), 0, nil, nil, nil, nil, nil)
"</OFX>"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrTransactionTypeInvalid.Message)
// Missing Amount Node
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"<OFX>\n"+
" <BANKMSGSRSV1>\n"+
" <STMTTRNRS>\n"+
@@ -736,6 +738,6 @@ func TestOFXTransactionDataFileParseImportedData_MissingTransactionRequiredNode(
" </STMTRS>\n"+
" </STMTTRNRS>\n"+
" </BANKMSGSRSV1>\n"+
"</OFX>"), 0, nil, nil, nil, nil, nil)
"</OFX>"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
}
@@ -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, 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, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezone, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
}
@@ -2,9 +2,11 @@ package qif
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/models"
@@ -12,7 +14,7 @@ import (
)
func TestQIFTransactionDataFileParseImportedData_MinimumValidData(t *testing.T) {
converter := QifYearMonthDayTransactionDataImporter
importer := QifYearMonthDayTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -20,7 +22,7 @@ func TestQIFTransactionDataFileParseImportedData_MinimumValidData(t *testing.T)
DefaultCurrency: "CNY",
}
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := converter.ParseImportedData(context, user, []byte(
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := importer.ParseImportedData(context, user, []byte(
"!Type:Bank\n"+
"D2024-09-01\n"+
"T123.45\n"+
@@ -42,7 +44,7 @@ func TestQIFTransactionDataFileParseImportedData_MinimumValidData(t *testing.T)
"D2024-09-05\n"+
"T0.06\n"+
"L[Test Account2]\n"+
"^\n"), 0, nil, nil, nil, nil, nil)
"^\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err)
@@ -113,7 +115,7 @@ func TestQIFTransactionDataFileParseImportedData_MinimumValidData(t *testing.T)
}
func TestQIFTransactionDataFileParseImportedData_ParseYearMonthDayDateFormatTime(t *testing.T) {
converter := QifYearMonthDayTransactionDataImporter
importer := QifYearMonthDayTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -121,7 +123,7 @@ func TestQIFTransactionDataFileParseImportedData_ParseYearMonthDayDateFormatTime
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"!Type:Bank\n"+
"D2024-09-01\n"+
"T-123.45\n"+
@@ -137,7 +139,7 @@ func TestQIFTransactionDataFileParseImportedData_ParseYearMonthDayDateFormatTime
"^\n"+
"D2024'9.5\n"+
"T-123.45\n"+
"^\n"), 0, nil, nil, nil, nil, nil)
"^\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err)
@@ -151,7 +153,7 @@ func TestQIFTransactionDataFileParseImportedData_ParseYearMonthDayDateFormatTime
}
func TestQIFTransactionDataFileParseImportedData_ParseMonthDayYearDateFormatTime(t *testing.T) {
converter := QifMonthDayYearTransactionDataImporter
importer := QifMonthDayYearTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -159,7 +161,7 @@ func TestQIFTransactionDataFileParseImportedData_ParseMonthDayYearDateFormatTime
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"!Type:Bank\n"+
"D09-01-2024\n"+
"T-123.45\n"+
@@ -175,7 +177,7 @@ func TestQIFTransactionDataFileParseImportedData_ParseMonthDayYearDateFormatTime
"^\n"+
"D9.5'2024\n"+
"T-123.45\n"+
"^\n"), 0, nil, nil, nil, nil, nil)
"^\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err)
@@ -189,7 +191,7 @@ func TestQIFTransactionDataFileParseImportedData_ParseMonthDayYearDateFormatTime
}
func TestQIFTransactionDataFileParseImportedData_ParseDayYearMonthDateFormatTime(t *testing.T) {
converter := QifDayMonthYearTransactionDataImporter
importer := QifDayMonthYearTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -197,7 +199,7 @@ func TestQIFTransactionDataFileParseImportedData_ParseDayYearMonthDateFormatTime
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"!Type:Bank\n"+
"D01-09-2024\n"+
"T-123.45\n"+
@@ -213,7 +215,7 @@ func TestQIFTransactionDataFileParseImportedData_ParseDayYearMonthDateFormatTime
"^\n"+
"D5'9.2024\n"+
"T-123.45\n"+
"^\n"), 0, nil, nil, nil, nil, nil)
"^\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err)
@@ -227,7 +229,7 @@ func TestQIFTransactionDataFileParseImportedData_ParseDayYearMonthDateFormatTime
}
func TestQIFTransactionDataFileParseImportedData_ParseShortYearMonthDayDateFormatTime(t *testing.T) {
converter := QifYearMonthDayTransactionDataImporter
importer := QifYearMonthDayTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -235,7 +237,7 @@ func TestQIFTransactionDataFileParseImportedData_ParseShortYearMonthDayDateForma
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"!Type:Bank\n"+
"D24-09-01\n"+
"T-123.45\n"+
@@ -251,7 +253,7 @@ func TestQIFTransactionDataFileParseImportedData_ParseShortYearMonthDayDateForma
"^\n"+
"D24'9.5\n"+
"T-123.45\n"+
"^\n"), 0, nil, nil, nil, nil, nil)
"^\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err)
@@ -265,7 +267,7 @@ func TestQIFTransactionDataFileParseImportedData_ParseShortYearMonthDayDateForma
}
func TestQIFTransactionDataFileParseImportedData_ParseInvalidTime(t *testing.T) {
converter := QifYearMonthDayTransactionDataImporter
importer := QifYearMonthDayTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -273,16 +275,16 @@ func TestQIFTransactionDataFileParseImportedData_ParseInvalidTime(t *testing.T)
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"!Type:Bank\n"+
"D2024 09 01\n"+
"T-123.45\n"+
"^\n"), 0, nil, nil, nil, nil, nil)
"^\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
}
func TestQIFTransactionDataFileParseImportedData_ParseAmountWithThousandsSeparator(t *testing.T) {
converter := QifYearMonthDayTransactionDataImporter
importer := QifYearMonthDayTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -290,11 +292,11 @@ func TestQIFTransactionDataFileParseImportedData_ParseAmountWithThousandsSeparat
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"!Type:Bank\n"+
"D2024-09-01\n"+
"T-123,456.78\n"+
"^\n"), 0, nil, nil, nil, nil, nil)
"^\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err)
@@ -304,7 +306,7 @@ func TestQIFTransactionDataFileParseImportedData_ParseAmountWithThousandsSeparat
}
func TestQIFTransactionDataFileParseImportedData_ParseInvalidAmount(t *testing.T) {
converter := QifYearMonthDayTransactionDataImporter
importer := QifYearMonthDayTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -312,16 +314,16 @@ func TestQIFTransactionDataFileParseImportedData_ParseInvalidAmount(t *testing.T
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"!Type:Bank\n"+
"D2024-09-01\n"+
"T-123 45\n"+
"^\n"), 0, nil, nil, nil, nil, nil)
"^\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
}
func TestQIFTransactionDataFileParseImportedData_ParseAccountType(t *testing.T) {
converter := QifYearMonthDayTransactionDataImporter
importer := QifYearMonthDayTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -329,11 +331,11 @@ func TestQIFTransactionDataFileParseImportedData_ParseAccountType(t *testing.T)
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"!Type:Cash\n"+
"D2024-09-01\n"+
"T-123.45\n"+
"^\n"), 0, 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))
@@ -342,11 +344,11 @@ func TestQIFTransactionDataFileParseImportedData_ParseAccountType(t *testing.T)
assert.Equal(t, int64(1725148800), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
assert.Equal(t, int64(12345), allNewTransactions[0].Amount)
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"!Type:CCard\n"+
"D2024-09-01\n"+
"T-123.45\n"+
"^\n"), 0, 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))
@@ -355,11 +357,11 @@ func TestQIFTransactionDataFileParseImportedData_ParseAccountType(t *testing.T)
assert.Equal(t, int64(1725148800), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
assert.Equal(t, int64(12345), allNewTransactions[0].Amount)
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"!Type:Oth A\n"+
"D2024-09-01\n"+
"T-123.45\n"+
"^\n"), 0, 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))
@@ -368,11 +370,11 @@ func TestQIFTransactionDataFileParseImportedData_ParseAccountType(t *testing.T)
assert.Equal(t, int64(1725148800), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
assert.Equal(t, int64(12345), allNewTransactions[0].Amount)
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"!Type:Oth L\n"+
"D2024-09-01\n"+
"T-123.45\n"+
"^\n"), 0, 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))
@@ -383,7 +385,7 @@ func TestQIFTransactionDataFileParseImportedData_ParseAccountType(t *testing.T)
}
func TestQIFTransactionDataFileParseImportedData_ParseAccount(t *testing.T) {
converter := QifYearMonthDayTransactionDataImporter
importer := QifYearMonthDayTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -391,14 +393,14 @@ func TestQIFTransactionDataFileParseImportedData_ParseAccount(t *testing.T) {
DefaultCurrency: "CNY",
}
allNewTransactions, allNewAccounts, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
allNewTransactions, allNewAccounts, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"!Account\n"+
"NTest Account\n"+
"^\n"+
"!Type:Bank\n"+
"D2024-09-01\n"+
"T-123.45\n"+
"^\n"), 0, 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))
@@ -412,7 +414,7 @@ func TestQIFTransactionDataFileParseImportedData_ParseAccount(t *testing.T) {
}
func TestQIFTransactionDataFileParseImportedData_ParseAmountWithLeadingPlusSign(t *testing.T) {
converter := QifYearMonthDayTransactionDataImporter
importer := QifYearMonthDayTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -420,11 +422,11 @@ func TestQIFTransactionDataFileParseImportedData_ParseAmountWithLeadingPlusSign(
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"!Type:Bank\n"+
"D2024-09-01\n"+
"T+123.45\n"+
"^\n"), 0, 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))
@@ -432,7 +434,7 @@ func TestQIFTransactionDataFileParseImportedData_ParseAmountWithLeadingPlusSign(
}
func TestQIFTransactionDataFileParseImportedData_ParseSubCategory(t *testing.T) {
converter := QifYearMonthDayTransactionDataImporter
importer := QifYearMonthDayTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -440,12 +442,12 @@ func TestQIFTransactionDataFileParseImportedData_ParseSubCategory(t *testing.T)
DefaultCurrency: "CNY",
}
allNewTransactions, _, allNewSubExpenseCategories, _, _, _, err := converter.ParseImportedData(context, user, []byte(
allNewTransactions, _, allNewSubExpenseCategories, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"!Type:Bank\n"+
"D2024-09-01\n"+
"T-123.45\n"+
"LTest Category:Sub Category\n"+
"^\n"), 0, 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))
@@ -458,7 +460,7 @@ func TestQIFTransactionDataFileParseImportedData_ParseSubCategory(t *testing.T)
}
func TestQIFTransactionDataFileParseImportedData_ParseDescription(t *testing.T) {
converter := QifYearMonthDayTransactionDataImporter
importer := QifYearMonthDayTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -466,7 +468,7 @@ func TestQIFTransactionDataFileParseImportedData_ParseDescription(t *testing.T)
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"!Type:Bank\n"+
"D2024-09-01\n"+
"T-123.45\n"+
@@ -476,7 +478,24 @@ func TestQIFTransactionDataFileParseImportedData_ParseDescription(t *testing.T)
"D2024-09-02\n"+
"T-234.56\n"+
"PTest2\n"+
"^\n"), 0, 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))
assert.Equal(t, "foo bar\t#test", allNewTransactions[0].Comment)
assert.Equal(t, "", allNewTransactions[1].Comment)
allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"!Type:Bank\n"+
"D2024-09-01\n"+
"T-123.45\n"+
"PTest\n"+
"Mfoo bar\t#test\n"+
"^\n"+
"D2024-09-02\n"+
"T-234.56\n"+
"PTest2\n"+
"^\n"), time.UTC, converter.DefaultImporterOptions.WithPayeeAsDescription(), nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 2, len(allNewTransactions))
@@ -484,8 +503,41 @@ func TestQIFTransactionDataFileParseImportedData_ParseDescription(t *testing.T)
assert.Equal(t, "Test2", allNewTransactions[1].Comment)
}
func TestQIFTransactionDataFileParseImportedData_WithAdditionalOptions(t *testing.T) {
importer := QifYearMonthDayTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
Uid: 1234567890,
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"!Type:Bank\n"+
"D2024-09-01\n"+
"T-123.45\n"+
"PTest2\n"+
"^\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, 0, len(allNewTransactions[0].OriginalTagNames))
allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"!Type:Bank\n"+
"D2024-09-01\n"+
"T-123.45\n"+
"PTest2\n"+
"^\n"), time.UTC, converter.DefaultImporterOptions.WithPayeeAsTag(), nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, 1, len(allNewTransactions[0].OriginalTagNames))
assert.Equal(t, "Test2", allNewTransactions[0].OriginalTagNames[0])
}
func TestQIFTransactionDataFileParseImportedData_MissingRequiredFields(t *testing.T) {
converter := QifYearMonthDayTransactionDataImporter
importer := QifYearMonthDayTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -494,16 +546,16 @@ func TestQIFTransactionDataFileParseImportedData_MissingRequiredFields(t *testin
}
// Missing Time Field
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"!Type:Bank\n"+
"T-123.45\n"+
"^\n"), 0, 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 = converter.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"!Type:Bank\n"+
"D2024-09-01\n"+
"^\n"), 0, nil, nil, nil, nil, nil)
"^\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
}
@@ -22,6 +22,7 @@ var qifTransactionSupportedColumns = map[datatable.TransactionDataTableColumn]bo
datatable.TRANSACTION_DATA_TABLE_AMOUNT: true,
datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME: true,
datatable.TRANSACTION_DATA_TABLE_DESCRIPTION: true,
datatable.TRANSACTION_DATA_TABLE_PAYEE: true,
}
// qifDateFormatType represents the quicken interchange format (qif) date format type
@@ -184,8 +185,10 @@ func (t *qifTransactionDataRowIterator) parseTransaction(ctx core.Context, user
if qifTransaction.Memo != "" {
data[datatable.TRANSACTION_DATA_TABLE_DESCRIPTION] = qifTransaction.Memo
} else if qifTransaction.Payee != "" && qifTransaction.Payee != qifOpeningBalancePayeeText {
data[datatable.TRANSACTION_DATA_TABLE_DESCRIPTION] = qifTransaction.Payee
}
if qifTransaction.Payee != "" && qifTransaction.Payee != qifOpeningBalancePayeeText {
data[datatable.TRANSACTION_DATA_TABLE_PAYEE] = qifTransaction.Payee
}
return data, nil
@@ -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, 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, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezone, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
}
@@ -6,6 +6,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/models"
@@ -13,7 +14,7 @@ import (
)
func TestWeChatPayCsvFileImporterParseImportedData_MinimumValidData(t *testing.T) {
converter := WeChatPayTransactionDataCsvFileImporter
importer := WeChatPayTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -31,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 := converter.ParseImportedData(context, user, []byte(data), 0, 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))
@@ -93,7 +94,7 @@ func TestWeChatPayCsvFileImporterParseImportedData_MinimumValidData(t *testing.T
}
func TestWeChatPayCsvFileImporterParseImportedData_ParseRefundTransaction(t *testing.T) {
converter := WeChatPayTransactionDataCsvFileImporter
importer := WeChatPayTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -108,7 +109,7 @@ func TestWeChatPayCsvFileImporterParseImportedData_ParseRefundTransaction(t *tes
"----------------------微信支付账单明细列表--------------------,,,,\n" +
"交易时间,交易类型,收/支,金额(元),当前状态\n" +
"2024-09-01 01:23:45,xxx-退款,收入,¥0.12,已全额退款\n"
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(data1), 0, 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)
@@ -120,7 +121,7 @@ func TestWeChatPayCsvFileImporterParseImportedData_ParseRefundTransaction(t *tes
}
func TestWeChatPayCsvFileImporterParseImportedData_ParseInvalidTime(t *testing.T) {
converter := WeChatPayTransactionDataCsvFileImporter
importer := WeChatPayTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -135,7 +136,7 @@ func TestWeChatPayCsvFileImporterParseImportedData_ParseInvalidTime(t *testing.T
"----------------------微信支付账单明细列表--------------------,,,,\n" +
"交易时间,交易类型,收/支,金额(元),当前状态\n" +
"2024-09-01T01:23:45,二维码收款,收入,¥0.12,已收钱\n"
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(data1), 0, 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" +
@@ -145,12 +146,12 @@ func TestWeChatPayCsvFileImporterParseImportedData_ParseInvalidTime(t *testing.T
"----------------------微信支付账单明细列表--------------------,,,,\n" +
"交易时间,交易类型,收/支,金额(元),当前状态\n" +
"09/01/2024 12:34:56,二维码收款,收入,¥0.12,已收钱\n"
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data2), 0, 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)
}
func TestWeChatPayCsvFileImporterParseImportedData_ParseInvalidType(t *testing.T) {
converter := WeChatPayTransactionDataCsvFileImporter
importer := WeChatPayTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -165,12 +166,12 @@ func TestWeChatPayCsvFileImporterParseImportedData_ParseInvalidType(t *testing.T
"----------------------微信支付账单明细列表--------------------,,,,\n" +
"交易时间,交易类型,收/支,金额(元),当前状态\n" +
"2024-09-01T01:23:45,xxx,,¥0.12,支付成功\n"
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(data), 0, 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)
}
func TestWeChatPayCsvFileImporterParseImportedData_ParseInvalidAmount(t *testing.T) {
converter := WeChatPayTransactionDataCsvFileImporter
importer := WeChatPayTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -185,12 +186,12 @@ func TestWeChatPayCsvFileImporterParseImportedData_ParseInvalidAmount(t *testing
"----------------------微信支付账单明细列表--------------------,,,,\n" +
"交易时间,交易类型,收/支,金额(元),当前状态\n" +
"2024-09-01 01:23:45,二维码收款,收入,¥,已收钱\n"
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(data), 0, 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)
}
func TestWeChatPayCsvFileImporterParseImportedData_ParseAccountName(t *testing.T) {
converter := WeChatPayTransactionDataCsvFileImporter
importer := WeChatPayTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -206,7 +207,7 @@ func TestWeChatPayCsvFileImporterParseImportedData_ParseAccountName(t *testing.T
"----------------------微信支付账单明细列表--------------------,,,,\n" +
"交易时间,交易类型,收/支,金额(元),支付方式,当前状态\n" +
"2024-09-01 01:23:45,二维码收款,收入,¥0.12,/,已收钱\n"
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(data1), 0, 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))
@@ -222,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 = converter.ParseImportedData(context, user, []byte(data2), 0, 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))
@@ -238,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 = converter.ParseImportedData(context, user, []byte(data3), 0, 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))
@@ -255,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 = converter.ParseImportedData(context, user, []byte(data4), 0, 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))
@@ -272,7 +273,7 @@ func TestWeChatPayCsvFileImporterParseImportedData_ParseAccountName(t *testing.T
"2024-09-03 23:59:59,信用卡还款,/,¥0.01,零钱,支付成功\n"
assert.Nil(t, err)
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data5), 0, 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))
@@ -281,7 +282,7 @@ func TestWeChatPayCsvFileImporterParseImportedData_ParseAccountName(t *testing.T
}
func TestWeChatPayCsvFileImporterParseImportedData_ParseDescription(t *testing.T) {
converter := WeChatPayTransactionDataCsvFileImporter
importer := WeChatPayTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -296,7 +297,7 @@ func TestWeChatPayCsvFileImporterParseImportedData_ParseDescription(t *testing.T
"----------------------微信支付账单明细列表--------------------,,,,\n" +
"交易时间,交易类型,收/支,金额(元),当前状态,备注\n" +
"2024-09-01 01:23:45,二维码收款,收入,¥0.12,已收钱,\"/\"\n"
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(data1), 0, 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))
@@ -309,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 = converter.ParseImportedData(context, user, []byte(data2), 0, 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)
@@ -320,13 +321,13 @@ func TestWeChatPayCsvFileImporterParseImportedData_ParseDescription(t *testing.T
"----------------------微信支付账单明细列表--------------------,,,,\n" +
"交易时间,交易类型,商品,收/支,金额(元),当前状态,备注\n" +
"2024-09-01 01:23:45,二维码收款,Test,收入,¥0.12,已收钱,\"\"\n"
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data3), 0, 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)
}
func TestWeChatPayCsvFileImporterParseImportedData_SkipUnknownTransferTransaction(t *testing.T) {
converter := WeChatPayTransactionDataCsvFileImporter
importer := WeChatPayTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -341,12 +342,12 @@ func TestWeChatPayCsvFileImporterParseImportedData_SkipUnknownTransferTransactio
"----------------------微信支付账单明细列表--------------------,,,,\n" +
"交易时间,交易类型,收/支,金额(元),当前状态\n" +
"2024-09-01 23:59:59,/,/,¥0.05,充值完成\n"
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(data), 0, 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)
}
func TestWeChatPayCsvFileImporterParseImportedData_MissingFileHeader(t *testing.T) {
converter := WeChatPayTransactionDataCsvFileImporter
importer := WeChatPayTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -356,15 +357,15 @@ func TestWeChatPayCsvFileImporterParseImportedData_MissingFileHeader(t *testing.
data := "交易时间,交易类型,收/支,金额(元),当前状态\n" +
"2024-09-01 01:23:45,二维码收款,收入,¥0.12,已收钱\n"
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(data), 0, 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 = converter.ParseImportedData(context, user, []byte(""), 0, 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)
}
func TestWeChatPayCsvFileImporterParseImportedData_MissingRequiredColumn(t *testing.T) {
converter := WeChatPayTransactionDataCsvFileImporter
importer := WeChatPayTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -380,7 +381,7 @@ func TestWeChatPayCsvFileImporterParseImportedData_MissingRequiredColumn(t *test
"----------------------微信支付账单明细列表--------------------,,,,\n" +
"交易类型,收/支,金额(元),当前状态\n" +
"二维码收款,收入,¥0.12,已收钱\n"
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(data1), 0, 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
@@ -391,7 +392,7 @@ func TestWeChatPayCsvFileImporterParseImportedData_MissingRequiredColumn(t *test
"----------------------微信支付账单明细列表--------------------,,,,\n" +
"交易时间,收/支,金额(元),当前状态\n" +
"2024-09-01 01:23:45,收入,¥0.12,已收钱\n"
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data2), 0, 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
@@ -402,7 +403,7 @@ func TestWeChatPayCsvFileImporterParseImportedData_MissingRequiredColumn(t *test
"----------------------微信支付账单明细列表--------------------,,,,\n" +
"交易时间,交易类型,金额(元),当前状态\n" +
"2024-09-01 01:23:45,二维码收款,¥0.12,已收钱\n"
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data3), 0, 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
@@ -413,7 +414,7 @@ func TestWeChatPayCsvFileImporterParseImportedData_MissingRequiredColumn(t *test
"----------------------微信支付账单明细列表--------------------,,,,\n" +
"交易时间,交易类型,收/支,当前状态\n" +
"2024-09-01 01:23:45,二维码收款,收入,已收钱\n"
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data4), 0, 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
@@ -424,12 +425,12 @@ func TestWeChatPayCsvFileImporterParseImportedData_MissingRequiredColumn(t *test
"----------------------微信支付账单明细列表--------------------,,,,\n" +
"交易时间,交易类型,收/支,金额(元)\n" +
"2024-09-01 01:23:45,二维码收款,收入,¥0.12\n"
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data5), 0, 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)
}
func TestWeChatPayCsvFileImporterParseImportedData_NoTransactionData(t *testing.T) {
converter := WeChatPayTransactionDataCsvFileImporter
importer := WeChatPayTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -443,6 +444,6 @@ func TestWeChatPayCsvFileImporterParseImportedData_NoTransactionData(t *testing.
",,,,\n" +
"----------------------微信支付账单明细列表--------------------,,,,\n" +
"交易时间,交易类型,收/支,金额(元),当前状态\n"
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(data), 0, 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)
}
@@ -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, 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, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezone, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
}
+4
View File
@@ -0,0 +1,4 @@
package core
// ApplicationName represents the application name
const ApplicationName = "ezBookkeeping"
+38 -7
View File
@@ -4,6 +4,7 @@ import (
"net"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
@@ -25,6 +26,9 @@ const RemoteClientPortHeader = "X-Real-Port"
// ClientTimezoneOffsetHeaderName represents the header name of client timezone offset
const ClientTimezoneOffsetHeaderName = "X-Timezone-Offset"
// ClientTimezoneNameHeaderName represents the header name of client timezone name
const ClientTimezoneNameHeaderName = "X-Timezone-Name"
const tokenHeaderName = "Authorization"
const tokenHeaderValuePrefix = "bearer "
const tokenQueryStringParam = "token"
@@ -183,16 +187,24 @@ func (c *WebContext) GetClientLocale() string {
return value
}
// GetClientTimezoneOffset returns the client timezone offset
func (c *WebContext) GetClientTimezoneOffset() (int16, error) {
value := c.GetHeader(ClientTimezoneOffsetHeaderName)
offset, err := strconv.Atoi(value)
func (c *WebContext) GetClientTimezone() (*time.Location, error) {
timezoneName := c.getClientTimezoneName()
if err != nil {
return 0, err
if timezoneName != "" {
location, err := time.LoadLocation(timezoneName)
if err == nil && location != nil {
return location, nil
}
}
return int16(offset), nil
utcOffset, err := c.getClientTimezoneOffset()
if err != nil {
return nil, err
}
return time.FixedZone("Client Fixed Timezone", int(utcOffset)*60), nil
}
// SetResponseError sets the response error
@@ -211,6 +223,25 @@ func (c *WebContext) GetResponseError() *errs.Error {
return err.(*errs.Error)
}
// GetClientTimezoneOffset returns the client timezone offset
func (c *WebContext) getClientTimezoneOffset() (int16, error) {
value := c.GetHeader(ClientTimezoneOffsetHeaderName)
offset, err := strconv.Atoi(value)
if err != nil {
return 0, err
}
return int16(offset), nil
}
// GetClientTimezoneName returns the client timezone name
func (c *WebContext) getClientTimezoneName() string {
value := c.GetHeader(ClientTimezoneNameHeaderName)
return value
}
// WrapWebContext returns a context wrapped by this file
func WrapWebContext(ginCtx *gin.Context) *WebContext {
return &WebContext{
+2
View File
@@ -43,6 +43,8 @@ const (
NormalSubcategoryLargeLanguageModel = 15
NormalSubcategoryUserExternalAuth = 16
NormalSubcategoryOAuth2 = 17
NormalSubcategoryInsightsExplorer = 18
NormalSubcategoryTagGroup = 19
)
// Error represents the specific error returned to user
+10
View File
@@ -0,0 +1,10 @@
package errs
import "net/http"
// Error codes related to insights explorers
var (
ErrInsightsExplorerIdInvalid = NewNormalError(NormalSubcategoryInsightsExplorer, 0, http.StatusBadRequest, "explorer id is invalid")
ErrInsightsExplorerNotFound = NewNormalError(NormalSubcategoryInsightsExplorer, 1, http.StatusBadRequest, "explorer not found")
ErrInsightsExplorerDataInvalid = NewNormalError(NormalSubcategoryInsightsExplorer, 2, http.StatusBadRequest, "explorer data is invalid")
)
+5
View File
@@ -94,3 +94,8 @@ func GetParameterInvalidHexRGBColorMessage(field string) string {
func GetParameterInvalidAmountFilterMessage(field string) string {
return fmt.Sprintf("parameter \"%s\" is invalid amount filter", field)
}
// GetParameterInvalidTagFilterMessage returns specific error message for invalid tag filter parameter error
func GetParameterInvalidTagFilterMessage(field string) string {
return fmt.Sprintf("parameter \"%s\" is invalid tag filter", field)
}
+5
View File
@@ -17,4 +17,9 @@ var (
ErrInvalidOAuth2Token = NewNormalError(NormalSubcategoryOAuth2, 8, http.StatusBadRequest, "invalid oauth2 token")
ErrCannotRetrieveUserInfo = NewNormalError(NormalSubcategoryOAuth2, 9, http.StatusBadRequest, "cannot retrieve user info from oauth2 provider")
ErrOAuth2UserAlreadyBoundToAnotherUser = NewNormalError(NormalSubcategoryOAuth2, 10, http.StatusBadRequest, "oauth2 user already bound to another user")
ErrOAuth2UserNameAndEmailEmpty = NewNormalError(NormalSubcategoryOAuth2, 11, http.StatusBadRequest, "user name and email from oauth2 provider are both empty")
ErrOAuth2UserNameEmpty = NewNormalError(NormalSubcategoryOAuth2, 12, http.StatusBadRequest, "user name from oauth2 provider is empty")
ErrOAuth2EmailEmpty = NewNormalError(NormalSubcategoryOAuth2, 13, http.StatusBadRequest, "email from oauth2 provider is empty")
ErrOAuth2UserNameEmptyCannotRegister = NewNormalError(NormalSubcategoryOAuth2, 14, http.StatusBadRequest, "user name from oauth2 provider is empty, cannot register new user")
ErrOAuth2EmailEmptyCannotRegister = NewNormalError(NormalSubcategoryOAuth2, 15, http.StatusBadRequest, "email from oauth2 provider is empty, cannot register new user")
)
+10
View File
@@ -0,0 +1,10 @@
package errs
import "net/http"
// Error codes related to transaction tag groups
var (
ErrTransactionTagGroupIdInvalid = NewNormalError(NormalSubcategoryTagGroup, 0, http.StatusBadRequest, "transaction tag group id is invalid")
ErrTransactionTagGroupNotFound = NewNormalError(NormalSubcategoryTagGroup, 1, http.StatusBadRequest, "transaction tag group not found")
ErrTransactionTagGroupInUseCannotBeDeleted = NewNormalError(NormalSubcategoryTagGroup, 2, http.StatusBadRequest, "transaction tag group is in use and cannot be deleted")
)
@@ -7,10 +7,10 @@ import (
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/httpclient"
"github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/settings"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
// HttpExchangeRatesDataSource defines the structure of http exchange rates data source
@@ -41,6 +41,10 @@ func (e *CommonHttpExchangeRatesDataProvider) GetLatestExchangeRates(c core.Cont
for i := 0; i < len(requests); i++ {
req := requests[i]
req = req.WithContext(httpclient.CustomHttpResponseLog(c, func(data []byte) {
log.Debugf(c, "[common_http_exchange_rates_data_provider.GetLatestExchangeRates] response#%d is %s", i, data)
}))
resp, err := e.httpClient.Do(req)
if err != nil {
@@ -51,8 +55,6 @@ func (e *CommonHttpExchangeRatesDataProvider) GetLatestExchangeRates(c core.Cont
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
log.Debugf(c, "[common_http_exchange_rates_data_provider.GetLatestExchangeRates] response#%d is %s", i, body)
if resp.StatusCode != 200 {
log.Errorf(c, "[common_http_exchange_rates_data_provider.GetLatestExchangeRates] failed to get latest exchange rate data response for user \"uid:%d\", because response code is %d", uid, resp.StatusCode)
return nil, errs.ErrFailedToRequestRemoteApi
@@ -106,6 +108,6 @@ func (e *CommonHttpExchangeRatesDataProvider) GetLatestExchangeRates(c core.Cont
func newCommonHttpExchangeRatesDataProvider(config *settings.Config, dataSource HttpExchangeRatesDataSource) *CommonHttpExchangeRatesDataProvider {
return &CommonHttpExchangeRatesDataProvider{
dataSource: dataSource,
httpClient: utils.NewHttpClient(config.ExchangeRatesRequestTimeout, config.ExchangeRatesProxy, config.ExchangeRatesSkipTLSVerify, settings.GetUserAgent()),
httpClient: httpclient.NewHttpClient(config.ExchangeRatesRequestTimeout, config.ExchangeRatesProxy, config.ExchangeRatesSkipTLSVerify, settings.GetUserAgent(), config.EnableDebugLog),
}
}
@@ -24,7 +24,7 @@ func TestExchangeRatesApiLatestExchangeRateHandler_ReserveBankOfAustraliaDataSou
assert.Equal(t, "AUD", exchangeRateResponse.BaseCurrency)
supportedCurrencyCodes := []string{"CAD", "CHF", "CNY", "EUR", "GBP", "HKD", "IDR", "INR", "JPY", "KRW",
"MYR", "NZD", "PHP", "SGD", "THB", "TWD", "USD", "VND"}
"MYR", "NZD", "PGK", "PHP", "SGD", "THB", "TWD", "USD", "VND"}
checkExchangeRatesHaveSpecifiedCurrencies(t, exchangeRateResponse.BaseCurrency, supportedCurrencyCodes, exchangeRateResponse.ExchangeRates)
}
@@ -55,7 +55,7 @@ func TestExchangeRatesApiLatestExchangeRateHandler_CzechNationalBankDataSource(t
assert.Equal(t, "CZK", exchangeRateResponse.BaseCurrency)
supportedCurrencyCodes := []string{"AED", "AFN", "ALL", "AMD", "AOA", "ARS", "AUD", "AWG", "AZN",
"BAM", "BBD", "BDT", "BGN", "BHD", "BIF", "BMD", "BND", "BOB", "BRL", "BSD", "BTN", "BWP", "BYN", "BZD",
"BAM", "BBD", "BDT", "BHD", "BIF", "BMD", "BND", "BOB", "BRL", "BSD", "BTN", "BWP", "BYN", "BZD",
"CAD", "CDF", "CHF", "CLP", "CNY", "COP", "CRC", "CUP", "CVE", "DJF", "DKK", "DOP", "DZD",
"EGP", "ERN", "ETB", "EUR", "FJD", "FKP", "GBP", "GEL", "GHS", "GIP", "GMD", "GNF", "GTQ", "GYD",
"HKD", "HNL", "HTG", "HUF", "IDR", "ILS", "INR", "IQD", "IRR", "ISK", "JMD", "JOD", "JPY",
@@ -79,7 +79,7 @@ func TestExchangeRatesApiLatestExchangeRateHandler_DanmarksNationalbankDataSourc
assert.Equal(t, "DKK", exchangeRateResponse.BaseCurrency)
supportedCurrencyCodes := []string{"AUD", "BGN", "BRL", "CAD", "CHF", "CNY", "CZK", "EUR", "GBP", "HKD", "HUF",
supportedCurrencyCodes := []string{"AUD", "BRL", "CAD", "CHF", "CNY", "CZK", "EUR", "GBP", "HKD", "HUF",
"IDR", "ILS", "INR", "ISK", "JPY", "KRW", "MXN", "MYR", "NOK", "NZD", "PHP", "PLN", "RON", "SEK", "SGD",
"THB", "TRY", "USD", "ZAR"}
@@ -95,7 +95,7 @@ func TestExchangeRatesApiLatestExchangeRateHandler_EuroCentralBankDataSource(t *
assert.Equal(t, "EUR", exchangeRateResponse.BaseCurrency)
supportedCurrencyCodes := []string{"AUD", "BGN", "BRL", "CAD", "CHF", "CNY", "CZK", "DKK", "GBP", "HKD", "HUF",
supportedCurrencyCodes := []string{"AUD", "BRL", "CAD", "CHF", "CNY", "CZK", "DKK", "GBP", "HKD", "HUF",
"IDR", "ILS", "INR", "ISK", "JPY", "KRW", "MXN", "MYR", "NOK", "NZD", "PHP", "PLN", "RON", "SEK", "SGD",
"THB", "TRY", "USD", "ZAR"}
@@ -111,7 +111,7 @@ func TestExchangeRatesApiLatestExchangeRateHandler_NationalBankOfGeorgiaDataSour
assert.Equal(t, "GEL", exchangeRateResponse.BaseCurrency)
supportedCurrencyCodes := []string{"AED", "AMD", "AUD", "AZN", "BGN", "BRL", "BYN", "CAD", "CHF", "CNY", "CZK",
supportedCurrencyCodes := []string{"AED", "AMD", "AUD", "AZN", "BRL", "BYN", "CAD", "CHF", "CNY", "CZK",
"DKK", "EGP", "EUR", "GBP", "HKD", "HUF", "ILS", "INR", "IRR", "ISK", "JPY", "KGS", "KRW", "KWD", "KZT",
"MDL", "NOK", "NZD", "PLN", "QAR", "RON", "RSD", "RUB", "SEK", "SGD", "TJS", "TMT", "TRY",
"UAH", "USD", "UZS", "ZAR"}
@@ -128,7 +128,7 @@ func TestExchangeRatesApiLatestExchangeRateHandler_CentralBankOfHungaryDataSourc
assert.Equal(t, "HUF", exchangeRateResponse.BaseCurrency)
supportedCurrencyCodes := []string{"AUD", "BGN", "BRL", "CAD", "CHF", "CNY", "CZK", "DKK", "EUR",
supportedCurrencyCodes := []string{"AUD", "BRL", "CAD", "CHF", "CNY", "CZK", "DKK", "EUR",
"GBP", "HKD", "IDR", "ILS", "INR", "ISK", "JPY", "KRW", "MXN", "MYR", "NOK", "NZD",
"PHP", "PLN", "RON", "RSD", "RUB", "SEK", "SGD", "THB", "TRY", "UAH", "USD", "ZAR"}
@@ -192,7 +192,7 @@ func TestExchangeRatesApiLatestExchangeRateHandler_NationalBankOfPolandDataSourc
assert.Equal(t, "PLN", exchangeRateResponse.BaseCurrency)
supportedCurrencyCodes := []string{"AED", "AFN", "ALL", "AMD", "AOA", "ARS", "AUD", "AWG", "AZN",
"BAM", "BBD", "BDT", "BGN", "BHD", "BIF", "BND", "BOB", "BRL", "BSD", "BWP", "BYN", "BZD",
"BAM", "BBD", "BDT", "BHD", "BIF", "BND", "BOB", "BRL", "BSD", "BWP", "BYN", "BZD",
"CAD", "CDF", "CHF", "CLP", "CNY", "COP", "CRC", "CUP", "CVE", "CZK",
"DJF", "DKK", "DOP", "DZD", "EGP", "ERN", "ETB", "EUR", "FJD",
"GBP", "GEL", "GHS", "GIP", "GMD", "GNF", "GTQ", "GYD",
@@ -218,7 +218,7 @@ func TestExchangeRatesApiLatestExchangeRateHandler_NationalBankOfRomaniaDataSour
assert.Equal(t, "RON", exchangeRateResponse.BaseCurrency)
supportedCurrencyCodes := []string{"AED", "AUD", "BGN", "BRL", "CAD", "CHF", "CNY", "CZK", "DKK", "EGP",
supportedCurrencyCodes := []string{"AED", "AUD", "BRL", "CAD", "CHF", "CNY", "CZK", "DKK", "EGP",
"EUR", "GBP", "HKD", "HUF", "IDR", "ILS", "INR", "ISK", "JPY", "KRW", "MDL", "MXN", "MYR",
"NOK", "NZD", "PHP", "PLN", "RSD", "RUB", "SEK", "SGD", "THB", "TRY", "UAH", "USD", "ZAR"}
@@ -234,7 +234,7 @@ func TestExchangeRatesApiLatestExchangeRateHandler_BankOfRussiaDataSource(t *tes
assert.Equal(t, "RUB", exchangeRateResponse.BaseCurrency)
supportedCurrencyCodes := []string{"AED", "AMD", "AUD", "AZN", "BDT", "BGN", "BHD", "BOB", "BRL", "BYN",
supportedCurrencyCodes := []string{"AED", "AMD", "AUD", "AZN", "BDT", "BHD", "BOB", "BRL", "BYN",
"CAD", "CHF", "CNY", "CUP", "CZK", "DKK", "DZD", "EGP", "ETB", "EUR", "GBP", "GEL", "HKD", "HUF",
"IDR", "INR", "IRR", "JPY", "KGS", "KRW", "KZT", "MDL", "MMK", "MNT", "NGN", "NOK", "NZD",
"OMR", "PLN", "QAR", "RON", "RSD", "SAR", "SEK", "SGD", "THB", "TJS", "TMT", "TRY",
@@ -267,7 +267,7 @@ func TestExchangeRatesApiLatestExchangeRateHandler_NationalBankOfUkraineDataSour
assert.Equal(t, "UAH", exchangeRateResponse.BaseCurrency)
supportedCurrencyCodes := []string{
"AED", "AUD", "AZN", "BDT", "BGN", "CAD", "CHF", "CNY", "CZK", "DKK",
"AED", "AUD", "AZN", "BDT", "CAD", "CHF", "CNY", "CZK", "DKK",
"DZD", "EGP", "EUR", "GBP", "GEL", "HKD", "HUF", "IDR", "ILS", "INR",
"JPY", "KRW", "KZT", "LBP", "MDL", "MXN", "MYR", "NOK", "NZD", "PLN",
"RON", "RSD", "SAR", "SEK", "SGD", "THB", "TND", "TRY", "USD", "VND", "ZAR"}
@@ -1,15 +1,18 @@
package utils
package httpclient
import (
"bytes"
"crypto/tls"
"io"
"net/http"
"net/url"
"time"
)
type defaultTransport struct {
defaultUserAgent string
baseTransport http.RoundTripper
defaultUserAgent string
enableHttpResponseLog bool
baseTransport http.RoundTripper
}
func (t *defaultTransport) RoundTrip(req *http.Request) (*http.Response, error) {
@@ -19,11 +22,30 @@ func (t *defaultTransport) RoundTrip(req *http.Request) (*http.Response, error)
req.Header.Del("User-Agent")
}
return t.baseTransport.RoundTrip(req)
resp, err := t.baseTransport.RoundTrip(req)
if t.enableHttpResponseLog && err == nil {
ctx := req.Context()
if handler, ok := ctx.Value(logHandleKey).(HttpResponseLogHandlerFunc); ok {
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
handler(body)
resp.Body = io.NopCloser(bytes.NewReader(body))
}
}
return resp, err
}
// NewHttpClient creates and returns a new http client with specified settings
func NewHttpClient(requestTimeout uint32, proxy string, skipTLSVerify bool, defaultUserAgent string) *http.Client {
func NewHttpClient(requestTimeout uint32, proxy string, skipTLSVerify bool, defaultUserAgent string, enableHttpResponseLog bool) *http.Client {
baseTransport := http.DefaultTransport.(*http.Transport).Clone()
SetProxyUrl(baseTransport, proxy)
@@ -35,8 +57,9 @@ func NewHttpClient(requestTimeout uint32, proxy string, skipTLSVerify bool, defa
return &http.Client{
Transport: &defaultTransport{
defaultUserAgent: defaultUserAgent,
baseTransport: baseTransport,
defaultUserAgent: defaultUserAgent,
enableHttpResponseLog: enableHttpResponseLog,
baseTransport: baseTransport,
},
Timeout: time.Duration(requestTimeout) * time.Millisecond,
}
+35
View File
@@ -0,0 +1,35 @@
package httpclient
import (
"github.com/mayswind/ezbookkeeping/pkg/core"
)
const (
logHandleKey = "log_handler"
)
// HttpResponseLogHandlerFunc represents the http response log handler function
type HttpResponseLogHandlerFunc func([]byte)
// httpRequestContext represents the context for http request
type httpRequestContext struct {
core.Context
logHandler HttpResponseLogHandlerFunc
}
// Value returns the value associated with key
func (c *httpRequestContext) Value(key any) any {
if key == logHandleKey {
return c.logHandler
}
return c.Context.Value(key)
}
// CustomHttpResponseLog returns a context with http response log handler
func CustomHttpResponseLog(c core.Context, responseLogHandler HttpResponseLogHandlerFunc) core.Context {
return &httpRequestContext{
Context: c,
logHandler: responseLogHandler,
}
}
@@ -26,7 +26,7 @@ func InitializeLargeLanguageModelProvider(config *settings.Config) error {
var err error = nil
if config.ReceiptImageRecognitionLLMConfig != nil {
Container.receiptImageRecognitionCurrentProvider, err = initializeLargeLanguageModelProvider(config.ReceiptImageRecognitionLLMConfig)
Container.receiptImageRecognitionCurrentProvider, err = initializeLargeLanguageModelProvider(config.ReceiptImageRecognitionLLMConfig, config.EnableDebugLog)
if err != nil {
return err
@@ -36,17 +36,17 @@ func InitializeLargeLanguageModelProvider(config *settings.Config) error {
return nil
}
func initializeLargeLanguageModelProvider(llmConfig *settings.LLMConfig) (provider.LargeLanguageModelProvider, error) {
func initializeLargeLanguageModelProvider(llmConfig *settings.LLMConfig, enableResponseLog bool) (provider.LargeLanguageModelProvider, error) {
if llmConfig.LLMProvider == settings.OpenAILLMProvider {
return openai.NewOpenAILargeLanguageModelProvider(llmConfig), nil
return openai.NewOpenAILargeLanguageModelProvider(llmConfig, enableResponseLog), nil
} else if llmConfig.LLMProvider == settings.OpenAICompatibleLLMProvider {
return openai.NewOpenAICompatibleLargeLanguageModelProvider(llmConfig), nil
return openai.NewOpenAICompatibleLargeLanguageModelProvider(llmConfig, enableResponseLog), nil
} else if llmConfig.LLMProvider == settings.OpenRouterLLMProvider {
return openai.NewOpenRouterLargeLanguageModelProvider(llmConfig), nil
return openai.NewOpenRouterLargeLanguageModelProvider(llmConfig, enableResponseLog), nil
} else if llmConfig.LLMProvider == settings.OllamaLLMProvider {
return ollama.NewOllamaLargeLanguageModelProvider(llmConfig), nil
return ollama.NewOllamaLargeLanguageModelProvider(llmConfig, enableResponseLog), nil
} else if llmConfig.LLMProvider == settings.GoogleAILLMProvider {
return googleai.NewGoogleAILargeLanguageModelProvider(llmConfig), nil
return googleai.NewGoogleAILargeLanguageModelProvider(llmConfig, enableResponseLog), nil
} else if llmConfig.LLMProvider == "" {
return nil, nil
}
@@ -7,11 +7,11 @@ import (
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/httpclient"
"github.com/mayswind/ezbookkeeping/pkg/llm/data"
"github.com/mayswind/ezbookkeeping/pkg/llm/provider"
"github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/settings"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
// HttpLargeLanguageModelAdapter defines the structure of http large language model adapter
@@ -57,6 +57,10 @@ func (p *CommonHttpLargeLanguageModelProvider) getTextualResponse(c core.Context
return nil, errs.ErrFailedToRequestRemoteApi
}
httpRequest = httpRequest.WithContext(httpclient.CustomHttpResponseLog(c, func(data []byte) {
log.Debugf(c, "[common_http_large_language_model_provider.getTextualResponse] response is %s", data)
}))
resp, err := p.httpClient.Do(httpRequest)
if err != nil {
@@ -67,8 +71,6 @@ func (p *CommonHttpLargeLanguageModelProvider) getTextualResponse(c core.Context
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
log.Debugf(c, "[common_http_large_language_model_provider.getTextualResponse] response is %s", body)
if resp.StatusCode != 200 {
log.Errorf(c, "[common_http_large_language_model_provider.getTextualResponse] failed to get large language model api response for user \"uid:%d\", because response code is %d", uid, resp.StatusCode)
return nil, errs.ErrFailedToRequestRemoteApi
@@ -78,9 +80,9 @@ func (p *CommonHttpLargeLanguageModelProvider) getTextualResponse(c core.Context
}
// NewCommonHttpLargeLanguageModelProvider creates a http adapter based large language model provider instance
func NewCommonHttpLargeLanguageModelProvider(llmConfig *settings.LLMConfig, adapter HttpLargeLanguageModelAdapter) *CommonHttpLargeLanguageModelProvider {
func NewCommonHttpLargeLanguageModelProvider(llmConfig *settings.LLMConfig, enableResponseLog bool, adapter HttpLargeLanguageModelAdapter) *CommonHttpLargeLanguageModelProvider {
return &CommonHttpLargeLanguageModelProvider{
adapter: adapter,
httpClient: utils.NewHttpClient(llmConfig.LargeLanguageModelAPIRequestTimeout, llmConfig.LargeLanguageModelAPIProxy, llmConfig.LargeLanguageModelAPISkipTLSVerify, settings.GetUserAgent()),
httpClient: httpclient.NewHttpClient(llmConfig.LargeLanguageModelAPIRequestTimeout, llmConfig.LargeLanguageModelAPIProxy, llmConfig.LargeLanguageModelAPISkipTLSVerify, settings.GetUserAgent(), enableResponseLog),
}
}
@@ -159,8 +159,8 @@ func (p *GoogleAILargeLanguageModelAdapter) buildJsonRequestBody(c core.Context,
}
// NewGoogleAILargeLanguageModelProvider creates a new Google AI large language model provider instance
func NewGoogleAILargeLanguageModelProvider(llmConfig *settings.LLMConfig) provider.LargeLanguageModelProvider {
return common.NewCommonHttpLargeLanguageModelProvider(llmConfig, &GoogleAILargeLanguageModelAdapter{
func NewGoogleAILargeLanguageModelProvider(llmConfig *settings.LLMConfig, enableResponseLog bool) provider.LargeLanguageModelProvider {
return common.NewCommonHttpLargeLanguageModelProvider(llmConfig, enableResponseLog, &GoogleAILargeLanguageModelAdapter{
GoogleAIAPIKey: llmConfig.GoogleAIAPIKey,
GoogleAIModelID: llmConfig.GoogleAIModelID,
})
@@ -158,8 +158,8 @@ func (p *OllamaLargeLanguageModelAdapter) getOllamaRequestUrl() string {
}
// NewOllamaLargeLanguageModelProvider creates a new Ollama large language model provider instance
func NewOllamaLargeLanguageModelProvider(llmConfig *settings.LLMConfig) provider.LargeLanguageModelProvider {
return common.NewCommonHttpLargeLanguageModelProvider(llmConfig, &OllamaLargeLanguageModelAdapter{
func NewOllamaLargeLanguageModelProvider(llmConfig *settings.LLMConfig, enableResponseLog bool) provider.LargeLanguageModelProvider {
return common.NewCommonHttpLargeLanguageModelProvider(llmConfig, enableResponseLog, &OllamaLargeLanguageModelAdapter{
OllamaServerURL: llmConfig.OllamaServerURL,
OllamaModelID: llmConfig.OllamaModelID,
})
@@ -36,8 +36,8 @@ func (p *OpenAIOfficialChatCompletionsAPIProvider) GetModelID() string {
}
// NewOpenAILargeLanguageModelProvider creates a new OpenAI large language model provider instance
func NewOpenAILargeLanguageModelProvider(llmConfig *settings.LLMConfig) provider.LargeLanguageModelProvider {
return newCommonOpenAIChatCompletionsAPILargeLanguageModelAdapter(llmConfig, &OpenAIOfficialChatCompletionsAPIProvider{
func NewOpenAILargeLanguageModelProvider(llmConfig *settings.LLMConfig, enableResponseLog bool) provider.LargeLanguageModelProvider {
return newCommonOpenAIChatCompletionsAPILargeLanguageModelAdapter(llmConfig, enableResponseLog, &OpenAIOfficialChatCompletionsAPIProvider{
OpenAIAPIKey: llmConfig.OpenAIAPIKey,
OpenAIModelID: llmConfig.OpenAIModelID,
})
@@ -213,8 +213,8 @@ func (p *CommonOpenAIChatCompletionsAPILargeLanguageModelAdapter) buildJsonReque
return requestBodyBytes, nil
}
func newCommonOpenAIChatCompletionsAPILargeLanguageModelAdapter(llmConfig *settings.LLMConfig, apiProvider OpenAIChatCompletionsAPIProvider) provider.LargeLanguageModelProvider {
return common.NewCommonHttpLargeLanguageModelProvider(llmConfig, &CommonOpenAIChatCompletionsAPILargeLanguageModelAdapter{
func newCommonOpenAIChatCompletionsAPILargeLanguageModelAdapter(llmConfig *settings.LLMConfig, enableResponseLog bool, apiProvider OpenAIChatCompletionsAPIProvider) provider.LargeLanguageModelProvider {
return common.NewCommonHttpLargeLanguageModelProvider(llmConfig, enableResponseLog, &CommonOpenAIChatCompletionsAPILargeLanguageModelAdapter{
apiProvider: apiProvider,
})
}
@@ -50,8 +50,8 @@ func (p *OpenAICompatibleChatCompletionsAPIProvider) getFinalChatCompletionsRequ
}
// NewOpenAICompatibleLargeLanguageModelProvider creates a new OpenAI compatible large language model provider instance
func NewOpenAICompatibleLargeLanguageModelProvider(llmConfig *settings.LLMConfig) provider.LargeLanguageModelProvider {
return newCommonOpenAIChatCompletionsAPILargeLanguageModelAdapter(llmConfig, &OpenAICompatibleChatCompletionsAPIProvider{
func NewOpenAICompatibleLargeLanguageModelProvider(llmConfig *settings.LLMConfig, enableResponseLog bool) provider.LargeLanguageModelProvider {
return newCommonOpenAIChatCompletionsAPILargeLanguageModelAdapter(llmConfig, enableResponseLog, &OpenAICompatibleChatCompletionsAPIProvider{
OpenAICompatibleBaseURL: llmConfig.OpenAICompatibleBaseURL,
OpenAICompatibleAPIKey: llmConfig.OpenAICompatibleAPIKey,
OpenAICompatibleModelID: llmConfig.OpenAICompatibleModelID,
@@ -27,7 +27,7 @@ func (p *OpenRouterChatCompletionsAPIProvider) BuildChatCompletionsHttpRequest(c
req.Header.Set("Authorization", "Bearer "+p.OpenRouterAPIKey)
req.Header.Set("HTTP-Referer", "https://ezbookkeeping.mayswind.net/")
req.Header.Set("X-Title", "ezBookkeeping")
req.Header.Set("X-Title", core.ApplicationName)
return req, nil
}
@@ -38,8 +38,8 @@ func (p *OpenRouterChatCompletionsAPIProvider) GetModelID() string {
}
// NewOpenRouterLargeLanguageModelProvider creates a new OpenRouter large language model provider instance
func NewOpenRouterLargeLanguageModelProvider(llmConfig *settings.LLMConfig) provider.LargeLanguageModelProvider {
return newCommonOpenAIChatCompletionsAPILargeLanguageModelAdapter(llmConfig, &OpenRouterChatCompletionsAPIProvider{
func NewOpenRouterLargeLanguageModelProvider(llmConfig *settings.LLMConfig, enableResponseLog bool) provider.LargeLanguageModelProvider {
return newCommonOpenAIChatCompletionsAPILargeLanguageModelAdapter(llmConfig, enableResponseLog, &OpenRouterChatCompletionsAPIProvider{
OpenRouterAPIKey: llmConfig.OpenRouterAPIKey,
OpenRouterModelID: llmConfig.OpenRouterModelID,
})
+9
View File
@@ -24,6 +24,9 @@ var AllLanguages = map[string]*LocaleInfo{
"ja": {
Content: ja,
},
"kn": {
Content: kn,
},
"ko": {
Content: ko,
},
@@ -36,9 +39,15 @@ var AllLanguages = map[string]*LocaleInfo{
"ru": {
Content: ru,
},
"sl": {
Content: sl,
},
"th": {
Content: th,
},
"tr": {
Content: tr,
},
"uk": {
Content: uk,
},
+6
View File
@@ -6,12 +6,18 @@ import (
// LocaleTextItems represents all text items need to be translated
type LocaleTextItems struct {
GlobalTextItems *GlobalTextItems
DefaultTypes *DefaultTypes
DataConverterTextItems *DataConverterTextItems
VerifyEmailTextItems *VerifyEmailTextItems
ForgetPasswordMailTextItems *ForgetPasswordMailTextItems
}
// GlobalTextItems represents global text items need to be translated
type GlobalTextItems struct {
AppName string
}
// DefaultTypes represents default types for the language
type DefaultTypes struct {
DecimalSeparator core.DecimalSeparator
+3
View File
@@ -5,6 +5,9 @@ import (
)
var de = &LocaleTextItems{
GlobalTextItems: &GlobalTextItems{
AppName: "ezBookkeeping",
},
DefaultTypes: &DefaultTypes{
DecimalSeparator: core.DECIMAL_SEPARATOR_COMMA,
DigitGroupingSymbol: core.DIGIT_GROUPING_SYMBOL_DOT,
+3
View File
@@ -5,6 +5,9 @@ import (
)
var en = &LocaleTextItems{
GlobalTextItems: &GlobalTextItems{
AppName: "ezBookkeeping",
},
DefaultTypes: &DefaultTypes{
DecimalSeparator: core.DECIMAL_SEPARATOR_DOT,
DigitGroupingSymbol: core.DIGIT_GROUPING_SYMBOL_COMMA,
+3
View File
@@ -5,6 +5,9 @@ import (
)
var es = &LocaleTextItems{
GlobalTextItems: &GlobalTextItems{
AppName: "ezBookkeeping",
},
DefaultTypes: &DefaultTypes{
DecimalSeparator: core.DECIMAL_SEPARATOR_COMMA,
DigitGroupingSymbol: core.DIGIT_GROUPING_SYMBOL_DOT,

Some files were not shown because too many files have changed in this diff Show More