Compare commits

...

381 Commits

Author SHA1 Message Date
zhengchen.tao d629dfe18c docs: README fork notice 改动清单翻译为英文(面向 GitHub mirror 受众)
FORK.md 仍保持中文。
2026-05-07 15:47:00 +08:00
zhengchen.tao 76a274e1cc docs: README 移除已回滚的「交易时间点击标题默认日期选择器」条目
与 FORK.md item #8 的回滚状态对齐(commit 78818470)。
2026-05-07 15:32:24 +08:00
zhengchen.tao 291bd86c94 LICENSE: fork 版权署名顺序订正 Tao Zhengchen → Zhengchen Tao 2026-05-05 18:57:09 +08:00
zhengchen.tao 8658e849e7 ci: 镜像路径 dev/ezbookkeeping → zhengchen.tao/ezbookkeeping
仓库已转移到 zhengchen.tao 命名空间,build-image.yml 里的 OCI source label /
image tags / build summary 三处硬编码路径同步更新,否则 push 会推到不存在的
dev 命名空间。

NAS 上 nas-infra/ezbookkeeping/docker-compose.yml 的 image 路径仍需手动更新。
2026-05-05 18:20:30 +08:00
zhengchen.tao 6d0329210f FORK.md 进度表同步:item #8 标记已回滚 2026-05-05 18:10:00 +08:00
zhengchen.tao c57c1e8490 revert(transaction-time): 回滚 #header label click 改 'date' 模式
实际无可见效果——用户点的是下方 #title 里的日期/时间文本(上游 commit 368322f9
已实现按点击内容路由),#header label 行很少被点。改回上游行为。

FORK.md item #8 标记为已回滚。
2026-05-05 18:09:31 +08:00
zhengchen.tao 2425c358e3 docs: 扩充 README fork notice,列出主要定制项;LICENSE 增加 fork 修改的版权行 2026-05-05 18:05:18 +08:00
zhengchen.tao 373ccba9d6 build-image.yml 注释微调(再次触发,验证 DSM API 1.43 锁定后 build 通)
"等" → "等等",顺便利用这次 push 验证 nas-infra cd71de1 把
DOCKER_API_VERSION=1.43 加到 runner 容器 env 后,docker login 是否过。
2026-05-04 01:18:07 +08:00
zhengchen.tao ce345f79ab build-image.yml 注释微调(触发 concurrency cancel-in-progress 测试)
把 "重新打包旧 commit / 用自定义 tag / 等" 里多余的斜杠改成顿号,
顺便利用这次 push 验证上一条 d92e4fe3 的 in-progress run 是否被取消。
2026-05-04 00:40:43 +08:00
zhengchen.tao 6baf668696 build/deploy 合并为单 workflow 双 job,删除 deploy.yml
原 workflow_run 链触发会在 Actions 列表产生两条独立 run,UX 割裂。
合并后单 run + dependency graph 显式串联 build → deploy。

代价:失去"不 rebuild 只 redeploy"的 UI 单点触发,临时只想
重启容器需直接 ssh NAS 跑 docker compose up -d。

paths-ignore 同步移除已不存在的 deploy.yml 项。
2026-05-04 00:37:44 +08:00
zhengchen.tao 9ef0e62b05 fix(deploy): 加 docker login 步骤,否则 pull 私有 registry 401
build-image 跑过 docker login,但 deploy 是独立 workflow 容器,
凭据不继承。需要在 deploy 这边也登一次同一个 PACKAGES_TOKEN。

deploy.yml 在 paths-ignore 里,这次提交不会触发 build。但会触发
deploy 自己(不在 paths-ignore,且 workflow_dispatch 仍可手动)。
要测的话手动 dispatch Deploy Docker Image 即可。
2026-05-02 23:39:15 +08:00
zhengchen.tao 7df2d49c56 build-image paths-ignore 加 deploy.yml / sync-upstream.yml
改 deploy.yml 或 sync-upstream.yml 都不影响镜像内容(前者是部署
脚本、后者是 main reset 脚本),原本会触发整套 ~10 min 的 build
是浪费。把它俩加进 paths-ignore 跳过。

build-image.yml 自己保留触发(不在 ignore 里),这样改 workflow
能 self-test 验证改动有效。
2026-05-02 21:23:18 +08:00
zhengchen.tao 65d52571de build/deploy workflow 加 concurrency cancel-in-progress
并发组 = workflow name + ref。同分支连续 push 时:
- 新 run 入组发现已有 in-progress run → 立即取消旧的,新的开跑
- 最终只构建 + 部署最新代码,省 CI 时间
- 不同分支的 build/deploy 互不干扰(虽然当前只 custom 用)
- build 与 deploy 是两个独立 workflow name,互不影响(build 跑时
  deploy 不会被取消,反之亦然)

CLAUDE.md 同步加"并发取消策略"段说明该行为。
2026-05-02 21:18:39 +08:00
zhengchen.tao 4bdd2c7195 deploy.yml 移除 CUSTOM_DEPLOY_SCRIPTS 变量,部署脚本内联
原 vars.CUSTOM_DEPLOY_SCRIPTS 来自 deploy.yml 上游模板设计,本意是
"通用 hook,让一份 deploy.yml 复用到不同项目"。本仓库只有一个项目
一种部署场景,这层抽象纯属累赘:
- 改部署逻辑得去 Gitea UI 点 Variables,没法 PR review
- git log 看不到部署逻辑改动
- 脚本到底跑啥得对照 yml + Variable 两处

直接把 docker compose pull/up 内联进 deploy.yml,单一事实源在 git。

支持私有 / 公开两种 nas-infra:
- secrets.NAS_INFRA_TOKEN 设了 → 用 token clone(私有适用)
- 没设 → 裸 URL clone(公开适用)

CLAUDE.md 同步更新 deploy.yml 的描述。
2026-05-02 21:10:35 +08:00
zhengchen.tao 9da91ad54f deploy.yml 改自动触发:build 成功后 workflow_run 链式触发
- on.workflow_run: 监听 Build Docker Image 完成事件,分支限 custom
- if 条件:仅在 build 成功时跑 deploy(失败时跳过,避免部署半成品)
- workflow_dispatch 保留作为手动备选(重新部署当前镜像 / 应急脚本)
- 脚本生成改 > 覆盖(原 >> 会累积历史脚本)+ 加 set -e 失败即停
- 加 Deploy summary 步骤把触发链路信息写入 GITHUB_STEP_SUMMARY
  方便从 UI 看到本次 deploy 跟在哪次 build 后面

CLAUDE.md 同步更新 workflow 清单 + 流程图:现在 push → build →
deploy 全自动 CD,仅需在 repo Variables 里配 CUSTOM_DEPLOY_SCRIPTS
脚本内容才能产生实际部署效果。
2026-05-02 21:07:09 +08:00
zhengchen.tao 55c175acca ci: 注释 paths-ignore 对 empty commit 的 vacuously-skip 行为
empirical discovery:c6bb0c85 那个 --allow-empty commit 推送后没有
触发 build。证实 Gitea Actions(同 GitHub Actions)对 paths-ignore
+ empty commit 的处理是"vacuously matches ignore list, skip"。

把这个 quirk 直接注释在 workflow 文件里,后续自己或 Claude 看到
build 没触发时不用再怀疑 trigger 配置错了,知道是 empty commit 的
正常行为。
2026-05-02 20:49:17 +08:00
zhengchen.tao 3ed37e7719 ci: trigger auto-build to verify push trigger works
empty commit, no code changes. 用于验证 build-image.yml 的 push trigger
确实在 commit 推送后自动跑起来,不需要去 Actions UI 手动点。

如果这次 push 后 Gitea Actions 没有自动出现新 run,说明 paths-ignore
对 empty commit 的过滤行为是"vacuously skip",需要至少一个非 ignore 路径
的真实改动才能触发。
2026-05-02 20:47:05 +08:00
zhengchen.tao 32a49be913 build-image workflow 改自动触发 + 保留手动备选
- on.push.branches: [custom] —— 推送 custom 自动跑
- on.push.paths-ignore:屏蔽 *.md / .gitignore / LICENSE / screenshot/**
  避免 doc-only 改动浪费 ~10 min 构建
- on.workflow_dispatch 保留作为应急通道(重打旧 commit / 自定义 tag)
- checkout ref 兼容两种触发:${{ inputs.branch || github.ref_name }}
  - workflow_dispatch:用用户填的 branch(默认 custom)
  - push:fallback 到 github.ref_name(即触发分支,永远是 custom)
- Build summary 加"触发方式"行,便于区分本次是自动还是手动

预期:本提交本身就会触发自动构建(改了 .gitea/workflows/build-image.yml
不在 paths-ignore 内),相当于 self-test。
2026-05-02 20:41:22 +08:00
zhengchen.tao ebcc03d3d0 docs: ci 分支已删除,更新 CLAUDE.md 与 build summary 措辞
ci 分支于 2026-05-02 删除(默认分支已切到 custom,workflow 文件已
迁回 custom)。更新各处反映"两分支模型 main + custom"的最终状态:

- CLAUDE.md "三个分支" → "两个分支",ci 段改写为"已退役 + 历史
  说明",给后续 Claude 解释 git log 里 555ecc1a 这条迁移提交
- 同步历史里 2026-05-02(后续) 那条加上"随后删掉 ci 分支"的事实
- build-image.yml 的 Build summary 步骤移除"UI 顶部 commit 是 ci"
  的警示注释(workflow 已在 custom,runs 列表 commit 直接就是
  代码 commit,不再需要这条解释)
2026-05-02 20:34:38 +08:00
zhengchen.tao 0be04287c8 docs: CLAUDE.md 更新为 workflow 已迁到 custom 的事实
- 三分支表更新:custom 是 default branch + 持有 workflow,ci
  降级为过渡态历史分支
- 改写"为什么 workflow 在 custom 不在独立分支"段,记录设计决策
  演进的真实理由(runs 列表 UX 优于 meta/code 分离的设计美感)
- workflow 清单从 5 项缩到 3 项,记录 docker-release/snapshot
  已删的事实
- "给后续 Claude" 提示中"不要把工作流提交到 custom" 改为相反
  方向(直接在 custom 改 workflow)
- 同步历史补 555ecc1a (workflow 迁移) 与 75b4d78d (numpad fix)
2026-05-02 18:38:00 +08:00
zhengchen.tao dfbc2b1440 ci: workflow 文件迁到 custom 分支
之前 workflow 在 ci 分支,导致每次 dispatch 后 Gitea Actions 列表
显示的 commit 都是 ci 分支的 workflow 文件 commit,不是被实际构建
的 custom 代码 commit,UX 上误导性强。

挪到 custom 后:
- runs 列表的 commit 字段直接显示真实代码 commit
- workflow_dispatch UI 自动从默认分支(待手动切到 custom)发现
  workflow
- rebase 上游时 workflow 文件随 custom 一起平移,无额外操作

同步移除上游残留的 docker-release.yml / docker-snapshot.yml:
- 触发依赖 secrets.DOCKER_REPO(未配),sync-upstream 推 main
  /tags 时空跑失败
- ci 上已禁用,但文件留着是噪声,本次清掉

ci 分支 .gitea/workflows/ 暂保留作过渡,待用户在 Gitea UI 把
默认分支切到 custom + 验证 build 跑通后,再单独 cleanup ci。
2026-05-02 18:35:53 +08:00
zhengchen.tao 47b5641597 docs: CLAUDE.md 解释 ci/custom 不需保持一致 + UI commit 不等于构建 commit
补充三分支拓扑后的"FAQ 段":

- ci 与 custom 内容不重叠是设计,不是 bug
- Gitea Actions UI 顶部显示的 commit 是 workflow dispatch 触发
  位置(即 ci 的 HEAD),不是构建源代码 commit
- 真实构建的代码 commit 在镜像 tag / OCI revision label /
  workflow 末尾 Build summary 三处都能看到,看 summary 区即可

附两条可选替代方案(workflow 挪 custom / 加 push trigger 自动构建),
说明当前选择的中间路径理由。
2026-05-02 18:26:40 +08:00
zhengchen.tao 11da502f75 fix(numpad): 修小键盘点击卡顿,touch-action: none → manipulation
诊断:用户反馈仅小键盘点击有延迟感,其他按钮正常。范围缩小后定位到
.numpad-button 上的 touch-action: none(上游 e178a079 引入)与 F7
内部 tap 事件处理叠加,让 click 事件合成慢一拍。backspace(自定义
.numpad-backspace-button 类)不受影响,刚好印证范围。

修复:改为 touch-action: manipulation(W3C 标准"快速点击"值),禁双
击缩放消除老 300ms 延迟,但保留 click 事件正常合成。

FORK.md #11 状态:🔍 调查中 → 🟢 已完成,附真因记录避免后续误诊。
2026-05-02 18:09:40 +08:00
zhengchen.tao 29c164439c docs: CLAUDE.md 加 ci 分支 workflow 清单 + 链 FORK.md
ci 分支 5 个 workflow(sync-upstream / build-image / deploy /
docker-snapshot / docker-release)全部列表化,含触发条件、职责、
当前状态。明确"日常只用 sync-upstream + build-image 两个,
其他三个要么按需配置要么后续清理"。

custom 分支说明里加链接指 FORK.md(feature 维度的清单),
顶部说明区域厘清"meta(CLAUDE.md)vs 改动清单(FORK.md)vs
通用决策框架(不入库)"三层关系。
2026-05-02 17:58:43 +08:00
zhengchen.tao 989ffef156 docs: rename MY_REQUIREMENTS.md → FORK.md, drop stale DEPLOY.md
- 重命名为 FORK.md,对接更通用的 fork-doc 命名约定
- DEPLOY.md 内容全过期(ghcr.io 镜像、myrequirement 分支、
  docker run 风格部署),全部已废 — 部署文档现在两层:
  nas-infra/README.md(compose level)+ CLAUDE.md(CI 排查)
- README.md 顶部加一行 fork notice,链到 FORK.md 与 CLAUDE.md
  (单行变更,rebase 友好)
- FORK.md 顶部加关联文档表
2026-05-02 17:58:34 +08:00
zhengchen.tao c929e950e1 docs: CLAUDE.md 加 backend 测试踩坑 + 通用排查原则
CI 故障排查路径表加一行"测试/lint 失败 → 先看 Dockerfile ARG",
强调先对齐上游 CI 跳过开关(BUILD_PIPELINE / CHECK_3RD_API /
SKIP_TESTS)再考虑改测试代码。同步历史补 2026-05-02 第三层修复。
2026-05-02 16:07:58 +08:00
zhengchen.tao 6b8d9fcb13 docs: 加仓库级 CLAUDE.md(分支拓扑 + 同步流程 + CI 排查路径)
记录 fork 工作流(main 锚 tag / custom 改动 / ci 工作流),
2026-05-01 rebase 与 2026-05-02 build 修复历史,
以及给后续 Claude 会话的 CI 故障分流路径表。
2026-05-02 15:35:47 +08:00
zhengchen.tao fe265259d7 更新个人需求清单,新增信用额度字段描述;新增部署说明文档,包含镜像地址及操作步骤 2026-05-01 17:29:29 +08:00
zhengchen.tao 69d66c8634 Revert "删除个人需求清单文档"
This reverts commit f045f0702a.
2026-05-01 17:29:29 +08:00
zhengchen.tao 729c04880f 更新 GitHub Actions 工作流:在推送事件中忽略 myrequirement 分支 2026-05-01 17:29:29 +08:00
zhengchen.tao 7cfb5c7457 优化 Docker 镜像构建工作流:新增小写图像名称设置,确保一致性 2026-05-01 17:29:29 +08:00
zhengchen.tao 93630a821d 新增构建和推送 Docker 镜像的 GitHub Actions 工作流 2026-05-01 17:29:29 +08:00
zhengchen.tao 501765d669 删除个人需求清单文档 2026-05-01 17:29:29 +08:00
zhengchen.tao 91fa3b65f3 新增默认展开分类列表设置,更新应用设置以支持云同步 2026-05-01 17:29:29 +08:00
zhengchen.tao b82533233e 优化分类选择功能:新增默认展开分类列表设置,更新相关页面以支持此功能 2026-05-01 17:29:29 +08:00
MaysWind 9c4a0493ee upgrade third party dependencies 2026-05-01 17:29:29 +08:00
MaysWind 9aa6c4102e delete all files before updating the translation progress files 2026-05-01 17:29:29 +08:00
MaysWind f058fa53eb update the translation progress calculation method 2026-05-01 17:29:28 +08:00
zhengchen.tao 4ff73b475a 更新个人需求清单,新增信用额度和可用额度的标注;在账户列表和交易列表中显示可用额度;修改记账页面的时间选择为默认日期选择器;优化账户余额显示逻辑 2026-04-06 01:16:38 +08:00
zhengchen.tao ba85852543 在交易列表页面顶部显示账户信息:新增账户图标、名称和余额信息卡片,优化单账户筛选时的显示逻辑 2026-04-05 18:51:27 +08:00
zhengchen.tao c7c84c74d3 优化账户余额调整功能:新增调整余额的逻辑,更新相关页面以显示账户余额和可用额度,调整路由配置以移除不必要的动画效果。 2026-04-05 18:40:52 +08:00
zhengchen.tao 5fbff39c4f 添加信用额度功能:在账户模型中新增信用额度字段,更新相关请求和响应结构,修改账户创建和修改逻辑,更新界面以支持信用额度的显示和编辑。 2026-04-05 17:04:16 +08:00
zhengchen.tao 285fef6eba 更新 .gitignore,添加 .claude/ 目录;新增 MY_REQUIREMENTS.md 文件,记录个人需求清单;优化小键盘组件,调整布局并添加删除按钮;修改路由配置,禁用某些页面的动画效果;全局样式中调整过渡时长。 2026-04-05 16:31:02 +08:00
MaysWind 97fb73ad43 add translation process badge 2026-03-30 00:40:14 +08:00
MaysWind ce0c9ec65e add new contributor 2026-03-28 17:34:42 +08:00
MaysWind ed084e1ce0 update README.md 2026-03-28 17:33:53 +08:00
1270o1 ec84065f73 Update DE translation (#540)
Big improvement to the German translation (frontend)
2026-03-28 17:26:09 +08:00
MaysWind 2e8aedcfa6 bump version to 1.5.0 2026-03-22 23:34:42 +08:00
MaysWind 422f18443a add transaction timezone offset to axis / category / series in insights explorer 2026-03-22 23:25:06 +08:00
MaysWind 0fbf185223 show year-over-year and period-over-period in trends chart 2026-03-22 01:38:35 +08:00
MaysWind 91cdffa9a6 fix incorrect ordinal translations 2026-03-21 00:49:21 +08:00
MaysWind 89199eed8b support importing WeChat statements with the latest format that includes thousand separators (#534) 2026-03-20 23:41:17 +08:00
MaysWind 1a65bb9db6 display the currency name instead of the currency code when using the source account currency or destination account currency as the axis, category or series in insights explorer 2026-03-18 00:11:55 +08:00
MaysWind 9772d9ca62 support custom quick save button styles on the mobile transaction edit page 2026-03-17 00:16:37 +08:00
MaysWind 5ee93a5db1 add attributes to disable spell check and automatic capitalization to all username input fields 2026-03-16 23:38:49 +08:00
MaysWind 85c4f686da add new contributor 2026-03-16 23:32:41 +08:00
Alex 1f066b0d1e fix:params for username field on login page mobile (#526) 2026-03-16 23:28:18 +08:00
MaysWind 38ddb7aaa3 add new contributor 2026-03-16 09:50:51 +08:00
Ivan Noleto a22931f96b Improve and standardize Brazilian Portuguese translation (#530)
* Improve and standardize Brazilian Portuguese translation

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* revert timezone translations

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-16 09:49:12 +08:00
MaysWind dcee067aea insights explorer supports sub condition 2026-03-16 02:07:36 +08:00
MaysWind 302d118ae0 remove unused code 2026-03-13 00:04:09 +08:00
MaysWind 09eea96cdc use const for variables that will not be modified 2026-03-13 00:03:58 +08:00
MaysWind 205dea9e58 move the agent skill files to the skills directory 2026-03-12 23:30:37 +08:00
MaysWind 089eabb806 clear legacy runtime cache when clearing the application code cache 2026-03-12 23:23:19 +08:00
MaysWind dd63500202 reorder the button display order 2026-03-11 01:08:11 +08:00
MaysWind 13488efdaf support clearing application code cache 2026-03-11 00:52:43 +08:00
MaysWind edcf33f49c add boxplot chart in reconciliation statement dialog 2026-03-08 23:33:46 +08:00
MaysWind d601e01029 fix incorrect html tag in axis charts tooltip 2026-03-08 23:19:32 +08:00
MaysWind 4d7c3650b5 support filtering by geographic longitude and latitude in insights explorer queries 2026-03-08 20:55:35 +08:00
MaysWind a0fd468309 fix the amounts in non-default currencies are not converted to the default currency in the statistics data shown in the insights explorer data table 2026-03-08 18:04:48 +08:00
MaysWind 0b7471879d add Coefficient of Variation to statistics data on data table tab and value metric on charts of insights explorer 2026-03-08 17:24:08 +08:00
MaysWind 282b74c95e support receiving images from the Web Share Target API Level 2 and directly opening AI image recognition on mobile version 2026-03-08 15:33:58 +08:00
MaysWind 5ce1dc973c add more icons 2026-03-08 02:15:42 +08:00
MaysWind 7ac1e0b69f add transaction hour of day to axis / category / series in insights explorer 2026-03-07 23:04:15 +08:00
MaysWind 127bed1026 fix incorrect display when use transaction year-quarter as axis / category / series in insights explorer 2026-03-07 22:52:32 +08:00
MaysWind d517a1862b add 90th percentile amount, range, interquartile range, variance, and standard deviation to the value metrics in insights explorer 2026-03-07 22:18:33 +08:00
MaysWind 8e5202b375 code refactor 2026-03-07 22:18:18 +08:00
MaysWind 301fb58917 hide some statistics when the number of transactions is not enough 2026-03-07 21:31:18 +08:00
MaysWind aedebb1461 fix test case failures when the original Chinese calendar data uses CRLF line endings 2026-03-07 21:01:23 +08:00
MaysWind 1336377598 add more statistic data on Data Table tab of Insights Explorer page 2026-03-07 18:47:57 +08:00
Dmitry Shemin 3b58dcbc4d translation/ru: fix translation to russian language (#521) 2026-03-07 16:12:04 +08:00
MaysWind 23a5f0a96f display total transactions, total amount, average amount, median amount, minimum amount, and maximum amount in the Data Table tab of Insights Explorer page 2026-03-07 01:04:47 +08:00
MaysWind b81d2ec63c fix the context menu disappears after the second long press on the add icon on the mobile home page 2026-03-06 00:43:05 +08:00
MaysWind cabe365907 disable native browser drag behavior on mobile version 2026-03-05 23:17:27 +08:00
MaysWind 54f61ecb18 add agent skill 2026-03-05 00:34:03 +08:00
MaysWind 404cd62d7b Support restricting API token access based on IP address 2026-03-04 23:49:14 +08:00
MaysWind f0f3143605 fix the user settings is reset after using the command line tool to change the user password (#516) 2026-03-04 23:18:14 +08:00
MaysWind b729fdedca update the command description 2026-03-04 22:58:16 +08:00
MaysWind 973cec2c6a automatically apply known transaction types when setting the transaction type column mapping 2026-03-04 00:36:09 +08:00
MaysWind 6e61aba050 remove deprecated Reserve Bank of Australia exchange rate data source 2026-03-03 22:49:19 +08:00
MaysWind 40a8deba12 optimize the performance of the retrieve all transactions api 2026-03-03 01:12:38 +08:00
MaysWind 0ba762ba6e support batch replacement of transaction time zones in the import tool 2026-03-02 23:54:48 +08:00
MaysWind 732c256db2 support "Add Another" in transaction add page / dialog (#471) 2026-03-02 00:54:17 +08:00
MaysWind d2ce801277 disable the sort button when fewer than two items are present 2026-03-01 23:05:27 +08:00
MaysWind 4845fdedfd automatically reload data after changed display order 2026-03-01 22:25:44 +08:00
MaysWind f5a7e2e2d6 sort transaction tags by name (#487) 2026-03-01 22:03:38 +08:00
MaysWind a84f48ae8a support syncing the settings autoUpdateExchangeRatesData, showAddTransactionButtonInDesktopNavbar, mapCacheExpiration, and exchangeRatesDataCacheExpiration 2026-03-01 21:08:34 +08:00
MaysWind c4c9503e31 update the styling used when reloading browser cache data 2026-03-01 19:31:34 +08:00
MaysWind 8c1f499ed8 upgrade golang to 1.25.7, node.js to 24.14.0 and alpine base image to 3.23.3 2026-03-01 16:50:41 +08:00
MaysWind c6eb3cfb74 support automatically applying known column mapping and transaction type mapping rules when importing custom files with column mapping handle method 2026-03-01 16:32:22 +08:00
MaysWind d7a0d253c4 support utf-32 file encoding 2026-03-01 16:04:29 +08:00
MaysWind 9d275a3051 merge UTF-8 and UTF-16 encodings with or without BOM, with BOM automatically detected and handled 2026-03-01 15:54:41 +08:00
MaysWind 8192a48bc5 support setting exchange rate cache expiration time 2026-02-28 21:36:00 +08:00
MaysWind 247181830c support caching map data when map_data_fetch_proxy is set true 2026-02-28 21:35:28 +08:00
MaysWind d5dfdc8c05 modify cache capacity calculation 2026-02-28 13:51:27 +08:00
MaysWind d95fcd8b00 add cache management page 2026-02-27 00:50:52 +08:00
MaysWind 40a366e68d modify background color of time zone tag to improve the dark theme experience 2026-02-26 00:42:02 +08:00
MaysWind 593ae10783 fix incorrect data when exporting 100% stacked charts data 2026-02-25 01:30:10 +08:00
MaysWind 75d9e11bab support exporting statistics & analysis result to mermaid 2026-02-25 01:16:42 +08:00
MaysWind 6d37d42e50 support exporting statistics & analysis result, reconciliation statement and import check result to SSV (semicolon separated values) file 2026-02-24 22:59:29 +08:00
MaysWind f9e9c9285f fix the updated transaction template is not reflected in the interface immediately after modification 2026-02-23 23:50:12 +08:00
MaysWind 314bf876f2 code refactor 2026-02-23 23:30:49 +08:00
MaysWind 61c52cc888 fix incorrect display name of sort type in the insight explorer 2026-02-23 23:25:44 +08:00
MaysWind b42f226aba update transaction edit dialog height 2026-02-23 21:17:35 +08:00
MaysWind 767b841866 add more detailed comments for amount fields 2026-02-23 01:16:27 +08:00
MaysWind fd08666f49 import transactions from custom xlsx/xls file 2026-02-23 00:50:01 +08:00
MaysWind eb662681a1 fix incorrect column count when importing mscfb excel file 2026-02-23 00:40:47 +08:00
MaysWind ef15eccc33 use consistent quotation marks in the help text 2026-02-19 10:45:12 +08:00
MaysWind e0286ff133 fix incorrect height for some toolbar buttons 2026-02-18 22:56:25 +08:00
MaysWind 2baffe3f11 reduce the size of the bottom save button 2026-02-17 23:04:16 +08:00
MaysWind 196657ee86 reduce tabbar height 2026-02-17 21:55:01 +08:00
MaysWind b4c4aafc99 support loading environment variables from .env file 2026-02-17 17:22:23 +08:00
MaysWind b907a79223 fix incorrect time for some time zones on the scheduled transaction edit page (#499) 2026-02-17 13:58:16 +08:00
MaysWind 0d213de580 code refactor 2026-02-17 13:55:35 +08:00
MaysWind 2e97d699e7 ezBookkeeping API Tools supports formatting response to table 2026-02-16 01:14:21 +08:00
MaysWind 22e4738b7a upgrade third party dependencies 2026-02-15 14:54:39 +08:00
MaysWind 4b68641043 move the user agent constants of special token into the core package 2026-02-15 01:17:43 +08:00
MaysWind 3a66a3d655 move the variables set during the building process into the core package 2026-02-15 01:17:43 +08:00
MaysWind 76d1d3aef3 fix the the Anthropic API key was not masked with asterisks in startup logs 2026-02-15 01:17:42 +08:00
Albert Brugués fe2aa5d28b update ES locale (#495) 2026-02-13 22:55:20 +08:00
MaysWind f474bbf09a update README.md 2026-02-09 00:35:52 +08:00
MaysWind c4d02db879 upgrade third party dependencies 2026-02-08 00:24:52 +08:00
MaysWind 75b36ec547 upgrade third party dependencies 2026-02-08 00:12:51 +08:00
MaysWind 43b7aea76e add new contributor 2026-02-05 09:42:47 +08:00
Dmitry Shemin 13a4a47d40 translation/ru: fix translation to russian language (#483) 2026-02-05 09:36:13 +08:00
MaysWind fd9f380922 update api command description 2026-02-03 23:51:19 +08:00
MaysWind a5fdb9d6b7 add server version command 2026-02-02 23:11:20 +08:00
MaysWind 983c65e4f8 add latest exchange rates command 2026-02-02 23:04:28 +08:00
MaysWind fa568056d3 update API tools script 2026-02-02 22:51:23 +08:00
MaysWind ea8b2812d4 add ezBookkeeping API tools 2026-02-02 09:18:54 +08:00
MaysWind b6a2aea8fd validate whether the transaction tag group exists when creating a transaction tag or move transaction tag to another group 2026-02-02 01:03:38 +08:00
MaysWind fa047bf303 llm provider supports Anthropic and Anthropic compatibility api 2026-02-01 16:17:22 +08:00
MaysWind 4177ac3d46 add missing stream field 2026-02-01 15:22:15 +08:00
MaysWind 7647f4f5b9 update README.md 2026-02-01 00:59:13 +08:00
MaysWind bab03dbde1 fix the the Google AI token was not masked with asterisks in startup logs 2026-01-31 22:43:01 +08:00
MaysWind 85db6e96af llm provider supports LM Studio 2026-01-31 22:38:08 +08:00
MaysWind 548461ade0 update git ignore file 2026-01-31 01:37:33 +08:00
MaysWind ecbf182173 bump version to 1.4.0 2026-01-31 00:48:54 +08:00
MaysWind ab38d33e31 remove deprecated International Monetary Fund exchange rate data source 2026-01-31 00:30:55 +08:00
MaysWind 0020f4ede9 support importing camt.052 bank statement file 2026-01-31 00:20:40 +08:00
MaysWind b470cb63b7 replace ambiguous names with clearer names 2026-01-30 23:35:37 +08:00
MaysWind 32f2eaef3c when pasting date time, if multiple formats match and one matches the current display order, use that format for parsing 2026-01-30 22:40:41 +08:00
MaysWind a7fc3c78eb fix incorrect style 2026-01-30 21:25:28 +08:00
MaysWind d42b3ecb5e fix the continue button was missing during two-factor authentication on the desktop version when both two-factor authentication and third-party login are enabled both 2026-01-30 21:05:45 +08:00
MaysWind 2d4a603d11 automatically focus to the passcode input when a passcode is required 2026-01-30 21:02:54 +08:00
MaysWind 7a369328b6 support date formats that use dashes as separators and date formats without leading zeros 2026-01-26 23:59:37 +08:00
MaysWind 545667e502 update the text display style for AI image recognition on the desktop version 2026-01-25 21:15:21 +08:00
MaysWind 8387a81a59 fix the incorrect line-wrapping of the third-pary login separator line on the login page in the Korean locale 2026-01-24 23:55:14 +08:00
MaysWind 1e3087ccf0 bump version to 1.3.2 2026-01-24 23:49:20 +08:00
MaysWind bee7772bfd add refresh accounts, categories and tags button on import dialog 2026-01-24 23:21:02 +08:00
MaysWind a8b6f72ee6 update translation 2026-01-24 23:02:33 +08:00
MaysWind 9484cf514d show more accurate status messages when reloading all account, category and tag data 2026-01-24 22:53:56 +08:00
MaysWind 70958c00d3 limit the maximum height of the add button dropdown menu on the desktop transaction list page 2026-01-24 22:06:45 +08:00
MaysWind 9467335536 hide change tag group display order button when there are fewer than two tag groups 2026-01-24 21:57:42 +08:00
MaysWind f916fdff06 disable the move tag button when there are fewer than two tag groups 2026-01-24 21:55:45 +08:00
MaysWind ced346506e format translation file 2026-01-24 21:44:16 +08:00
MaysWind e0cd96f87e format code 2026-01-24 21:41:44 +08:00
Minhyung Park ed7e906903 Update Korean translation 2026-01-24 21:41:22 +08:00
MaysWind 3bb7f5abf4 support exporting data when checking pending import data 2026-01-22 23:58:04 +08:00
MaysWind 5d801a2343 remove redundant code 2026-01-22 23:15:36 +08:00
MaysWind 0d9e59dad9 fix the import dialog sometimes fails to automatically remember the last selected type 2026-01-22 22:23:05 +08:00
MaysWind 5fd1396b5c fix wrong mime type 2026-01-22 22:02:52 +08:00
MaysWind b3b9d9293b support semicolon-separated data when importing delimiter-separated values files / data (#458) 2026-01-22 22:01:59 +08:00
MaysWind 8b405e513f add new contributor 2026-01-22 00:00:17 +08:00
MaysWind 2bfcfbf03d update translation 2026-01-21 23:59:12 +08:00
MaysWind 10388a5ffa improved Chinese translation 2026-01-21 23:33:41 +08:00
MaysWind a127a381cc remove unused entries 2026-01-21 23:33:15 +08:00
MaysWind 4aa0dc20af update translation 2026-01-21 23:32:40 +08:00
Diego Fernández Criado 012cc04107 Update ES locales 2026-01-21 23:31:33 +08:00
MaysWind 25a84ad3af add new contributor 2026-01-21 22:30:29 +08:00
MaysWind c0036d230a update locale default settings 2026-01-21 22:30:03 +08:00
Harsh Vardan 869970a4ab refactor(locale): remove wrongfully formatted fsl translation file by ide 2026-01-21 11:06:02 +08:00
Harsh Vardan 42f8aa410c refactor(locale): proper import order in locales index.ts file 2026-01-21 11:06:02 +08:00
Harsh Vardan 80e1223505 feat(locale): add Tamil translation 2026-01-21 11:06:02 +08:00
MaysWind fc9581580c fix the system paste button appears again after the user tap outside 2026-01-19 22:23:24 +08:00
MaysWind b0e6764bfe do not allow switching tag groups before saving changes to the tag order 2026-01-19 01:02:46 +08:00
MaysWind 03fef81414 fix no error message is shown when a tag cannot be moved 2026-01-19 00:56:07 +08:00
MaysWind 8dcb8648a5 support tag group for transaction tags in the import transaction tool 2026-01-19 00:55:34 +08:00
MaysWind 50b4c96a99 fix the placeholder values in "root_url" cannot be resolved using values from environment variables, and do not allow placeholders in other options 2026-01-19 00:29:08 +08:00
MaysWind c9b894fdbe add new contributor 2026-01-18 13:42:11 +08:00
Andres Teller a2f1d944ad fix: correct typo in Spanish translation 2026-01-18 13:32:53 +08:00
MaysWind be4ec2bcce bump version to 1.3.1 2026-01-18 13:26:27 +08:00
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
464 changed files with 50919 additions and 13998 deletions
+167
View File
@@ -0,0 +1,167 @@
name: Build Docker Image
on:
# 自动触发:push 到 custom 分支时跑(force-push 后的 rebase 也会触发,可接受)
# paths-ignore:纯文档/配置改动跳过,避免浪费 ~10 分钟构建
# ⚠️ 已知 quirk2026-05-02 验证):empty commitgit commit --allow-empty
# 不会触发 paths-ignore 过滤的 workflowGitea 把 zero-paths-changed 当作
# "vacuously matches ignore list" 跳过。要强制触发必须至少改一个非 ignore 路径
# 的真实文件(改这个 yml 自己最稳)。
push:
branches: [custom]
paths-ignore:
- '**.md'
- '.gitignore'
- 'LICENSE'
- 'screenshot/**'
# sync-upstream.yml 改的是 main reset 逻辑,跟 build 无关
# build-image.yml 自己留着会触发,作为 workflow 改动的 self-test
- '.gitea/workflows/sync-upstream.yml'
# 手动触发:保留作为应急通道(重新打包旧 commit、用自定义 tag 等等)
# 注意:手动触发也会跑 deploy job —— 如果只想 build 不部署,临时把 deploy
# job 注释掉或在 deploy 里加 if 条件
workflow_dispatch:
inputs:
branch:
description: '要打包的分支(仅手动触发生效)'
required: true
default: 'custom'
tag:
description: '镜像 tag(留空则用 commit short hash'
required: false
default: ''
# 并发控制:同一分支的连续 push 只跑最新的,旧 in-progress run 会被取消
# 例:连续 3 次 push,第 1 次 build 跑了 30s,第 2 次开始 → 取消第 1,第 2 跑;
# 期间第 3 次又来 → 取消第 2,第 3 跑。最后只构建+部署最新代码,省 CI 时间。
# group 包含 ref 是为了不同分支的 build 互不干扰(虽然当前只有 custom 用)
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout target branch
uses: actions/checkout@v4
with:
# workflow_dispatch 时用用户填的 branchpush 触发时 inputs.branch 为空,
# fallback 到 github.ref_name(即触发的分支名,push 到 custom 时就是 custom
ref: ${{ inputs.branch || github.ref_name }}
fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
# 钉到 v0.13.2(自带 runc 1.1.x),避免 runc 1.2+ 的 procfs 安全检查
# 在 DSM 老内核(4.4.x)上撞 openat2/fsmount 不存在导致 build 失败
driver-opts: |
image=moby/buildkit:v0.13.2
- name: Login to Gitea Container Registry
uses: docker/login-action@v3
with:
registry: git.zhengchentao.win
username: ${{ gitea.actor }}
password: ${{ secrets.PACKAGES_TOKEN }}
- name: Determine image tag and revision
id: meta
run: |
if [ -n "${{ inputs.tag }}" ]; then
IMAGE_TAG="${{ inputs.tag }}"
else
IMAGE_TAG="$(git rev-parse --short HEAD)"
fi
echo "image_tag=$IMAGE_TAG" >> $GITHUB_OUTPUT
echo "full_sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
echo "==> Image tag: $IMAGE_TAG"
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
# 上游 Dockerfile 用 BUILD_PIPELINE 作为 CI 跳过开关:
# 设为 "1" 时 pkg/exchangerates 跳过依赖第三方 API 的活测试
# (加拿大银行/乌兹别克央行 API 国内不稳,跑就超时)
# CHECK_3RD_API 留空 → 三方 API 测试不跑;想跑设 "1"
build-args: |
BUILD_PIPELINE=1
# OCI 标签:
# - source 让 Gitea 收包时自动把镜像关联到对应 repo(不再需要手动去
# "包设置 → 链接到仓库")
# - revision 把构建时的 commit full SHA 烙进镜像 manifest
# docker inspect 能反推回源码版本
labels: |
org.opencontainers.image.source=https://git.zhengchentao.win/zhengchen.tao/ezbookkeeping
org.opencontainers.image.revision=${{ steps.meta.outputs.full_sha }}
tags: |
git.zhengchentao.win/zhengchen.tao/ezbookkeeping:${{ steps.meta.outputs.image_tag }}
git.zhengchentao.win/zhengchen.tao/ezbookkeeping:latest
- name: Build summary
# 把构建出的镜像 tag 与源 commit 显式列在 Action run summary 区,
# 方便从 UI 一眼看到本次 build 产出。always() 保证 build 失败也输出。
if: always()
run: |
{
echo "## Build Summary"
echo ""
echo "| 项 | 值 |"
echo "|---|---|"
echo "| 触发方式 | \`${{ github.event_name }}\` |"
echo "| 源分支 | \`${{ inputs.branch || github.ref_name }}\` |"
echo "| 源 commit (full) | \`${{ steps.meta.outputs.full_sha }}\` |"
echo "| 源 commit (short) | \`${{ steps.meta.outputs.image_tag }}\` |"
echo "| 镜像 tag | \`git.zhengchentao.win/zhengchen.tao/ezbookkeeping:${{ steps.meta.outputs.image_tag }}\` + \`:latest\` |"
} >> "$GITHUB_STEP_SUMMARY"
deploy:
# needs: build 串起来 —— build 失败 deploy 自动跳过,无需 if 条件
needs: build
runs-on: ubuntu-latest
steps:
# 登录 Gitea Container Registry,否则 docker compose pull 私有镜像 401。
# 跟 build job 那步是同一个 PACKAGES_TOKEN,但每个 job 跑在独立 runner 上,
# 凭据不会从 build job 继承,必须在这里再登一次。
- name: Login to Gitea Container Registry
uses: docker/login-action@v3
with:
registry: git.zhengchentao.win
username: ${{ gitea.actor }}
password: ${{ secrets.PACKAGES_TOKEN }}
- name: Pull and restart ezbookkeeping
# 部署逻辑直接内联在这。runner 容器挂了 host docker.sock
# 所以这里 docker 命令直接操作的是宿主机 docker daemon
# 容器层面相当于 "ssh 到 NAS 跑 docker compose"。
#
# NAS_INFRA_TOKEN secret 仅在 nas-infra 是私有仓库时需要;
# 公开仓库不设这个 secret 也能拉。
env:
NAS_INFRA_TOKEN: ${{ secrets.NAS_INFRA_TOKEN }}
run: |
set -e
TMPDIR=$(mktemp -d)
trap 'rm -rf "$TMPDIR"' EXIT
# 决定 clone URL:有 token 用 token(私有),没有用裸 URL(公开)
if [ -n "$NAS_INFRA_TOKEN" ]; then
CLONE_URL="https://x-access-token:${NAS_INFRA_TOKEN}@git.zhengchentao.win/dev/nas-infra.git"
else
CLONE_URL="https://git.zhengchentao.win/dev/nas-infra.git"
fi
git clone --depth 1 "$CLONE_URL" "$TMPDIR/nas-infra"
cd "$TMPDIR/nas-infra/ezbookkeeping"
docker compose pull
docker compose up -d
# 简单 health:列容器状态 + 输出最近日志
sleep 3
docker compose ps
docker compose logs --tail=30 ezbookkeeping
-17
View File
@@ -1,17 +0,0 @@
name: Deploy Docker Image
on:
workflow_dispatch
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Execute custom script
run: |
cat >> deploy.sh <<EOF
#!/bin/sh
${{ vars.CUSTOM_DEPLOY_SCRIPTS }}
EOF
chmod +x deploy.sh
./deploy.sh
-64
View File
@@ -1,64 +0,0 @@
name: Docker Release
on:
push:
tags:
- v*
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
${{ secrets.DOCKER_REPO }}/mayswind/ezbookkeeping
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,value=latest
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Set up the environment
id: setup
run: |
echo "build_unix_time=$(date '+%s')" >> "$GITHUB_OUTPUT"
echo "build_date=$(date '+%Y%m%d')" >> "$GITHUB_OUTPUT"
sed -r -i 's#FROM( --.*)? (.*:.*)?#FROM\1 ${{ secrets.DOCKER_REPO }}/mirrors/\2#g' Dockerfile
cat >> docker/custom-backend-pre-setup.sh <<EOF
#!/bin/sh
${{ vars.CUSTOM_BACKEND_PRE_SETUP }}
EOF
cat >> docker/custom-frontend-pre-setup.sh <<EOF
#!/bin/sh
${{ vars.CUSTOM_FRONTEND_PRE_SETUP }}
EOF
chmod +x docker/custom-backend-pre-setup.sh
chmod +x docker/custom-frontend-pre-setup.sh
- name: Build and push
uses: docker/build-push-action@v6
with:
file: Dockerfile
context: .
platforms: ${{ vars.BUILD_RELEASE_PLATFORMS }}
push: true
build-args: |
RELEASE_BUILD=1
BUILD_PIPELINE=1
BUILD_UNIXTIME=${{ steps.setup.outputs.build_unix_time }}
BUILD_DATE=${{ steps.setup.outputs.build_date }}
CHECK_3RD_API=${{ vars.CHECK_3RD_API }}
SKIP_TESTS=${{ vars.SKIP_TESTS }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
-63
View File
@@ -1,63 +0,0 @@
name: Docker Snapshot
on:
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
${{ secrets.DOCKER_REPO }}/mayswind/ezbookkeeping
tags: |
type=raw,value=SNAPSHOT-{{date 'YYYYMMDD'}}
type=raw,value=latest-snapshot
type=sha,format=short,prefix=SNAPSHOT-
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Set up the environment
id: setup
run: |
echo "build_unix_time=$(date '+%s')" >> "$GITHUB_OUTPUT"
echo "build_date=$(date '+%Y%m%d')" >> "$GITHUB_OUTPUT"
sed -r -i 's#FROM( --.*)? (.*:.*)?#FROM\1 ${{ secrets.DOCKER_REPO }}/mirrors/\2#g' Dockerfile
cat >> docker/custom-backend-pre-setup.sh <<EOF
#!/bin/sh
${{ vars.CUSTOM_BACKEND_PRE_SETUP }}
EOF
cat >> docker/custom-frontend-pre-setup.sh <<EOF
#!/bin/sh
${{ vars.CUSTOM_FRONTEND_PRE_SETUP }}
EOF
chmod +x docker/custom-backend-pre-setup.sh
chmod +x docker/custom-frontend-pre-setup.sh
- name: Build and push
uses: docker/build-push-action@v6
with:
file: Dockerfile
context: .
platforms: ${{ vars.BUILD_SNAPSHOT_PLATFORMS }}
push: true
build-args: |
BUILD_PIPELINE=1
BUILD_UNIXTIME=${{ steps.setup.outputs.build_unix_time }}
BUILD_DATE=${{ steps.setup.outputs.build_date }}
CHECK_3RD_API=${{ vars.CHECK_3RD_API }}
SKIP_TESTS=${{ vars.SKIP_TESTS }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
+39
View File
@@ -0,0 +1,39 @@
name: Sync from upstream
on:
workflow_dispatch:
inputs:
tag:
description: '要同步的 release tag(留空则同步到 upstream/main 的最新 tag'
required: false
default: ''
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.SYNC_TOKEN }}
- name: Sync main to release tag
run: |
git config user.name "gitea-actions"
git config user.email "actions@gitea.local"
git remote add upstream https://git.zhengchentao.win/mirror/ezbookkeeping.git
git fetch upstream --tags
if [ -n "${{ inputs.tag }}" ]; then
TARGET="${{ inputs.tag }}"
else
TARGET=$(git tag -l --sort=-v:refname | head -n 1)
fi
echo "==> Syncing main to $TARGET"
git rev-parse "$TARGET" || { echo "❌ Tag $TARGET not found"; exit 1; }
git checkout -B main origin/main
git reset --hard "$TARGET"
git push origin main --force-with-lease
git push origin --tags
@@ -3,13 +3,13 @@ name: Build backend file for windows
inputs: inputs:
go-version: go-version:
required: false required: false
default: "1.25.3" default: "1.25.7"
mingw-version: mingw-version:
required: false required: false
default: "14.2.0" default: "15.2.0"
mingw-revison: mingw-revison:
required: false required: false
default: "v12-rev2" default: "v13-rev1"
release-build: release-build:
required: false required: false
type: string type: string
+352
View File
@@ -0,0 +1,352 @@
const fs = require('fs');
const path = require('path');
const FRONTEND_LOCALES_DIR = path.join(__dirname, '..', '..', 'src', 'locales');
const BACKEND_LOCALES_DIR = path.join(__dirname, '..', '..', 'pkg', 'locales');
const OUTPUT_DIR = process.argv[2] || path.join(__dirname, '..', '..', 'i18n-badge');
const DEFAULT_LANGUAGE_TAG = 'en';
const BACKEND_SKIP_STRUCTS = new Set([
'GlobalTextItems',
'DefaultTypes',
'DataConverterTextItems',
]);
function discoverFrontendLanguages() {
const indexPath = path.join(FRONTEND_LOCALES_DIR, 'index.ts');
const content = fs.readFileSync(indexPath, 'utf-8');
const importMap = {};
const importRegex = /import\s+(\w+)\s+from\s+['"]\.\/([\w_]+\.json)['"]/g;
let match;
while ((match = importRegex.exec(content)) !== null) {
importMap[match[1]] = match[2];
}
const result = {};
const langRegex = /['"]([^'"]+)['"]\s*:\s*\{[^}]*content\s*:\s*(\w+)/g;
while ((match = langRegex.exec(content)) !== null) {
const tag = match[1];
const varName = match[2];
if (importMap[varName]) {
result[tag] = importMap[varName];
}
}
return result;
}
function discoverBackendLanguages() {
const allLocalesPath = path.join(BACKEND_LOCALES_DIR, 'all_locales.go');
const content = fs.readFileSync(allLocalesPath, 'utf-8');
const result = {};
const entryRegex = /"([^"]+)"\s*:\s*\{[^}]*Content\s*:\s*(\w+)/g;
let match;
while ((match = entryRegex.exec(content)) !== null) {
const tag = match[1];
const fileName = tag.toLowerCase().replace(/-/g, '_') + '.go';
const filePath = path.join(BACKEND_LOCALES_DIR, fileName);
if (fs.existsSync(filePath)) {
result[tag] = fileName;
}
}
return result;
}
function flattenJSON(obj, prefix) {
const result = {};
for (const key of Object.keys(obj)) {
const fullKey = prefix ? prefix + '.' + key : key;
if (typeof obj[key] === 'object' && obj[key] !== null) {
Object.assign(result, flattenJSON(obj[key], fullKey));
} else {
result[fullKey] = obj[key];
}
}
return result;
}
function shouldSkipFrontendKey(key) {
if (key.startsWith('global.')) {
return true;
} else if (key.startsWith('default.')) {
return true;
} else if (key.startsWith('currency.')) {
if (key.startsWith('currency.unit.')) {
return true;
} else {
return false;
}
} else if (key.startsWith('mapprovider.')) {
return true;
} else if (key.startsWith('encoding.')) {
return true;
} else if (key.startsWith('document.')) {
if (key.startsWith('document.anchor.')) {
return true;
} else {
return false;
}
} else {
return false;
}
}
function isFrontendAlwaysTranslatedKey(key) {
if (key.startsWith('language.')) {
return true;
} else if (key.startsWith('format.')) {
if (key.startsWith('format.misc.')) {
if (key === 'format.misc.multiTextJoinSeparator') {
return true;
} else if (key === 'format.misc.eachMonthDayInMonthDays') {
return true;
} else {
return false;
}
} else {
return true;
}
} else if (key.startsWith('datetime.')) {
return true;
} else if (key.startsWith('timezone.')) {
return true;
} else if (key.startsWith('currency.')) {
if (key === 'currency.name.EUR') {
return true;
} else {
return false;
}
} else if (key.startsWith('parameter.')) {
if (key === 'parameter.id') {
return true;
} else {
return false;
}
} else {
if (key === 'OK') {
return true;
} else {
return false;
}
}
}
function extractGoStringFields(content) {
const fields = [];
const structBlockRegex = /(\w+):\s*&\w+\{([^}]*)\}/gs;
let blockMatch;
while ((blockMatch = structBlockRegex.exec(content)) !== null) {
const structName = blockMatch[1];
const blockBody = blockMatch[2];
const fieldRegex = /(\w+):\s+"((?:[^"\\]|\\.)*)"/g;
let fieldMatch;
while ((fieldMatch = fieldRegex.exec(blockBody)) !== null) {
fields.push({
struct: structName,
name: fieldMatch[1],
value: fieldMatch[2],
});
}
}
return fields;
}
function getProgressColor(progress) {
if (progress >= 95) {
return 'brightgreen';
} else if (progress >= 90) {
return 'green';
} else if (progress >= 70) {
return 'yellowgreen';
} else if (progress >= 50) {
return 'yellow';
} else if (progress >= 20) {
return 'orange';
} else {
return 'red';
}
}
function main() {
const frontendLangs = discoverFrontendLanguages();
const backendLangs = discoverBackendLanguages();
const allTags = new Set([...Object.keys(frontendLangs), ...Object.keys(backendLangs)]);
console.log('Discovered ' + allTags.size + ' languages: ' + [...allTags].sort().join(', '));
const defaultFrontendJSON = JSON.parse(fs.readFileSync(path.join(FRONTEND_LOCALES_DIR, `${DEFAULT_LANGUAGE_TAG}.json`), 'utf-8'));
const defaultFrontendItemsMap = flattenJSON(defaultFrontendJSON, '');
const defaultFrontendKeys = Object.keys(defaultFrontendItemsMap);
const frontendTranslatableKeys = defaultFrontendKeys.filter(function (k) {
return !shouldSkipFrontendKey(k);
});
const frontendSkippedCount = defaultFrontendKeys.length - frontendTranslatableKeys.length;
const frontendTotal = frontendTranslatableKeys.length;
const defaultBackendContent = fs.readFileSync(path.join(BACKEND_LOCALES_DIR, `${DEFAULT_LANGUAGE_TAG}.go`), 'utf-8');
const defaultBackendItems = extractGoStringFields(defaultBackendContent);
const defaultBackendTranslatableItems = defaultBackendItems.filter(function (f) {
return !BACKEND_SKIP_STRUCTS.has(f.struct);
});
const backendSkippedCount = defaultBackendItems.length - defaultBackendTranslatableItems.length;
const backendTotal = defaultBackendTranslatableItems.length;
console.log('Frontend: ' + frontendTotal + ' translatable keys (' + frontendSkippedCount + ' excluded)');
console.log('Backend: ' + backendTotal + ' translatable fields (' + backendSkippedCount + ' excluded)');
const results = {};
const untranslatedKeys = {};
for (const tag of allTags) {
results[tag] = {
languageTag: tag,
frontendTranslated: 0,
frontendTotal: frontendTotal,
backendTranslated: 0,
backendTotal: backendTotal
};
untranslatedKeys[tag] = [];
}
for (const tag of Object.keys(frontendLangs)) {
if (tag === DEFAULT_LANGUAGE_TAG) {
results[tag].frontendTranslated = frontendTotal;
continue;
}
const file = frontendLangs[tag];
const filePath = path.join(FRONTEND_LOCALES_DIR, file);
if (!fs.existsSync(filePath)) {
continue;
}
const json = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
const kv = flattenJSON(json, '');
let translated = 0;
for (const key of frontendTranslatableKeys) {
if (kv[key] !== undefined && kv[key] !== '' && (kv[key] !== defaultFrontendItemsMap[key] || isFrontendAlwaysTranslatedKey(key))) {
translated++;
} else {
untranslatedKeys[tag].push({ source: path.join('src', 'locales', file), key: key, defaultValue: defaultFrontendItemsMap[key], value: kv[key] });
}
}
results[tag].frontendTranslated = translated;
}
for (const tag of Object.keys(backendLangs)) {
if (tag === DEFAULT_LANGUAGE_TAG) {
results[tag].backendTranslated = backendTotal;
continue;
}
const file = backendLangs[tag];
const filePath = path.join(BACKEND_LOCALES_DIR, file);
if (!fs.existsSync(filePath)) {
continue;
}
const content = fs.readFileSync(filePath, 'utf-8');
const fields = extractGoStringFields(content).filter(function (f) {
return !BACKEND_SKIP_STRUCTS.has(f.struct);
});
let translated = 0;
for (let i = 0; i < defaultBackendTranslatableItems.length; i++) {
if (i < fields.length && fields[i].value !== defaultBackendTranslatableItems[i].value) {
translated++;
} else {
untranslatedKeys[tag].push({ source: path.join('pkg', 'locales', file), key: defaultBackendTranslatableItems[i].struct + '.' + defaultBackendTranslatableItems[i].name, defaultValue: defaultBackendTranslatableItems[i].value, value: (i < fields.length) ? fields[i].value : null });
}
}
results[tag].backendTranslated = translated;
}
for (const tag of Object.keys(results)) {
const r = results[tag];
const totalTranslated = r.frontendTranslated + r.backendTranslated;
const totalItems = r.frontendTotal + r.backendTotal;
r.totalProgress = Math.round((totalTranslated / totalItems) * 10000) / 100;
}
const sortedResults = {};
var sortedTags = Object.keys(results).sort();
for (const tag of sortedTags) {
sortedResults[tag] = results[tag];
}
if (!fs.existsSync(OUTPUT_DIR)) {
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
}
var badgesDir = path.join(OUTPUT_DIR, 'badges');
if (!fs.existsSync(badgesDir)) {
fs.mkdirSync(badgesDir, { recursive: true });
}
fs.writeFileSync(
path.join(OUTPUT_DIR, 'i18n-progress.json'),
JSON.stringify(sortedResults, null, 4) + '\n'
);
for (const tag of sortedTags) {
const data = sortedResults[tag];
const badge = {
schemaVersion: 1,
label: 'translation',
message: data.totalProgress + '%',
color: getProgressColor(data.totalProgress)
};
fs.writeFileSync(
path.join(badgesDir, tag + '.json'),
JSON.stringify(badge, null, 4) + '\n'
);
}
var untranslatedDir = path.join(OUTPUT_DIR, 'untranslated');
if (!fs.existsSync(untranslatedDir)) {
fs.mkdirSync(untranslatedDir, { recursive: true });
}
for (const tag of sortedTags) {
const items = untranslatedKeys[tag] || [];
fs.writeFileSync(
path.join(untranslatedDir, tag + '.json'),
JSON.stringify(items, null, 4) + '\n'
);
}
for (const tag of sortedTags) {
const data = sortedResults[tag];
const missingCount = (untranslatedKeys[tag] || []).length;
console.log(tag + ': ' + data.totalProgress + '% (frontend: ' + data.frontendTranslated + '/' + data.frontendTotal + ', backend: ' + data.backendTranslated + '/' + data.backendTotal + ', untranslated: ' + missingCount + ')');
}
console.log('\nResults written to ' + OUTPUT_DIR);
}
main();
+39
View File
@@ -0,0 +1,39 @@
name: Build and Push Docker Image
on:
push:
branches:
- myrequirement
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Set lowercase image name
run: echo "IMAGE_NAME=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ghcr.io/${{ env.IMAGE_NAME }}:latest
cache-from: type=gha
cache-to: type=gha,mode=max
@@ -4,6 +4,7 @@ on:
push: push:
branches-ignore: branches-ignore:
- main - main
- myrequirement
jobs: jobs:
setup: setup:
@@ -0,0 +1,76 @@
name: Update i18n Translation Progress Badges
on:
push:
branches:
- main
paths:
- 'src/locales/**'
- 'pkg/locales/**'
workflow_dispatch:
jobs:
update-i18n-progress:
if: vars.UPDATE_I18N_BADGE_REPO == 'true'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '24'
- name: Update translation progress data
run: |
node .github/scripts/update-i18n-progress.js ${{ runner.temp }}/i18n-badge
- name: Checkout badge repository
uses: actions/checkout@v5
with:
repository: mayswind/ezbookkeeping-i18n-badge
token: ${{ secrets.I18N_BADGE_REPO_TOKEN }}
path: ezbookkeeping-i18n-badge
- name: Update badge data
run: |
rm -rf ezbookkeeping-i18n-badge/i18n-progress.json
cp ${{ runner.temp }}/i18n-badge/i18n-progress.json ezbookkeeping-i18n-badge/
mkdir -p ezbookkeeping-i18n-badge/badges
rm -rf ezbookkeeping-i18n-badge/badges/*
cp ${{ runner.temp }}/i18n-badge/badges/*.json ezbookkeeping-i18n-badge/badges/
mkdir -p ezbookkeeping-i18n-badge/untranslated
rm -rf ezbookkeeping-i18n-badge/untranslated/*
cp ${{ runner.temp }}/i18n-badge/untranslated/*.json ezbookkeeping-i18n-badge/untranslated/
- name: Commit and push
run: |
cd ezbookkeeping-i18n-badge
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add -A
if git diff --cached --quiet; then
echo "No changes to commit"
else
git commit -m "Update i18n progress data (${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }})"
git push
fi
- name: Purge GitHub camo image cache
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
CAMO_URLS=$(curl -s -H "Accept: application/vnd.github.html+json" -H "Authorization: token $GITHUB_TOKEN" "https://api.github.com/repos/${{ github.repository }}/readme" | grep -oP 'https://camo\.githubusercontent\.com/[^"]+' | sort -u)
if [ -z "$CAMO_URLS" ]; then
echo "No camo URLs found, skipping cache purge"
exit 0
fi
for url in $CAMO_URLS; do
echo "Purging: $url"
curl -s -X PURGE "$url" > /dev/null
done
echo "Purged $(echo "$CAMO_URLS" | wc -l) camo URLs"
+16
View File
@@ -147,3 +147,19 @@ dist/
# Roo Code # Roo Code
.roo/ .roo/
# Binary and build files
ezbookkeeping
!**/ezbookkeeping/
package/
# Environment variable files
.env
**/.env
# Other directories
data/
storage/
log/
.claude/
+127
View File
@@ -0,0 +1,127 @@
# CLAUDE.md
本项目是 [mayswind/ezbookkeeping](https://github.com/mayswind/ezbookkeeping) 的个人 fork。
> **本文件**:仓库分支模型、上游同步流程、CI 故障排查 —— meta 层
> **[`FORK.md`](FORK.md)**fork 相对上游的具体改动清单(feature 维度 + 进度状态)
> **个人笔记**:通用 fork 工作流决策框架在 `fork-工作流决策框架.md`(不入库)
本文件只记录**这个仓库的具体事实**,避免 Claude 会话误判。
---
## 仓库拓扑
```
github.com/mayswind/ezbookkeeping (上游)
│ Gitea pull mirror(后台异步)
git.zhengchentao.win/mirror/ezbookkeeping (只读镜像)
│ CI workflow 拉过来
git.zhengchentao.win/dev/ezbookkeeping origin,本地唯一 remote
```
本地 `git remote -v` 只有 origin 一项,**没有手工配 upstream**。上游同步通过 custom 分支上的 workflow 在服务端完成,不是本地操作。
---
## 两个分支的职责(必须先理解,否则会改错地方)
| 分支 | 职责 | force push? |
|---|---|---|
| `main` | **锚定上游 release tag**(当前 v1.4.0)。被 `.gitea/workflows/sync-upstream.yml` `git reset --hard <tag>` 覆写。**别在 main 上做任何改动** | 是(由 CI 做) |
| `custom` | **所有个人改动 + workflow 文件都在这**:信用额度功能、UI 调整、个人需求清单、`.gitea/workflows/*.yml` 等。具体改动清单见 [`FORK.md`](FORK.md)。日常开发分支,**default branch** | 是(rebase 后人工做) |
⚠️ **default branch 是 `custom`**`git clone` 默认 checkout custom,直接是开发分支。
### 历史:曾经存在过的 ci 分支(已退役)
2026-05-02 之前曾经有第三个分支 `ci`,最初设计是把 `.gitea/workflows/*.yml` 单独放它上面以"meta/code 分离"。两周后发现 Gitea Actions runs 列表显示的 commit 是 workflow 文件所在 commit(即 ci 的 HEAD),不是被构建的代码 commit,UX 误导性强。
把 workflow 挪回 custom 之后:
- runs 列表 commit = 真实代码 commit ✅
- `git clone` 默认落 custom 直接是开发分支 ✅
- rebase 上游时 workflow 跟 custom 一起平移 ✅
- 代价:失去"workflow 与代码完全独立"的设计美感 —— 这个分离原本就是过度设计
**ci 分支于 2026-05-02 删除**,仅保留这段说明给后续 Claude 会话理解 git log 里"workflow 文件迁到 custom"这条提交(commit `555ecc1a`)的来龙去脉。**workflow 改动直接在 custom 上做**。
---
## custom 分支 workflow 清单
`.gitea/workflows/` 当前有 2 个 workflow2026-05-04 起 build+deploy 合并为单 workflow 双 job):
| 文件 | 触发 | 干什么 | 状态 |
|---|---|---|---|
| `sync-upstream.yml` | 手动(`workflow_dispatch`,可填 tag | 服务端把 `dev/main` 强制 reset 到 mirror 上的指定 release tag(默认最新),然后 `push --force-with-lease` + 推 tags | ✅ 在用 |
| `build-image.yml` | **自动**push 到 custom 触发,`paths-ignore` 屏蔽 `**.md` / `.gitignore` / `LICENSE` / `screenshot/**` / `sync-upstream.yml`+ 手动备选 | **两个 job 串联在同一 run 里**:①`build` job 装 buildkit v0.13.2 → 登录 Gitea registry → 构建镜像(带 OCI 标签 source/revisionGitea 自动关联包到 repo)→ push 到 `git.zhengchentao.win/dev/ezbookkeeping:<hash>``:latest``build-args: BUILD_PIPELINE=1` 跳过活 API 测试。②`deploy` job (`needs: build`) 登录 registry → clone nas-infra → `docker compose pull && up -d` 重启 ezbookkeeping。私有 nas-infra 需要 `secrets.NAS_INFRA_TOKEN`,公开仓库不需要。UI 上 Actions 列表显示一条 runrun 详情里 dependency graph 显示 build → deploy | ✅ 日常发布通道 + 自动 CD |
**已删**
- `docker-snapshot.yml` / `docker-release.yml`2026-05-02,依赖未配的 `secrets.DOCKER_REPO`,永远失败)
- `deploy.yml`2026-05-04,合并进 `build-image.yml` 作为第二个 job,理由:原先 `workflow_run` 链触发会在 Actions 列表产生两条独立 runUX 割裂;合并后单 run + dependency graph 看 build/deploy 状态一目了然)
需要时再从 git 历史 cherry-pick 回来。
---
## 同步发布流程(rebase 模型)
1. 上游出新 release(如 v1.4.0)→ Gitea pull mirror 自动把 tag 同步到 mirror
2. 人工触发 `Sync from upstream` workflow → 服务端把 dev/main reset 到该 tag
3. 本地 `git fetch && git checkout custom && git rebase origin/main`
4. 解冲突(如有)→ 验证 → `git push --force-with-lease origin custom`
5. **build-image workflow 自动触发**force-push 也算 push 事件),构建新镜像;不需要手动点
日常 feature commit 流程(全自动 CD):
1. 在 custom 上改代码 → commit → push
2. **自动触发 build job**(除非只改了 `**.md` / `.gitignore` / `LICENSE` / `screenshot/**` / `sync-upstream.yml`
3. build 成功 → **同 run 内 deploy job 接力跑**`needs: build` 串联):clone nas-infra → docker compose pull → up -d
4. 整条 push → build → deploy 链路无人工介入,UI 上是单条 run
**并发取消策略**`build-image.yml` 设了 `concurrency.cancel-in-progress: true`,连续多次 push 时**只构建+部署最新那一次**(同 run 里 build/deploy 是原子单元,一起取消)。例:连续 3 次 push 间隔 30 秒,第 1 次 build 跑到 30%、第 2 次到来取消它、第 3 次又取消第 2,最终只 build + deploy 第 3 次的代码。省 CI 时间又保证最终一致性。
如果想跳过 build/deploy(例如手动多次 push 调试),commit 时只改文档相关文件即可(落在 paths-ignore 范围内)。如果想强制重打某个旧 commit,去 Actions UI 手动触发 `Build Docker Image`,填要打包的 branch / tag —— 注意手动触发也会跑 deploy job,**没有"只重新部署不重新 build"的单点入口了**(合并的代价,原 `deploy.yml` 那条路径已废)。临时只想重启容器:直接到 NAS 上 `docker compose up -d` 或在 Actions UI 临时禁用 deploy job。
**为什么 rebase 不 merge**:个人项目,无团队协作语义要保留,线性历史更清爽。
---
## 给后续 Claude 会话的明确提示
- 用户说"我的分支" / "切换到我的分支" → 指 `custom`
- 用户说"rebase main" → 指 `git rebase origin/main`,目标是把 custom 的改动叠到最新上游 tag 之上
- **不要在 `main` 分支上提交任何东西**(会被 CI 覆写)
- **workflow 文件改动直接在 custom 上做**2026-05-02 起,不再是 ci 分支)
- force-push custom 是常规操作,但每次用 `--force-with-lease`,不直接 `--force`
- 如果发现本地配了 upstream remote,那是历史遗留,不要依赖;以 origin/main 为准
- `.claude/``.gitignore` 里(个人本地配置不入库),但 `CLAUDE.md` 本身入库
---
## 同步历史
- **2026-05-01**rebase custom → origin/main (v1.4.0)。22 个 custom-only 提交(含一个旧的 `Merge branch 'main' into myrequirement` commit)压平为 21 个线性提交。已 force-push origin/custom`08c69042``fe265259`)。
- **2026-05-02**:修 Gitea Actions `Build Docker Image` 工作流。三层故障,全部不在本仓库代码里:
- **TLS 雷**`docker login` 走 host 进程不命中 PREROUTING REDIRECT,且 v6 撞 DSM nginx 的 CF Origin Cert。NAS 侧修:iptables 补 OUTPUT 对称规则 + `/etc/hosts` 显式 v4 兜底。详见 obsidian vault [[NAS/notes/内网证书路径]] §三.5/§三.6
- **buildkit 内核兼容**runc 1.2+ 撞 DSM 4.4 内核。`.gitea/workflows/build-image.yml``moby/buildkit:v0.13.2`commit `acdbb5bf`
- **backend 单元测试撞活 API**`pkg/exchangerates/``TestExchangeRatesApiLatestExchangeRateHandler_*` 跑活 API(加拿大银行 / 乌兹别克央行),国内访问超时。upstream Dockerfile 已设 `ARG BUILD_PIPELINE`,测试代码看到 `BUILD_PIPELINE=1 && CHECK_3RD_API!=1` 时早退。修:workflow 加 `build-args: BUILD_PIPELINE=1`commit `2dd8f099`),对齐上游 GH Actions
- **2026-05-02 (后续)**workflow 文件从 ci 分支迁到 customdefault branch 切到 customcommit `555ecc1a`),随后**删掉 ci 分支**。原因:Gitea Actions runs 列表的 commit 字段一直显示 ci 的 workflow commit,不是被构建的代码 commit,UX 误导性强。挪到 custom 后列表直接显示真实代码 commit。同时清理上游残留的 `docker-release.yml` / `docker-snapshot.yml`(依赖未配的 `secrets.DOCKER_REPO`,永远失败)。仓库回到朴素的 main + custom 双分支模型
- **2026-05-02 (numpad fix)**FORK.md #11 定位 + 修复。小键盘点击卡顿真因是 `.numpad-button``touch-action: none`(上游 e178a079 引入)与 F7 tap 处理叠加,改为 `touch-action: manipulation`commit `75b4d78d`
- **2026-05-04**:把 `deploy.yml` 合并进 `build-image.yml` 作为第二个 job`needs: build`),删除 `deploy.yml`。原先 `workflow_run` 链路会在 Actions 列表产生两条独立 runbuild 完一条、deploy 又一条),用户视角割裂;合并后 UI 列表单条 runrun 详情里 dependency graph 显示 build → deploy 串联。代价:失去"不 rebuild 只 redeploy"的 UI 单点触发,临时只想重启容器需直接 ssh NAS 跑 compose。`paths-ignore` 移除已不存在的 `deploy.yml`
## 给后续 Claude 会话:CI 故障排查路径
如果 Gitea Actions build 又炸,按 NAS 域问题 vs 仓库代码问题分别排查:
| 现象 | 大概率位置 | 文档 |
|---|---|---|
| `Login to Gitea Container Registry` 步骤报 `x509: certificate signed by unknown authority` | NAS 网络层(iptables / dnsmasq / DSM nginx 占 443 | obsidian vault `NAS/notes/内网证书路径.md` + `NAS/notes/IPv6 设计.md` |
| `Build and push` 步骤里 `RUN ...` 在第二条之内就炸 `unsafe procfs detected` 之类 | buildkit/runc 与 DSM 内核版本 | `.gitea/workflows/build-image.yml``driver-opts` |
| `Failed to pass unit testing` / `Failed to pass lint checking`build.sh 报) | **先看 Dockerfile 顶部 `ARG`**,多半是 CI 跳过开关没传(如 `BUILD_PIPELINE` / `CHECK_3RD_API` / `SKIP_TESTS`)。**不要先去改测试代码** | `Dockerfile` 顶部 ARG + `.gitea/workflows/build-image.yml``build-args` |
| `actions/checkout` 报 fetch 失败 | Gitea SSH/HTTPS 路径或 token 权限 | gitea-runner 的 `GITEA_RUNNER_REGISTRATION_TOKEN` + NPM `git.zhengchentao.win` 的 Advanced 配置 |
| Dockerfile 里某条指令业务逻辑报错 | 真正的代码问题 | 本仓库 `Dockerfile` |
**通用排查原则**build.sh 报的"测试失败 / lint 失败"先看是不是上游已经设计了 CI 跳过路径。Dockerfile 的 `ARG` + `build.sh` 内的 `os.Getenv()` 检查通常成对出现(如 `BUILD_PIPELINE=1` → 跳过 3rd API 测试,`SKIP_TESTS=...` → 跳过指定测试名)。对齐上游 `.github/actions/` 下的传参,绝大多数情况能直接对齐。
+3 -3
View File
@@ -1,5 +1,5 @@
# Build backend binary file # Build backend binary file
FROM golang:1.25.3-alpine3.22 AS be-builder FROM golang:1.25.7-alpine3.23 AS be-builder
ARG RELEASE_BUILD ARG RELEASE_BUILD
ARG BUILD_PIPELINE ARG BUILD_PIPELINE
ARG BUILD_UNIXTIME ARG BUILD_UNIXTIME
@@ -19,7 +19,7 @@ RUN apk add git gcc g++ libc-dev
RUN ./build.sh backend RUN ./build.sh backend
# Build frontend files # Build frontend files
FROM --platform=$BUILDPLATFORM node:24.10.0-alpine3.22 AS fe-builder FROM --platform=$BUILDPLATFORM node:24.14.0-alpine3.23 AS fe-builder
ARG RELEASE_BUILD ARG RELEASE_BUILD
ARG BUILD_PIPELINE ARG BUILD_PIPELINE
ARG BUILD_UNIXTIME ARG BUILD_UNIXTIME
@@ -35,7 +35,7 @@ RUN apk add git
RUN ./build.sh frontend RUN ./build.sh frontend
# Package docker image # Package docker image
FROM alpine:3.22.2 FROM alpine:3.23.3
LABEL maintainer="MaysWind <i@mayswind.net>" LABEL maintainer="MaysWind <i@mayswind.net>"
RUN addgroup -S -g 1000 ezbookkeeping && adduser -S -G ezbookkeeping -u 1000 ezbookkeeping RUN addgroup -S -g 1000 ezbookkeeping && adduser -S -G ezbookkeeping -u 1000 ezbookkeeping
RUN apk --no-cache add tzdata RUN apk --no-cache add tzdata
+182
View File
@@ -0,0 +1,182 @@
# ezBookkeeping 个人 fork 改动清单
> 本文件记录这个 fork 相对上游 [mayswind/ezbookkeeping](https://github.com/mayswind/ezbookkeeping) 的所有定制改动 + 进度状态。
> 关联文档:
> - [`CLAUDE.md`](CLAUDE.md) —— 仓库分支模型 / 上游同步流程 / CI 排查路径(meta 层)
> - 部署:见自家 NAS infra repo `git.zhengchentao.win/dev/nas-infra` 的 READMEcompose-level
>
> 标注:❌ 难/暂缓 | ❓ 待定 | 🔍 调查中 | 🟢 已完成
---
## 一、账户功能
### 1. 🟢 信用卡账户:额度与可用额度
**描述:** 为信用卡类型账户新增「信用额度」字段,在账户列表显示可用额度。
**已完成:**
- 后端:`AccountExtend` JSON blob 新增 `CreditLimit` 字段(无需数据库迁移)
- API`AccountCreateRequest` / `AccountModifyRequest` / `AccountInfoResponse` 增加 `creditLimit`
- 前端 model`Account` 类增加 `creditLimit` 字段,同步序列化/反序列化
- 移动端 EditPageCreditCard 分类时显示信用额度输入项(数字键盘)
- 桌面端 EditDialogCreditCard 分类时显示信用额度输入框(`amount-input`
- 移动端 ListPage:账户名下方显示「可用额度: ¥xxx」(= `creditLimit + balance`
- 桌面端 ListPage:账户卡片余额旁显示「Available: ¥xxx」
- 语言包:中英繁均已添加 `"Credit Limit"` / `"Available"`
### 2. 🟢 按账户筛选交易时顶部显示账户信息卡
**描述:** 在按单个账户筛选的交易列表顶部,显示账户图标、名称和余额/可用额度。
**已完成:**
- 仅单账户筛选时(`queryAllFilterAccountIdsCount === 1`)显示,多账户/全量时隐藏
- 信用卡账户显示「欠款 · 可用 ¥xxx」,普通账户显示余额
- 移动端:toolbar 下方插入账户信息卡片;桌面端:日期范围行下方插入 tonal 样式账户卡片
- 多子账户(`MultiSubAccounts`)一级账户:使用 `getAccountSubAccountBalance` 获取汇总余额及正确币种,修复 `currency = '---'` 导致货币符号不显示的问题
- 涉及文件:`src/views/mobile/transactions/ListPage.vue``src/views/desktop/transactions/ListPage.vue`
### 3. 🟢 账户编辑页直接修改余额(自动插入调整记录)
**描述:** 在账户编辑页修改余额字段,保存时自动计算差值并插入一条「余额调整」类型交易。
**已完成:**
- 后端:移除「账户已有交易时不允许添加 ModifyBalance」的限制
- 后端:`Amount``RelatedAccountAmount` 均存储 delta;创建时 `balance += delta`,删除时 `balance -= delta`,修改时 `balance = balance - oldDelta + newDelta`
- 后端响应:`ToTransactionInfoResponse` 对 ModifyBalance 类型返回 `RelatedAccountAmount` 作为 `sourceAmount`
- 前端 store:新增 `adjustAccountBalance({ accountId, targetBalance, currentBalance })` 函数,发送 `sourceAmount = delta`
- 移动端 EditPage:余额字段对已有账户解除只读;保存时若余额变化先调 `adjustAccountBalance`;捕获 `NothingWillBeUpdated (200004)` 视为成功
- 桌面端 EditDialog:同上逻辑,支持多子账户逐一调整
- 「调整余额」入口仅在账户编辑页,已从账户列表「更多」菜单移除
- 涉及文件:`pkg/services/transactions.go``pkg/models/transaction.go``src/stores/transaction.ts``src/views/mobile/accounts/EditPage.vue``src/views/desktop/accounts/list/dialogs/EditDialog.vue`
---
## 二、记账页面
### 4. 🟢 记账页选择账户后显示余额/可用额度
**描述:** 在记账页面选择账户后,在账户行显示该账户的当前余额或信用卡可用额度。
**已完成:**
- 信用卡账户(有 `creditLimit`):显示「欠款金额 · 可用 ¥xxx」
- 普通负债账户:显示欠款正数;普通资产账户:显示余额
- 转账类型时源账户和目标账户均显示
- 移动端:账户列表项 `footer` 字段;桌面端:`two-column-select``custom-selection-secondary-text`
- 涉及文件:`src/views/mobile/transactions/EditPage.vue``src/views/desktop/transactions/list/dialogs/EditDialog.vue`
### 5. ❓ 记录上次选择的账户(待定)
**描述:** 新建交易时,默认选中上次使用的账户。
**实现思路:**
- 保存账户 ID 到 `localStorage`,打开记账页时读取并预选
- 涉及文件:`src/views/mobile/transactions/EditPage.vue``src/lib/settings.ts`
---
## 三、小键盘
### 6. 🟢 小键盘布局调整
**描述:** 调整数字键盘布局。
**已完成:**
```
[数值显示区 ] [ ⌫ ]
7 8 9 ×
4 5 6
1 2 3 +
C 0 . OK
```
- ⌫ 单击退格,长按清除;C 清除全部
- 涉及文件:`src/components/mobile/NumberPadSheet.vue`
---
## 四、交易详情
### 7. 🟢 交易详情页增加「编辑」和「删除」入口(仅移动端)
**描述:** 移动端交易详情页三点菜单中增加编辑和删除操作。PC 端详情直接在编辑弹窗中展示,无需额外入口。
**已完成:**
- 三点菜单:第一项「Edit」跳转编辑页,最后一项红色「Delete」确认后删除并返回
- 从详情返回编辑页后自动刷新数据
- 涉及文件:`src/views/mobile/transactions/EditPage.vue`
---
## 五、交易时间选择
### 8. ❌ 点击交易时间标题默认打开日期选择(已回滚)
**描述:** 原想让点击「Transaction Time」标题行时默认弹日期选择器。
**为何回滚:** 改动改的是 `template #header` 那行 label 的点击 handler`'time'``'date'`),实际操作中用户点的是 `template #title` 里的日期/时间文本。上游早在 commit `368322f9` 已实现"点哪走哪"的智能路由——点日期开日期选择器、点时间开时间选择器。所以这条改动**用户视角无可见差异**,纯空改,回滚到上游行为。
**留档教训:** 改 UI 行为前先把"用户实际点哪个元素"摸清楚,别只看着 DOM 结构想当然。`#header` slot 只是上方的 label 行,正常用户极少触发。
---
## 六、分类选择
### 9. 🟢 分类选择默认全部展开(仅移动端)
**描述:** 在移动端记账页选择分类时,默认展开所有一级分类。PC 端使用不同的分类选择组件,无需此设置。
**已完成:**
- 设置存 localStorage(字段 `expandCategoryTreeByDefault`,默认 `false`),即时生效无需 reload;已加入云同步白名单(`ALL_ALLOWED_CLOUD_SYNC_APP_SETTING_KEY_TYPES`),可跨设备同步
- `TreeViewSelectionSheet` 新增可选 prop `defaultExpanded``:opened` 改为 `props.defaultExpanded || isPrimaryItemHasSecondaryValue(item)`
- 移动端 EditPage 三个分类 sheet(支出/收入/转账)均传入 `:default-expanded`
- 设置仅在移动端设置页显示,PC 端无对应入口
- 涉及文件:`src/components/mobile/TreeViewSelectionSheet.vue``src/views/mobile/transactions/EditPage.vue``src/views/mobile/SettingsPage.vue`
---
## 七、性能与动画
### 10. 🟢 全局动画加速(仅移动端)
**描述:** 移动端全局页面跳转及各类弹层动画加速。
**已完成:**
- 页面跳转、Sheet、ActionSheet、Popup、Dialog、Popover 动画时长从 300ms → 150ms
- Tab 切换动画保持原样(设置中已有开关可控制)
- 涉及文件:`src/styles/mobile/global.scss`
### 11. 🟢 小键盘点击卡顿(修正:范围非全局)
**描述:** 移动端点击按钮有延迟感。
**真因(2026-05-02 定位):** **不是**全局点击/接口响应问题。诊断后确认仅小键盘有卡顿,其他按钮正常。根因是上游在 `.numpad-button` 上设了 `touch-action: none`commit `e178a079` "code refactor" by MaysWind),与 F7 内部 tap 处理叠加后让 click 事件合成慢一拍。backspace(个人新增 `.numpad-backspace-button` 类)不受影响,刚好佐证范围。
**已完成:**
- `.numpad-button``touch-action: none` 改为 `touch-action: manipulation`
- `manipulation` 是 W3C 标准的"快速点击"值:禁双击缩放(消除老 300ms 延迟)但保留 click 事件正常合成
- 涉及文件:`src/components/mobile/NumberPadSheet.vue`
**附带认知:**#11 假设是"全局点击响应慢"或"接口慢",与 #12 离线缓存挂钩调研。实际诊断后跟那两条都无关,纯 CSS `touch-action` 与框架 tap 处理叠加导致。该认知值得记录避免后续误诊路径。
---
## 八、离线 / 缓存
### 12. ❌ 本地优先 / 离线数据缓存(暂缓)
**描述:** 交易数据本地缓存,优先展示缓存数据,后台静默拉取更新。
**现状:** Service Worker 已实现静态资源缓存,但交易业务数据目前不做本地缓存。
**为何难:**
- 需引入 IndexedDB 存储交易/账户/分类数据
- 需处理本地与服务端的数据同步、冲突解决
- 属于架构级改动,工作量较大
---
## 进度总览
| # | 需求 | 状态 |
|---|------|------|
| 1 | 信用卡额度 | 🟢 已完成 |
| 2 | 账户信息卡片 | 🟢 已完成 |
| 3 | 调整余额入口 | 🟢 已完成 |
| 4 | 记账页显示余额 | 🟢 已完成 |
| 5 | 记住上次账户 | ❓ 待定 |
| 6 | 小键盘布局 | 🟢 已完成 |
| 7 | 详情编辑/删除 | 🟢 已完成 |
| 8 | 点击时间默认日期 | ❌ 已回滚(无效改动) |
| 9 | 分类默认展开 | 🟢 已完成 |
| 10 | 全局动画加速 | 🟢 已完成 |
| 11 | 小键盘点击卡顿(touch-action 修复) | 🟢 已完成 |
| 12 | 离线缓存 | ❌ 暂缓 |
+2 -1
View File
@@ -1,6 +1,7 @@
MIT License MIT License
Copyright (c) 2020-2025 MaysWind (i@mayswind.net) Copyright (c) 2020-2026 MaysWind (i@mayswind.net)
Copyright (c) 2026 Zhengchen Tao (fork modifications)
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
+68 -37
View File
@@ -1,4 +1,29 @@
# ezBookkeeping # ezBookkeeping
> ## Personal fork notice
>
> This repository is a personal fork of [mayswind/ezbookkeeping](https://github.com/mayswind/ezbookkeeping) (MIT) with the following custom additions on top of upstream releases:
>
> - **Credit card accounts**: credit-limit field; account list shows available credit
> - **Account-filtered transactions**: when filtering by a single account, show an account info card on top (icon / name / balance / available credit)
> - **Account editing**: edit the balance field directly; a "balance adjustment" transaction is generated automatically
> - **Add-transaction page**: live display of balance or available credit after selecting an account
> - **Numpad**: custom layout (4-column calculator style) + `touch-action` fix for tap latency
> - **Mobile animations**: generic transitions 300ms → 150ms
> - **Transaction detail**: edit / delete entries added to the mobile three-dot menu
> - **Category picker**: optional "expand all by default" on mobile (cloud-sync allowlisted)
>
> Full list with implementation details: [`FORK.md`](FORK.md)
> Branch model / upstream sync / CI troubleshooting: [`CLAUDE.md`](CLAUDE.md)
>
> All modifications are released under the same MIT License as upstream — see [`LICENSE`](LICENSE).
>
> ---
>
> Upstream README content follows below.
---
[![License](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/mayswind/ezbookkeeping/blob/master/LICENSE) [![License](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/mayswind/ezbookkeeping/blob/master/LICENSE)
[![Go Report](https://goreportcard.com/badge/github.com/mayswind/ezbookkeeping)](https://goreportcard.com/report/github.com/mayswind/ezbookkeeping) [![Go Report](https://goreportcard.com/badge/github.com/mayswind/ezbookkeeping)](https://goreportcard.com/report/github.com/mayswind/ezbookkeeping)
[![Latest Release](https://img.shields.io/github/release/mayswind/ezbookkeeping.svg?style=flat)](https://github.com/mayswind/ezbookkeeping/releases) [![Latest Release](https://img.shields.io/github/release/mayswind/ezbookkeeping.svg?style=flat)](https://github.com/mayswind/ezbookkeeping/releases)
@@ -11,7 +36,7 @@
[![Trending](https://trendshift.io/api/badge/repositories/12917)](https://trendshift.io/repositories/12917) [![Trending](https://trendshift.io/api/badge/repositories/12917)](https://trendshift.io/repositories/12917)
## Introduction ## Introduction
ezBookkeeping is a lightweight, self-hosted personal finance app with a user-friendly interface and powerful bookkeeping features. It's easy to deploy, and you can start it with just one single Docker command. Designed to be resource-efficient and highly scalable, it can run smoothly on devices as small as a Raspberry Pi, or scale up to NAS, MicroServers, and even large cluster environments. ezBookkeeping is a lightweight, self-hosted personal finance app with a user-friendly interface and powerful bookkeeping features. It helps you record daily transactions, import data from various sources, and quickly search and filter your bills. You can analyze historical data using built-in charts or perform custom queries with your own chart dimensions to better understand spending patterns and financial trends. ezBookkeeping is easy to deploy, and you can start it with just one single Docker command. Designed to be resource-efficient, it runs smoothly on devices such as Raspberry Pi, NAS, and MicroServers.
ezBookkeeping offers tailored interfaces for both mobile and desktop devices. With support for PWA (Progressive Web Apps), you can even [add it to your mobile home screen](https://raw.githubusercontent.com/wiki/mayswind/ezbookkeeping/img/mobile/add_to_home_screen.gif) and use it like a native app. ezBookkeeping offers tailored interfaces for both mobile and desktop devices. With support for PWA (Progressive Web Apps), you can even [add it to your mobile home screen](https://raw.githubusercontent.com/wiki/mayswind/ezbookkeeping/img/mobile/add_to_home_screen.gif) and use it like a native app.
@@ -21,9 +46,9 @@ Live Demo: [https://ezbookkeeping-demo.mayswind.net](https://ezbookkeeping-demo.
- **Open Source & Self-Hosted** - **Open Source & Self-Hosted**
- Built for privacy and control - Built for privacy and control
- **Lightweight & Fast** - **Lightweight & Fast**
- Optimized for performance, runs smoothly even on low-resource environments - Minimal resource usage, runs smoothly even on low-resource devices
- **Easy Installation** - **Easy Installation**
- Docker-ready - Docker support
- Supports SQLite, MySQL, PostgreSQL - Supports SQLite, MySQL, PostgreSQL
- Cross-platform (Windows, macOS, Linux) - Cross-platform (Windows, macOS, Linux)
- Works on x86, amd64, ARM architectures - Works on x86, amd64, ARM architectures
@@ -33,24 +58,28 @@ Live Demo: [https://ezbookkeeping-demo.mayswind.net](https://ezbookkeeping-demo.
- Dark mode - Dark mode
- **AI-Powered Features** - **AI-Powered Features**
- Receipt image recognition - Receipt image recognition
- Supports MCP (Model Context Protocol) for AI integration - MCP (Model Context Protocol) support for AI integration
- Agent Skill and API command-line script tools support for AI integration
- **Powerful Bookkeeping** - **Powerful Bookkeeping**
- Two-level accounts and categories - Two-level accounts and categories
- Attach images to transactions - Image attachments for transactions
- Location tracking with maps - Location tracking with maps
- Recurring transactions - Scheduled transactions
- Advanced filtering, search, visualization, and analysis - Advanced filtering, search, visualization and analysis
- **Localization & Globalization** - **Localization & Internationalization**
- Multi-language and multi-currency support - Multi-language and multi-currency support
- Automatic exchange rates - Multiple exchange rate sources with automatic updates
- Multi-timezone awareness - Multi-timezone support
- Custom formats for dates, numbers, and currencies - Custom formats for dates, numbers and currencies
- **Security** - **Security**
- Two-factor authentication (2FA) - Two-factor authentication (2FA)
- OIDC external authentication
- Login rate limiting - Login rate limiting
- Application lock (PIN code / WebAuthn) - Application lock (PIN code / WebAuthn)
- **Data Import/Export** - **Data Import & Export**
- Supports CSV, OFX, QFX, QIF, IIF, Camt.053, MT940, GnuCash, Firefly III, Beancount, and more - Supports CSV, OFX, QFX, QIF, IIF, Camt.052, Camt.053, MT940, GnuCash, Firefly III, Beancount and more
For a full list of features, visit the [Full Feature List](https://ezbookkeeping.mayswind.net/comparison/).
## Screenshots ## Screenshots
### Desktop Version ### Desktop Version
@@ -112,38 +141,40 @@ You can also build a Docker image. Make sure you have [Docker](https://www.docke
## Contributing ## Contributing
We welcome contributions of all kinds. We welcome contributions of all kinds.
Found a bug? [Submit an issue](https://github.com/mayswind/ezbookkeeping/issues) If you find a bug, please [submit an issue](https://github.com/mayswind/ezbookkeeping/issues) on GitHub.
Want to contribute code? Feel free to fork and send a pull request. If you would like to contribute code, you can fork the repository and open a pull request.
Contributions of all kinds — bug reports, feature suggestions, documentation improvements, or code — are highly appreciated. Improvements to documentation, feature suggestions, and other forms of feedback are also appreciated.
Check out our [Contributor Graph](https://github.com/mayswind/ezbookkeeping/graphs/contributors) to see the amazing people who've already helped. You can view existing contributors on the [Contributor Graph](https://github.com/mayswind/ezbookkeeping/graphs/contributors).
## Translating ## Translating
Help make ezBookkeeping accessible to users around the world. If you want to contribute a translation, please refer to our [translation guide](https://ezbookkeeping.mayswind.net/translating). Help make ezBookkeeping accessible to users around the world. We welcome help to improve existing translations or add new ones. If you would like to contribute a translation, please refer to the [translation guide](https://ezbookkeeping.mayswind.net/translating).
Currently available translations: Currently available translations:
| Tag | Language | Contributors | | Tag | Language | Progress | Contributors |
| --- | --- | --- | | --- | --- | --- | --- |
| de | Deutsch | [@chrgm](https://github.com/chrgm) | | de | Deutsch | ![Translation Progress](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fmayswind%2FezBookkeeping-i18n-badge%2Fmain%2Fbadges%2Fde.json) | [@chrgm](https://github.com/chrgm), [@1270o1](https://github.com/1270o1) |
| en | English | / | | en | English | ![Translation Progress](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fmayswind%2FezBookkeeping-i18n-badge%2Fmain%2Fbadges%2Fen.json) | / |
| es | Español | [@Miguelonlonlon](https://github.com/Miguelonlonlon) | | es | Español | ![Translation Progress](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fmayswind%2FezBookkeeping-i18n-badge%2Fmain%2Fbadges%2Fes.json) | [@Miguelonlonlon](https://github.com/Miguelonlonlon), [@abrugues](https://github.com/abrugues), [@AndresTeller](https://github.com/AndresTeller), [@diegofercri](https://github.com/diegofercri) |
| fr | Français | [@brieucdlf](https://github.com/brieucdlf) | | fr | Français | ![Translation Progress](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fmayswind%2FezBookkeeping-i18n-badge%2Fmain%2Fbadges%2Ffr.json) | [@brieucdlf](https://github.com/brieucdlf) |
| it | Italiano | [@waron97](https://github.com/waron97) | | it | Italiano | ![Translation Progress](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fmayswind%2FezBookkeeping-i18n-badge%2Fmain%2Fbadges%2Fit.json) | [@waron97](https://github.com/waron97) |
| ja | 日本語 | [@tkymmm](https://github.com/tkymmm) | | ja | 日本語 | ![Translation Progress](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fmayswind%2FezBookkeeping-i18n-badge%2Fmain%2Fbadges%2Fja.json) | [@tkymmm](https://github.com/tkymmm) |
| ko | 한국어 | [@overworks](https://github.com/overworks) | | kn | ಕನ್ನಡ | ![Translation Progress](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fmayswind%2FezBookkeeping-i18n-badge%2Fmain%2Fbadges%2Fkn.json) | [@Darshanbm05](https://github.com/Darshanbm05) |
| nl | Nederlands | [@automagic](https://github.com/automagics) | | ko | 한국어 | ![Translation Progress](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fmayswind%2FezBookkeeping-i18n-badge%2Fmain%2Fbadges%2Fko.json) | [@overworks](https://github.com/overworks) |
| pt-BR | Português (Brasil) | [@thecodergus](https://github.com/thecodergus) | | nl | Nederlands | ![Translation Progress](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fmayswind%2FezBookkeeping-i18n-badge%2Fmain%2Fbadges%2Fnl.json) | [@automagics](https://github.com/automagics) |
| ru | Русский | [@artegoser](https://github.com/artegoser) | | pt-BR | Português (Brasil) | ![Translation Progress](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fmayswind%2FezBookkeeping-i18n-badge%2Fmain%2Fbadges%2Fpt-BR.json) | [@thecodergus](https://github.com/thecodergus), [@balaios](https://github.com/balaios) |
| th | ไทย | [@natthavat28](https://github.com/natthavat28) | | ru | Русский | ![Translation Progress](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fmayswind%2FezBookkeeping-i18n-badge%2Fmain%2Fbadges%2Fru.json) | [@artegoser](https://github.com/artegoser), [@dshemin](https://github.com/dshemin) |
| uk | Українська | [@nktlitvinenko](https://github.com/nktlitvinenko) | | sl | Slovenščina | ![Translation Progress](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fmayswind%2FezBookkeeping-i18n-badge%2Fmain%2Fbadges%2Fsl.json) | [@thehijacker](https://github.com/thehijacker) |
| vi | Tiếng Việt | [@f97](https://github.com/f97) | | ta | தமிழ் | ![Translation Progress](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fmayswind%2FezBookkeeping-i18n-badge%2Fmain%2Fbadges%2Fta.json) | [@hhharsha36](https://github.com/hhharsha36) |
| zh-Hans | 中文 (简体) | / | | th | ไทย | ![Translation Progress](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fmayswind%2FezBookkeeping-i18n-badge%2Fmain%2Fbadges%2Fth.json) | [@natthavat28](https://github.com/natthavat28) |
| zh-Hant | 中文 (繁體) | / | | tr | Türkçe | ![Translation Progress](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fmayswind%2FezBookkeeping-i18n-badge%2Fmain%2Fbadges%2Ftr.json) | [@aydnykn](https://github.com/aydnykn) |
| uk | Українська | ![Translation Progress](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fmayswind%2FezBookkeeping-i18n-badge%2Fmain%2Fbadges%2Fuk.json) | [@nktlitvinenko](https://github.com/nktlitvinenko) |
Don't see your language? Help us add it. | vi | Tiếng Việt | ![Translation Progress](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fmayswind%2FezBookkeeping-i18n-badge%2Fmain%2Fbadges%2Fvi.json) | [@f97](https://github.com/f97) |
| zh-Hans | 中文 (简体) | ![Translation Progress](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fmayswind%2FezBookkeeping-i18n-badge%2Fmain%2Fbadges%2Fzh-Hans.json) | / |
| zh-Hant | 中文 (繁體) | ![Translation Progress](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fmayswind%2FezBookkeeping-i18n-badge%2Fmain%2Fbadges%2Fzh-Hant.json) | / |
## Documentation ## Documentation
1. [English](https://ezbookkeeping.mayswind.net) 1. [English](https://ezbookkeeping.mayswind.net)
+16
View File
@@ -101,6 +101,14 @@ func updateAllDatabaseTablesStructure(c *core.CliContext) error {
log.BootInfof(c, "[database.updateAllDatabaseTablesStructure] transaction category table maintained successfully") 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)) err = datastore.Container.UserDataStore.SyncStructs(new(models.TransactionTag))
if err != nil { if err != nil {
@@ -157,5 +165,13 @@ func updateAllDatabaseTablesStructure(c *core.CliContext) error {
log.BootInfof(c, "[database.updateAllDatabaseTablesStructure] user external auth table maintained successfully") 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 return nil
} }
+16
View File
@@ -195,9 +195,25 @@ func getConfigWithoutSensitiveData(config *settings.Config) *settings.Config {
clonedConfig.ReceiptImageRecognitionLLMConfig.OpenAICompatibleAPIKey = "****" clonedConfig.ReceiptImageRecognitionLLMConfig.OpenAICompatibleAPIKey = "****"
} }
if clonedConfig.ReceiptImageRecognitionLLMConfig.AnthropicCompatibleAPIKey != "" {
clonedConfig.ReceiptImageRecognitionLLMConfig.AnthropicCompatibleAPIKey = "****"
}
if clonedConfig.ReceiptImageRecognitionLLMConfig.AnthropicAPIKey != "" {
clonedConfig.ReceiptImageRecognitionLLMConfig.AnthropicAPIKey = "****"
}
if clonedConfig.ReceiptImageRecognitionLLMConfig.OpenRouterAPIKey != "" { if clonedConfig.ReceiptImageRecognitionLLMConfig.OpenRouterAPIKey != "" {
clonedConfig.ReceiptImageRecognitionLLMConfig.OpenRouterAPIKey = "****" clonedConfig.ReceiptImageRecognitionLLMConfig.OpenRouterAPIKey = "****"
} }
if clonedConfig.ReceiptImageRecognitionLLMConfig.LMStudioToken != "" {
clonedConfig.ReceiptImageRecognitionLLMConfig.LMStudioToken = "****"
}
if clonedConfig.ReceiptImageRecognitionLLMConfig.GoogleAIAPIKey != "" {
clonedConfig.ReceiptImageRecognitionLLMConfig.GoogleAIAPIKey = "****"
}
} }
if clonedConfig.OAuth2ClientSecret != "" { if clonedConfig.OAuth2ClientSecret != "" {
+21 -1
View File
@@ -116,6 +116,7 @@ func startWebServer(c *core.CliContext) error {
_ = v.RegisterValidation("validCurrency", validators.ValidCurrency) _ = v.RegisterValidation("validCurrency", validators.ValidCurrency)
_ = v.RegisterValidation("validHexRGBColor", validators.ValidHexRGBColor) _ = v.RegisterValidation("validHexRGBColor", validators.ValidHexRGBColor)
_ = v.RegisterValidation("validAmountFilter", validators.ValidAmountFilter) _ = v.RegisterValidation("validAmountFilter", validators.ValidAmountFilter)
_ = v.RegisterValidation("validTagFilter", validators.ValidTagFilter)
_ = v.RegisterValidation("validFiscalYearStart", validators.ValidateFiscalYearStart) _ = v.RegisterValidation("validFiscalYearStart", validators.ValidateFiscalYearStart)
} }
@@ -315,6 +316,7 @@ func startWebServer(c *core.CliContext) error {
apiV1Route := apiRoute.Group("/v1") apiV1Route := apiRoute.Group("/v1")
apiV1Route.Use(bindMiddleware(middlewares.JWTAuthorization(config))) apiV1Route.Use(bindMiddleware(middlewares.JWTAuthorization(config)))
apiV1Route.Use(bindMiddleware(middlewares.APITokenIpLimit(config)))
{ {
// Tokens // Tokens
apiV1Route.GET("/tokens/list.json", bindApi(api.Tokens.TokenListHandler)) apiV1Route.GET("/tokens/list.json", bindApi(api.Tokens.TokenListHandler))
@@ -382,6 +384,7 @@ func startWebServer(c *core.CliContext) error {
apiV1Route.GET("/transactions/count.json", bindApi(api.Transactions.TransactionCountHandler)) apiV1Route.GET("/transactions/count.json", bindApi(api.Transactions.TransactionCountHandler))
apiV1Route.GET("/transactions/list.json", bindApi(api.Transactions.TransactionListHandler)) apiV1Route.GET("/transactions/list.json", bindApi(api.Transactions.TransactionListHandler))
apiV1Route.GET("/transactions/list/by_month.json", bindApi(api.Transactions.TransactionMonthListHandler)) 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/reconciliation_statements.json", bindApi(api.Transactions.TransactionReconciliationStatementHandler))
apiV1Route.GET("/transactions/statistics.json", bindApi(api.Transactions.TransactionStatisticsHandler)) apiV1Route.GET("/transactions/statistics.json", bindApi(api.Transactions.TransactionStatisticsHandler))
apiV1Route.GET("/transactions/statistics/trends.json", bindApi(api.Transactions.TransactionStatisticsTrendsHandler)) apiV1Route.GET("/transactions/statistics/trends.json", bindApi(api.Transactions.TransactionStatisticsTrendsHandler))
@@ -394,7 +397,7 @@ func startWebServer(c *core.CliContext) error {
apiV1Route.POST("/transactions/delete.json", bindApi(api.Transactions.TransactionDeleteHandler)) apiV1Route.POST("/transactions/delete.json", bindApi(api.Transactions.TransactionDeleteHandler))
if config.EnableDataImport { if config.EnableDataImport {
apiV1Route.POST("/transactions/parse_dsv_file.json", bindApi(api.Transactions.TransactionParseImportDsvFileDataHandler)) apiV1Route.POST("/transactions/parse_custom_file.json", bindApi(api.Transactions.TransactionParseImportCustomFileDataHandler))
apiV1Route.POST("/transactions/parse_import.json", bindApi(api.Transactions.TransactionParseImportFileHandler)) apiV1Route.POST("/transactions/parse_import.json", bindApi(api.Transactions.TransactionParseImportFileHandler))
apiV1Route.POST("/transactions/import.json", bindApi(api.Transactions.TransactionImportHandler)) apiV1Route.POST("/transactions/import.json", bindApi(api.Transactions.TransactionImportHandler))
apiV1Route.GET("/transactions/import/process.json", bindApi(api.Transactions.TransactionImportProcessHandler)) apiV1Route.GET("/transactions/import/process.json", bindApi(api.Transactions.TransactionImportProcessHandler))
@@ -416,6 +419,14 @@ func startWebServer(c *core.CliContext) error {
apiV1Route.POST("/transaction/categories/move.json", bindApi(api.TransactionCategories.CategoryMoveHandler)) apiV1Route.POST("/transaction/categories/move.json", bindApi(api.TransactionCategories.CategoryMoveHandler))
apiV1Route.POST("/transaction/categories/delete.json", bindApi(api.TransactionCategories.CategoryDeleteHandler)) 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 // Transaction Tags
apiV1Route.GET("/transaction/tags/list.json", bindApi(api.TransactionTags.TagListHandler)) apiV1Route.GET("/transaction/tags/list.json", bindApi(api.TransactionTags.TagListHandler))
apiV1Route.GET("/transaction/tags/get.json", bindApi(api.TransactionTags.TagGetHandler)) apiV1Route.GET("/transaction/tags/get.json", bindApi(api.TransactionTags.TagGetHandler))
@@ -435,6 +446,15 @@ func startWebServer(c *core.CliContext) error {
apiV1Route.POST("/transaction/templates/move.json", bindApi(api.TransactionTemplates.TemplateMoveHandler)) apiV1Route.POST("/transaction/templates/move.json", bindApi(api.TransactionTemplates.TemplateMoveHandler))
apiV1Route.POST("/transaction/templates/delete.json", bindApi(api.TransactionTemplates.TemplateDeleteHandler)) 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 // Large Language Models
if config.ReceiptImageRecognitionLLMConfig != nil && config.ReceiptImageRecognitionLLMConfig.LLMProvider != "" { if config.ReceiptImageRecognitionLLMConfig != nil && config.ReceiptImageRecognitionLLMConfig.LLMProvider != "" {
if config.TransactionFromAIImageRecognition { if config.TransactionFromAIImageRecognition {
+38 -7
View File
@@ -1,7 +1,4 @@
[global] [global]
# Application instance name
app_name = ezBookkeeping
# Either "production", "development" # Either "production", "development"
mode = production mode = production
@@ -18,7 +15,7 @@ http_port = 8080
# The domain name used to access ezBookkeeping # The domain name used to access ezBookkeeping
domain = localhost domain = localhost
# The full url used to access ezBookkeeping in browser # The full url used to access ezBookkeeping in browser, supports placeholders: %(protocol)s, %(domain)s, %(http_port)s
root_url = %(protocol)s://%(domain)s:%(http_port)s/ root_url = %(protocol)s://%(domain)s:%(http_port)s/
# https certification and its key file # https certification and its key file
@@ -172,7 +169,7 @@ transaction_from_ai_image_recognition = false
max_ai_recognition_picture_size = 10485760 max_ai_recognition_picture_size = 10485760
[llm_image_recognition] [llm_image_recognition]
# Large Language Model (LLM) provider for receipt image recognition, supports the following types: "openai", "openai_compatible", "openrouter", "ollama", "google_ai" # Large Language Model (LLM) provider for receipt image recognition, supports the following types: "openai", "openai_compatible", "anthropic", "anthropic_compatible", "openrouter", "ollama", "lm_studio", "google_ai"
llm_provider = llm_provider =
# For "openai" llm provider only, OpenAI API secret key, please visit https://platform.openai.com/api-keys for more information # For "openai" llm provider only, OpenAI API secret key, please visit https://platform.openai.com/api-keys for more information
@@ -190,6 +187,30 @@ openai_compatible_api_key =
# For "openai_compatible" llm provider only, receipt image recognition model for creating transactions from images # For "openai_compatible" llm provider only, receipt image recognition model for creating transactions from images
openai_compatible_model_id = openai_compatible_model_id =
# For "anthropic" llm provider only, Anthropic API key, please visit https://platform.claude.com/settings/keys for more information
anthropic_api_key =
# For "anthropic" llm provider only, receipt image recognition model for creating transactions from images
anthropic_model_id =
# For "anthropic" llm provider only, maximum allowed number of generated tokens for creating transactions from images, default is 1024
anthropic_max_tokens = 1024
# For "anthropic_compatible" llm provider only, Anthropic compatible API base url, e.g. "https://api.anthropic.com/v1/"
anthropic_compatible_base_url =
# For "anthropic_compatible" llm provider only, Anthropic compatible API version, e.g. "2023-06-01". If the LLM service does not require API versioning, leave it blank
anthropic_compatible_api_version =
# For "anthropic_compatible" llm provider only, Anthropic compatible API secret key
anthropic_compatible_api_key =
# For "anthropic_compatible" llm provider only, receipt image recognition model for creating transactions from images
anthropic_compatible_model_id =
# For "anthropic_compatible" llm provider only, maximum allowed number of generated tokens for creating transactions from images, default is 1024
anthropic_compatible_max_tokens = 1024
# For "openrouter" llm provider only, OpenRouter API key, please visit https://openrouter.ai/settings/keys for more information # For "openrouter" llm provider only, OpenRouter API key, please visit https://openrouter.ai/settings/keys for more information
openrouter_api_key = openrouter_api_key =
@@ -202,6 +223,15 @@ ollama_server_url =
# For "ollama" llm provider only, receipt image recognition model for creating transactions from images # For "ollama" llm provider only, receipt image recognition model for creating transactions from images
ollama_model_id = ollama_model_id =
# For "lm_studio" llm provider only, LM Studio server url, e.g. "http://127.0.0.1:1234/"
lm_studio_server_url =
# For "lm_studio" llm provider only, LM Studio API token, if "require authentication" is not enabled in LM Studio, leave it blank
lm_studio_token =
# For "lm_studio" llm provider only, receipt image recognition model for creating transactions from images
lm_studio_model_id =
# For "google_ai" llm provider only, Google AI Studio API key, please visit https://aistudio.google.com/apikey for more information # For "google_ai" llm provider only, Google AI Studio API key, please visit https://aistudio.google.com/apikey for more information
google_ai_api_key = google_ai_api_key =
@@ -266,6 +296,9 @@ password_reset_token_expired_time = 3600
# Set to true to enable API token generation # Set to true to enable API token generation
enable_api_token = false enable_api_token = false
# Allowed remote IPs for using the API token, a comma-separated list of allowed remote IPs (asterisk * for any addresses, e.g. 192.168.1.* means any IPs in the 192.168.1.x subnet), leave blank to allow all remote IPs
api_token_allowed_remote_ips =
# Maximum count of password / token check failures (0 - 4294967295) per IP per minute (use the above duplicate checker), default is 5, set to 0 to disable # Maximum count of password / token check failures (0 - 4294967295) per IP per minute (use the above duplicate checker), default is 5, set to 0 to disable
max_failures_per_ip_per_minute = 5 max_failures_per_ip_per_minute = 5
@@ -496,7 +529,6 @@ custom_map_tile_server_default_zoom_level = 14
[exchange_rates] [exchange_rates]
# Exchange rates data source, supports the following types: # Exchange rates data source, supports the following types:
# "reserve_bank_of_australia": https://www.rba.gov.au/statistics/frequency/exchange-rates.html
# "bank_of_canada": https://www.bankofcanada.ca/rates/exchange/daily-exchange-rates/ # "bank_of_canada": https://www.bankofcanada.ca/rates/exchange/daily-exchange-rates/
# "czech_national_bank": https://www.cnb.cz/en/financial-markets/foreign-exchange-market/central-bank-exchange-rate-fixing/central-bank-exchange-rate-fixing/ # "czech_national_bank": https://www.cnb.cz/en/financial-markets/foreign-exchange-market/central-bank-exchange-rate-fixing/central-bank-exchange-rate-fixing/
# "danmarks_national_bank": https://www.nationalbanken.dk/en/what-we-do/stable-prices-monetary-policy-and-the-danish-economy/exchange-rates # "danmarks_national_bank": https://www.nationalbanken.dk/en/what-we-do/stable-prices-monetary-policy-and-the-danish-economy/exchange-rates
@@ -512,7 +544,6 @@ custom_map_tile_server_default_zoom_level = 14
# "swiss_national_bank": https://www.snb.ch/en/the-snb/mandates-goals/statistics/statistics-pub/current_interest_exchange_rates # "swiss_national_bank": https://www.snb.ch/en/the-snb/mandates-goals/statistics/statistics-pub/current_interest_exchange_rates
# "national_bank_of_ukraine": https://bank.gov.ua/ua/markets/exchangerates # "national_bank_of_ukraine": https://bank.gov.ua/ua/markets/exchangerates
# "central_bank_of_uzbekistan": https://cbu.uz/en/arkhiv-kursov-valyut/ # "central_bank_of_uzbekistan": https://cbu.uz/en/arkhiv-kursov-valyut/
# "international_monetary_fund": https://www.imf.org/external/np/fin/data/param_rms_mth.aspx
# "user_custom": users set their own exchange rates data in the UI # "user_custom": users set their own exchange rates data in the UI
data_source = euro_central_bank data_source = euro_central_bank
+74
View File
@@ -0,0 +1,74 @@
{
"code": [
"jiangshengwu",
"vigdail",
"f97",
"Miguelonlonlon",
"seb26",
"nktlitvinenko",
"lvdou-bing",
"dshemin",
"lucdsouza",
"OuIChien",
"RasterCrow"
],
"translators": {
"de": [
"chrgm",
"1270o1"
],
"en": [],
"es": [
"Miguelonlonlon",
"abrugues",
"AndresTeller",
"diegofercri"
],
"fr": [
"brieucdlf"
],
"it": [
"waron97"
],
"ja": [
"tkymmm"
],
"kn": [
"Darshanbm05"
],
"ko": [
"overworks"
],
"nl": [
"automagics"
],
"pt-BR": [
"thecodergus",
"balaios"
],
"ru": [
"artegoser",
"dshemin"
],
"sl": [
"thehijacker"
],
"ta": [
"hhharsha36"
],
"th": [
"natthavat28"
],
"tr": [
"aydnykn"
],
"uk": [
"nktlitvinenko"
],
"vi": [
"f97"
],
"zh-Hans": [],
"zh-Hant": []
}
}
+4 -4
View File
@@ -10,7 +10,7 @@ import (
"github.com/urfave/cli/v3" "github.com/urfave/cli/v3"
"github.com/mayswind/ezbookkeeping/cmd" "github.com/mayswind/ezbookkeeping/cmd"
"github.com/mayswind/ezbookkeeping/pkg/settings" "github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/utils" "github.com/mayswind/ezbookkeeping/pkg/utils"
) )
@@ -26,9 +26,9 @@ var (
) )
func main() { func main() {
settings.Version = Version core.Version = Version
settings.CommitHash = CommitHash core.CommitHash = CommitHash
settings.BuildTime = BuildUnixTime core.BuildTime = BuildUnixTime
cmd := &cli.Command{ cmd := &cli.Command{
Name: "ezBookkeeping", Name: "ezBookkeeping",
+39 -36
View File
@@ -1,34 +1,34 @@
module github.com/mayswind/ezbookkeeping module github.com/mayswind/ezbookkeeping
go 1.25 go 1.25.0
require ( require (
github.com/boombuler/barcode v1.1.0 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/extrame/xls v0.0.2-0.20200426124601-4a6cf263071b
github.com/gin-contrib/cache v1.4.1 github.com/gin-contrib/cache v1.4.3
github.com/gin-contrib/gzip v1.2.5 github.com/gin-contrib/gzip v1.2.6
github.com/gin-gonic/gin v1.11.0 github.com/gin-gonic/gin v1.12.0
github.com/go-co-op/gocron/v2 v2.17.0 github.com/go-co-op/gocron/v2 v2.19.1
github.com/go-playground/validator/v10 v10.28.0 github.com/go-playground/validator/v10 v10.30.2
github.com/go-sql-driver/mysql v1.9.3 github.com/go-sql-driver/mysql v1.9.3
github.com/golang-jwt/jwt/v5 v5.3.0 github.com/golang-jwt/jwt/v5 v5.3.1
github.com/invopop/jsonschema v0.13.0 github.com/invopop/jsonschema v0.13.0
github.com/lib/pq v1.10.9 github.com/lib/pq v1.12.1
github.com/mattn/go-sqlite3 v1.14.32 github.com/mattn/go-sqlite3 v1.14.38
github.com/minio/minio-go/v7 v7.0.95 github.com/minio/minio-go/v7 v7.0.99
github.com/patrickmn/go-cache v2.1.0+incompatible github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/pquerna/otp v1.5.0 github.com/pquerna/otp v1.5.0
github.com/sirupsen/logrus v1.9.3 github.com/sirupsen/logrus v1.9.4
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
github.com/urfave/cli/v3 v3.5.0 github.com/urfave/cli/v3 v3.8.0
github.com/wk8/go-ordered-map/v2 v2.1.8 github.com/wk8/go-ordered-map/v2 v2.1.8
github.com/xuri/excelize/v2 v2.10.0 github.com/xuri/excelize/v2 v2.10.1
golang.org/x/crypto v0.43.0 golang.org/x/crypto v0.49.0
golang.org/x/net v0.46.0 golang.org/x/net v0.52.0
golang.org/x/oauth2 v0.32.0 golang.org/x/oauth2 v0.36.0
golang.org/x/text v0.30.0 golang.org/x/text v0.35.0
gopkg.in/ini.v1 v1.67.0 gopkg.in/ini.v1 v1.67.1
gopkg.in/mail.v2 v2.3.1 gopkg.in/mail.v2 v2.3.1
xorm.io/builder v0.3.13 xorm.io/builder v0.3.13
xorm.io/xorm v1.3.11 xorm.io/xorm v1.3.11
@@ -40,8 +40,8 @@ require (
github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf // indirect github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf // indirect
github.com/buger/jsonparser v1.1.1 // indirect github.com/buger/jsonparser v1.1.1 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.14.1 // indirect github.com/bytedance/sonic v1.15.0 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect github.com/bytedance/sonic/loader v0.5.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
github.com/chenzhuoyu/iasm v0.9.1 // indirect github.com/chenzhuoyu/iasm v0.9.1 // indirect
@@ -52,26 +52,27 @@ require (
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/extrame/goyymmdd v0.0.0-20210114090516-7cc815f00d1a // indirect github.com/extrame/goyymmdd v0.0.0-20210114090516-7cc815f00d1a // indirect
github.com/extrame/ole2 v0.0.0-20160812065207-d69429661ad7 // indirect github.com/extrame/ole2 v0.0.0-20160812065207-d69429661ad7 // indirect
github.com/gabriel-vasile/mimetype v1.4.10 // indirect github.com/gabriel-vasile/mimetype v1.4.13 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-ini/ini v1.67.0 // indirect github.com/go-ini/ini v1.67.0 // indirect
github.com/go-jose/go-jose/v4 v4.1.3 // indirect github.com/go-jose/go-jose/v4 v4.1.3 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect github.com/goccy/go-yaml v1.19.2 // indirect
github.com/golang/snappy v0.0.4 // indirect github.com/golang/snappy v0.0.4 // indirect
github.com/gomodule/redigo v1.9.2 // indirect github.com/gomodule/redigo v1.9.2 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/jonboulle/clockwork v0.5.0 // indirect github.com/jonboulle/clockwork v0.5.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/compress v1.18.2 // indirect
github.com/klauspost/cpuid/v2 v2.3.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/leodido/go-urn v1.4.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/memcachier/mc/v3 v3.0.3 // indirect github.com/memcachier/mc/v3 v3.0.3 // indirect
github.com/minio/crc64nvme v1.0.2 // indirect github.com/minio/crc64nvme v1.1.1 // indirect
github.com/minio/md5-simd v1.1.2 // 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/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
@@ -79,29 +80,31 @@ require (
github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/philhofer/fwd v1.2.0 // indirect github.com/philhofer/fwd v1.2.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/quic-go/qpack v0.5.1 // indirect github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.55.0 // indirect github.com/quic-go/quic-go v0.59.0 // indirect
github.com/richardlehane/mscfb v1.0.4 // indirect github.com/richardlehane/mscfb v1.0.6 // indirect
github.com/richardlehane/msoleps v1.0.4 // indirect github.com/richardlehane/msoleps v1.0.6 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/robfig/go-cache v0.0.0-20130306151617-9fc39e0dbf62 // indirect github.com/robfig/go-cache v0.0.0-20130306151617-9fc39e0dbf62 // indirect
github.com/rs/xid v1.6.0 // indirect github.com/rs/xid v1.6.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/syndtr/goleveldb v1.0.0 // indirect github.com/syndtr/goleveldb v1.0.0 // indirect
github.com/tealeg/xlsx v1.0.5 // indirect github.com/tealeg/xlsx v1.0.5 // indirect
github.com/tiendc/go-deepcopy v1.7.1 // indirect github.com/tiendc/go-deepcopy v1.7.2 // indirect
github.com/tinylib/msgp v1.3.0 // indirect github.com/tinylib/msgp v1.6.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect github.com/ugorji/go/codec v1.3.1 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
github.com/xuri/efp v0.0.1 // indirect github.com/xuri/efp v0.0.1 // indirect
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // indirect github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // indirect
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/arch v0.22.0 // indirect golang.org/x/arch v0.22.0 // indirect
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect
golang.org/x/mod v0.29.0 // indirect golang.org/x/mod v0.33.0 // indirect
golang.org/x/sync v0.17.0 // indirect golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.37.0 // indirect golang.org/x/sys v0.42.0 // indirect
golang.org/x/tools v0.38.0 // indirect golang.org/x/tools v0.42.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect google.golang.org/protobuf v1.36.10 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
+76 -72
View File
@@ -14,10 +14,10 @@ github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM= github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7w= github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc= github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
@@ -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 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= 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/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.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc=
github.com/coreos/go-oidc/v3 v3.16.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8= 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/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.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -43,18 +43,18 @@ github.com/extrame/ole2 v0.0.0-20160812065207-d69429661ad7/go.mod h1:GPpMrAfHdb8
github.com/extrame/xls v0.0.2-0.20200426124601-4a6cf263071b h1:jqW/h4gcXYEB6kVf6iuxjU9ONWA0ugUB94TP9UNmgdg= github.com/extrame/xls v0.0.2-0.20200426124601-4a6cf263071b h1:jqW/h4gcXYEB6kVf6iuxjU9ONWA0ugUB94TP9UNmgdg=
github.com/extrame/xls v0.0.2-0.20200426124601-4a6cf263071b/go.mod h1:iACcgahst7BboCpIMSpnFs4SKyU9ZjsvZBfNbUxZOJI= github.com/extrame/xls v0.0.2-0.20200426124601-4a6cf263071b/go.mod h1:iACcgahst7BboCpIMSpnFs4SKyU9ZjsvZBfNbUxZOJI=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gin-contrib/cache v1.4.1 h1:HcLwLfw7p+FasNp5VAnFbbBj9SzB4bDtswvon7wYSg4= github.com/gin-contrib/cache v1.4.3 h1:6rmIlmTf2Vyfd/ue53+BLdTxC7hrQ7FqRgfjz31nEEE=
github.com/gin-contrib/cache v1.4.1/go.mod h1:tykDV+FgItJHYEO0eCasuRYsZKPPyb4BYhAjuTlG6RM= github.com/gin-contrib/cache v1.4.3/go.mod h1:Znf5Qa8HTQ+QHku6ODf72WOPnJ2fHUd2nXD6mSi+6+g=
github.com/gin-contrib/gzip v1.2.5 h1:fIZs0S+l17pIu1P5XRJOo/YNqfIuPCrZZ3TWB7pjckI= github.com/gin-contrib/gzip v1.2.6 h1:OtN8DplD5DNZCSLAnQ5HxRkD2qZ5VU+JhOrcfJrcRvg=
github.com/gin-contrib/gzip v1.2.5/go.mod h1:aomRgR7ftdZV3uWY0gW/m8rChfxau0n8YVvwlOHONzw= github.com/gin-contrib/gzip v1.2.6/go.mod h1:BQy8/+JApnRjAVUplSGZiVtD2k8GmIE2e9rYu/hLzzU=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= 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-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.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
github.com/go-co-op/gocron/v2 v2.17.0 h1:e/oj6fcAM8vOOKZxv2Cgfmjo+s8AXC46po5ZPtaSea4= github.com/go-co-op/gocron/v2 v2.19.1 h1:B4iLeA0NB/2iO3EKQ7NfKn5KsQgZfjb2fkvoZJU3yBI=
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.19.1/go.mod h1:5lEiCKk1oVJV39Zg7/YG10OnaVrDAV5GGR6O0663k6U=
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= 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-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
@@ -63,16 +63,16 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688= github.com/go-playground/validator/v10 v10.30.2 h1:JiFIMtSSHb2/XBUbWM4i/MpeQm9ZK2xqPNk8vgvu5JQ=
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU= github.com/go-playground/validator/v10 v10.30.2/go.mod h1:mAf2pIOVXjTEBrwUMGKkCWKKPs9NheYGabeB04txQSc=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
@@ -90,34 +90,36 @@ github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7X
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 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 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= 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/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/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.12.1 h1:x1nbl/338GLqeDJ/FAiILallhAsqubLzEZu/pXtHUow=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.12.1/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= github.com/mattn/go-sqlite3 v1.14.38 h1:tDUzL85kMvOrvpCt8P64SbGgVFtJB11GPi2AdmITgb4=
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v1.14.38/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 h1:qii+lDiPKi36O4Xg+HVKwHu6Oq+Gt17b+uEiA0Drwv4=
github.com/memcachier/mc/v3 v3.0.3/go.mod h1:GzjocBahcXPxt2cmqzknrgqCOmMxiSzhVKPOe90Tpug= 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.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI=
github.com/minio/crc64nvme v1.0.2/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg= github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= 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/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.99 h1:2vH/byrwUkIpFQFOilvTfaUpvAX3fEFhEzO+DR3DlCE=
github.com/minio/minio-go/v7 v7.0.95/go.mod h1:wOOX3uxS334vImCNRVyIDdXX9OsXDm89ToynKgqUKlo= github.com/minio/minio-go/v7 v7.0.99/go.mod h1:EtGNKtlX20iL2yaYnxEigaIvj0G0GwSDnifnG8ClIdw=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 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 h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -137,15 +139,14 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs= github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk= github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U= github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM= github.com/richardlehane/mscfb v1.0.6 h1:eN3bvvZCp00bs7Zf52bxNwAx5lJDBK1tCuH19qq5aC8=
github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk= github.com/richardlehane/mscfb v1.0.6/go.mod h1:pe0+IUIc0AHh0+teNzBlJCtSyZdFOGgV4ZK9bsoV+Jo=
github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= github.com/richardlehane/msoleps v1.0.6 h1:9BvkpjvD+iUBalUY4esMwv6uBkfOip/Lzvd93jvR9gg=
github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00= github.com/richardlehane/msoleps v1.0.6/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/robfig/go-cache v0.0.0-20130306151617-9fc39e0dbf62 h1:pyecQtsPmlkCsMkYhT5iZ+sUXuwee+OvfuJjinEA3ko= github.com/robfig/go-cache v0.0.0-20130306151617-9fc39e0dbf62 h1:pyecQtsPmlkCsMkYhT5iZ+sUXuwee+OvfuJjinEA3ko=
@@ -153,67 +154,70 @@ github.com/robfig/go-cache v0.0.0-20130306151617-9fc39e0dbf62/go.mod h1:65XQgovT
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
github.com/tealeg/xlsx v1.0.5 h1:+f8oFmvY8Gw1iUXzPk+kz+4GpbDZPK1FhPiQRd+ypgE= github.com/tealeg/xlsx v1.0.5 h1:+f8oFmvY8Gw1iUXzPk+kz+4GpbDZPK1FhPiQRd+ypgE=
github.com/tealeg/xlsx v1.0.5/go.mod h1:btRS8dz54TDnvKNosuAqxrM1QgN1udgk9O34bDCnORM= github.com/tealeg/xlsx v1.0.5/go.mod h1:btRS8dz54TDnvKNosuAqxrM1QgN1udgk9O34bDCnORM=
github.com/tiendc/go-deepcopy v1.7.1 h1:LnubftI6nYaaMOcaz0LphzwraqN8jiWTwm416sitff4= github.com/tiendc/go-deepcopy v1.7.2 h1:Ut2yYR7W9tWjTQitganoIue4UGxZwCcJy3orjrrIj44=
github.com/tiendc/go-deepcopy v1.7.1/go.mod h1:4bKjNC2r7boYOkD2IOuZpYjmlDdzjbpTRyCx+goBCJQ= github.com/tiendc/go-deepcopy v1.7.2/go.mod h1:4bKjNC2r7boYOkD2IOuZpYjmlDdzjbpTRyCx+goBCJQ=
github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww= github.com/tinylib/msgp v1.6.1 h1:ESRv8eL3u+DNHUoSAAQRE50Hm162zqAnBoGv9PzScPY=
github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= github.com/tinylib/msgp v1.6.1/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 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.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/urfave/cli/v3 v3.5.0 h1:qCuFMmdayTF3zmjG8TSsoBzrDqszNrklYg2x3g4MSgw= github.com/urfave/cli/v3 v3.8.0 h1:XqKPrm0q4P0q5JpoclYoCAv0/MIvH/jZ2umzuf8pNTI=
github.com/urfave/cli/v3 v3.5.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso= github.com/urfave/cli/v3 v3.8.0/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 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= 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= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8= github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8=
github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
github.com/xuri/excelize/v2 v2.10.0 h1:8aKsP7JD39iKLc6dH5Tw3dgV3sPRh8uRVXu/fMstfW4= github.com/xuri/excelize/v2 v2.10.1 h1:V62UlqopMqha3kOpnlHy2CcRVw1V8E63jFoWUmMzxN0=
github.com/xuri/excelize/v2 v2.10.0/go.mod h1:SC5TzhQkaOsTWpANfm+7bJCldzcnU/jrhqkTi/iBHBU= github.com/xuri/excelize/v2 v2.10.1/go.mod h1:iG5tARpgaEeIhTqt3/fgXCGoBRt4hNXgCp3tfXKoOIc=
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBLbf3WdLgC29pgyhTjAT/0nuE= github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBLbf3WdLgC29pgyhTjAT/0nuE=
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 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 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= 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.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= 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.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 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.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sync v0.17.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-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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 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.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= 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= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
@@ -221,8 +225,8 @@ gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.1 h1:tVBILHy0R6e4wkYOn3XmiITt/hEVH4TFMYvAX2Ytz6k=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.67.1/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss=
gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk= gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk=
gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw= gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+2997 -2778
View File
File diff suppressed because it is too large Load Diff
+57 -56
View File
@@ -1,6 +1,6 @@
{ {
"name": "ezbookkeeping", "name": "ezbookkeeping",
"version": "1.2.0", "version": "1.5.0",
"private": true, "private": true,
"repository": { "repository": {
"type": "git", "type": "git",
@@ -19,63 +19,64 @@
"test": "cross-env TS_NODE_PROJECT=\"./tsconfig.jest.json\" jest" "test": "cross-env TS_NODE_PROJECT=\"./tsconfig.jest.json\" jest"
}, },
"dependencies": { "dependencies": {
"@mdi/js": "^7.4.47", "@mdi/js": "7.4.47",
"@vuepic/vue-datepicker": "^11.0.3", "@vuepic/vue-datepicker": "12.1.0",
"axios": "^1.12.2", "axios": "1.14.0",
"cbor-js": "^0.1.0", "cbor-js": "0.1.0",
"clipboard": "^2.0.11", "chardet": "2.1.1",
"crypto-js": "^4.2.0", "clipboard": "2.0.11",
"dom7": "^4.0.6", "crypto-js": "4.2.0",
"echarts": "^6.0.0", "dom7": "4.0.6",
"framework7": "^8.3.4", "echarts": "6.0.0",
"framework7-icons": "^5.0.5", "framework7": "9.0.3",
"framework7-vue": "^8.3.4", "framework7-icons": "5.0.5",
"jalaali-js": "^1.2.8", "framework7-vue": "9.0.3",
"leaflet": "^1.9.4", "jalaali-js": "1.2.8",
"line-awesome": "^1.3.0", "leaflet": "1.9.4",
"moment": "^2.30.1", "line-awesome": "1.3.0",
"moment-timezone": "^0.6.0", "moment": "2.30.1",
"pinia": "^3.0.3", "moment-timezone": "0.6.1",
"register-service-worker": "^1.7.2", "pinia": "3.0.4",
"skeleton-elements": "^4.0.1", "register-service-worker": "1.7.2",
"swiper": "^10.2.0", "skeleton-elements": "4.0.1",
"ua-parser-js": "^1.0.39", "swiper": "12.1.3",
"vue": "^3.5.22", "ua-parser-js": "1.0.39",
"vue-echarts": "^8.0.1", "vue": "3.5.31",
"vue-i18n": "^11.1.12", "vue-echarts": "8.0.1",
"vue-router": "^4.6.3", "vue-i18n": "11.3.0",
"vue3-perfect-scrollbar": "^2.0.0", "vue-router": "5.0.4",
"vuedraggable": "^4.1.0", "vue3-perfect-scrollbar": "2.0.0",
"vuetify": "^3.10.7" "vuedraggable": "4.1.0",
"vuetify": "3.12.4"
}, },
"devDependencies": { "devDependencies": {
"@jest/globals": "^30.2.0", "@jest/globals": "30.3.0",
"@tsconfig/node24": "^24.0.1", "@tsconfig/node24": "24.0.4",
"@types/cbor-js": "^0.1.1", "@types/cbor-js": "0.1.1",
"@types/crypto-js": "^4.2.2", "@types/crypto-js": "4.2.2",
"@types/git-rev-sync": "^2.0.2", "@types/git-rev-sync": "2.0.2",
"@types/jalaali-js": "^1.2.0", "@types/jalaali-js": "1.2.0",
"@types/jest": "^30.0.0", "@types/jest": "30.0.0",
"@types/node": "^24.9.1", "@types/node": "25.5.0",
"@types/ua-parser-js": "^0.7.39", "@types/ua-parser-js": "0.7.39",
"@vitejs/plugin-vue": "^6.0.1", "@vitejs/plugin-vue": "6.0.5",
"@vue/eslint-config-typescript": "^14.6.0", "@vue/eslint-config-typescript": "14.7.0",
"@vue/tsconfig": "^0.8.1", "@vue/tsconfig": "0.9.1",
"cross-env": "^10.1.0", "cross-env": "10.1.0",
"eslint": "^9.38.0", "eslint": "10.1.0",
"eslint-plugin-vue": "^10.5.1", "eslint-plugin-vue": "10.8.0",
"git-rev-sync": "^3.0.2", "git-rev-sync": "3.0.2",
"jest": "^30.2.0", "jest": "30.3.0",
"postcss-preset-env": "^10.4.0", "postcss-preset-env": "11.2.0",
"sass": "^1.93.2", "sass": "1.98.0",
"ts-jest": "^29.4.5", "ts-jest": "29.4.6",
"ts-node": "^10.9.2", "ts-node": "10.9.2",
"typescript": "^5.9.3", "typescript": "5.9.3",
"vite": "^7.1.12", "vite": "7.3.1",
"vite-plugin-checker": "^0.11.0", "vite-plugin-checker": "0.12.0",
"vite-plugin-pwa": "^1.1.0", "vite-plugin-pwa": "1.2.0",
"vite-plugin-vuetify": "^2.1.2", "vite-plugin-vuetify": "2.1.3",
"vue-tsc": "^3.1.2" "vue-tsc": "3.2.6"
}, },
"browserslist": [ "browserslist": [
"last 5 Chrome versions", "last 5 Chrome versions",
+39 -16
View File
@@ -150,10 +150,10 @@ func (a *AccountsApi) AccountCreateHandler(c *core.WebContext) (any, *errs.Error
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err) return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
} }
utcOffset, err := c.GetClientTimezoneOffset() clientTimezone, err := c.GetClientTimezone()
if err != nil { 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 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 { 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()) 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 return nil, errs.ErrAccountIdInvalid
} }
utcOffset, err := c.GetClientTimezoneOffset() clientTimezone, err := c.GetClientTimezone()
if err != nil { 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 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) toUpdateAccount := a.getToUpdateAccount(uid, &accountModifyReq, mainAccount, false)
if toUpdateAccount != nil { 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 anythingUpdate = true
toUpdateAccounts = append(toUpdateAccounts, toUpdateAccount) 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 { 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()) 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.Type = oldAccount.Type
account.ParentAccountId = oldAccount.ParentAccountId account.ParentAccountId = oldAccount.ParentAccountId
account.DisplayOrder = oldAccount.DisplayOrder
account.Currency = oldAccount.Currency account.Currency = oldAccount.Currency
account.Balance = oldAccount.Balance account.Balance = oldAccount.Balance
@@ -703,6 +713,9 @@ func (a *AccountsApi) createNewAccountModel(uid int64, accountCreateReq *models.
if !isSubAccount && accountCreateReq.Category == models.ACCOUNT_CATEGORY_CREDIT_CARD { if !isSubAccount && accountCreateReq.Category == models.ACCOUNT_CATEGORY_CREDIT_CARD {
accountExtend.CreditCardStatementDate = &accountCreateReq.CreditCardStatementDate accountExtend.CreditCardStatementDate = &accountCreateReq.CreditCardStatementDate
if accountCreateReq.CreditLimit > 0 {
accountExtend.CreditLimit = &accountCreateReq.CreditLimit
}
} }
return &models.Account{ return &models.Account{
@@ -759,18 +772,22 @@ func (a *AccountsApi) getToUpdateAccount(uid int64, accountModifyReq *models.Acc
if !isSubAccount && accountModifyReq.Category == models.ACCOUNT_CATEGORY_CREDIT_CARD { if !isSubAccount && accountModifyReq.Category == models.ACCOUNT_CATEGORY_CREDIT_CARD {
newAccountExtend.CreditCardStatementDate = &accountModifyReq.CreditCardStatementDate newAccountExtend.CreditCardStatementDate = &accountModifyReq.CreditCardStatementDate
if accountModifyReq.CreditLimit > 0 {
newAccountExtend.CreditLimit = &accountModifyReq.CreditLimit
}
} }
newAccount := &models.Account{ newAccount := &models.Account{
AccountId: oldAccount.AccountId, AccountId: oldAccount.AccountId,
Uid: uid, Uid: uid,
Name: accountModifyReq.Name, Name: accountModifyReq.Name,
Category: accountModifyReq.Category, DisplayOrder: oldAccount.DisplayOrder,
Icon: accountModifyReq.Icon, Category: accountModifyReq.Category,
Color: accountModifyReq.Color, Icon: accountModifyReq.Icon,
Comment: accountModifyReq.Comment, Color: accountModifyReq.Color,
Extend: newAccountExtend, Comment: accountModifyReq.Comment,
Hidden: accountModifyReq.Hidden, Extend: newAccountExtend,
Hidden: accountModifyReq.Hidden,
} }
if newAccount.Name != oldAccount.Name || if newAccount.Name != oldAccount.Name ||
@@ -793,6 +810,12 @@ func (a *AccountsApi) getToUpdateAccount(uid int64, accountModifyReq *models.Acc
return newAccount return newAccount
} }
if newAccountExtend.CreditLimit != oldAccountExtend.CreditLimit {
if newAccountExtend.CreditLimit == nil || oldAccountExtend.CreditLimit == nil || *newAccountExtend.CreditLimit != *oldAccountExtend.CreditLimit {
return newAccount
}
}
return nil return nil
} }
+47 -23
View File
@@ -28,9 +28,11 @@ type DataManagementsApi struct {
transactions *services.TransactionService transactions *services.TransactionService
categories *services.TransactionCategoryService categories *services.TransactionCategoryService
tags *services.TransactionTagService tags *services.TransactionTagService
tagGroups *services.TransactionTagGroupService
pictures *services.TransactionPictureService pictures *services.TransactionPictureService
templates *services.TransactionTemplateService templates *services.TransactionTemplateService
userCustomExchangeRates *services.UserCustomExchangeRatesService userCustomExchangeRates *services.UserCustomExchangeRatesService
insightsExploreres *services.InsightsExplorerService
} }
// Initialize a data management api singleton instance // Initialize a data management api singleton instance
@@ -45,9 +47,11 @@ var (
transactions: services.Transactions, transactions: services.Transactions,
categories: services.TransactionCategories, categories: services.TransactionCategories,
tags: services.TransactionTags, tags: services.TransactionTags,
tagGroups: services.TransactionTagGroups,
pictures: services.TransactionPictures, pictures: services.TransactionPictures,
templates: services.TransactionTemplates, templates: services.TransactionTemplates,
userCustomExchangeRates: services.UserCustomExchangeRates, userCustomExchangeRates: services.UserCustomExchangeRates,
insightsExploreres: services.InsightsExplorers,
} }
) )
@@ -99,6 +103,13 @@ func (a *DataManagementsApi) DataStatisticsHandler(c *core.WebContext) (any, *er
return nil, errs.ErrOperationFailed 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) totalTransactionTemplateCount, err := a.templates.GetTotalNormalTemplateCountByUid(c, uid)
if err != nil { if err != nil {
@@ -119,6 +130,7 @@ func (a *DataManagementsApi) DataStatisticsHandler(c *core.WebContext) (any, *er
TotalTransactionTagCount: totalTransactionTagCount, TotalTransactionTagCount: totalTransactionTagCount,
TotalTransactionCount: totalTransactionCount, TotalTransactionCount: totalTransactionCount,
TotalTransactionPictureCount: totalTransactionPictureCount, TotalTransactionPictureCount: totalTransactionPictureCount,
TotalInsightsExplorerCount: totalInsightsExplorerCount,
TotalTransactionTemplateCount: totalTransactionTemplateCount, TotalTransactionTemplateCount: totalTransactionTemplateCount,
TotalScheduledTransactionCount: totalScheduledTransactionCount, TotalScheduledTransactionCount: totalScheduledTransactionCount,
} }
@@ -183,6 +195,13 @@ func (a *DataManagementsApi) ClearAllDataHandler(c *core.WebContext) (any, *errs
return nil, errs.Or(err, errs.ErrOperationFailed) 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) err = a.userCustomExchangeRates.DeleteAllCustomExchangeRates(c, uid)
if err != nil { if err != nil {
@@ -190,6 +209,13 @@ func (a *DataManagementsApi) ClearAllDataHandler(c *core.WebContext) (any, *errs
return nil, errs.Or(err, errs.ErrOperationFailed) 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) log.Infof(c, "[data_managements.ClearAllDataHandler] user \"uid:%d\" has cleared all data", uid)
return true, nil return true, nil
} }
@@ -298,17 +324,15 @@ func (a *DataManagementsApi) getExportedFileContent(c *core.WebContext, fileType
err := c.ShouldBindQuery(&exportTransactionDataReq) err := c.ShouldBindQuery(&exportTransactionDataReq)
if err != nil { 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) return nil, "", errs.NewIncompleteOrIncorrectSubmissionError(err)
} }
timezone := time.Local clientTimezone, err := c.GetClientTimezone()
utcOffset, err := c.GetClientTimezoneOffset()
if err != nil { if err != nil {
log.Warnf(c, "[data_managements.ExportDataHandler] cannot get client timezone offset, because %s", err.Error()) log.Warnf(c, "[data_managements.getExportedFileContent] cannot get client timezone, because %s", err.Error())
} else { clientTimezone = time.Local
timezone = time.FixedZone("Client Timezone", int(utcOffset)*60)
} }
uid := c.GetCurrentUid() uid := c.GetCurrentUid()
@@ -316,7 +340,7 @@ func (a *DataManagementsApi) getExportedFileContent(c *core.WebContext, fileType
if err != nil { if err != nil {
if !errs.IsCustomError(err) { 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 return nil, "", errs.ErrUserNotFound
@@ -329,28 +353,28 @@ func (a *DataManagementsApi) getExportedFileContent(c *core.WebContext, fileType
accounts, err := a.accounts.GetAllAccountsByUid(c, uid) accounts, err := a.accounts.GetAllAccountsByUid(c, uid)
if err != nil { 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 return nil, "", errs.ErrOperationFailed
} }
categories, err := a.categories.GetAllCategoriesByUid(c, uid, 0, -1) categories, err := a.categories.GetAllCategoriesByUid(c, uid, 0, -1)
if err != nil { 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 return nil, "", errs.ErrOperationFailed
} }
tags, err := a.tags.GetAllTagsByUid(c, uid) tags, err := a.tags.GetAllTagsByUid(c, uid)
if err != nil { 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 return nil, "", errs.ErrOperationFailed
} }
tagIndexes, err := a.tags.GetAllTagIdsMapOfAllTransactions(c, uid) tagIndexes, err := a.tags.GetAllTagIdsMapOfAllTransactions(c, uid)
if err != nil { 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 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) allAccountIds, err := a.accounts.GetAccountOrSubAccountIds(c, exportTransactionDataReq.AccountIds, uid)
if err != nil { 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) return nil, "", errs.Or(err, errs.ErrOperationFailed)
} }
allCategoryIds, err := a.categories.GetCategoryOrSubCategoryIds(c, exportTransactionDataReq.CategoryIds, uid) allCategoryIds, err := a.categories.GetCategoryOrSubCategoryIds(c, exportTransactionDataReq.CategoryIds, uid)
if err != nil { 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) return nil, "", errs.Or(err, errs.ErrOperationFailed)
} }
var allTagIds []int64 noTags := exportTransactionDataReq.TagFilter == models.TransactionNoTagFilterValue
noTags := exportTransactionDataReq.TagIds == "none" var tagFilters []*models.TransactionTagFilter
if !noTags { if !noTags {
allTagIds, err = a.tags.GetTagIds(exportTransactionDataReq.TagIds) tagFilters, err = models.ParseTransactionTagFilter(exportTransactionDataReq.TagFilter)
if err != nil { 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) return nil, "", errs.Or(err, errs.ErrOperationFailed)
} }
} }
@@ -395,10 +419,10 @@ func (a *DataManagementsApi) getExportedFileContent(c *core.WebContext, fileType
minTransactionTime = utils.GetMinTransactionTimeFromUnixTime(exportTransactionDataReq.MinTime) 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 { 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 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) result, err := dataExporter.ToExportedContent(c, uid, allTransactions, accountMap, categoryMap, tagMap, tagIndexes)
if err != nil { 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) return nil, "", errs.Or(err, errs.ErrOperationFailed)
} }
fileName := a.getFileName(user, timezone, fileType) fileName := a.getFileName(user, clientTimezone, fileType)
return result, fileName, nil return result, fileName, nil
} }
func (a *DataManagementsApi) getFileName(user *models.User, timezone *time.Location, fileExtension string) string { func (a *DataManagementsApi) getFileName(user *models.User, clientTimezone *time.Location, fileExtension string) string {
currentTime := utils.FormatUnixTimeToLongDateTimeWithoutSecond(time.Now().Unix(), timezone) currentTime := utils.FormatUnixTimeToLongDateTimeWithoutSecond(time.Now().Unix(), clientTimezone)
currentTime = strings.Replace(currentTime, "-", "_", -1) currentTime = strings.Replace(currentTime, "-", "_", -1)
currentTime = strings.Replace(currentTime, " ", "_", -1) 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
}
+2 -3
View File
@@ -3,7 +3,6 @@ package api
import ( import (
"github.com/mayswind/ezbookkeeping/pkg/core" "github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs" "github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/settings"
) )
// HealthsApi represents health api // HealthsApi represents health api
@@ -18,8 +17,8 @@ var (
func (a *HealthsApi) HealthStatusHandler(c *core.WebContext) (any, *errs.Error) { func (a *HealthsApi) HealthStatusHandler(c *core.WebContext) (any, *errs.Error) {
result := make(map[string]string) result := make(map[string]string)
result["version"] = settings.Version result["version"] = core.Version
result["commit"] = settings.CommitHash result["commit"] = core.CommitHash
result["status"] = "ok" result["status"] = "ok"
return result, nil return result, nil
+9 -7
View File
@@ -47,14 +47,13 @@ func (a *LargeLanguageModelsApi) RecognizeReceiptImageHandler(c *core.WebContext
return nil, errs.ErrLargeLanguageModelProviderNotEnabled return nil, errs.ErrLargeLanguageModelProviderNotEnabled
} }
utcOffset, err := c.GetClientTimezoneOffset() clientTimezone, err := c.GetClientTimezone()
if err != nil { 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 return nil, errs.ErrClientTimezoneOffsetInvalid
} }
timezone := time.FixedZone("Client Timezone", int(utcOffset)*60)
uid := c.GetCurrentUid() uid := c.GetCurrentUid()
user, err := a.users.GetUserById(c, uid) 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) systemPrompt, err := templates.GetTemplate(templates.SYSTEM_PROMPT_RECEIPT_IMAGE_RECOGNITION)
if err != nil { 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) return nil, errs.Or(err, errs.ErrOperationFailed)
} }
systemPromptParams := map[string]any{ systemPromptParams := map[string]any{
"CurrentDateTime": utils.FormatUnixTimeToLongDateTime(time.Now().Unix(), timezone), "CurrentDateTime": utils.FormatUnixTimeToLongDateTime(time.Now().Unix(), clientTimezone),
"AllExpenseCategoryNames": strings.Join(expenseCategoryNames, "\n"), "AllExpenseCategoryNames": strings.Join(expenseCategoryNames, "\n"),
"AllIncomeCategoryNames": strings.Join(incomeCategoryNames, "\n"), "AllIncomeCategoryNames": strings.Join(incomeCategoryNames, "\n"),
"AllTransferCategoryNames": strings.Join(transferCategoryNames, "\n"), "AllTransferCategoryNames": strings.Join(transferCategoryNames, "\n"),
@@ -208,6 +208,7 @@ func (a *LargeLanguageModelsApi) RecognizeReceiptImageHandler(c *core.WebContext
err = systemPrompt.Execute(&bodyBuffer, systemPromptParams) err = systemPrompt.Execute(&bodyBuffer, systemPromptParams)
if err != nil { 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) 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) llmResponse, err := llm.Container.GetJsonResponseByReceiptImageRecognitionModel(c, c.GetCurrentUid(), a.CurrentConfig(), llmRequest)
if err != nil { 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) 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 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{ recognizedReceiptImageResponse := &models.RecognizedReceiptImageResponse{
Type: models.TRANSACTION_TYPE_EXPENSE, Type: models.TRANSACTION_TYPE_EXPENSE,
} }
@@ -288,7 +290,7 @@ func (a *LargeLanguageModelsApi) parseRecognizedReceiptImageResponse(c *core.Web
if len(recognizedResult.Time) > 0 { if len(recognizedResult.Time) > 0 {
longDateTime := a.getLongDateTime(recognizedResult.Time) longDateTime := a.getLongDateTime(recognizedResult.Time)
timestamp, err := utils.ParseFromLongDateTime(longDateTime, utcOffset) timestamp, err := utils.ParseFromLongDateTimeInTimeZone(longDateTime, clientTimezone)
if err != nil { if err != nil {
log.Warnf(c, "[large_language_models.parseRecognizedReceiptImageResponse] recoginzed time \"%s\" is invalid", recognizedResult.Time) 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/http/httputil"
"net/url" "net/url"
"strings" "strings"
"sync"
"github.com/mayswind/ezbookkeeping/pkg/core" "github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs" "github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/httpclient"
"github.com/mayswind/ezbookkeeping/pkg/settings" "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 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 // MapImageProxy represents map image proxy
type MapImageProxy struct { type MapImageProxy struct {
ApiUsingConfig ApiUsingConfig
mutex sync.Mutex
transport *http.Transport
} }
// Initialize a map image proxy singleton instance // 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 // MapTileImageProxyHandler returns map tile image
func (p *MapImageProxy) MapTileImageProxyHandler(c *core.WebContext) (*httputil.ReverseProxy, *errs.Error) { func (p *MapImageProxy) MapTileImageProxyHandler(c *core.WebContext) (*httputil.ReverseProxy, *errs.Error) {
return p.mapImageProxyHandler(c, func(c *core.WebContext, mapProvider string) (string, *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 return nil, err
} }
transport := http.DefaultTransport.(*http.Transport).Clone() if p.transport == nil {
utils.SetProxyUrl(transport, p.CurrentConfig().MapProxy) p.initializeHttpTransport()
}
director := func(req *http.Request) { director := func(req *http.Request) {
imageRawUrl := targetUrl imageRawUrl := targetUrl
@@ -126,7 +142,7 @@ func (p *MapImageProxy) mapImageProxyHandler(c *core.WebContext, fn func(c *core
} }
return &httputil.ReverseProxy{ return &httputil.ReverseProxy{
Transport: transport, Transport: p.transport,
Director: director, Director: director,
}, nil }, nil
} }
+3 -3
View File
@@ -13,7 +13,7 @@ import (
"github.com/mayswind/ezbookkeeping/pkg/utils" "github.com/mayswind/ezbookkeeping/pkg/utils"
) )
const mcpServerName = "ezBookkeeping-mcp" const mcpServerName = core.ApplicationName + "-mcp"
// ModelContextProtocolAPI represents model context protocol api // ModelContextProtocolAPI represents model context protocol api
type ModelContextProtocolAPI struct { type ModelContextProtocolAPI struct {
@@ -102,8 +102,8 @@ func (a *ModelContextProtocolAPI) InitializeHandler(c *core.WebContext, jsonRPCR
}, },
ServerInfo: &mcp.MCPImplementation{ ServerInfo: &mcp.MCPImplementation{
Name: mcpServerName, Name: mcpServerName,
Title: a.CurrentConfig().AppName, Title: core.ApplicationName,
Version: settings.Version, Version: core.Version,
}, },
} }
+28 -9
View File
@@ -20,10 +20,10 @@ import (
"github.com/mayswind/ezbookkeeping/pkg/validators" "github.com/mayswind/ezbookkeeping/pkg/validators"
) )
const oauth2CallbackPageUrlSuccessFormat = "%sdesktop/#/oauth2_callback?platform=%s&provider=%s&token=%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 oauth2CallbackPageUrlNeedVerifyFormat = "%sdesktop#/oauth2_callback?platform=%s&provider=%s&userName=%s&token=%s"
const oauth2CallbackPageUrlFailedFormat = "%sdesktop/#/oauth2_callback?errorCode=%d&errorMessage=%s" const oauth2CallbackPageUrlFailedFormat = "%sdesktop#/oauth2_callback?errorCode=%d&errorMessage=%s"
const oauth2CallbackPageUrlErrorMessageFormat = "%sdesktop/#/oauth2_callback?errorMessage=%s" const oauth2CallbackPageUrlErrorMessageFormat = "%sdesktop#/oauth2_callback?errorMessage=%s"
// OAuth2AuthenticationApi represents OAuth 2.0 authorization api // OAuth2AuthenticationApi represents OAuth 2.0 authorization api
type OAuth2AuthenticationApi struct { type OAuth2AuthenticationApi struct {
@@ -208,9 +208,20 @@ func (a *OAuth2AuthenticationApi) CallbackHandler(c *core.WebContext) (string, *
return a.redirectToFailedCallbackPage(c, errs.ErrCannotRetrieveUserInfo) return a.redirectToFailedCallbackPage(c, errs.ErrCannotRetrieveUserInfo)
} }
if oauth2UserInfo.UserName == "" || oauth2UserInfo.Email == "" { log.Infof(c, "[oauth2_authentications.CallbackHandler] oauth 2.0 user info, userName: %s, email: %s", 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) 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() userExternalAuthType := oauth2.GetExternalUserAuthType()
@@ -221,7 +232,7 @@ func (a *OAuth2AuthenticationApi) CallbackHandler(c *core.WebContext) (string, *
} else if a.CurrentConfig().OAuth2UserIdentifier == settings.OAuth2UserIdentifierUsername { } else if a.CurrentConfig().OAuth2UserIdentifier == settings.OAuth2UserIdentifierUsername {
userExternalAuth, err = a.userExternalAuths.GetUserExternalAuthByExternalUserName(c, oauth2UserInfo.UserName, userExternalAuthType) userExternalAuth, err = a.userExternalAuths.GetUserExternalAuthByExternalUserName(c, oauth2UserInfo.UserName, userExternalAuthType)
} else { } else {
userExternalAuth, err = a.userExternalAuths.GetUserExternalAuthByExternalEmail(c, oauth2UserInfo.Email, userExternalAuthType) return a.redirectToFailedCallbackPage(c, errs.ErrNotSupported)
} }
if err != nil && !errors.Is(err, errs.ErrUserExternalAuthNotFound) { 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 { } else if a.CurrentConfig().OAuth2UserIdentifier == settings.OAuth2UserIdentifierUsername {
user, err = a.users.GetUserByUsername(c, oauth2UserInfo.UserName) user, err = a.users.GetUserByUsername(c, oauth2UserInfo.UserName)
} else { } else {
user, err = a.users.GetUserByEmail(c, oauth2UserInfo.Email) err = errs.ErrNotSupported
} }
if err != nil && !errors.Is(err, errs.ErrUserNotFound) { 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 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) userName := strings.TrimSpace(oauth2UserInfo.UserName)
email := strings.TrimSpace(oauth2UserInfo.Email) email := strings.TrimSpace(oauth2UserInfo.Email)
nickName := strings.TrimSpace(oauth2UserInfo.NickName) nickName := strings.TrimSpace(oauth2UserInfo.NickName)
+4 -5
View File
@@ -3,7 +3,6 @@ package api
import ( import (
"github.com/mayswind/ezbookkeeping/pkg/core" "github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs" "github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/settings"
) )
// SystemsApi represents system api // SystemsApi represents system api
@@ -18,11 +17,11 @@ var (
func (a *SystemsApi) VersionHandler(c *core.WebContext) (any, *errs.Error) { func (a *SystemsApi) VersionHandler(c *core.WebContext) (any, *errs.Error) {
result := make(map[string]string) result := make(map[string]string)
result["version"] = settings.Version result["version"] = core.Version
result["commitHash"] = settings.CommitHash result["commitHash"] = core.CommitHash
if settings.BuildTime != "" { if core.BuildTime != "" {
result["buildTime"] = settings.BuildTime result["buildTime"] = core.BuildTime
} }
return result, nil return result, nil
+4 -4
View File
@@ -69,10 +69,10 @@ func (a *TokensApi) TokenListHandler(c *core.WebContext) (any, *errs.Error) {
tokenResp.IsCurrent = true tokenResp.IsCurrent = true
} }
if token.TokenType == core.USER_TOKEN_TYPE_API && token.UserAgent != services.TokenUserAgentCreatedViaCli { if token.TokenType == core.USER_TOKEN_TYPE_API && token.UserAgent != core.TokenUserAgentCreatedViaCli {
tokenResp.UserAgent = services.TokenUserAgentForAPI tokenResp.UserAgent = core.TokenUserAgentForAPI
} else if token.TokenType == core.USER_TOKEN_TYPE_MCP && token.UserAgent != services.TokenUserAgentCreatedViaCli { } else if token.TokenType == core.USER_TOKEN_TYPE_MCP && token.UserAgent != core.TokenUserAgentCreatedViaCli {
tokenResp.UserAgent = services.TokenUserAgentForMCP tokenResp.UserAgent = core.TokenUserAgentForMCP
} }
tokenResps[i] = tokenResp tokenResps[i] = tokenResp
+10 -1
View File
@@ -214,6 +214,7 @@ func (a *TransactionCategoriesApi) CategoryModifyHandler(c *core.WebContext) (an
Uid: uid, Uid: uid,
ParentCategoryId: categoryModifyReq.ParentId, ParentCategoryId: categoryModifyReq.ParentId,
Name: categoryModifyReq.Name, Name: categoryModifyReq.Name,
DisplayOrder: category.DisplayOrder,
Icon: categoryModifyReq.Icon, Icon: categoryModifyReq.Icon,
Color: categoryModifyReq.Color, Color: categoryModifyReq.Color,
Comment: categoryModifyReq.Comment, Comment: categoryModifyReq.Comment,
@@ -259,6 +260,15 @@ func (a *TransactionCategoriesApi) CategoryModifyHandler(c *core.WebContext) (an
if toPrimaryCategory.ParentCategoryId != models.LevelOneTransactionCategoryParentId { if toPrimaryCategory.ParentCategoryId != models.LevelOneTransactionCategoryParentId {
return nil, errs.Or(err, errs.ErrNotAllowUseSecondaryTransactionAsPrimaryCategory) 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) 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) log.Infof(c, "[transaction_categories.CategoryModifyHandler] user \"uid:%d\" has updated category \"id:%d\" successfully", uid, categoryModifyReq.Id)
newCategory.Type = category.Type newCategory.Type = category.Type
newCategory.DisplayOrder = category.DisplayOrder
categoryResp := newCategory.ToTransactionCategoryInfoResponse() categoryResp := newCategory.ToTransactionCategoryInfoResponse()
return categoryResp, nil 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,
}
}
+80 -10
View File
@@ -12,13 +12,15 @@ import (
// TransactionTagsApi represents transaction tag api // TransactionTagsApi represents transaction tag api
type TransactionTagsApi struct { type TransactionTagsApi struct {
tags *services.TransactionTagService tags *services.TransactionTagService
tagGroups *services.TransactionTagGroupService
} }
// Initialize a transaction tag api singleton instance // Initialize a transaction tag api singleton instance
var ( var (
TransactionTags = &TransactionTagsApi{ TransactionTags = &TransactionTagsApi{
tags: services.TransactionTags, tags: services.TransactionTags,
tagGroups: services.TransactionTagGroups,
} }
) )
@@ -78,7 +80,21 @@ func (a *TransactionTagsApi) TagCreateHandler(c *core.WebContext) (any, *errs.Er
uid := c.GetCurrentUid() uid := c.GetCurrentUid()
maxOrderId, err := a.tags.GetMaxDisplayOrder(c, uid) if tagCreateReq.GroupId > 0 {
tagGroup, err := a.tagGroups.GetTagGroupByTagGroupId(c, uid, tagCreateReq.GroupId)
if err != nil {
log.Errorf(c, "[transaction_tags.TagCreateHandler] failed to get tag group \"id:%d\" for user \"uid:%d\", because %s", tagCreateReq.GroupId, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
if tagGroup == nil {
log.Warnf(c, "[transaction_tags.TagCreateHandler] the tag group \"id:%d\" does not exist for user \"uid:%d\"", tagCreateReq.GroupId, uid)
return nil, errs.ErrTransactionTagGroupNotFound
}
}
maxOrderId, err := a.tags.GetMaxDisplayOrder(c, uid, tagCreateReq.GroupId)
if err != nil { if err != nil {
log.Errorf(c, "[transaction_tags.TagCreateHandler] failed to get max display order for user \"uid:%d\", because %s", uid, err.Error()) log.Errorf(c, "[transaction_tags.TagCreateHandler] failed to get max display order for user \"uid:%d\", because %s", uid, err.Error())
@@ -111,9 +127,30 @@ func (a *TransactionTagsApi) TagCreateBatchHandler(c *core.WebContext) (any, *er
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err) 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() uid := c.GetCurrentUid()
maxOrderId, err := a.tags.GetMaxDisplayOrder(c, uid) if tagCreateBatchReq.GroupId > 0 {
tagGroup, err := a.tagGroups.GetTagGroupByTagGroupId(c, uid, tagCreateBatchReq.GroupId)
if err != nil {
log.Errorf(c, "[transaction_tags.TagCreateBatchHandler] failed to get tag group \"id:%d\" for user \"uid:%d\", because %s", tagCreateBatchReq.GroupId, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
if tagGroup == nil {
log.Warnf(c, "[transaction_tags.TagCreateBatchHandler] the tag group \"id:%d\" does not exist for user \"uid:%d\"", tagCreateBatchReq.GroupId, uid)
return nil, errs.ErrTransactionTagGroupNotFound
}
}
maxOrderId, err := a.tags.GetMaxDisplayOrder(c, uid, tagCreateBatchReq.GroupId)
if err != nil { if err != nil {
log.Errorf(c, "[transaction_tags.TagCreateBatchHandler] failed to get max display order for user \"uid:%d\", because %s", uid, err.Error()) log.Errorf(c, "[transaction_tags.TagCreateBatchHandler] failed to get max display order for user \"uid:%d\", because %s", uid, err.Error())
@@ -160,17 +197,46 @@ func (a *TransactionTagsApi) TagModifyHandler(c *core.WebContext) (any, *errs.Er
return nil, errs.Or(err, errs.ErrOperationFailed) return nil, errs.Or(err, errs.ErrOperationFailed)
} }
newTag := &models.TransactionTag{ if tagModifyReq.GroupId != tag.TagGroupId && tagModifyReq.GroupId > 0 {
TagId: tag.TagId, tagGroup, err := a.tagGroups.GetTagGroupByTagGroupId(c, uid, tagModifyReq.GroupId)
Uid: uid,
Name: tagModifyReq.Name, if err != nil {
log.Errorf(c, "[transaction_tags.TagModifyHandler] failed to get tag group \"id:%d\" for user \"uid:%d\", because %s", tagModifyReq.GroupId, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
if tagGroup == nil {
log.Warnf(c, "[transaction_tags.TagModifyHandler] the tag group \"id:%d\" does not exist for user \"uid:%d\"", tagModifyReq.GroupId, uid)
return nil, errs.ErrTransactionTagGroupNotFound
}
} }
if newTag.Name == tag.Name { newTag := &models.TransactionTag{
TagId: tag.TagId,
Uid: uid,
Name: tagModifyReq.Name,
TagGroupId: tagModifyReq.GroupId,
DisplayOrder: tag.DisplayOrder,
}
tagNameChanged := newTag.Name != tag.Name
if !tagNameChanged && newTag.TagGroupId == tag.TagGroupId {
return nil, errs.ErrNothingWillBeUpdated 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 { 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()) 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 +246,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) log.Infof(c, "[transaction_tags.TagModifyHandler] user \"uid:%d\" has updated tag \"id:%d\" successfully", uid, tagModifyReq.Id)
tag.Name = newTag.Name tag.Name = newTag.Name
tag.TagGroupId = newTag.TagGroupId
tag.DisplayOrder = newTag.DisplayOrder
tagResp := tag.ToTransactionTagInfoResponse() tagResp := tag.ToTransactionTagInfoResponse()
return tagResp, nil return tagResp, nil
@@ -268,6 +336,7 @@ func (a *TransactionTagsApi) createNewTagModel(uid int64, tagCreateReq *models.T
return &models.TransactionTag{ return &models.TransactionTag{
Uid: uid, Uid: uid,
Name: tagCreateReq.Name, Name: tagCreateReq.Name,
TagGroupId: tagCreateReq.GroupId,
DisplayOrder: order, DisplayOrder: order,
} }
} }
@@ -278,6 +347,7 @@ func (a *TransactionTagsApi) createNewTagModels(uid int64, tagCreateBatchReq *mo
for i := 0; i < len(tagCreateBatchReq.Tags); i++ { for i := 0; i < len(tagCreateBatchReq.Tags); i++ {
tagCreateReq := tagCreateBatchReq.Tags[i] tagCreateReq := tagCreateBatchReq.Tags[i]
tag := a.createNewTagModel(uid, tagCreateReq, order+int32(i)) tag := a.createNewTagModel(uid, tagCreateReq, order+int32(i))
tag.TagGroupId = tagCreateBatchReq.GroupId
tags[i] = tag tags[i] = tag
} }
+318 -102
View File
@@ -4,8 +4,10 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"math"
"sort" "sort"
"strings" "strings"
"time"
orderedmap "github.com/wk8/go-ordered-map/v2" 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) return nil, errs.Or(err, errs.ErrOperationFailed)
} }
var allTagIds []int64 noTags := transactionCountReq.TagFilter == models.TransactionNoTagFilterValue
noTags := transactionCountReq.TagIds == "none" var tagFilters []*models.TransactionTagFilter
if !noTags { if !noTags {
allTagIds, err = a.transactionTags.GetTagIds(transactionCountReq.TagIds) tagFilters, err = models.ParseTransactionTagFilter(transactionCountReq.TagFilter)
if err != nil { 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) 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 { if err != nil {
log.Errorf(c, "[transactions.TransactionCountHandler] failed to get transaction count for user \"uid:%d\", because %s", uid, err.Error()) 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) return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
} }
utcOffset, err := c.GetClientTimezoneOffset() clientTimezone, err := c.GetClientTimezone()
if err != nil { 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 return nil, errs.ErrClientTimezoneOffsetInvalid
} }
@@ -151,14 +153,14 @@ func (a *TransactionsApi) TransactionListHandler(c *core.WebContext) (any, *errs
return nil, errs.Or(err, errs.ErrOperationFailed) return nil, errs.Or(err, errs.ErrOperationFailed)
} }
var allTagIds []int64 noTags := transactionListReq.TagFilter == models.TransactionNoTagFilterValue
noTags := transactionListReq.TagIds == "none" var tagFilters []*models.TransactionTagFilter
if !noTags { if !noTags {
allTagIds, err = a.transactionTags.GetTagIds(transactionListReq.TagIds) tagFilters, err = models.ParseTransactionTagFilter(transactionListReq.TagFilter)
if err != nil { 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) return nil, errs.Or(err, errs.ErrOperationFailed)
} }
} }
@@ -166,7 +168,7 @@ func (a *TransactionsApi) TransactionListHandler(c *core.WebContext) (any, *errs
var totalCount int64 var totalCount int64
if transactionListReq.WithCount { 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 { if err != nil {
log.Errorf(c, "[transactions.TransactionListHandler] failed to get transaction count for user \"uid:%d\", because %s", uid, err.Error()) 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 { 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()) 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,15 @@ func (a *TransactionsApi) TransactionListHandler(c *core.WebContext) (any, *errs
transactions = transactions[:transactionListReq.Count] transactions = transactions[:transactionListReq.Count]
} }
transactionResult, err := a.getTransactionResponseListResult(c, user, transactions, utcOffset, transactionListReq.WithPictures, transactionListReq.TrimAccount, transactionListReq.TrimCategory, transactionListReq.TrimTag) accountMap, categoryMap, tagMap, allTransactionTagIds, pictureInfoMap, err := a.getTransactionEssentialDataByTransactionIds(c, user, transactions, transactionListReq.WithPictures, transactionListReq.TrimCategory, transactionListReq.TrimTag)
if err != nil {
log.Errorf(c, "[transactions.TransactionListHandler] failed to get essential data for assembling transaction result for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
transactions = a.filterTransactions(c, uid, transactions, accountMap)
transactionResult, err := a.getTransactionResponseListResult(c, user, transactions, accountMap, categoryMap, tagMap, allTransactionTagIds, pictureInfoMap, clientTimezone, transactionListReq.WithPictures, transactionListReq.TrimAccount, transactionListReq.TrimCategory, transactionListReq.TrimTag)
if err != nil { if err != nil {
log.Errorf(c, "[transactions.TransactionListHandler] failed to assemble transaction result for user \"uid:%d\", because %s", uid, err.Error()) log.Errorf(c, "[transactions.TransactionListHandler] failed to assemble transaction result for user \"uid:%d\", because %s", uid, err.Error())
@@ -222,10 +232,10 @@ func (a *TransactionsApi) TransactionMonthListHandler(c *core.WebContext) (any,
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err) return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
} }
utcOffset, err := c.GetClientTimezoneOffset() clientTimezone, err := c.GetClientTimezone()
if err != nil { 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 return nil, errs.ErrClientTimezoneOffsetInvalid
} }
@@ -254,26 +264,34 @@ func (a *TransactionsApi) TransactionMonthListHandler(c *core.WebContext) (any,
return nil, errs.Or(err, errs.ErrOperationFailed) return nil, errs.Or(err, errs.ErrOperationFailed)
} }
var allTagIds []int64 noTags := transactionListReq.TagFilter == models.TransactionNoTagFilterValue
noTags := transactionListReq.TagIds == "none" var tagFilters []*models.TransactionTagFilter
if !noTags { if !noTags {
allTagIds, err = a.transactionTags.GetTagIds(transactionListReq.TagIds) tagFilters, err = models.ParseTransactionTagFilter(transactionListReq.TagFilter)
if err != nil { 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) 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 { 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()) 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) return nil, errs.Or(err, errs.ErrOperationFailed)
} }
transactionResult, err := a.getTransactionResponseListResult(c, user, transactions, utcOffset, transactionListReq.WithPictures, transactionListReq.TrimAccount, transactionListReq.TrimCategory, transactionListReq.TrimTag) accountMap, categoryMap, tagMap, allTransactionTagIds, pictureInfoMap, err := a.getTransactionEssentialDataByTransactionIds(c, user, transactions, transactionListReq.WithPictures, transactionListReq.TrimCategory, transactionListReq.TrimTag)
if err != nil {
log.Errorf(c, "[transactions.TransactionMonthListHandler] failed to get essential data for assembling transaction result for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
transactions = a.filterTransactions(c, uid, transactions, accountMap)
transactionResult, err := a.getTransactionResponseListResult(c, user, transactions, accountMap, categoryMap, tagMap, allTransactionTagIds, pictureInfoMap, clientTimezone, transactionListReq.WithPictures, transactionListReq.TrimAccount, transactionListReq.TrimCategory, transactionListReq.TrimTag)
if err != nil { if err != nil {
log.Errorf(c, "[transactions.TransactionMonthListHandler] failed to assemble transaction result for user \"uid:%d\", because %s", uid, err.Error()) log.Errorf(c, "[transactions.TransactionMonthListHandler] failed to assemble transaction result for user \"uid:%d\", because %s", uid, err.Error())
@@ -288,6 +306,106 @@ func (a *TransactionsApi) TransactionMonthListHandler(c *core.WebContext) (any,
return transactionResps, nil 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)
}
var accountMap map[int64]*models.Account
var categoryMap map[int64]*models.TransactionCategory
var tagMap map[int64]*models.TransactionTag
var allTransactionTagIds map[int64][]int64
var pictureInfoMap map[int64][]*models.TransactionPictureInfo
if minTransactionTime == 0 && maxTransactionTime == math.MaxInt64 && len(allCategoryIds) < 1 && len(allAccountIds) < 1 && len(tagFilters) < 1 && transactionAllListReq.AmountFilter == "" && transactionAllListReq.Keyword == "" {
accountMap, categoryMap, tagMap, allTransactionTagIds, pictureInfoMap, err = a.getTransactionAllEssentialData(c, user, transactionAllListReq.WithPictures, transactionAllListReq.TrimCategory, transactionAllListReq.TrimTag)
} else {
accountMap, categoryMap, tagMap, allTransactionTagIds, pictureInfoMap, err = a.getTransactionEssentialDataByTransactionIds(c, user, allTransactions, transactionAllListReq.WithPictures, transactionAllListReq.TrimCategory, transactionAllListReq.TrimTag)
}
if err != nil {
log.Errorf(c, "[transactions.TransactionListAllHandler] failed to get essential data for assembling transaction result for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
allTransactions = a.filterTransactions(c, uid, allTransactions, accountMap)
transactionResult, err := a.getTransactionResponseListResult(c, user, allTransactions, accountMap, categoryMap, tagMap, allTransactionTagIds, pictureInfoMap, 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 // TransactionReconciliationStatementHandler returns transaction reconciliation statement list of current user
func (a *TransactionsApi) TransactionReconciliationStatementHandler(c *core.WebContext) (any, *errs.Error) { func (a *TransactionsApi) TransactionReconciliationStatementHandler(c *core.WebContext) (any, *errs.Error) {
var reconciliationStatementRequest models.TransactionReconciliationStatementRequest var reconciliationStatementRequest models.TransactionReconciliationStatementRequest
@@ -298,10 +416,10 @@ func (a *TransactionsApi) TransactionReconciliationStatementHandler(c *core.WebC
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err) return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
} }
utcOffset, err := c.GetClientTimezoneOffset() clientTimezone, err := c.GetClientTimezone()
if err != nil { 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 return nil, errs.ErrClientTimezoneOffsetInvalid
} }
@@ -357,7 +475,24 @@ func (a *TransactionsApi) TransactionReconciliationStatementHandler(c *core.WebC
transactionAccountBalanceMap[transactionWithBalance.RelatedId] = transactionWithBalance transactionAccountBalanceMap[transactionWithBalance.RelatedId] = transactionWithBalance
} }
transactionResult, err := a.getTransactionResponseListResult(c, user, transactions, utcOffset, false, true, true, true) allAccountIds := make([]int64, 0, len(transactions)*2)
for i := 0; i < len(transactions); i++ {
allAccountIds = append(allAccountIds, transactions[i].AccountId)
if transactions[i].Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN || transactions[i].Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT {
allAccountIds = append(allAccountIds, transactions[i].RelatedAccountId)
}
}
allAccounts, err := a.accounts.GetAccountsByAccountIds(c, uid, utils.ToUniqueInt64Slice(allAccountIds))
if err != nil {
log.Errorf(c, "[transactions.TransactionReconciliationStatementHandler] failed to get essential data for assembling transaction result for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
transactionResult, err := a.getTransactionResponseListResult(c, user, transactions, allAccounts, nil, nil, nil, nil, clientTimezone, false, true, true, true)
if err != nil { if err != nil {
log.Errorf(c, "[transactions.TransactionReconciliationStatementHandler] failed to assemble transaction result for user \"uid:%d\", because %s", uid, err.Error()) log.Errorf(c, "[transactions.TransactionReconciliationStatementHandler] failed to assemble transaction result for user \"uid:%d\", because %s", uid, err.Error())
@@ -406,27 +541,27 @@ func (a *TransactionsApi) TransactionStatisticsHandler(c *core.WebContext) (any,
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err) return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
} }
utcOffset, err := c.GetClientTimezoneOffset() clientTimezone, err := c.GetClientTimezone()
if err != nil { 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 return nil, errs.ErrClientTimezoneOffsetInvalid
} }
var allTagIds []int64 noTags := statisticReq.TagFilter == models.TransactionNoTagFilterValue
noTags := statisticReq.TagIds == "none" var tagFilters []*models.TransactionTagFilter
if !noTags { if !noTags {
allTagIds, err = a.transactionTags.GetTagIds(statisticReq.TagIds) tagFilters, err = models.ParseTransactionTagFilter(statisticReq.TagFilter)
if err != nil { 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) return nil, errs.Or(err, errs.ErrOperationFailed)
} }
} }
uid := c.GetCurrentUid() 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 { 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()) 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 +602,10 @@ func (a *TransactionsApi) TransactionStatisticsTrendsHandler(c *core.WebContext)
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err) return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
} }
utcOffset, err := c.GetClientTimezoneOffset() clientTimezone, err := c.GetClientTimezone()
if err != nil { 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 return nil, errs.ErrClientTimezoneOffsetInvalid
} }
@@ -481,20 +616,20 @@ func (a *TransactionsApi) TransactionStatisticsTrendsHandler(c *core.WebContext)
return nil, errs.Or(err, errs.ErrOperationFailed) return nil, errs.Or(err, errs.ErrOperationFailed)
} }
var allTagIds []int64 noTags := statisticTrendsReq.TagFilter == models.TransactionNoTagFilterValue
noTags := statisticTrendsReq.TagIds == "none" var tagFilters []*models.TransactionTagFilter
if !noTags { if !noTags {
allTagIds, err = a.transactionTags.GetTagIds(statisticTrendsReq.TagIds) tagFilters, err = models.ParseTransactionTagFilter(statisticTrendsReq.TagFilter)
if err != nil { 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) return nil, errs.Or(err, errs.ErrOperationFailed)
} }
} }
uid := c.GetCurrentUid() 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 { 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()) 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 +677,10 @@ func (a *TransactionsApi) TransactionStatisticsAssetTrendsHandler(c *core.WebCon
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err) return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
} }
utcOffset, err := c.GetClientTimezoneOffset() clientTimezone, err := c.GetClientTimezone()
if err != nil { 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 return nil, errs.ErrClientTimezoneOffsetInvalid
} }
@@ -563,7 +698,7 @@ func (a *TransactionsApi) TransactionStatisticsAssetTrendsHandler(c *core.WebCon
minTransactionTime = utils.GetMinTransactionTimeFromUnixTime(statisticAssetTrendsReq.StartTime) 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 { 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()) 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 +778,10 @@ func (a *TransactionsApi) TransactionAmountsHandler(c *core.WebContext) (any, *e
} }
} }
utcOffset, err := c.GetClientTimezoneOffset() clientTimezone, err := c.GetClientTimezone()
if err != nil { 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 return nil, errs.ErrClientTimezoneOffsetInvalid
} }
@@ -665,7 +800,7 @@ func (a *TransactionsApi) TransactionAmountsHandler(c *core.WebContext) (any, *e
for i := 0; i < len(requestItems); i++ { for i := 0; i < len(requestItems); i++ {
requestItem := 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 { if err != nil {
log.Errorf(c, "[transactions.TransactionAmountsHandler] failed to get transaction amounts item for user \"uid:%d\", because %s", uid, err.Error()) log.Errorf(c, "[transactions.TransactionAmountsHandler] failed to get transaction amounts item for user \"uid:%d\", because %s", uid, err.Error())
@@ -746,10 +881,10 @@ func (a *TransactionsApi) TransactionGetHandler(c *core.WebContext) (any, *errs.
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err) return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
} }
utcOffset, err := c.GetClientTimezoneOffset() clientTimezone, err := c.GetClientTimezone()
if err != nil { 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 return nil, errs.ErrClientTimezoneOffsetInvalid
} }
@@ -835,7 +970,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] transactionTagIds := allTransactionTagIds[transaction.TransactionId]
transactionResp := transaction.ToTransactionInfoResponse(transactionTagIds, transactionEditable) transactionResp := transaction.ToTransactionInfoResponse(transactionTagIds, transactionEditable)
@@ -876,6 +1011,13 @@ func (a *TransactionsApi) TransactionCreateHandler(c *core.WebContext) (any, *er
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err) 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) tagIds, err := utils.StringArrayToInt64Array(transactionCreateReq.TagIds)
if err != nil { if err != nil {
@@ -933,7 +1075,7 @@ func (a *TransactionsApi) TransactionCreateHandler(c *core.WebContext) (any, *er
} }
transaction := a.createNewTransactionModel(uid, &transactionCreateReq, c.ClientIP()) transaction := a.createNewTransactionModel(uid, &transactionCreateReq, c.ClientIP())
transactionEditable := user.CanEditTransactionByTransactionTime(transaction.TransactionTime, transactionCreateReq.UtcOffset) transactionEditable := user.CanEditTransactionByTransactionTime(transaction.TransactionTime, clientTimezone)
if !transactionEditable { if !transactionEditable {
return nil, errs.ErrCannotCreateTransactionWithThisTransactionTime return nil, errs.ErrCannotCreateTransactionWithThisTransactionTime
@@ -1006,6 +1148,13 @@ func (a *TransactionsApi) TransactionModifyHandler(c *core.WebContext) (any, *er
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err) 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) tagIds, err := utils.StringArrayToInt64Array(transactionModifyReq.TagIds)
if err != nil { if err != nil {
@@ -1119,8 +1268,8 @@ func (a *TransactionsApi) TransactionModifyHandler(c *core.WebContext) (any, *er
return nil, errs.ErrNothingWillBeUpdated return nil, errs.ErrNothingWillBeUpdated
} }
transactionEditable := user.CanEditTransactionByTransactionTime(transaction.TransactionTime, transaction.TimezoneUtcOffset) transactionEditable := user.CanEditTransactionByTransactionTime(transaction.TransactionTime, clientTimezone)
newTransactionEditable := user.CanEditTransactionByTransactionTime(newTransaction.TransactionTime, transactionModifyReq.UtcOffset) newTransactionEditable := user.CanEditTransactionByTransactionTime(newTransaction.TransactionTime, clientTimezone)
if !transactionEditable || !newTransactionEditable { if !transactionEditable || !newTransactionEditable {
return nil, errs.ErrCannotModifyTransactionWithThisTransactionTime return nil, errs.ErrCannotModifyTransactionWithThisTransactionTime
@@ -1256,10 +1405,10 @@ func (a *TransactionsApi) TransactionDeleteHandler(c *core.WebContext) (any, *er
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err) return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
} }
utcOffset, err := c.GetClientTimezoneOffset() clientTimezone, err := c.GetClientTimezone()
if err != nil { 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 return nil, errs.ErrClientTimezoneOffsetInvalid
} }
@@ -1286,7 +1435,7 @@ func (a *TransactionsApi) TransactionDeleteHandler(c *core.WebContext) (any, *er
return nil, errs.ErrTransactionTypeInvalid return nil, errs.ErrTransactionTypeInvalid
} }
transactionEditable := user.CanEditTransactionByTransactionTime(transaction.TransactionTime, utcOffset) transactionEditable := user.CanEditTransactionByTransactionTime(transaction.TransactionTime, clientTimezone)
if !transactionEditable { if !transactionEditable {
return nil, errs.ErrCannotDeleteTransactionWithThisTransactionTime return nil, errs.ErrCannotDeleteTransactionWithThisTransactionTime
@@ -1303,13 +1452,13 @@ func (a *TransactionsApi) TransactionDeleteHandler(c *core.WebContext) (any, *er
return true, nil return true, nil
} }
// TransactionParseImportDsvFileDataHandler returns the parsed file data by request parameters for current user // TransactionParseImportCustomFileDataHandler returns the parsed file data by request parameters for current user
func (a *TransactionsApi) TransactionParseImportDsvFileDataHandler(c *core.WebContext) (any, *errs.Error) { func (a *TransactionsApi) TransactionParseImportCustomFileDataHandler(c *core.WebContext) (any, *errs.Error) {
uid := c.GetCurrentUid() uid := c.GetCurrentUid()
form, err := c.MultipartForm() form, err := c.MultipartForm()
if err != nil { if err != nil {
log.Errorf(c, "[transactions.TransactionParseImportDsvFileDataHandler] failed to get multi-part form data for user \"uid:%d\", because %s", uid, err.Error()) log.Errorf(c, "[transactions.TransactionParseImportCustomFileDataHandler] failed to get multi-part form data for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.ErrParameterInvalid return nil, errs.ErrParameterInvalid
} }
@@ -1321,18 +1470,18 @@ func (a *TransactionsApi) TransactionParseImportDsvFileDataHandler(c *core.WebCo
fileType := fileTypes[0] fileType := fileTypes[0]
if !converters.IsCustomDelimiterSeparatedValuesFileType(fileType) { if !converters.IsCustomFileFormatFileType(fileType) {
return nil, errs.Or(err, errs.ErrImportFileTypeNotSupported) return nil, errs.Or(err, errs.ErrImportFileTypeNotSupported)
} }
fileEncodings := form.Value["fileEncoding"] fileEncodings := form.Value["fileEncoding"]
fileEncoding := ""
if len(fileEncodings) < 1 || fileEncodings[0] == "" { if len(fileEncodings) > 0 {
return nil, errs.ErrImportFileEncodingIsEmpty fileEncoding = fileEncodings[0]
} }
fileEncoding := fileEncodings[0] dataParser, err := converters.CreateNewCustomFileFormatTransactionDataParser(fileType, fileEncoding)
dataParser, err := converters.CreateNewDelimiterSeparatedValuesDataParser(fileType, fileEncoding)
if err != nil { if err != nil {
return nil, errs.Or(err, errs.ErrImportFileTypeNotSupported) return nil, errs.Or(err, errs.ErrImportFileTypeNotSupported)
@@ -1341,24 +1490,24 @@ func (a *TransactionsApi) TransactionParseImportDsvFileDataHandler(c *core.WebCo
importFiles := form.File["file"] importFiles := form.File["file"]
if len(importFiles) < 1 { if len(importFiles) < 1 {
log.Warnf(c, "[transactions.TransactionParseImportDsvFileDataHandler] there is no import file in request for user \"uid:%d\"", uid) log.Warnf(c, "[transactions.TransactionParseImportCustomFileDataHandler] there is no import file in request for user \"uid:%d\"", uid)
return nil, errs.ErrNoFilesUpload return nil, errs.ErrNoFilesUpload
} }
if importFiles[0].Size < 1 { if importFiles[0].Size < 1 {
log.Warnf(c, "[transactions.TransactionParseImportDsvFileDataHandler] the size of import file in request is zero for user \"uid:%d\"", uid) log.Warnf(c, "[transactions.TransactionParseImportCustomFileDataHandler] the size of import file in request is zero for user \"uid:%d\"", uid)
return nil, errs.ErrUploadedFileEmpty return nil, errs.ErrUploadedFileEmpty
} }
if importFiles[0].Size > int64(a.CurrentConfig().MaxImportFileSize) { if importFiles[0].Size > int64(a.CurrentConfig().MaxImportFileSize) {
log.Warnf(c, "[transactions.TransactionParseImportDsvFileDataHandler] the upload file size \"%d\" exceeds the maximum size \"%d\" of import file for user \"uid:%d\"", importFiles[0].Size, a.CurrentConfig().MaxImportFileSize, uid) log.Warnf(c, "[transactions.TransactionParseImportCustomFileDataHandler] the upload file size \"%d\" exceeds the maximum size \"%d\" of import file for user \"uid:%d\"", importFiles[0].Size, a.CurrentConfig().MaxImportFileSize, uid)
return nil, errs.ErrExceedMaxUploadFileSize return nil, errs.ErrExceedMaxUploadFileSize
} }
importFile, err := importFiles[0].Open() importFile, err := importFiles[0].Open()
if err != nil { if err != nil {
log.Errorf(c, "[transactions.TransactionParseImportDsvFileDataHandler] failed to get import file from request for user \"uid:%d\", because %s", uid, err.Error()) log.Errorf(c, "[transactions.TransactionParseImportCustomFileDataHandler] failed to get import file from request for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.ErrOperationFailed return nil, errs.ErrOperationFailed
} }
@@ -1366,14 +1515,14 @@ func (a *TransactionsApi) TransactionParseImportDsvFileDataHandler(c *core.WebCo
fileData, err := io.ReadAll(importFile) fileData, err := io.ReadAll(importFile)
if err != nil { if err != nil {
log.Errorf(c, "[transactions.TransactionParseImportDsvFileDataHandler] failed to read import file data for user \"uid:%d\", because %s", uid, err.Error()) log.Errorf(c, "[transactions.TransactionParseImportCustomFileDataHandler] failed to read import file data for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed) return nil, errs.Or(err, errs.ErrOperationFailed)
} }
allLines, err := dataParser.ParseDsvFileLines(c, fileData) allLines, err := dataParser.ParseDataLines(c, fileData)
if err != nil { if err != nil {
log.Errorf(c, "[transactions.TransactionParseImportDsvFileDataHandler] failed to parse import file data for user \"uid:%d\", because %s", uid, err.Error()) log.Errorf(c, "[transactions.TransactionParseImportCustomFileDataHandler] failed to parse import file data for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed) return nil, errs.Or(err, errs.ErrOperationFailed)
} }
@@ -1390,10 +1539,10 @@ func (a *TransactionsApi) TransactionParseImportFileHandler(c *core.WebContext)
return nil, errs.ErrParameterInvalid return nil, errs.ErrParameterInvalid
} }
utcOffset, err := c.GetClientTimezoneOffset() clientTimezone, err := c.GetClientTimezone()
if err != nil { 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 return nil, errs.ErrClientTimezoneOffsetInvalid
} }
@@ -1405,17 +1554,25 @@ func (a *TransactionsApi) TransactionParseImportFileHandler(c *core.WebContext)
fileType := fileTypes[0] fileType := fileTypes[0]
textualOptions := form.Value["options"]
textualOption := ""
if len(textualOptions) > 0 {
textualOption = textualOptions[0]
}
additionalOptions := converter.ParseImporterOptions(textualOption)
var dataImporter converter.TransactionDataImporter var dataImporter converter.TransactionDataImporter
if converters.IsCustomDelimiterSeparatedValuesFileType(fileType) { if converters.IsCustomFileFormatFileType(fileType) {
fileEncodings := form.Value["fileEncoding"] fileEncodings := form.Value["fileEncoding"]
fileEncoding := ""
if len(fileEncodings) < 1 || fileEncodings[0] == "" { if len(fileEncodings) > 0 {
return nil, errs.ErrImportFileEncodingIsEmpty fileEncoding = fileEncodings[0]
} }
fileEncoding := fileEncodings[0]
columnMappings := form.Value["columnMapping"] columnMappings := form.Value["columnMapping"]
if len(columnMappings) < 1 || columnMappings[0] == "" { if len(columnMappings) < 1 || columnMappings[0] == "" {
@@ -1499,7 +1656,7 @@ func (a *TransactionsApi) TransactionParseImportFileHandler(c *core.WebContext)
transactionTagSeparator = transactionTagSeparators[0] transactionTagSeparator = transactionTagSeparators[0]
} }
dataImporter, err = converters.CreateNewDelimiterSeparatedValuesDataImporter(fileType, fileEncoding, columnIndexMapping, transactionTypeNameMapping, hasHeaderLine, timeFormats[0], timezoneFormat, amountDecimalSeparator, amountDigitGroupingSymbol, geoLocationSeparator, geoLocationOrder, transactionTagSeparator) dataImporter, err = converters.CreateNewCustomTransactionDataImporter(fileType, fileEncoding, columnIndexMapping, transactionTypeNameMapping, hasHeaderLine, timeFormats[0], timezoneFormat, amountDecimalSeparator, amountDigitGroupingSymbol, geoLocationSeparator, geoLocationOrder, transactionTagSeparator)
} else { } else {
dataImporter, err = converters.GetTransactionDataImporter(fileType) dataImporter, err = converters.GetTransactionDataImporter(fileType)
} }
@@ -1581,7 +1738,7 @@ func (a *TransactionsApi) TransactionParseImportFileHandler(c *core.WebContext)
tagMap := a.transactionTags.GetVisibleTagNameMapByList(tags) 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 { if err != nil {
log.Errorf(c, "[transactions.TransactionParseImportFileHandler] failed to parse imported data for user \"uid:%d\", because %s", user.Uid, err.Error()) log.Errorf(c, "[transactions.TransactionParseImportFileHandler] failed to parse imported data for user \"uid:%d\", because %s", user.Uid, err.Error())
@@ -1612,6 +1769,13 @@ func (a *TransactionsApi) TransactionImportHandler(c *core.WebContext) (any, *er
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err) 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() uid := c.GetCurrentUid()
if a.CurrentConfig().EnableDuplicateSubmissionsCheck && transactionImportReq.ClientSessionId != "" { if a.CurrentConfig().EnableDuplicateSubmissionsCheck && transactionImportReq.ClientSessionId != "" {
@@ -1697,7 +1861,7 @@ func (a *TransactionsApi) TransactionImportHandler(c *core.WebContext) (any, *er
for i := 0; i < len(transactionImportReq.Transactions); i++ { for i := 0; i < len(transactionImportReq.Transactions); i++ {
transactionCreateReq := transactionImportReq.Transactions[i] transactionCreateReq := transactionImportReq.Transactions[i]
transaction := a.createNewTransactionModel(uid, transactionCreateReq, c.ClientIP()) transaction := a.createNewTransactionModel(uid, transactionCreateReq, c.ClientIP())
transactionEditable := user.CanEditTransactionByTransactionTime(transaction.TransactionTime, transactionCreateReq.UtcOffset) transactionEditable := user.CanEditTransactionByTransactionTime(transaction.TransactionTime, clientTimezone)
if !transactionEditable { if !transactionEditable {
return nil, errs.ErrCannotCreateTransactionWithThisTransactionTime return nil, errs.ErrCannotCreateTransactionWithThisTransactionTime
@@ -1814,7 +1978,61 @@ func (a *TransactionsApi) getTransactionTagInfoResponses(tagIds []int64, allTran
return allTags 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) getTransactionAllEssentialData(c *core.WebContext, user *models.User, withPictures bool, trimCategory bool, trimTag bool) (accountMap map[int64]*models.Account, categoryMap map[int64]*models.TransactionCategory, tagMap map[int64]*models.TransactionTag, allTransactionTagIds map[int64][]int64, pictureInfoMap map[int64][]*models.TransactionPictureInfo, err error) {
uid := user.Uid
allAccounts, err := a.accounts.GetAllAccountsByUid(c, uid)
if err != nil {
log.Errorf(c, "[transactions.getTransactionAllEssentialData] failed to get all accounts for user \"uid:%d\", because %s", uid, err.Error())
return nil, nil, nil, nil, nil, err
}
accountMap = a.accounts.GetAccountMapByList(allAccounts)
allTagIndexes, err := a.transactionTags.GetAllTagIdsOfAllTransactions(c, uid)
if err != nil {
log.Errorf(c, "[transactions.getTransactionAllEssentialData] failed to get all transactions tag ids for user \"uid:%d\", because %s", uid, err.Error())
return nil, nil, nil, nil, nil, err
}
allTransactionTagIds = a.transactionTags.GetGroupedTransactionTagIds(allTagIndexes)
if !trimCategory {
allCategories, err := a.transactionCategories.GetAllCategoriesByUid(c, uid, 0, -1)
if err != nil {
log.Errorf(c, "[transactions.getTransactionAllEssentialData] failed to get all transactions categories for user \"uid:%d\", because %s", uid, err.Error())
return nil, nil, nil, nil, nil, err
}
categoryMap = a.transactionCategories.GetCategoryMapByList(allCategories)
}
if !trimTag {
allTags, err := a.transactionTags.GetAllTagsByUid(c, uid)
if err != nil {
log.Errorf(c, "[transactions.getTransactionAllEssentialData] failed to get all transactions tags for user \"uid:%d\", because %s", uid, err.Error())
return nil, nil, nil, nil, nil, err
}
tagMap = a.transactionTags.GetTagMapByList(allTags)
}
if withPictures && a.CurrentConfig().EnableTransactionPictures {
pictureInfoMap, err = a.transactionPictures.GetAllPictureInfosOfAllTransactions(c, uid)
if err != nil {
log.Errorf(c, "[transactions.getTransactionAllEssentialData] failed to get all transactions pictures for user \"uid:%d\", because %s", uid, err.Error())
return nil, nil, nil, nil, nil, err
}
}
return accountMap, categoryMap, tagMap, allTransactionTagIds, pictureInfoMap, nil
}
func (a *TransactionsApi) getTransactionEssentialDataByTransactionIds(c *core.WebContext, user *models.User, transactions []*models.Transaction, withPictures bool, trimCategory bool, trimTag bool) (accountMap map[int64]*models.Account, categoryMap map[int64]*models.TransactionCategory, tagMap map[int64]*models.TransactionTag, allTransactionTagIds map[int64][]int64, pictureInfoMap map[int64][]*models.TransactionPictureInfo, err error) {
uid := user.Uid uid := user.Uid
transactionIds := make([]int64, len(transactions)) transactionIds := make([]int64, len(transactions))
accountIds := make([]int64, 0, len(transactions)*2) accountIds := make([]int64, 0, len(transactions)*2)
@@ -1837,32 +2055,26 @@ func (a *TransactionsApi) getTransactionResponseListResult(c *core.WebContext, u
categoryIds = append(categoryIds, transactions[i].CategoryId) categoryIds = append(categoryIds, transactions[i].CategoryId)
} }
allAccounts, err := a.accounts.GetAccountsByAccountIds(c, uid, utils.ToUniqueInt64Slice(accountIds)) accountMap, err = a.accounts.GetAccountsByAccountIds(c, uid, utils.ToUniqueInt64Slice(accountIds))
if err != nil { if err != nil {
log.Errorf(c, "[transactions.getTransactionResponseListResult] failed to get accounts for user \"uid:%d\", because %s", uid, err.Error()) log.Errorf(c, "[transactions.getTransactionEssentialDataByTransactionIds] failed to get accounts for user \"uid:%d\", because %s", uid, err.Error())
return nil, err return nil, nil, nil, nil, nil, err
} }
transactions = a.filterTransactions(c, uid, transactions, allAccounts) allTransactionTagIds, err = a.transactionTags.GetAllTagIdsOfTransactions(c, uid, transactionIds)
allTransactionTagIds, err := a.transactionTags.GetAllTagIdsOfTransactions(c, uid, transactionIds)
if err != nil { if err != nil {
log.Errorf(c, "[transactions.getTransactionResponseListResult] failed to get transactions tag ids for user \"uid:%d\", because %s", uid, err.Error()) log.Errorf(c, "[transactions.getTransactionEssentialDataByTransactionIds] failed to get transactions tag ids for user \"uid:%d\", because %s", uid, err.Error())
return nil, err return nil, nil, nil, nil, nil, err
} }
var categoryMap map[int64]*models.TransactionCategory
var tagMap map[int64]*models.TransactionTag
var pictureInfoMap map[int64][]*models.TransactionPictureInfo
if !trimCategory { if !trimCategory {
categoryMap, err = a.transactionCategories.GetCategoriesByCategoryIds(c, uid, utils.ToUniqueInt64Slice(categoryIds)) categoryMap, err = a.transactionCategories.GetCategoriesByCategoryIds(c, uid, utils.ToUniqueInt64Slice(categoryIds))
if err != nil { if err != nil {
log.Errorf(c, "[transactions.getTransactionResponseListResult] failed to get transactions categories for user \"uid:%d\", because %s", uid, err.Error()) log.Errorf(c, "[transactions.getTransactionEssentialDataByTransactionIds] failed to get transactions categories for user \"uid:%d\", because %s", uid, err.Error())
return nil, err return nil, nil, nil, nil, nil, err
} }
} }
@@ -1870,8 +2082,8 @@ func (a *TransactionsApi) getTransactionResponseListResult(c *core.WebContext, u
tagMap, err = a.transactionTags.GetTagsByTagIds(c, uid, utils.ToUniqueInt64Slice(a.transactionTags.GetTransactionTagIds(allTransactionTagIds))) tagMap, err = a.transactionTags.GetTagsByTagIds(c, uid, utils.ToUniqueInt64Slice(a.transactionTags.GetTransactionTagIds(allTransactionTagIds)))
if err != nil { if err != nil {
log.Errorf(c, "[transactions.getTransactionResponseListResult] failed to get transactions tags for user \"uid:%d\", because %s", uid, err.Error()) log.Errorf(c, "[transactions.getTransactionEssentialDataByTransactionIds] failed to get transactions tags for user \"uid:%d\", because %s", uid, err.Error())
return nil, err return nil, nil, nil, nil, nil, err
} }
} }
@@ -1879,11 +2091,15 @@ func (a *TransactionsApi) getTransactionResponseListResult(c *core.WebContext, u
pictureInfoMap, err = a.transactionPictures.GetPictureInfosByTransactionIds(c, uid, utils.ToUniqueInt64Slice(a.transactions.GetTransactionIds(transactions))) pictureInfoMap, err = a.transactionPictures.GetPictureInfosByTransactionIds(c, uid, utils.ToUniqueInt64Slice(a.transactions.GetTransactionIds(transactions)))
if err != nil { if err != nil {
log.Errorf(c, "[transactions.getTransactionResponseListResult] failed to get transactions pictures for user \"uid:%d\", because %s", uid, err.Error()) log.Errorf(c, "[transactions.getTransactionEssentialDataByTransactionIds] failed to get transactions pictures for user \"uid:%d\", because %s", uid, err.Error())
return nil, err return nil, nil, nil, nil, nil, err
} }
} }
return accountMap, categoryMap, tagMap, allTransactionTagIds, pictureInfoMap, nil
}
func (a *TransactionsApi) getTransactionResponseListResult(c *core.WebContext, user *models.User, transactions []*models.Transaction, allAccounts map[int64]*models.Account, categoryMap map[int64]*models.TransactionCategory, tagMap map[int64]*models.TransactionTag, allTransactionTagIds map[int64][]int64, pictureInfoMap map[int64][]*models.TransactionPictureInfo, clientTimezone *time.Location, withPictures bool, trimAccount bool, trimCategory bool, trimTag bool) (models.TransactionInfoResponseSlice, error) {
result := make(models.TransactionInfoResponseSlice, len(transactions)) result := make(models.TransactionInfoResponseSlice, len(transactions))
for i := 0; i < len(transactions); i++ { for i := 0; i < len(transactions); i++ {
@@ -1893,7 +2109,7 @@ func (a *TransactionsApi) getTransactionResponseListResult(c *core.WebContext, u
transaction = a.transactions.GetRelatedTransferTransaction(transaction) 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] transactionTagIds := allTransactionTagIds[transaction.TransactionId]
result[i] = transaction.ToTransactionInfoResponse(transactionTagIds, transactionEditable) result[i] = transaction.ToTransactionInfoResponse(transactionTagIds, transactionEditable)
@@ -1907,17 +2123,17 @@ func (a *TransactionsApi) getTransactionResponseListResult(c *core.WebContext, u
} }
} }
if !trimCategory { if !trimCategory && categoryMap != nil {
if category := categoryMap[transaction.CategoryId]; category != nil { if category := categoryMap[transaction.CategoryId]; category != nil {
result[i].Category = category.ToTransactionCategoryInfoResponse() result[i].Category = category.ToTransactionCategoryInfoResponse()
} }
} }
if !trimTag { if !trimTag && tagMap != nil && transactionTagIds != nil {
result[i].Tags = a.getTransactionTagInfoResponses(transactionTagIds, tagMap) result[i].Tags = a.getTransactionTagInfoResponses(transactionTagIds, tagMap)
} }
if withPictures && a.CurrentConfig().EnableTransactionPictures { if withPictures && a.CurrentConfig().EnableTransactionPictures && pictureInfoMap != nil {
pictureInfos, exists := pictureInfoMap[transaction.TransactionId] pictureInfos, exists := pictureInfoMap[transaction.TransactionId]
if exists { if exists {
+1 -1
View File
@@ -85,7 +85,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorEnableRequestHandler(c *core.WebCo
return nil, errs.ErrNotPermittedToPerformThisAction return nil, errs.ErrNotPermittedToPerformThisAction
} }
key, err := a.twoFactorAuthorizations.GenerateTwoFactorSecret(c, user) key, err := a.twoFactorAuthorizations.GenerateTwoFactorSecret(c, user, c.GetClientLocale())
if err != nil { if err != nil {
log.Errorf(c, "[twofactor_authorizations.TwoFactorEnableRequestHandler] failed to generate two-factor secret, because %s", err.Error()) 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/auth/oauth2/provider/oidc"
"github.com/mayswind/ezbookkeeping/pkg/core" "github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs" "github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/httpclient"
"github.com/mayswind/ezbookkeeping/pkg/settings" "github.com/mayswind/ezbookkeeping/pkg/settings"
"github.com/mayswind/ezbookkeeping/pkg/utils"
) )
// OAuth2Container contains the current OAuth 2.0 authentication provider // OAuth2Container contains the current OAuth 2.0 authentication provider
@@ -67,7 +67,7 @@ func InitializeOAuth2Provider(config *settings.Config) error {
Container.current = oauth2Provider Container.current = oauth2Provider
Container.usePKCE = config.OAuth2UsePKCE 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, core.GetOutgoingUserAgent(), config.EnableDebugLog)
Container.externalUserAuthType = externalUserAuthType Container.externalUserAuthType = externalUserAuthType
return nil return nil
@@ -10,6 +10,7 @@ import (
"github.com/mayswind/ezbookkeeping/pkg/auth/oauth2/provider" "github.com/mayswind/ezbookkeeping/pkg/auth/oauth2/provider"
"github.com/mayswind/ezbookkeeping/pkg/core" "github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs" "github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/httpclient"
"github.com/mayswind/ezbookkeeping/pkg/log" "github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/settings" "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)) 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) resp, err := oauth2Client.Do(req)
if err != nil { if err != nil {
@@ -69,8 +75,6 @@ func (p *CommonOAuth2Provider) GetUserInfo(c core.Context, oauth2Token *oauth2.T
defer resp.Body.Close() defer resp.Body.Close()
body, err := io.ReadAll(resp.Body) body, err := io.ReadAll(resp.Body)
log.Debugf(c, "[common_oauth2_provider.GetUserInfo] response is %s", body)
if resp.StatusCode != 200 { if resp.StatusCode != 200 {
log.Errorf(c, "[common_oauth2_provider.GetUserInfo] failed to get user info response, because response code is %d", resp.StatusCode) log.Errorf(c, "[common_oauth2_provider.GetUserInfo] failed to get user info response, because response code is %d", resp.StatusCode)
return nil, errs.ErrFailedToRequestRemoteApi return nil, errs.ErrFailedToRequestRemoteApi
@@ -11,6 +11,7 @@ import (
"github.com/mayswind/ezbookkeeping/pkg/auth/oauth2/provider" "github.com/mayswind/ezbookkeeping/pkg/auth/oauth2/provider"
"github.com/mayswind/ezbookkeeping/pkg/core" "github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs" "github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/httpclient"
"github.com/mayswind/ezbookkeeping/pkg/log" "github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/settings" "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)) 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) resp, err := oauth2Client.Do(req)
if err != nil { if err != nil {
@@ -71,8 +77,6 @@ func (p *GithubOAuth2Provider) GetUserInfo(c core.Context, oauth2Token *oauth2.T
defer resp.Body.Close() defer resp.Body.Close()
body, err := io.ReadAll(resp.Body) body, err := io.ReadAll(resp.Body)
log.Debugf(c, "[github_oauth2_provider.GetUserInfo] user profile response is %s", body)
if resp.StatusCode != 200 { if resp.StatusCode != 200 {
log.Errorf(c, "[github_oauth2_provider.GetUserInfo] failed to get user info response, because response code is %d", resp.StatusCode) log.Errorf(c, "[github_oauth2_provider.GetUserInfo] failed to get user info response, because response code is %d", resp.StatusCode)
return nil, errs.ErrFailedToRequestRemoteApi return nil, errs.ErrFailedToRequestRemoteApi
@@ -92,6 +96,10 @@ func (p *GithubOAuth2Provider) GetUserInfo(c core.Context, oauth2Token *oauth2.T
return nil, errs.ErrFailedToRequestRemoteApi 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) resp, err = oauth2Client.Do(req)
if err != nil { if err != nil {
@@ -102,8 +110,6 @@ func (p *GithubOAuth2Provider) GetUserInfo(c core.Context, oauth2Token *oauth2.T
defer resp.Body.Close() defer resp.Body.Close()
body, err = io.ReadAll(resp.Body) body, err = io.ReadAll(resp.Body)
log.Debugf(c, "[github_oauth2_provider.GetUserInfo] user emails response is %s", body)
if resp.StatusCode != 200 { if resp.StatusCode != 200 {
log.Errorf(c, "[github_oauth2_provider.GetUserInfo] failed to get user emails response, because response code is %d", resp.StatusCode) log.Errorf(c, "[github_oauth2_provider.GetUserInfo] failed to get user emails response, because response code is %d", resp.StatusCode)
return nil, errs.ErrFailedToRequestRemoteApi return nil, errs.ErrFailedToRequestRemoteApi
@@ -11,6 +11,7 @@ import (
"github.com/mayswind/ezbookkeeping/pkg/auth/oauth2/provider" "github.com/mayswind/ezbookkeeping/pkg/auth/oauth2/provider"
"github.com/mayswind/ezbookkeeping/pkg/core" "github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs" "github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/httpclient"
"github.com/mayswind/ezbookkeeping/pkg/log" "github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/settings" "github.com/mayswind/ezbookkeeping/pkg/settings"
) )
@@ -18,6 +19,7 @@ import (
// OIDCClaims represents OIDC claims // OIDCClaims represents OIDC claims
type OIDCClaims struct { type OIDCClaims struct {
PreferredUserName string `json:"preferred_username"` PreferredUserName string `json:"preferred_username"`
UserName string `json:"username"`
Name string `json:"name"` Name string `json:"name"`
Email string `json:"email"` Email string `json:"email"`
} }
@@ -92,7 +94,9 @@ func (p *OIDCProvider) GetUserInfo(c core.Context, oauth2Token *oauth2.Token) (*
nickName := claims.Name nickName := claims.Name
if userName == "" || email == "" || nickName == "" { 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 { if err != nil {
log.Errorf(c, "[oidc_provider.GetUserInfo] failed to get user info, because %s", err.Error()) 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 userName = claims.PreferredUserName
} }
if userName == "" {
userName = claims.UserName
}
if email == "" { if email == "" {
email = claims.Email email = claims.Email
} }
+3 -2
View File
@@ -5,6 +5,7 @@ import (
"time" "time"
"github.com/mayswind/ezbookkeeping/pkg/converters" "github.com/mayswind/ezbookkeeping/pkg/converters"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/core" "github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs" "github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/log" "github.com/mayswind/ezbookkeeping/pkg/log"
@@ -149,7 +150,7 @@ func (l *UserDataCli) ModifyUserPassword(c *core.CliContext, username string, pa
Password: password, Password: password,
} }
_, _, err = l.users.UpdateUser(c, userNew, false) err = l.users.UpdateUserPassword(c, userNew)
if err != nil { if err != nil {
log.CliErrorf(c, "[user_data.ModifyUserPassword] failed to update user \"%s\" password, because %s", user.Username, err.Error()) log.CliErrorf(c, "[user_data.ModifyUserPassword] failed to update user \"%s\" password, because %s", user.Username, err.Error())
@@ -818,7 +819,7 @@ func (l *UserDataCli) ImportTransaction(c *core.CliContext, username string, fil
return err 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 { if err != nil {
log.CliErrorf(c, "[user_data.ImportTransaction] failed to parse imported data for \"%s\", because %s", username, err.Error()) 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 ( import (
"bytes" "bytes"
"time"
"golang.org/x/text/encoding/simplifiedchinese" "golang.org/x/text/encoding/simplifiedchinese"
"golang.org/x/text/transform" "golang.org/x/text/transform"
@@ -53,7 +54,7 @@ type alipayTransactionDataCsvFileImporter struct {
} }
// ParseImportedData returns the imported data by parsing the alipay transaction csv data // 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 enc := simplifiedchinese.GB18030
reader := transform.NewReader(bytes.NewReader(data), enc.NewDecoder()) 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) transactionDataTable := datatable.CreateNewTransactionDataTableFromCommonDataTable(commonDataTable, alipayTransactionSupportedColumns, transactionRowParser)
dataTableImporter := converter.CreateNewSimpleImporterWithTypeNameMapping(alipayTransactionTypeNameMapping) 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" "golang.org/x/text/encoding/simplifiedchinese"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/core" "github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs" "github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/models" "github.com/mayswind/ezbookkeeping/pkg/models"
@@ -15,7 +16,7 @@ import (
) )
func TestAlipayCsvFileImporterParseImportedData_MinimumValidData(t *testing.T) { func TestAlipayCsvFileImporterParseImportedData_MinimumValidData(t *testing.T) {
converter := AlipayWebTransactionDataCsvFileImporter importer := AlipayWebTransactionDataCsvFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -35,7 +36,7 @@ func TestAlipayCsvFileImporterParseImportedData_MinimumValidData(t *testing.T) {
"------------------------------------------------------------------------------------\n") "------------------------------------------------------------------------------------\n")
assert.Nil(t, err) 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.Nil(t, err)
assert.Equal(t, 4, len(allNewTransactions)) assert.Equal(t, 4, len(allNewTransactions))
@@ -94,7 +95,7 @@ func TestAlipayCsvFileImporterParseImportedData_MinimumValidData(t *testing.T) {
} }
func TestAlipayCsvFileImporterParseImportedData_ParseRefundTransaction(t *testing.T) { func TestAlipayCsvFileImporterParseImportedData_ParseRefundTransaction(t *testing.T) {
converter := AlipayWebTransactionDataCsvFileImporter importer := AlipayWebTransactionDataCsvFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -112,7 +113,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseRefundTransaction(t *testin
"------------------------------------------------------------------------------------\n") "------------------------------------------------------------------------------------\n")
assert.Nil(t, err) 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.Nil(t, err)
assert.Equal(t, int64(1234567890), allNewTransactions[0].Uid) assert.Equal(t, int64(1234567890), allNewTransactions[0].Uid)
@@ -132,7 +133,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseRefundTransaction(t *testin
"------------------------------------------------------------------------------------\n") "------------------------------------------------------------------------------------\n")
assert.Nil(t, err) 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.Nil(t, err)
assert.Equal(t, int64(1234567890), allNewTransactions[0].Uid) assert.Equal(t, int64(1234567890), allNewTransactions[0].Uid)
@@ -144,7 +145,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseRefundTransaction(t *testin
} }
func TestAlipayCsvFileImporterParseImportedData_ParseInvestmentRefundTransaction(t *testing.T) { func TestAlipayCsvFileImporterParseImportedData_ParseInvestmentRefundTransaction(t *testing.T) {
converter := AlipayAppTransactionDataCsvFileImporter importer := AlipayAppTransactionDataCsvFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ 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 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") "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.Nil(t, err)
assert.Equal(t, 2, len(allNewTransactions)) assert.Equal(t, 2, len(allNewTransactions))
@@ -184,7 +185,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseInvestmentRefundTransaction
} }
func TestAlipayCsvFileImporterParseImportedData_ParseInvalidTime(t *testing.T) { func TestAlipayCsvFileImporterParseImportedData_ParseInvalidTime(t *testing.T) {
converter := AlipayWebTransactionDataCsvFileImporter importer := AlipayWebTransactionDataCsvFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -201,7 +202,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseInvalidTime(t *testing.T) {
"------------------------------------------------------------------------------------\n") "------------------------------------------------------------------------------------\n")
assert.Nil(t, err) 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) assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
data2, err := simplifiedchinese.GB18030.NewEncoder().String("支付宝交易记录明细查询\n" + data2, err := simplifiedchinese.GB18030.NewEncoder().String("支付宝交易记录明细查询\n" +
@@ -213,12 +214,12 @@ func TestAlipayCsvFileImporterParseImportedData_ParseInvalidTime(t *testing.T) {
"------------------------------------------------------------------------------------\n") "------------------------------------------------------------------------------------\n")
assert.Nil(t, err) 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) assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
} }
func TestAlipayCsvFileImporterParseImportedData_ParseInvalidType(t *testing.T) { func TestAlipayCsvFileImporterParseImportedData_ParseInvalidType(t *testing.T) {
converter := AlipayWebTransactionDataCsvFileImporter importer := AlipayWebTransactionDataCsvFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -235,12 +236,12 @@ func TestAlipayCsvFileImporterParseImportedData_ParseInvalidType(t *testing.T) {
"------------------------------------------------------------------------------------\n") "------------------------------------------------------------------------------------\n")
assert.Nil(t, err) 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) assert.EqualError(t, err, errs.ErrNotFoundTransactionDataInFile.Message)
} }
func TestAlipayCsvFileImporterParseImportedData_ParseAccountName(t *testing.T) { func TestAlipayCsvFileImporterParseImportedData_ParseAccountName(t *testing.T) {
converter := AlipayWebTransactionDataCsvFileImporter importer := AlipayWebTransactionDataCsvFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -258,7 +259,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseAccountName(t *testing.T) {
"------------------------------------------------------------------------------------\n") "------------------------------------------------------------------------------------\n")
assert.Nil(t, err) 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.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
@@ -274,7 +275,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseAccountName(t *testing.T) {
"------------------------------------------------------------------------------------\n") "------------------------------------------------------------------------------------\n")
assert.Nil(t, err) 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.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
@@ -290,7 +291,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseAccountName(t *testing.T) {
"------------------------------------------------------------------------------------\n") "------------------------------------------------------------------------------------\n")
assert.Nil(t, err) 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.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
@@ -307,7 +308,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseAccountName(t *testing.T) {
"------------------------------------------------------------------------------------\n") "------------------------------------------------------------------------------------\n")
assert.Nil(t, err) 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.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
@@ -324,7 +325,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseAccountName(t *testing.T) {
"------------------------------------------------------------------------------------\n") "------------------------------------------------------------------------------------\n")
assert.Nil(t, err) 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.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
@@ -341,7 +342,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseAccountName(t *testing.T) {
"------------------------------------------------------------------------------------\n") "------------------------------------------------------------------------------------\n")
assert.Nil(t, err) 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.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
@@ -358,7 +359,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseAccountName(t *testing.T) {
"------------------------------------------------------------------------------------\n") "------------------------------------------------------------------------------------\n")
assert.Nil(t, err) 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.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
@@ -367,7 +368,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseAccountName(t *testing.T) {
} }
func TestAlipayCsvFileImporterParseImportedData_ParseCategory(t *testing.T) { func TestAlipayCsvFileImporterParseImportedData_ParseCategory(t *testing.T) {
converter := AlipayAppTransactionDataCsvFileImporter importer := AlipayAppTransactionDataCsvFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -388,7 +389,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseCategory(t *testing.T) {
"2024-09-01 23:59:59,Test Category3,充值-普通充值,不计收支,0.05,交易成功,\n") "2024-09-01 23:59:59,Test Category3,充值-普通充值,不计收支,0.05,交易成功,\n")
assert.Nil(t, err) 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.Nil(t, err)
assert.Equal(t, 3, len(allNewTransactions)) assert.Equal(t, 3, len(allNewTransactions))
@@ -407,7 +408,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseCategory(t *testing.T) {
} }
func TestAlipayCsvFileImporterParseImportedData_ParseRelatedAccount(t *testing.T) { func TestAlipayCsvFileImporterParseImportedData_ParseRelatedAccount(t *testing.T) {
converter := AlipayAppTransactionDataCsvFileImporter importer := AlipayAppTransactionDataCsvFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ 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") "2024-09-01 08:00:00,Test Account4,信用卡还款,不计收支,0.01,Test Account,还款成功,repayment,\n")
assert.Nil(t, err) 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.Nil(t, err)
assert.Equal(t, 9, len(allNewTransactions)) assert.Equal(t, 9, len(allNewTransactions))
@@ -529,7 +530,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseRelatedAccount(t *testing.T
} }
func TestAlipayCsvFileImporterParseImportedData_ParseDescription(t *testing.T) { func TestAlipayCsvFileImporterParseImportedData_ParseDescription(t *testing.T) {
converter := AlipayWebTransactionDataCsvFileImporter importer := AlipayWebTransactionDataCsvFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -546,7 +547,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseDescription(t *testing.T) {
"------------------------------------------------------------------------------------\n") "------------------------------------------------------------------------------------\n")
assert.Nil(t, err) 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.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
@@ -561,7 +562,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseDescription(t *testing.T) {
"------------------------------------------------------------------------------------\n") "------------------------------------------------------------------------------------\n")
assert.Nil(t, err) 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.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
@@ -569,7 +570,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseDescription(t *testing.T) {
} }
func TestAlipayCsvFileImporterParseImportedData_SkipClosedIncomeOrTransferTransaction(t *testing.T) { func TestAlipayCsvFileImporterParseImportedData_SkipClosedIncomeOrTransferTransaction(t *testing.T) {
converter := AlipayWebTransactionDataCsvFileImporter importer := AlipayWebTransactionDataCsvFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -586,12 +587,12 @@ func TestAlipayCsvFileImporterParseImportedData_SkipClosedIncomeOrTransferTransa
"2024-09-01 23:59:59 ,充值-普通充值 ,0.05 ,不计收支 ,交易关闭 ,\n" + "2024-09-01 23:59:59 ,充值-普通充值 ,0.05 ,不计收支 ,交易关闭 ,\n" +
"------------------------------------------------------------------------------------\n") "------------------------------------------------------------------------------------\n")
assert.Nil(t, err) 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) assert.EqualError(t, err, errs.ErrNotFoundTransactionDataInFile.Message)
} }
func TestAlipayCsvFileImporterParseImportedData_SkipUnknownProductTransferTransaction(t *testing.T) { func TestAlipayCsvFileImporterParseImportedData_SkipUnknownProductTransferTransaction(t *testing.T) {
converter := AlipayWebTransactionDataCsvFileImporter importer := AlipayWebTransactionDataCsvFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -607,12 +608,12 @@ func TestAlipayCsvFileImporterParseImportedData_SkipUnknownProductTransferTransa
"2024-09-01 23:59:59 ,xxxx ,0.05 ,不计收支 ,交易成功 ,\n" + "2024-09-01 23:59:59 ,xxxx ,0.05 ,不计收支 ,交易成功 ,\n" +
"------------------------------------------------------------------------------------\n") "------------------------------------------------------------------------------------\n")
assert.Nil(t, err) 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) assert.EqualError(t, err, errs.ErrNotFoundTransactionDataInFile.Message)
} }
func TestAlipayCsvFileImporterParseImportedData_SkipUnknownStatusTransaction(t *testing.T) { func TestAlipayCsvFileImporterParseImportedData_SkipUnknownStatusTransaction(t *testing.T) {
converter := AlipayWebTransactionDataCsvFileImporter importer := AlipayWebTransactionDataCsvFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -628,12 +629,12 @@ func TestAlipayCsvFileImporterParseImportedData_SkipUnknownStatusTransaction(t *
"2024-09-01 01:23:45 ,xxxx ,0.12 ,收入 ,xxxx ,\n" + "2024-09-01 01:23:45 ,xxxx ,0.12 ,收入 ,xxxx ,\n" +
"------------------------------------------------------------------------------------\n") "------------------------------------------------------------------------------------\n")
assert.Nil(t, err) 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) assert.EqualError(t, err, errs.ErrNotFoundTransactionDataInFile.Message)
} }
func TestAlipayCsvFileImporterParseImportedData_MissingFileHeader(t *testing.T) { func TestAlipayCsvFileImporterParseImportedData_MissingFileHeader(t *testing.T) {
converter := AlipayWebTransactionDataCsvFileImporter importer := AlipayWebTransactionDataCsvFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -647,15 +648,15 @@ func TestAlipayCsvFileImporterParseImportedData_MissingFileHeader(t *testing.T)
"------------------------------------------------------------------------------------\n") "------------------------------------------------------------------------------------\n")
assert.Nil(t, err) 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) 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) assert.EqualError(t, err, errs.ErrInvalidFileHeader.Message)
} }
func TestAlipayCsvFileImporterParseImportedData_MissingRequiredColumn(t *testing.T) { func TestAlipayCsvFileImporterParseImportedData_MissingRequiredColumn(t *testing.T) {
converter := AlipayWebTransactionDataCsvFileImporter importer := AlipayWebTransactionDataCsvFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -671,7 +672,7 @@ func TestAlipayCsvFileImporterParseImportedData_MissingRequiredColumn(t *testing
"金额(元),收/支 ,交易状态 ,\n" + "金额(元),收/支 ,交易状态 ,\n" +
"0.12 ,收入 ,交易成功 ,\n" + "0.12 ,收入 ,交易成功 ,\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.ErrMissingRequiredFieldInHeaderRow.Message) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
// Missing Amount Column // Missing Amount Column
@@ -682,7 +683,7 @@ func TestAlipayCsvFileImporterParseImportedData_MissingRequiredColumn(t *testing
"交易创建时间 ,收/支 ,交易状态 ,\n" + "交易创建时间 ,收/支 ,交易状态 ,\n" +
"2024-09-01 12:34:56 ,收入 ,交易成功 ,\n" + "2024-09-01 12:34:56 ,收入 ,交易成功 ,\n" +
"------------------------------------------------------------------------------------\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) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
// Missing Status Column // Missing Status Column
@@ -693,7 +694,7 @@ func TestAlipayCsvFileImporterParseImportedData_MissingRequiredColumn(t *testing
"交易创建时间 ,金额(元),收/支 ,\n" + "交易创建时间 ,金额(元),收/支 ,\n" +
"2024-09-01 12:34:56 ,0.12 ,收入 ,\n" + "2024-09-01 12:34:56 ,0.12 ,收入 ,\n" +
"------------------------------------------------------------------------------------\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) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
// Missing Type Column // Missing Type Column
@@ -704,12 +705,12 @@ func TestAlipayCsvFileImporterParseImportedData_MissingRequiredColumn(t *testing
"交易创建时间 ,金额(元),交易状态 ,\n" + "交易创建时间 ,金额(元),交易状态 ,\n" +
"2024-09-01 12:34:56 ,0.12 ,交易成功 ,\n" + "2024-09-01 12:34:56 ,0.12 ,交易成功 ,\n" +
"------------------------------------------------------------------------------------\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) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
} }
func TestAlipayCsvFileImporterParseImportedData_NoTransactionData(t *testing.T) { func TestAlipayCsvFileImporterParseImportedData_NoTransactionData(t *testing.T) {
converter := AlipayWebTransactionDataCsvFileImporter importer := AlipayWebTransactionDataCsvFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -723,6 +724,6 @@ func TestAlipayCsvFileImporterParseImportedData_NoTransactionData(t *testing.T)
"---------------------------------交易记录明细列表------------------------------------\n" + "---------------------------------交易记录明细列表------------------------------------\n" +
"交易创建时间 ,金额(元),收/支 ,交易状态 ,\n" + "交易创建时间 ,金额(元),收/支 ,交易状态 ,\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) assert.EqualError(t, err, errs.ErrNotFoundTransactionDataInFile.Message)
} }
@@ -13,6 +13,7 @@ import (
const alipayTransactionDataStatusSuccessName = "交易成功" const alipayTransactionDataStatusSuccessName = "交易成功"
const alipayTransactionDataStatusPaymentSuccessName = "支付成功" const alipayTransactionDataStatusPaymentSuccessName = "支付成功"
const alipayTransactionDataStatusPendingGoodsReceiptConfirmationName = "等待确认收货"
const alipayTransactionDataStatusRepaymentSuccessName = "还款成功" const alipayTransactionDataStatusRepaymentSuccessName = "还款成功"
const alipayTransactionDataStatusClosedName = "交易关闭" const alipayTransactionDataStatusClosedName = "交易关闭"
const alipayTransactionDataStatusRefundSuccessName = "退款成功" const alipayTransactionDataStatusRefundSuccessName = "退款成功"
@@ -46,6 +47,7 @@ func (p *alipayTransactionDataRowParser) Parse(ctx core.Context, user *models.Us
if dataRow.GetData(p.columns.statusColumnName) != alipayTransactionDataStatusSuccessName && if dataRow.GetData(p.columns.statusColumnName) != alipayTransactionDataStatusSuccessName &&
dataRow.GetData(p.columns.statusColumnName) != alipayTransactionDataStatusPaymentSuccessName && dataRow.GetData(p.columns.statusColumnName) != alipayTransactionDataStatusPaymentSuccessName &&
dataRow.GetData(p.columns.statusColumnName) != alipayTransactionDataStatusPendingGoodsReceiptConfirmationName &&
dataRow.GetData(p.columns.statusColumnName) != alipayTransactionDataStatusRepaymentSuccessName && dataRow.GetData(p.columns.statusColumnName) != alipayTransactionDataStatusRepaymentSuccessName &&
dataRow.GetData(p.columns.statusColumnName) != alipayTransactionDataStatusClosedName && dataRow.GetData(p.columns.statusColumnName) != alipayTransactionDataStatusClosedName &&
dataRow.GetData(p.columns.statusColumnName) != alipayTransactionDataStatusRefundSuccessName && dataRow.GetData(p.columns.statusColumnName) != alipayTransactionDataStatusRefundSuccessName &&
@@ -1,6 +1,8 @@
package beancount package beancount
import ( import (
"time"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter" "github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/core" "github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/models" "github.com/mayswind/ezbookkeeping/pkg/models"
@@ -24,7 +26,7 @@ var (
) )
// ParseImportedData returns the imported data by parsing the Beancount transaction data // 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) beancountDataReader, err := createNewBeancountDataReader(ctx, data)
if err != nil { if err != nil {
@@ -45,5 +47,5 @@ func (c *beancountTransactionDataImporter) ParseImportedData(ctx core.Context, u
dataTableImporter := converter.CreateNewImporterWithTypeNameMapping(beancountTransactionTypeNameMapping, "", "", BEANCOUNT_TRANSACTION_TAG_SEPARATOR) 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 ( import (
"testing" "testing"
"time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/core" "github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs" "github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/models" "github.com/mayswind/ezbookkeeping/pkg/models"
@@ -12,7 +14,7 @@ import (
) )
func TestBeancountTransactionDataFileParseImportedData_MinimumValidData(t *testing.T) { func TestBeancountTransactionDataFileParseImportedData_MinimumValidData(t *testing.T) {
converter := BeancountTransactionDataImporter importer := BeancountTransactionDataImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -20,7 +22,7 @@ func TestBeancountTransactionDataFileParseImportedData_MinimumValidData(t *testi
DefaultCurrency: "CNY", 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"+ "2024-09-01 *\n"+
" Equity:Opening-Balances -123.45 CNY\n"+ " Equity:Opening-Balances -123.45 CNY\n"+
" Assets:TestAccount 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"+ " Expenses:TestCategory2 1.00 CNY\n"+
"2024-09-04 *\n"+ "2024-09-04 *\n"+
" Assets:TestAccount -0.05 CNY\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) assert.Nil(t, err)
@@ -91,7 +93,7 @@ func TestBeancountTransactionDataFileParseImportedData_MinimumValidData(t *testi
} }
func TestBeancountTransactionDataFileParseImportedData_MinimumValidData2(t *testing.T) { func TestBeancountTransactionDataFileParseImportedData_MinimumValidData2(t *testing.T) {
converter := BeancountTransactionDataImporter importer := BeancountTransactionDataImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -99,7 +101,7 @@ func TestBeancountTransactionDataFileParseImportedData_MinimumValidData2(t *test
DefaultCurrency: "CNY", 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"+ "2024-09-01 *\n"+
" Assets:TestAccount 123.45 CNY\n"+ " Assets:TestAccount 123.45 CNY\n"+
" Equity:Opening-Balances -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"+ " Assets:TestAccount -1.00 CNY\n"+
"2024-09-04 *\n"+ "2024-09-04 *\n"+
" Assets:TestAccount2 0.05 CNY\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) assert.Nil(t, err)
@@ -170,7 +172,7 @@ func TestBeancountTransactionDataFileParseImportedData_MinimumValidData2(t *test
} }
func TestBeancountTransactionDataFileParseImportedData_ParseInvalidTime(t *testing.T) { func TestBeancountTransactionDataFileParseImportedData_ParseInvalidTime(t *testing.T) {
converter := BeancountTransactionDataImporter importer := BeancountTransactionDataImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -178,15 +180,15 @@ func TestBeancountTransactionDataFileParseImportedData_ParseInvalidTime(t *testi
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"2024/09/01 *\n"+ "2024/09/01 *\n"+
" Equity:Opening-Balances -123.45 CNY\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) assert.EqualError(t, err, errs.ErrNotFoundTransactionDataInFile.Message)
} }
func TestBeancountTransactionDataFileParseImportedData_ParseValidCurrency(t *testing.T) { func TestBeancountTransactionDataFileParseImportedData_ParseValidCurrency(t *testing.T) {
converter := BeancountTransactionDataImporter importer := BeancountTransactionDataImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -194,10 +196,10 @@ func TestBeancountTransactionDataFileParseImportedData_ParseValidCurrency(t *tes
DefaultCurrency: "CNY", 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"+ "2024-09-01 * \"Payee Name\" \"Hello\nWorld\"\n"+
" Assets:TestAccount -0.12 USD\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) assert.Nil(t, err)
@@ -222,7 +224,7 @@ func TestBeancountTransactionDataFileParseImportedData_ParseValidCurrency(t *tes
} }
func TestBeancountTransactionDataFileParseImportedData_ParseInvalidAmount(t *testing.T) { func TestBeancountTransactionDataFileParseImportedData_ParseInvalidAmount(t *testing.T) {
converter := BeancountTransactionDataImporter importer := BeancountTransactionDataImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -230,21 +232,21 @@ func TestBeancountTransactionDataFileParseImportedData_ParseInvalidAmount(t *tes
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"2024-09-01 *\n"+ "2024-09-01 *\n"+
" Equity:Opening-Balances -abc CNY\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) assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"2024-09-01 *\n"+ "2024-09-01 *\n"+
" Equity:Opening-Balances -1/0 CNY\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) assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
} }
func TestBeancountTransactionDataFileParseImportedData_ParseDescription(t *testing.T) { func TestBeancountTransactionDataFileParseImportedData_ParseDescription(t *testing.T) {
converter := BeancountTransactionDataImporter importer := BeancountTransactionDataImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -252,13 +254,13 @@ func TestBeancountTransactionDataFileParseImportedData_ParseDescription(t *testi
DefaultCurrency: "CNY", 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"+ "2024-09-01 * \"foo bar\t#test\n\"\n"+
" Equity:Opening-Balances -123.45 CNY\n"+ " Equity:Opening-Balances -123.45 CNY\n"+
" Assets:TestAccount 123.45 CNY\n"+ " Assets:TestAccount 123.45 CNY\n"+
"2024-09-02 * \"Payee Name\" \"Hello\nWorld\"\n"+ "2024-09-02 * \"Payee Name\" \"Hello\nWorld\"\n"+
" Income:TestCategory -0.12 CNY\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) assert.Nil(t, err)
@@ -269,7 +271,7 @@ func TestBeancountTransactionDataFileParseImportedData_ParseDescription(t *testi
} }
func TestBeancountTransactionDataFileParseImportedData_InvalidTransaction(t *testing.T) { func TestBeancountTransactionDataFileParseImportedData_InvalidTransaction(t *testing.T) {
converter := BeancountTransactionDataImporter importer := BeancountTransactionDataImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -277,33 +279,33 @@ func TestBeancountTransactionDataFileParseImportedData_InvalidTransaction(t *tes
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"2024-09-02 * \"Payee Name\" \"Hello\nWorld\"\n"+ "2024-09-02 * \"Payee Name\" \"Hello\nWorld\"\n"+
" Assets:TestAccount 0.11 CNY\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) 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"+ "2024-09-02 * \"Payee Name\" \"Hello\nWorld\"\n"+
" Expenses:TestCategory -0.11 CNY\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) 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"+ "2024-09-02 * \"Payee Name\" \"Hello\nWorld\"\n"+
" Income:TestCategory -0.11 CNY\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) 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"+ "2024-09-02 * \"Payee Name\" \"Hello\nWorld\"\n"+
" Equity:TestCategory -0.11 CNY\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) assert.EqualError(t, err, errs.ErrThereAreNotSupportedTransactionType.Message)
} }
func TestBeancountTransactionDataFileParseImportedData_NotSupportedToParseSplitTransaction(t *testing.T) { func TestBeancountTransactionDataFileParseImportedData_NotSupportedToParseSplitTransaction(t *testing.T) {
converter := BeancountTransactionDataImporter importer := BeancountTransactionDataImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -311,16 +313,16 @@ func TestBeancountTransactionDataFileParseImportedData_NotSupportedToParseSplitT
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"2024-09-02 * \"Payee Name\" \"Hello\nWorld\"\n"+ "2024-09-02 * \"Payee Name\" \"Hello\nWorld\"\n"+
" Assets:TestAccount -0.23 CNY\n"+ " Assets:TestAccount -0.23 CNY\n"+
" Assets:TestAccount2 0.11 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) assert.EqualError(t, err, errs.ErrNotSupportedSplitTransactions.Message)
} }
func TestBeancountTransactionDataFileParseImportedData_MissingTransactionRequiredData(t *testing.T) { func TestBeancountTransactionDataFileParseImportedData_MissingTransactionRequiredData(t *testing.T) {
converter := BeancountTransactionDataImporter importer := BeancountTransactionDataImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -329,30 +331,30 @@ func TestBeancountTransactionDataFileParseImportedData_MissingTransactionRequire
} }
// Missing Transaction Time // Missing Transaction Time
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"* \"narration\"\n"+ "* \"narration\"\n"+
" Equity:Opening-Balances -123.45 CNY\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) assert.EqualError(t, err, errs.ErrNotFoundTransactionDataInFile.Message)
// Missing Account Name // Missing Account Name
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"2024-09-01 * \"narration\"\n"+ "2024-09-01 * \"narration\"\n"+
" Equity:Opening-Balances -123.45 CNY\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) assert.EqualError(t, err, errs.ErrInvalidBeancountFile.Message)
// Missing Amount // Missing Amount
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"2024-09-01 * \"narration\"\n"+ "2024-09-01 * \"narration\"\n"+
" Equity:Opening-Balances\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) assert.EqualError(t, err, errs.ErrInvalidBeancountFile.Message)
// Missing Commodity // Missing Commodity
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"2024-09-01 * \"narration\"\n"+ "2024-09-01 * \"narration\"\n"+
" Equity:Opening-Balances -123.45\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) assert.EqualError(t, err, errs.ErrInvalidBeancountFile.Message)
} }
+9
View File
@@ -9,11 +9,20 @@ const (
CAMT_INDICATOR_DEBIT camtCreditDebitIndicator = "DBIT" CAMT_INDICATOR_DEBIT camtCreditDebitIndicator = "DBIT"
) )
type camt052File struct {
XMLName xml.Name `xml:"Document"`
BankToCustomerAccountReport *camtBankToCustomerAccountReport `xml:"BkToCstmrAcctRpt"`
}
type camt053File struct { type camt053File struct {
XMLName xml.Name `xml:"Document"` XMLName xml.Name `xml:"Document"`
BankToCustomerStatement *camtBankToCustomerStatement `xml:"BkToCstmrStmt"` BankToCustomerStatement *camtBankToCustomerStatement `xml:"BkToCstmrStmt"`
} }
type camtBankToCustomerAccountReport struct {
Statements []*camtStatement `xml:"Rpt"`
}
type camtBankToCustomerStatement struct { type camtBankToCustomerStatement struct {
Statements []*camtStatement `xml:"Stmt"` Statements []*camtStatement `xml:"Stmt"`
} }
+32
View File
@@ -10,11 +10,30 @@ import (
"github.com/mayswind/ezbookkeeping/pkg/errs" "github.com/mayswind/ezbookkeeping/pkg/errs"
) )
// camt052FileReader defines the structure of camt.052 file reader
type camt052FileReader struct {
xmlDecoder *xml.Decoder
}
// camt053FileReader defines the structure of camt.053 file reader // camt053FileReader defines the structure of camt.053 file reader
type camt053FileReader struct { type camt053FileReader struct {
xmlDecoder *xml.Decoder xmlDecoder *xml.Decoder
} }
// read returns the imported camt.052 data
// Reference: https://www.iso20022.org/message-set/1196/download
func (r *camt052FileReader) read(ctx core.Context) (*camt052File, error) {
file := &camt052File{}
err := r.xmlDecoder.Decode(&file)
if err != nil {
return nil, err
}
return file, nil
}
// read returns the imported camt.053 data // read returns the imported camt.053 data
// Reference: https://www.iso20022.org/message-set/1196/download // Reference: https://www.iso20022.org/message-set/1196/download
func (r *camt053FileReader) read(ctx core.Context) (*camt053File, error) { func (r *camt053FileReader) read(ctx core.Context) (*camt053File, error) {
@@ -29,6 +48,19 @@ func (r *camt053FileReader) read(ctx core.Context) (*camt053File, error) {
return file, nil return file, nil
} }
func createNewCamt052FileReader(data []byte) (*camt052FileReader, error) {
if len(data) > 5 && data[0] == 0x3C && data[1] == 0x3F && data[2] == 0x78 && data[3] == 0x6D && data[4] == 0x6C { // <?xml
xmlDecoder := xml.NewDecoder(bytes.NewReader(data))
xmlDecoder.CharsetReader = charset.NewReaderLabel
return &camt052FileReader{
xmlDecoder: xmlDecoder,
}, nil
}
return nil, errs.ErrInvalidXmlFile
}
func createNewCamt053FileReader(data []byte) (*camt053FileReader, error) { func createNewCamt053FileReader(data []byte) (*camt053FileReader, error) {
if len(data) > 5 && data[0] == 0x3C && data[1] == 0x3F && data[2] == 0x78 && data[3] == 0x6D && data[4] == 0x6C { // <?xml if len(data) > 5 && data[0] == 0x3C && data[1] == 0x3F && data[2] == 0x78 && data[3] == 0x6D && data[4] == 0x6C { // <?xml
xmlDecoder := xml.NewDecoder(bytes.NewReader(data)) xmlDecoder := xml.NewDecoder(bytes.NewReader(data))
@@ -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_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 != "" { } 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_TIME] = fmt.Sprintf("%s 00:00:00", entry.BookingDate.Date)
data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE] = datatable.TRANSACTION_DATA_TABLE_TIMEZONE_NOT_AVAILABLE data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE] = datatable.TRANSACTION_DATA_TABLE_TIMEZONE_NOT_AVAILABLE
@@ -303,12 +303,12 @@ func (t *camtStatementTransactionDataRowIterator) parseTransaction(ctx core.Cont
return data, nil return data, nil
} }
func createNewCamtStatementTransactionDataTable(file *camt053File) (*camtStatementTransactionDataTable, error) { func createNewCamtStatementTransactionDataTable(camtStatements []*camtStatement) (*camtStatementTransactionDataTable, error) {
if file == nil || file.BankToCustomerStatement == nil || len(file.BankToCustomerStatement.Statements) == 0 { if len(camtStatements) == 0 {
return nil, errs.ErrNotFoundTransactionDataInFile return nil, errs.ErrNotFoundTransactionDataInFile
} }
return &camtStatementTransactionDataTable{ return &camtStatementTransactionDataTable{
allStatements: file.BankToCustomerStatement.Statements, allStatements: camtStatements,
}, nil }, nil
} }
@@ -1,8 +1,11 @@
package camt package camt
import ( import (
"time"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter" "github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/core" "github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/models" "github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/utils" "github.com/mayswind/ezbookkeeping/pkg/utils"
) )
@@ -13,17 +16,51 @@ var camtTransactionTypeNameMapping = map[models.TransactionType]string{
models.TRANSACTION_TYPE_TRANSFER: utils.IntToString(int(models.TRANSACTION_TYPE_TRANSFER)), models.TRANSACTION_TYPE_TRANSFER: utils.IntToString(int(models.TRANSACTION_TYPE_TRANSFER)),
} }
// camt052TransactionDataImporter defines the structure of camt.052 file importer for transaction data
type camt052TransactionDataImporter struct {
}
// camt053TransactionDataImporter defines the structure of camt.053 file importer for transaction data // camt053TransactionDataImporter defines the structure of camt.053 file importer for transaction data
type camt053TransactionDataImporter struct { type camt053TransactionDataImporter struct {
} }
// Initialize a camt.053 transaction data importer singleton instance // Initialize camt.052 and camt.053 transaction data importer singleton instances
var ( var (
Camt052TransactionDataImporter = &camt052TransactionDataImporter{}
Camt053TransactionDataImporter = &camt053TransactionDataImporter{} Camt053TransactionDataImporter = &camt053TransactionDataImporter{}
) )
// ParseImportedData returns the imported data by parsing the camt.052 file transaction data
func (c *camt052TransactionDataImporter) 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) {
camt052DataReader, err := createNewCamt052FileReader(data)
if err != nil {
return nil, nil, nil, nil, nil, nil, err
}
camt052Data, err := camt052DataReader.read(ctx)
if err != nil {
return nil, nil, nil, nil, nil, nil, err
}
if camt052Data.BankToCustomerAccountReport == nil || camt052Data.BankToCustomerAccountReport.Statements == nil {
return nil, nil, nil, nil, nil, nil, errs.ErrNotFoundTransactionDataInFile
}
transactionDataTable, err := createNewCamtStatementTransactionDataTable(camt052Data.BankToCustomerAccountReport.Statements)
if err != nil {
return nil, nil, nil, nil, nil, nil, err
}
dataTableImporter := converter.CreateNewSimpleImporterWithTypeNameMapping(camtTransactionTypeNameMapping)
return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezone, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
}
// ParseImportedData returns the imported data by parsing the camt.053 file transaction data // 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) camt053DataReader, err := createNewCamt053FileReader(data)
if err != nil { if err != nil {
@@ -36,7 +73,11 @@ func (c *camt053TransactionDataImporter) ParseImportedData(ctx core.Context, use
return nil, nil, nil, nil, nil, nil, err return nil, nil, nil, nil, nil, nil, err
} }
transactionDataTable, err := createNewCamtStatementTransactionDataTable(camt053Data) if camt053Data.BankToCustomerStatement == nil || camt053Data.BankToCustomerStatement.Statements == nil {
return nil, nil, nil, nil, nil, nil, errs.ErrNotFoundTransactionDataInFile
}
transactionDataTable, err := createNewCamtStatementTransactionDataTable(camt053Data.BankToCustomerStatement.Statements)
if err != nil { if err != nil {
return nil, nil, nil, nil, nil, nil, err return nil, nil, nil, nil, nil, nil, err
@@ -44,5 +85,5 @@ func (c *camt053TransactionDataImporter) ParseImportedData(ctx core.Context, use
dataTableImporter := converter.CreateNewSimpleImporterWithTypeNameMapping(camtTransactionTypeNameMapping) 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,17 +2,19 @@ package camt
import ( import (
"testing" "testing"
"time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/core" "github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs" "github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/models" "github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/utils" "github.com/mayswind/ezbookkeeping/pkg/utils"
) )
func TestCamt053TransactionDataFileParseImportedData_MinimumValidData(t *testing.T) { func TestCamt052TransactionDataFileParseImportedData_MinimumValidData(t *testing.T) {
converter := Camt053TransactionDataImporter importer := Camt052TransactionDataImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -20,7 +22,110 @@ func TestCamt053TransactionDataFileParseImportedData_MinimumValidData(t *testing
DefaultCurrency: "CNY", 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.052.001.02">
<BkToCstmrAcctRpt>
<Rpt>
<Acct>
<Id>
<IBAN>123</IBAN>
</Id>
<Ccy>CNY</Ccy>
</Acct>
<Ntry>
<BookgDt>
<DtTm>2024-09-01T01:23:45+08:00</DtTm>
</BookgDt>
<CdtDbtInd>CRDT</CdtDbtInd>
<Amt Ccy="CNY">123.45</Amt>
</Ntry>
<Ntry>
<BookgDt>
<DtTm>2024-09-01T12:34:56+08:00</DtTm>
</BookgDt>
<CdtDbtInd>DBIT</CdtDbtInd>
<Amt Ccy="CNY">0.12</Amt>
</Ntry>
</Rpt>
<Rpt>
<Acct>
<Id>
<Othr>
<Id>456</Id>
</Othr>
</Id>
<Ccy>USD</Ccy>
</Acct>
<Ntry>
<BookgDt>
<DtTm>2024-09-01T23:59:59+08:00</DtTm>
</BookgDt>
<CdtDbtInd>CRDT</CdtDbtInd>
<Amt Ccy="USD">1.23</Amt>
</Ntry>
</Rpt>
</BkToCstmrAcctRpt>
</Document>`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 3, len(allNewTransactions))
assert.Equal(t, 2, len(allNewAccounts))
assert.Equal(t, 1, len(allNewSubExpenseCategories))
assert.Equal(t, 1, len(allNewSubIncomeCategories))
assert.Equal(t, 0, len(allNewSubTransferCategories))
assert.Equal(t, 0, len(allNewTags))
assert.Equal(t, int64(1234567890), allNewTransactions[0].Uid)
assert.Equal(t, models.TRANSACTION_DB_TYPE_INCOME, allNewTransactions[0].Type)
assert.Equal(t, int64(1725125025), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
assert.Equal(t, int64(12345), allNewTransactions[0].Amount)
assert.Equal(t, "123", allNewTransactions[0].OriginalSourceAccountName)
assert.Equal(t, "CNY", allNewTransactions[0].OriginalSourceAccountCurrency)
assert.Equal(t, "", allNewTransactions[0].OriginalCategoryName)
assert.Equal(t, int64(1234567890), allNewTransactions[1].Uid)
assert.Equal(t, models.TRANSACTION_DB_TYPE_EXPENSE, allNewTransactions[1].Type)
assert.Equal(t, int64(1725165296), utils.GetUnixTimeFromTransactionTime(allNewTransactions[1].TransactionTime))
assert.Equal(t, int64(12), allNewTransactions[1].Amount)
assert.Equal(t, "123", allNewTransactions[1].OriginalSourceAccountName)
assert.Equal(t, "CNY", allNewTransactions[1].OriginalSourceAccountCurrency)
assert.Equal(t, "", allNewTransactions[1].OriginalCategoryName)
assert.Equal(t, int64(1234567890), allNewTransactions[2].Uid)
assert.Equal(t, models.TRANSACTION_DB_TYPE_INCOME, allNewTransactions[2].Type)
assert.Equal(t, int64(1725206399), utils.GetUnixTimeFromTransactionTime(allNewTransactions[2].TransactionTime))
assert.Equal(t, int64(123), allNewTransactions[2].Amount)
assert.Equal(t, "456", allNewTransactions[2].OriginalSourceAccountName)
assert.Equal(t, "USD", allNewTransactions[2].OriginalSourceAccountCurrency)
assert.Equal(t, "", allNewTransactions[2].OriginalCategoryName)
assert.Equal(t, int64(1234567890), allNewAccounts[0].Uid)
assert.Equal(t, "123", allNewAccounts[0].Name)
assert.Equal(t, "CNY", allNewAccounts[0].Currency)
assert.Equal(t, int64(1234567890), allNewAccounts[1].Uid)
assert.Equal(t, "456", allNewAccounts[1].Name)
assert.Equal(t, "USD", allNewAccounts[1].Currency)
assert.Equal(t, int64(1234567890), allNewSubExpenseCategories[0].Uid)
assert.Equal(t, "", allNewSubExpenseCategories[0].Name)
assert.Equal(t, int64(1234567890), allNewSubIncomeCategories[0].Uid)
assert.Equal(t, "", allNewSubIncomeCategories[0].Name)
}
func TestCamt053TransactionDataFileParseImportedData_MinimumValidData(t *testing.T) {
importer := Camt053TransactionDataImporter
context := core.NewNullContext()
user := &models.User{
Uid: 1234567890,
DefaultCurrency: "CNY",
}
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := importer.ParseImportedData(context, user, []byte(
`<?xml version="1.0" encoding="UTF-8"?> `<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02"> <Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt> <BkToCstmrStmt>
@@ -64,7 +169,7 @@ func TestCamt053TransactionDataFileParseImportedData_MinimumValidData(t *testing
</Ntry> </Ntry>
</Stmt> </Stmt>
</BkToCstmrStmt> </BkToCstmrStmt>
</Document>`), 0, nil, nil, nil, nil, nil) </Document>`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err) assert.Nil(t, err)
@@ -115,7 +220,7 @@ func TestCamt053TransactionDataFileParseImportedData_MinimumValidData(t *testing
} }
func TestCamt053TransactionDataFileParseImportedData_ParseValidTransactionTime(t *testing.T) { func TestCamt053TransactionDataFileParseImportedData_ParseValidTransactionTime(t *testing.T) {
converter := Camt053TransactionDataImporter importer := Camt053TransactionDataImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -123,7 +228,7 @@ func TestCamt053TransactionDataFileParseImportedData_ParseValidTransactionTime(t
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
`<?xml version="1.0" encoding="UTF-8"?> `<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02"> <Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt> <BkToCstmrStmt>
@@ -157,7 +262,7 @@ func TestCamt053TransactionDataFileParseImportedData_ParseValidTransactionTime(t
</Ntry> </Ntry>
</Stmt> </Stmt>
</BkToCstmrStmt> </BkToCstmrStmt>
</Document>`), 0, nil, nil, nil, nil, nil) </Document>`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 3, len(allNewTransactions)) assert.Equal(t, 3, len(allNewTransactions))
@@ -168,7 +273,7 @@ func TestCamt053TransactionDataFileParseImportedData_ParseValidTransactionTime(t
} }
func TestCamt053TransactionDataFileParseImportedData_ParseInvalidTransactionTime(t *testing.T) { func TestCamt053TransactionDataFileParseImportedData_ParseInvalidTransactionTime(t *testing.T) {
converter := Camt053TransactionDataImporter importer := Camt053TransactionDataImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -176,7 +281,7 @@ func TestCamt053TransactionDataFileParseImportedData_ParseInvalidTransactionTime
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
`<?xml version="1.0" encoding="UTF-8"?> `<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02"> <Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt> <BkToCstmrStmt>
@@ -196,10 +301,10 @@ func TestCamt053TransactionDataFileParseImportedData_ParseInvalidTransactionTime
</Ntry> </Ntry>
</Stmt> </Stmt>
</BkToCstmrStmt> </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) 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"?> `<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02"> <Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt> <BkToCstmrStmt>
@@ -219,10 +324,10 @@ func TestCamt053TransactionDataFileParseImportedData_ParseInvalidTransactionTime
</Ntry> </Ntry>
</Stmt> </Stmt>
</BkToCstmrStmt> </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) 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"?> `<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02"> <Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt> <BkToCstmrStmt>
@@ -242,10 +347,10 @@ func TestCamt053TransactionDataFileParseImportedData_ParseInvalidTransactionTime
</Ntry> </Ntry>
</Stmt> </Stmt>
</BkToCstmrStmt> </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) 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"?> `<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02"> <Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt> <BkToCstmrStmt>
@@ -265,12 +370,12 @@ func TestCamt053TransactionDataFileParseImportedData_ParseInvalidTransactionTime
</Ntry> </Ntry>
</Stmt> </Stmt>
</BkToCstmrStmt> </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) assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
} }
func TestCamt053TransactionDataFileParseImportedData_ParseTransactionValidAmountAndCurrency(t *testing.T) { func TestCamt053TransactionDataFileParseImportedData_ParseTransactionValidAmountAndCurrency(t *testing.T) {
converter := Camt053TransactionDataImporter importer := Camt053TransactionDataImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -278,7 +383,7 @@ func TestCamt053TransactionDataFileParseImportedData_ParseTransactionValidAmount
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
`<?xml version="1.0" encoding="UTF-8"?> `<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02"> <Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt> <BkToCstmrStmt>
@@ -314,7 +419,7 @@ func TestCamt053TransactionDataFileParseImportedData_ParseTransactionValidAmount
</Ntry> </Ntry>
</Stmt> </Stmt>
</BkToCstmrStmt> </BkToCstmrStmt>
</Document>`), 0, nil, nil, nil, nil, nil) </Document>`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 2, len(allNewTransactions)) assert.Equal(t, 2, len(allNewTransactions))
@@ -323,7 +428,7 @@ func TestCamt053TransactionDataFileParseImportedData_ParseTransactionValidAmount
assert.Equal(t, "USD", allNewTransactions[1].OriginalSourceAccountCurrency) assert.Equal(t, "USD", allNewTransactions[1].OriginalSourceAccountCurrency)
assert.Equal(t, int64(10023), allNewTransactions[1].Amount) 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"?> `<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02"> <Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt> <BkToCstmrStmt>
@@ -365,7 +470,7 @@ func TestCamt053TransactionDataFileParseImportedData_ParseTransactionValidAmount
</Ntry> </Ntry>
</Stmt> </Stmt>
</BkToCstmrStmt> </BkToCstmrStmt>
</Document>`), 0, nil, nil, nil, nil, nil) </Document>`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 2, len(allNewTransactions)) assert.Equal(t, 2, len(allNewTransactions))
@@ -374,7 +479,7 @@ func TestCamt053TransactionDataFileParseImportedData_ParseTransactionValidAmount
assert.Equal(t, "USD", allNewTransactions[1].OriginalSourceAccountCurrency) assert.Equal(t, "USD", allNewTransactions[1].OriginalSourceAccountCurrency)
assert.Equal(t, int64(9999), allNewTransactions[1].Amount) 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"?> `<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02"> <Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt> <BkToCstmrStmt>
@@ -403,14 +508,14 @@ func TestCamt053TransactionDataFileParseImportedData_ParseTransactionValidAmount
</Ntry> </Ntry>
</Stmt> </Stmt>
</BkToCstmrStmt> </BkToCstmrStmt>
</Document>`), 0, nil, nil, nil, nil, nil) </Document>`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, "USD", allNewTransactions[0].OriginalSourceAccountCurrency) assert.Equal(t, "USD", allNewTransactions[0].OriginalSourceAccountCurrency)
assert.Equal(t, int64(12345), allNewTransactions[0].Amount) 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"?> `<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02"> <Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt> <BkToCstmrStmt>
@@ -430,7 +535,7 @@ func TestCamt053TransactionDataFileParseImportedData_ParseTransactionValidAmount
</Ntry> </Ntry>
</Stmt> </Stmt>
</BkToCstmrStmt> </BkToCstmrStmt>
</Document>`), 0, nil, nil, nil, nil, nil) </Document>`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
@@ -439,7 +544,7 @@ func TestCamt053TransactionDataFileParseImportedData_ParseTransactionValidAmount
} }
func TestCamt053TransactionDataFileParseImportedData_ParseTransactionInvalidAmountAndCurrency(t *testing.T) { func TestCamt053TransactionDataFileParseImportedData_ParseTransactionInvalidAmountAndCurrency(t *testing.T) {
converter := Camt053TransactionDataImporter importer := Camt053TransactionDataImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -447,7 +552,7 @@ func TestCamt053TransactionDataFileParseImportedData_ParseTransactionInvalidAmou
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
`<?xml version="1.0" encoding="UTF-8"?> `<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02"> <Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt> <BkToCstmrStmt>
@@ -467,10 +572,10 @@ func TestCamt053TransactionDataFileParseImportedData_ParseTransactionInvalidAmou
</Ntry> </Ntry>
</Stmt> </Stmt>
</BkToCstmrStmt> </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) 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"?> `<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02"> <Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt> <BkToCstmrStmt>
@@ -498,10 +603,10 @@ func TestCamt053TransactionDataFileParseImportedData_ParseTransactionInvalidAmou
</Ntry> </Ntry>
</Stmt> </Stmt>
</BkToCstmrStmt> </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) 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"?> `<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02"> <Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt> <BkToCstmrStmt>
@@ -529,12 +634,12 @@ func TestCamt053TransactionDataFileParseImportedData_ParseTransactionInvalidAmou
</Ntry> </Ntry>
</Stmt> </Stmt>
</BkToCstmrStmt> </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) assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
} }
func TestCamt053TransactionDataFileParseImportedData_ParseDescription(t *testing.T) { func TestCamt053TransactionDataFileParseImportedData_ParseDescription(t *testing.T) {
converter := Camt053TransactionDataImporter importer := Camt053TransactionDataImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -542,7 +647,7 @@ func TestCamt053TransactionDataFileParseImportedData_ParseDescription(t *testing
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
`<?xml version="1.0" encoding="UTF-8"?> `<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02"> <Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt> <BkToCstmrStmt>
@@ -572,13 +677,13 @@ func TestCamt053TransactionDataFileParseImportedData_ParseDescription(t *testing
</Ntry> </Ntry>
</Stmt> </Stmt>
</BkToCstmrStmt> </BkToCstmrStmt>
</Document>`), 0, nil, nil, nil, nil, nil) </Document>`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, "Test Transaction", allNewTransactions[0].Comment) 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"?> `<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02"> <Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt> <BkToCstmrStmt>
@@ -607,13 +712,13 @@ func TestCamt053TransactionDataFileParseImportedData_ParseDescription(t *testing
</Ntry> </Ntry>
</Stmt> </Stmt>
</BkToCstmrStmt> </BkToCstmrStmt>
</Document>`), 0, nil, nil, nil, nil, nil) </Document>`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, "Test Line 1\nTest Line 2", allNewTransactions[0].Comment) 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"?> `<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02"> <Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt> <BkToCstmrStmt>
@@ -634,7 +739,7 @@ func TestCamt053TransactionDataFileParseImportedData_ParseDescription(t *testing
</Ntry> </Ntry>
</Stmt> </Stmt>
</BkToCstmrStmt> </BkToCstmrStmt>
</Document>`), 0, nil, nil, nil, nil, nil) </Document>`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
@@ -642,7 +747,7 @@ func TestCamt053TransactionDataFileParseImportedData_ParseDescription(t *testing
} }
func TestCamt053TransactionDataFileParseImportedData_MissingAccountNode(t *testing.T) { func TestCamt053TransactionDataFileParseImportedData_MissingAccountNode(t *testing.T) {
converter := Camt053TransactionDataImporter importer := Camt053TransactionDataImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -650,7 +755,7 @@ func TestCamt053TransactionDataFileParseImportedData_MissingAccountNode(t *testi
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
`<?xml version="1.0" encoding="UTF-8"?> `<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02"> <Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt> <BkToCstmrStmt>
@@ -664,12 +769,12 @@ func TestCamt053TransactionDataFileParseImportedData_MissingAccountNode(t *testi
</Ntry> </Ntry>
</Stmt> </Stmt>
</BkToCstmrStmt> </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) assert.EqualError(t, err, errs.ErrMissingAccountData.Message)
} }
func TestCamt053TransactionDataFileParseImportedData_MissingTransactionRequiredNode(t *testing.T) { func TestCamt053TransactionDataFileParseImportedData_MissingTransactionRequiredNode(t *testing.T) {
converter := Camt053TransactionDataImporter importer := Camt053TransactionDataImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -677,7 +782,7 @@ func TestCamt053TransactionDataFileParseImportedData_MissingTransactionRequiredN
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
`<?xml version="1.0" encoding="UTF-8"?> `<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02"> <Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt> <BkToCstmrStmt>
@@ -694,10 +799,10 @@ func TestCamt053TransactionDataFileParseImportedData_MissingTransactionRequiredN
</Ntry> </Ntry>
</Stmt> </Stmt>
</BkToCstmrStmt> </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) 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"?> `<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02"> <Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt> <BkToCstmrStmt>
@@ -716,10 +821,10 @@ func TestCamt053TransactionDataFileParseImportedData_MissingTransactionRequiredN
</Ntry> </Ntry>
</Stmt> </Stmt>
</BkToCstmrStmt> </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) 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"?> `<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02"> <Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt> <BkToCstmrStmt>
@@ -738,10 +843,10 @@ func TestCamt053TransactionDataFileParseImportedData_MissingTransactionRequiredN
</Ntry> </Ntry>
</Stmt> </Stmt>
</BkToCstmrStmt> </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) 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"?> `<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02"> <Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt> <BkToCstmrStmt>
@@ -760,6 +865,6 @@ func TestCamt053TransactionDataFileParseImportedData_MissingTransactionRequiredN
</Ntry> </Ntry>
</Stmt> </Stmt>
</BkToCstmrStmt> </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) 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) dataRowMap := make(map[datatable.TransactionDataTableColumn]string, 15)
transactionUnixTime := utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime)
transactionTimeZone := time.FixedZone("Transaction Timezone", int(transaction.TimezoneUtcOffset)*60) 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_TIME] = utils.FormatUnixTimeToLongDateTime(transactionUnixTime, transactionTimeZone)
dataRowMap[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE] = utils.FormatTimezoneOffset(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_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_CATEGORY] = c.getExportedTransactionCategoryName(dataTableBuilder, transaction.CategoryId, categoryMap)
dataRowMap[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY] = c.getExportedTransactionSubCategoryName(dataTableBuilder, transaction.CategoryId, categoryMap) dataRowMap[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY] = c.getExportedTransactionSubCategoryName(dataTableBuilder, transaction.CategoryId, categoryMap)
@@ -3,6 +3,7 @@ package converter
import ( import (
"sort" "sort"
"strings" "strings"
"time"
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable" "github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
"github.com/mayswind/ezbookkeeping/pkg/core" "github.com/mayswind/ezbookkeeping/pkg/core"
@@ -29,7 +30,7 @@ type DataTableTransactionDataImporter struct {
} }
// ParseImportedData returns the imported transaction data // 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 { 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) 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 return nil, nil, nil, nil, nil, nil, errs.ErrNotFoundTransactionDataInFile
@@ -94,7 +95,7 @@ func (c *DataTableTransactionDataImporter) ParseImportedData(ctx core.Context, u
continue continue
} }
timezoneOffset := defaultTimezoneOffset timezone := defaultTimezone
if dataTable.HasColumn(datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE) && if dataTable.HasColumn(datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE) &&
dataRow.GetData(datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE) != datatable.TRANSACTION_DATA_TABLE_TIMEZONE_NOT_AVAILABLE { 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 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 { 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()) 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 tagIds []string
var tagNames []string var tagNames []string
tagNamesMap := make(map[string]bool)
if dataTable.HasColumn(datatable.TRANSACTION_DATA_TABLE_TAGS) { if dataTable.HasColumn(datatable.TRANSACTION_DATA_TABLE_TAGS) {
var tagNameItems []string var tagNameItems []string
@@ -320,19 +322,39 @@ func (c *DataTableTransactionDataImporter) ParseImportedData(ctx core.Context, u
continue continue
} }
tag, exists := tagMap[tagName] allNewTags, tagIds, tagNames = c.addTag(user, tagName, tagNamesMap, tagMap, allNewTags, tagIds, tagNames)
}
}
if !exists { if dataTable.HasColumn(datatable.TRANSACTION_DATA_TABLE_PAYEE) && additionalOptions.IsPayeeAsTag() {
tag = c.createNewTransactionTagModel(user.Uid, tagName) payee := dataRow.GetData(datatable.TRANSACTION_DATA_TABLE_PAYEE)
allNewTags = append(allNewTags, tag)
tagMap[tagName] = tag
}
if tag != nil { if payee != "" {
tagIds = append(tagIds, utils.Int64ToString(tag.TagId)) 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) 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.ImportTransaction{
Transaction: &models.Transaction{ Transaction: &models.Transaction{
Uid: user.Uid, Uid: user.Uid,
Type: transactionDbType, Type: transactionDbType,
CategoryId: categoryId, CategoryId: categoryId,
TransactionTime: utils.GetMinTransactionTimeFromUnixTime(transactionTime.Unix()), TransactionTime: utils.GetMinTransactionTimeFromUnixTime(transactionTime.Unix()),
TimezoneUtcOffset: timezoneOffset, TimezoneUtcOffset: utils.GetTimezoneOffsetMinutes(transactionTime.Unix(), timezone),
AccountId: account.AccountId, AccountId: account.AccountId,
Amount: amount, Amount: amount,
HideAmount: false, HideAmount: false,
@@ -459,6 +485,27 @@ func (c *DataTableTransactionDataImporter) getTransactionCategory(categories map
return subCategory, exists 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 { func (c *DataTableTransactionDataImporter) createNewAccountModel(uid int64, accountName string, currency string) *models.Account {
return &models.Account{ return &models.Account{
Uid: uid, Uid: uid,
@@ -1,6 +1,8 @@
package converter package converter
import ( import (
"time"
"github.com/mayswind/ezbookkeeping/pkg/core" "github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/models" "github.com/mayswind/ezbookkeeping/pkg/models"
) )
@@ -14,7 +16,7 @@ type TransactionDataExporter interface {
// TransactionDataImporter defines the structure of transaction data importer // TransactionDataImporter defines the structure of transaction data importer
type TransactionDataImporter interface { type TransactionDataImporter interface {
// ParseImportedData returns the imported data // 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 // 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)
}
@@ -0,0 +1,8 @@
package custom
import "github.com/mayswind/ezbookkeeping/pkg/core"
// CustomTransactionDataParser represents the parser for custom transaction data files
type CustomTransactionDataParser interface {
ParseDataLines(ctx core.Context, data []byte) ([][]string, error)
}
@@ -1,10 +1,11 @@
package dsv package custom
import ( import (
"bytes" "bytes"
"encoding/csv" "encoding/csv"
"io" "io"
"strings" "strings"
"time"
"golang.org/x/text/encoding" "golang.org/x/text/encoding"
"golang.org/x/text/encoding/charmap" "golang.org/x/text/encoding/charmap"
@@ -13,6 +14,7 @@ import (
"golang.org/x/text/encoding/simplifiedchinese" "golang.org/x/text/encoding/simplifiedchinese"
"golang.org/x/text/encoding/traditionalchinese" "golang.org/x/text/encoding/traditionalchinese"
"golang.org/x/text/encoding/unicode" "golang.org/x/text/encoding/unicode"
"golang.org/x/text/encoding/unicode/utf32"
"golang.org/x/text/transform" "golang.org/x/text/transform"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter" "github.com/mayswind/ezbookkeeping/pkg/converters/converter"
@@ -28,59 +30,61 @@ import (
var supportedFileTypeSeparators = map[string]rune{ var supportedFileTypeSeparators = map[string]rune{
"custom_csv": ',', "custom_csv": ',',
"custom_tsv": '\t', "custom_tsv": '\t',
"custom_ssv": ';',
} }
var supportedFileEncodings = map[string]encoding.Encoding{ var supportedFileEncodings = map[string]encoding.Encoding{
"utf-8": unicode.UTF8, // UTF-8 "utf-8": unicode.UTF8BOM, // UTF-8
"utf-8-bom": unicode.UTF8BOM, // UTF-8 with BOM "utf-16le": unicode.UTF16(unicode.LittleEndian, unicode.UseBOM), // UTF-16 Little Endian
"utf-16le": unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM), // UTF-16 Little Endian "utf-16be": unicode.UTF16(unicode.BigEndian, unicode.UseBOM), // UTF-16 Big Endian
"utf-16be": unicode.UTF16(unicode.BigEndian, unicode.IgnoreBOM), // UTF-16 Big Endian "utf-32le": utf32.UTF32(utf32.LittleEndian, utf32.UseBOM), // UTF-32 Little Endian
"cp437": charmap.CodePage437, // OEM United States (CP-437) "utf-32be": utf32.UTF32(utf32.BigEndian, utf32.UseBOM), // UTF-32 Big Endian
"cp863": charmap.CodePage863, // OEM Canadian French (CP-863) "cp437": charmap.CodePage437, // OEM United States (CP-437)
"cp037": charmap.CodePage037, // IBM EBCDIC US/Canada (CP-037) "cp863": charmap.CodePage863, // OEM Canadian French (CP-863)
"cp1047": charmap.CodePage1047, // IBM EBCDIC Open Systems (CP-1047) "cp037": charmap.CodePage037, // IBM EBCDIC US/Canada (CP-037)
"cp1140": charmap.CodePage1140, // IBM EBCDIC US/Canada with Euro (CP-1140) "cp1047": charmap.CodePage1047, // IBM EBCDIC Open Systems (CP-1047)
"iso-8859-1": charmap.ISO8859_1, // Western European (ISO-8859-1) "cp1140": charmap.CodePage1140, // IBM EBCDIC US/Canada with Euro (CP-1140)
"cp850": charmap.CodePage850, // Western European (CP-850) "iso-8859-1": charmap.ISO8859_1, // Western European (ISO-8859-1)
"cp858": charmap.CodePage858, // Western European with Euro (CP-858) "cp850": charmap.CodePage850, // Western European (CP-850)
"windows-1252": charmap.Windows1252, // Western European (Windows-1252) "cp858": charmap.CodePage858, // Western European with Euro (CP-858)
"iso-8859-15": charmap.ISO8859_15, // Western European (ISO-8859-15) "windows-1252": charmap.Windows1252, // Western European (Windows-1252)
"iso-8859-4": charmap.ISO8859_4, // North European (ISO-8859-4) "iso-8859-15": charmap.ISO8859_15, // Western European (ISO-8859-15)
"iso-8859-10": charmap.ISO8859_10, // North European (ISO-8859-10) "iso-8859-4": charmap.ISO8859_4, // North European (ISO-8859-4)
"cp865": charmap.CodePage865, // North European (CP-865) "iso-8859-10": charmap.ISO8859_10, // North European (ISO-8859-10)
"iso-8859-2": charmap.ISO8859_2, // Central European (ISO-8859-2) "cp865": charmap.CodePage865, // North European (CP-865)
"cp852": charmap.CodePage852, // Central European (CP-852) "iso-8859-2": charmap.ISO8859_2, // Central European (ISO-8859-2)
"windows-1250": charmap.Windows1250, // Central European (Windows-1250) "cp852": charmap.CodePage852, // Central European (CP-852)
"iso-8859-14": charmap.ISO8859_14, // Celtic (ISO-8859-14) "windows-1250": charmap.Windows1250, // Central European (Windows-1250)
"iso-8859-3": charmap.ISO8859_3, // South European (ISO-8859-3) "iso-8859-14": charmap.ISO8859_14, // Celtic (ISO-8859-14)
"cp860": charmap.CodePage860, // Portuguese (CP-860) "iso-8859-3": charmap.ISO8859_3, // South European (ISO-8859-3)
"iso-8859-7": charmap.ISO8859_7, // Greek (ISO-8859-7) "cp860": charmap.CodePage860, // Portuguese (CP-860)
"windows-1253": charmap.Windows1253, // Greek (Windows-1253) "iso-8859-7": charmap.ISO8859_7, // Greek (ISO-8859-7)
"iso-8859-9": charmap.ISO8859_9, // Turkish (ISO-8859-9) "windows-1253": charmap.Windows1253, // Greek (Windows-1253)
"windows-1254": charmap.Windows1254, // Turkish (Windows-1254) "iso-8859-9": charmap.ISO8859_9, // Turkish (ISO-8859-9)
"iso-8859-13": charmap.ISO8859_13, // Baltic (ISO-8859-13) "windows-1254": charmap.Windows1254, // Turkish (Windows-1254)
"windows-1257": charmap.Windows1257, // Baltic (Windows-1257) "iso-8859-13": charmap.ISO8859_13, // Baltic (ISO-8859-13)
"iso-8859-16": charmap.ISO8859_16, // South-Eastern European (ISO-8859-16) "windows-1257": charmap.Windows1257, // Baltic (Windows-1257)
"iso-8859-5": charmap.ISO8859_5, // Cyrillic (ISO-8859-5) "iso-8859-16": charmap.ISO8859_16, // South-Eastern European (ISO-8859-16)
"cp855": charmap.CodePage855, // Cyrillic (CP-855) "iso-8859-5": charmap.ISO8859_5, // Cyrillic (ISO-8859-5)
"cp866": charmap.CodePage866, // Cyrillic (CP-866) "cp855": charmap.CodePage855, // Cyrillic (CP-855)
"windows-1251": charmap.Windows1251, // Cyrillic (Windows-1251) "cp866": charmap.CodePage866, // Cyrillic (CP-866)
"koi8r": charmap.KOI8R, // Cyrillic (KOI8-R) "windows-1251": charmap.Windows1251, // Cyrillic (Windows-1251)
"koi8u": charmap.KOI8U, // Cyrillic (KOI8-U) "koi8r": charmap.KOI8R, // Cyrillic (KOI8-R)
"iso-8859-6": charmap.ISO8859_6, // Arabic (ISO-8859-6) "koi8u": charmap.KOI8U, // Cyrillic (KOI8-U)
"windows-1256": charmap.Windows1256, // Arabic (Windows-1256) "iso-8859-6": charmap.ISO8859_6, // Arabic (ISO-8859-6)
"iso-8859-8": charmap.ISO8859_8, // Hebrew (ISO-8859-8) "windows-1256": charmap.Windows1256, // Arabic (Windows-1256)
"cp862": charmap.CodePage862, // Hebrew (CP-862) "iso-8859-8": charmap.ISO8859_8, // Hebrew (ISO-8859-8)
"windows-1255": charmap.Windows1255, // Hebrew (Windows-1255) "cp862": charmap.CodePage862, // Hebrew (CP-862)
"windows-874": charmap.Windows874, // Thai (Windows-874) "windows-1255": charmap.Windows1255, // Hebrew (Windows-1255)
"windows-1258": charmap.Windows1258, // Vietnamese (Windows-1258) "windows-874": charmap.Windows874, // Thai (Windows-874)
"gb18030": simplifiedchinese.GB18030, // Chinese (Simplified, GB18030) "windows-1258": charmap.Windows1258, // Vietnamese (Windows-1258)
"gbk": simplifiedchinese.GBK, // Chinese (Simplified, GBK) "gb18030": simplifiedchinese.GB18030, // Chinese (Simplified, GB18030)
"big5": traditionalchinese.Big5, // Chinese (Traditional, Big5) "gbk": simplifiedchinese.GBK, // Chinese (Simplified, GBK)
"euc-kr": korean.EUCKR, // Korean (EUC-KR) "big5": traditionalchinese.Big5, // Chinese (Traditional, Big5)
"euc-jp": japanese.EUCJP, // Japanese (EUC-JP) "euc-kr": korean.EUCKR, // Korean (EUC-KR)
"iso-2022-jp": japanese.ISO2022JP, // Japanese (ISO-2022-JP) "euc-jp": japanese.EUCJP, // Japanese (EUC-JP)
"shift_jis": japanese.ShiftJIS, // Japanese (Shift JIS) "iso-2022-jp": japanese.ISO2022JP, // Japanese (ISO-2022-JP)
"shift_jis": japanese.ShiftJIS, // Japanese (Shift JIS)
} }
var customTransactionTypeNameMapping = map[models.TransactionType]string{ var customTransactionTypeNameMapping = map[models.TransactionType]string{
@@ -90,10 +94,6 @@ var customTransactionTypeNameMapping = map[models.TransactionType]string{
models.TRANSACTION_TYPE_TRANSFER: utils.IntToString(int(models.TRANSACTION_TYPE_TRANSFER)), models.TRANSACTION_TYPE_TRANSFER: utils.IntToString(int(models.TRANSACTION_TYPE_TRANSFER)),
} }
type CustomTransactionDataDsvFileParser interface {
ParseDsvFileLines(ctx core.Context, data []byte) ([][]string, error)
}
// customTransactionDataDsvFileImporter defines the structure of custom dsv importer for transaction data // customTransactionDataDsvFileImporter defines the structure of custom dsv importer for transaction data
type customTransactionDataDsvFileImporter struct { type customTransactionDataDsvFileImporter struct {
fileEncoding encoding.Encoding fileEncoding encoding.Encoding
@@ -110,8 +110,8 @@ type customTransactionDataDsvFileImporter struct {
transactionTagSeparator string transactionTagSeparator string
} }
// ParseDsvFileLines returns the parsed file lines for specified the dsv file data // ParseDataLines returns the parsed file lines for specified the dsv file data
func (c *customTransactionDataDsvFileImporter) ParseDsvFileLines(ctx core.Context, data []byte) ([][]string, error) { func (c *customTransactionDataDsvFileImporter) ParseDataLines(ctx core.Context, data []byte) ([][]string, error) {
reader := transform.NewReader(bytes.NewReader(data), c.fileEncoding.NewDecoder()) reader := transform.NewReader(bytes.NewReader(data), c.fileEncoding.NewDecoder())
csvReader := csv.NewReader(reader) csvReader := csv.NewReader(reader)
csvReader.Comma = c.separator csvReader.Comma = c.separator
@@ -127,7 +127,7 @@ func (c *customTransactionDataDsvFileImporter) ParseDsvFileLines(ctx core.Contex
} }
if err != nil { if err != nil {
log.Errorf(ctx, "[custom_transaction_data_dsv_file_importer.ParseDsvFileLines] cannot parse dsv data, because %s", err.Error()) log.Errorf(ctx, "[custom_transaction_data_dsv_file_importer.ParseDataLines] cannot parse dsv data, because %s", err.Error())
return nil, errs.ErrInvalidCSVFile return nil, errs.ErrInvalidCSVFile
} }
@@ -146,8 +146,8 @@ func (c *customTransactionDataDsvFileImporter) ParseDsvFileLines(ctx core.Contex
} }
// ParseImportedData returns the imported data by parsing the custom transaction dsv data // 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) allLines, err := c.ParseDataLines(ctx, data)
if err != nil { if err != nil {
return nil, nil, nil, nil, nil, nil, err return nil, nil, nil, nil, nil, nil, err
@@ -157,7 +157,7 @@ func (c *customTransactionDataDsvFileImporter) ParseImportedData(ctx core.Contex
transactionDataTable := CreateNewCustomPlainTextDataTable(dataTable, c.columnIndexMapping, c.transactionTypeNameMapping, c.timeFormat, c.timezoneFormat, c.amountDecimalSeparator, c.amountDigitGroupingSymbol) 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) 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 // IsDelimiterSeparatedValuesFileType returns whether the file type is the delimiter-separated values file type
@@ -166,14 +166,18 @@ func IsDelimiterSeparatedValuesFileType(fileType string) bool {
return exists return exists
} }
// CreateNewCustomTransactionDataDsvFileParser returns a new custom dsv parser for transaction data // CreateNewCustomTransactionDataDsvFileParser returns a new custom transaction data parser
func CreateNewCustomTransactionDataDsvFileParser(fileType string, fileEncoding string) (CustomTransactionDataDsvFileParser, error) { func CreateNewCustomTransactionDataDsvFileParser(fileType string, fileEncoding string) (CustomTransactionDataParser, error) {
separator, exists := supportedFileTypeSeparators[fileType] separator, exists := supportedFileTypeSeparators[fileType]
if !exists { if !exists {
return nil, errs.ErrImportFileTypeNotSupported return nil, errs.ErrImportFileTypeNotSupported
} }
if fileEncoding == "" {
return nil, errs.ErrImportFileEncodingIsEmpty
}
enc, exists := supportedFileEncodings[fileEncoding] enc, exists := supportedFileEncodings[fileEncoding]
if !exists { if !exists {
@@ -194,6 +198,10 @@ func CreateNewCustomTransactionDataDsvFileImporter(fileType string, fileEncoding
return nil, errs.ErrImportFileTypeNotSupported return nil, errs.ErrImportFileTypeNotSupported
} }
if fileEncoding == "" {
return nil, errs.ErrImportFileEncodingIsEmpty
}
enc, exists := supportedFileEncodings[fileEncoding] enc, exists := supportedFileEncodings[fileEncoding]
if !exists { if !exists {
@@ -1,10 +1,12 @@
package dsv package custom
import ( import (
"testing" "testing"
"time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable" "github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
"github.com/mayswind/ezbookkeeping/pkg/core" "github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs" "github.com/mayswind/ezbookkeeping/pkg/errs"
@@ -15,19 +17,21 @@ import (
func TestIsDelimiterSeparatedValuesFileType(t *testing.T) { func TestIsDelimiterSeparatedValuesFileType(t *testing.T) {
assert.True(t, IsDelimiterSeparatedValuesFileType("custom_csv")) assert.True(t, IsDelimiterSeparatedValuesFileType("custom_csv"))
assert.True(t, IsDelimiterSeparatedValuesFileType("custom_tsv")) assert.True(t, IsDelimiterSeparatedValuesFileType("custom_tsv"))
assert.True(t, IsDelimiterSeparatedValuesFileType("custom_ssv"))
assert.False(t, IsDelimiterSeparatedValuesFileType("dsv")) assert.False(t, IsDelimiterSeparatedValuesFileType("dsv"))
assert.False(t, IsDelimiterSeparatedValuesFileType("csv")) assert.False(t, IsDelimiterSeparatedValuesFileType("csv"))
assert.False(t, IsDelimiterSeparatedValuesFileType("tsv")) assert.False(t, IsDelimiterSeparatedValuesFileType("tsv"))
assert.False(t, IsDelimiterSeparatedValuesFileType("ssv"))
} }
func TestCustomTransactionDataDsvFileParser_ParseDsvFileLines(t *testing.T) { func TestCustomTransactionDataDsvFileParser_ParseDataLines(t *testing.T) {
converter, err := CreateNewCustomTransactionDataDsvFileParser("custom_csv", "utf-8") importer, err := CreateNewCustomTransactionDataDsvFileParser("custom_csv", "utf-8")
assert.Nil(t, err) assert.Nil(t, err)
context := core.NewNullContext() context := core.NewNullContext()
allLines, err := converter.ParseDsvFileLines(context, []byte( allLines, err := importer.ParseDataLines(context, []byte(
"2024-09-01 00:00:00,B,123.45\n"+ "2024-09-01 00:00:00,B,123.45\n"+
"2024-09-01 01:23:45,I,0.12\n")) "2024-09-01 01:23:45,I,0.12\n"))
assert.Nil(t, err) assert.Nil(t, err)
@@ -44,10 +48,10 @@ func TestCustomTransactionDataDsvFileParser_ParseDsvFileLines(t *testing.T) {
assert.Equal(t, "I", allLines[1][1]) assert.Equal(t, "I", allLines[1][1])
assert.Equal(t, "0.12", allLines[1][2]) 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) assert.Nil(t, err)
allLines, err = converter.ParseDsvFileLines(context, []byte( allLines, err = importer.ParseDataLines(context, []byte(
"2024-09-01 12:34:56\tE\t1.00\n"+ "2024-09-01 12:34:56\tE\t1.00\n"+
"2024-09-01 23:59:59\tT\t0.05")) "2024-09-01 23:59:59\tT\t0.05"))
assert.Nil(t, err) assert.Nil(t, err)
@@ -63,6 +67,26 @@ func TestCustomTransactionDataDsvFileParser_ParseDsvFileLines(t *testing.T) {
assert.Equal(t, "2024-09-01 23:59:59", allLines[1][0]) assert.Equal(t, "2024-09-01 23:59:59", allLines[1][0])
assert.Equal(t, "T", allLines[1][1]) assert.Equal(t, "T", allLines[1][1])
assert.Equal(t, "0.05", allLines[1][2]) assert.Equal(t, "0.05", allLines[1][2])
importer, err = CreateNewCustomTransactionDataDsvFileParser("custom_ssv", "utf-8")
assert.Nil(t, err)
allLines, err = importer.ParseDataLines(context, []byte(
"2024-09-01 12:34:56;E;1.00\n"+
"2024-09-01 23:59:59;T;0.05"))
assert.Nil(t, err)
assert.Equal(t, 2, len(allLines))
assert.Equal(t, 3, len(allLines[0]))
assert.Equal(t, "2024-09-01 12:34:56", allLines[0][0])
assert.Equal(t, "E", allLines[0][1])
assert.Equal(t, "1.00", allLines[0][2])
assert.Equal(t, 3, len(allLines[1]))
assert.Equal(t, "2024-09-01 23:59:59", allLines[1][0])
assert.Equal(t, "T", allLines[1][1])
assert.Equal(t, "0.05", allLines[1][2])
} }
func TestCustomTransactionDataDsvFileImporter_MinimumValidData(t *testing.T) { func TestCustomTransactionDataDsvFileImporter_MinimumValidData(t *testing.T) {
@@ -77,7 +101,7 @@ func TestCustomTransactionDataDsvFileImporter_MinimumValidData(t *testing.T) {
"E": models.TRANSACTION_TYPE_EXPENSE, "E": models.TRANSACTION_TYPE_EXPENSE,
"T": models.TRANSACTION_TYPE_TRANSFER, "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) assert.Nil(t, err)
context := core.NewNullContext() context := core.NewNullContext()
@@ -87,11 +111,11 @@ func TestCustomTransactionDataDsvFileImporter_MinimumValidData(t *testing.T) {
DefaultCurrency: "CNY", 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 00:00:00,B,123.45\n"+
"2024-09-01 01:23:45,I,0.12\n"+ "2024-09-01 01:23:45,I,0.12\n"+
"2024-09-01 12:34:56,E,1.00\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) assert.Nil(t, err)
@@ -168,7 +192,7 @@ func TestCustomTransactionDataDsvFileImporter_WithAllSupportedColumns(t *testing
"Expense": models.TRANSACTION_TYPE_EXPENSE, "Expense": models.TRANSACTION_TYPE_EXPENSE,
"Transfer": models.TRANSACTION_TYPE_TRANSFER, "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) assert.Nil(t, err)
context := core.NewNullContext() context := core.NewNullContext()
@@ -178,12 +202,12 @@ func TestCustomTransactionDataDsvFileImporter_WithAllSupportedColumns(t *testing
DefaultCurrency: "CNY", 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"+ "\"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 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 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 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) assert.Nil(t, err)
@@ -261,7 +285,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidTime(t *testing.T) {
transactionTypeMapping := map[string]models.TransactionType{ transactionTypeMapping := map[string]models.TransactionType{
"E": models.TRANSACTION_TYPE_EXPENSE, "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) assert.Nil(t, err)
context := core.NewNullContext() context := core.NewNullContext()
@@ -271,12 +295,12 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidTime(t *testing.T) {
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"2024-09-01T12:34:56,E,123.45"), 0, nil, nil, nil, nil, nil) "2024-09-01T12:34:56,E,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message) assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"09/01/2024 12:34:56,E,123.45"), 0, nil, nil, nil, nil, nil) "09/01/2024 12:34:56,E,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message) assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
} }
@@ -292,7 +316,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseTransactionWithoutType(t *tes
"E": models.TRANSACTION_TYPE_EXPENSE, "E": models.TRANSACTION_TYPE_EXPENSE,
"T": models.TRANSACTION_TYPE_TRANSFER, "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) assert.Nil(t, err)
context := core.NewNullContext() context := core.NewNullContext()
@@ -302,8 +326,8 @@ func TestCustomTransactionDataDsvFileImporter_ParseTransactionWithoutType(t *tes
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,A,123.45"), 0, nil, nil, nil, nil, nil) "2024-09-01 12:34:56,A,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrNotFoundTransactionDataInFile.Message) assert.EqualError(t, err, errs.ErrNotFoundTransactionDataInFile.Message)
} }
@@ -316,7 +340,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidType(t *testing.T) {
transactionTypeMapping := map[string]models.TransactionType{ transactionTypeMapping := map[string]models.TransactionType{
"B": 0, "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) assert.Nil(t, err)
context := core.NewNullContext() context := core.NewNullContext()
@@ -326,8 +350,8 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidType(t *testing.T) {
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,B,123.45"), 0, nil, nil, nil, nil, nil) "2024-09-01 12:34:56,B,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrTransactionTypeInvalid.Message) assert.EqualError(t, err, errs.ErrTransactionTypeInvalid.Message)
} }
@@ -340,7 +364,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseTimeWithTimezone(t *testing.T
transactionTypeMapping := map[string]models.TransactionType{ transactionTypeMapping := map[string]models.TransactionType{
"E": models.TRANSACTION_TYPE_EXPENSE, "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) assert.Nil(t, err)
context := core.NewNullContext() context := core.NewNullContext()
@@ -350,20 +374,20 @@ func TestCustomTransactionDataDsvFileImporter_ParseTimeWithTimezone(t *testing.T
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56-10:00,E,123.45"), 0, nil, nil, nil, nil, nil) "2024-09-01 12:34:56-10:00,E,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, int64(1725230096), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime)) assert.Equal(t, int64(1725230096), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte( allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56+00:00,E,123.45"), 0, nil, nil, nil, nil, nil) "2024-09-01 12:34:56+00:00,E,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, int64(1725194096), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime)) assert.Equal(t, int64(1725194096), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte( allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56+12:45,E,123.45"), 0, nil, nil, nil, nil, nil) "2024-09-01 12:34:56+12:45,E,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, int64(1725148196), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime)) assert.Equal(t, int64(1725148196), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
@@ -378,7 +402,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseTimeWithTimezone2(t *testing.
transactionTypeMapping := map[string]models.TransactionType{ transactionTypeMapping := map[string]models.TransactionType{
"E": models.TRANSACTION_TYPE_EXPENSE, "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) assert.Nil(t, err)
context := core.NewNullContext() context := core.NewNullContext()
@@ -388,20 +412,20 @@ func TestCustomTransactionDataDsvFileImporter_ParseTimeWithTimezone2(t *testing.
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56-1000,E,123.45"), 0, nil, nil, nil, nil, nil) "2024-09-01 12:34:56-1000,E,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, int64(1725230096), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime)) assert.Equal(t, int64(1725230096), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte( allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56+0000,E,123.45"), 0, nil, nil, nil, nil, nil) "2024-09-01 12:34:56+0000,E,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, int64(1725194096), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime)) assert.Equal(t, int64(1725194096), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte( allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56+1245,E,123.45"), 0, nil, nil, nil, nil, nil) "2024-09-01 12:34:56+1245,E,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, int64(1725148196), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime)) assert.Equal(t, int64(1725148196), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
@@ -417,7 +441,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseValidTimezone(t *testing.T) {
transactionTypeMapping := map[string]models.TransactionType{ transactionTypeMapping := map[string]models.TransactionType{
"E": models.TRANSACTION_TYPE_EXPENSE, "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) assert.Nil(t, err)
context := core.NewNullContext() context := core.NewNullContext()
@@ -427,20 +451,20 @@ func TestCustomTransactionDataDsvFileImporter_ParseValidTimezone(t *testing.T) {
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,-10:00,E,123.45"), 0, nil, nil, nil, nil, nil) "2024-09-01 12:34:56,-10:00,E,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, int64(1725230096), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime)) assert.Equal(t, int64(1725230096), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte( allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,+00:00,E,123.45"), 0, nil, nil, nil, nil, nil) "2024-09-01 12:34:56,+00:00,E,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, int64(1725194096), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime)) assert.Equal(t, int64(1725194096), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte( allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,+12:45,E,123.45"), 0, nil, nil, nil, nil, nil) "2024-09-01 12:34:56,+12:45,E,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, int64(1725148196), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime)) assert.Equal(t, int64(1725148196), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
@@ -456,7 +480,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseValidTimezone2(t *testing.T)
transactionTypeMapping := map[string]models.TransactionType{ transactionTypeMapping := map[string]models.TransactionType{
"E": models.TRANSACTION_TYPE_EXPENSE, "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) assert.Nil(t, err)
context := core.NewNullContext() context := core.NewNullContext()
@@ -466,20 +490,20 @@ func TestCustomTransactionDataDsvFileImporter_ParseValidTimezone2(t *testing.T)
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,-1000,E,123.45"), 0, nil, nil, nil, nil, nil) "2024-09-01 12:34:56,-1000,E,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, int64(1725230096), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime)) assert.Equal(t, int64(1725230096), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte( allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,+0000,E,123.45"), 0, nil, nil, nil, nil, nil) "2024-09-01 12:34:56,+0000,E,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, int64(1725194096), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime)) assert.Equal(t, int64(1725194096), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte( allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,+1245,E,123.45"), 0, nil, nil, nil, nil, nil) "2024-09-01 12:34:56,+1245,E,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, int64(1725148196), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime)) assert.Equal(t, int64(1725148196), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
@@ -495,7 +519,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidTimezoneFormat(t *test
transactionTypeMapping := map[string]models.TransactionType{ transactionTypeMapping := map[string]models.TransactionType{
"E": models.TRANSACTION_TYPE_EXPENSE, "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) assert.Nil(t, err)
context := core.NewNullContext() context := core.NewNullContext()
@@ -505,8 +529,8 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidTimezoneFormat(t *test
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,CST,E,123.45"), 0, nil, nil, nil, nil, nil) "2024-09-01 12:34:56,CST,E,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrImportFileTransactionTimezoneFormatInvalid.Message) assert.EqualError(t, err, errs.ErrImportFileTransactionTimezoneFormatInvalid.Message)
} }
@@ -520,7 +544,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidTimezone(t *testing.T)
transactionTypeMapping := map[string]models.TransactionType{ transactionTypeMapping := map[string]models.TransactionType{
"E": models.TRANSACTION_TYPE_EXPENSE, "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) assert.Nil(t, err)
context := core.NewNullContext() context := core.NewNullContext()
@@ -530,12 +554,12 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidTimezone(t *testing.T)
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,Asia/Shanghai,E,123.45"), 0, nil, nil, nil, nil, nil) "2024-09-01 12:34:56,Asia/Shanghai,E,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrTransactionTimeZoneInvalid.Message) assert.EqualError(t, err, errs.ErrTransactionTimeZoneInvalid.Message)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,-0700,E,123.45"), 0, nil, nil, nil, nil, nil) "2024-09-01 12:34:56,-0700,E,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrTransactionTimeZoneInvalid.Message) assert.EqualError(t, err, errs.ErrTransactionTimeZoneInvalid.Message)
} }
@@ -549,7 +573,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidTimezone2(t *testing.T
transactionTypeMapping := map[string]models.TransactionType{ transactionTypeMapping := map[string]models.TransactionType{
"E": models.TRANSACTION_TYPE_EXPENSE, "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) assert.Nil(t, err)
context := core.NewNullContext() context := core.NewNullContext()
@@ -559,12 +583,12 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidTimezone2(t *testing.T
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,Asia/Shanghai,E,123.45"), 0, nil, nil, nil, nil, nil) "2024-09-01 12:34:56,Asia/Shanghai,E,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrTransactionTimeZoneInvalid.Message) assert.EqualError(t, err, errs.ErrTransactionTimeZoneInvalid.Message)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,0700,E,123.45"), 0, nil, nil, nil, nil, nil) "2024-09-01 12:34:56,0700,E,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrTransactionTimeZoneInvalid.Message) assert.EqualError(t, err, errs.ErrTransactionTimeZoneInvalid.Message)
} }
@@ -577,7 +601,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseAmountWithCustomFormat(t *tes
transactionTypeMapping := map[string]models.TransactionType{ transactionTypeMapping := map[string]models.TransactionType{
"E": models.TRANSACTION_TYPE_EXPENSE, "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) assert.Nil(t, err)
context := core.NewNullContext() context := core.NewNullContext()
@@ -587,8 +611,8 @@ func TestCustomTransactionDataDsvFileImporter_ParseAmountWithCustomFormat(t *tes
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56\tE\t1.234,56"), 0, nil, nil, nil, nil, nil) "2024-09-01 12:34:56\tE\t1.234,56"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, int64(123456), allNewTransactions[0].Amount) assert.Equal(t, int64(123456), allNewTransactions[0].Amount)
@@ -603,7 +627,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidAmountWithCustomFormat
transactionTypeMapping := map[string]models.TransactionType{ transactionTypeMapping := map[string]models.TransactionType{
"E": models.TRANSACTION_TYPE_EXPENSE, "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) assert.Nil(t, err)
context := core.NewNullContext() context := core.NewNullContext()
@@ -613,8 +637,8 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidAmountWithCustomFormat
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56\tE\t1.234,56"), 0, nil, nil, nil, nil, nil) "2024-09-01 12:34:56\tE\t1.234,56"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAmountInvalid.Message) assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
} }
@@ -627,7 +651,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidAmountWithCustomFormat
transactionTypeMapping := map[string]models.TransactionType{ transactionTypeMapping := map[string]models.TransactionType{
"E": models.TRANSACTION_TYPE_EXPENSE, "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) assert.Nil(t, err)
context := core.NewNullContext() context := core.NewNullContext()
@@ -637,8 +661,8 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidAmountWithCustomFormat
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56\tE\t1.234,56"), 0, nil, nil, nil, nil, nil) "2024-09-01 12:34:56\tE\t1.234,56"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAmountInvalid.Message) assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
} }
@@ -655,7 +679,7 @@ func TestCustomTransactionDataDsvFileImporter_ParsePrimaryCategory(t *testing.T)
"E": models.TRANSACTION_TYPE_EXPENSE, "E": models.TRANSACTION_TYPE_EXPENSE,
"T": models.TRANSACTION_TYPE_TRANSFER, "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) assert.Nil(t, err)
context := core.NewNullContext() context := core.NewNullContext()
@@ -665,11 +689,11 @@ func TestCustomTransactionDataDsvFileImporter_ParsePrimaryCategory(t *testing.T)
DefaultCurrency: "CNY", 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 00:00:00,B,,123.45\n"+
"2024-09-01 01:23:45,I,Test Category,0.12\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 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) assert.Nil(t, err)
@@ -724,7 +748,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseValidAccountCurrency(t *testi
"B": models.TRANSACTION_TYPE_MODIFY_BALANCE, "B": models.TRANSACTION_TYPE_MODIFY_BALANCE,
"T": models.TRANSACTION_TYPE_TRANSFER, "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) assert.Nil(t, err)
context := core.NewNullContext() context := core.NewNullContext()
@@ -734,9 +758,9 @@ func TestCustomTransactionDataDsvFileImporter_ParseValidAccountCurrency(t *testi
DefaultCurrency: "CNY", 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 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) assert.Nil(t, err)
@@ -767,7 +791,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidAccountCurrency(t *tes
"B": models.TRANSACTION_TYPE_MODIFY_BALANCE, "B": models.TRANSACTION_TYPE_MODIFY_BALANCE,
"T": models.TRANSACTION_TYPE_TRANSFER, "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) assert.Nil(t, err)
context := core.NewNullContext() context := core.NewNullContext()
@@ -777,14 +801,14 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidAccountCurrency(t *tes
DefaultCurrency: "CNY", 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 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) 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 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) assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message)
} }
@@ -803,7 +827,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseNotSupportedCurrency(t *testi
"B": models.TRANSACTION_TYPE_MODIFY_BALANCE, "B": models.TRANSACTION_TYPE_MODIFY_BALANCE,
"T": models.TRANSACTION_TYPE_TRANSFER, "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) assert.Nil(t, err)
context := core.NewNullContext() context := core.NewNullContext()
@@ -813,12 +837,12 @@ func TestCustomTransactionDataDsvFileImporter_ParseNotSupportedCurrency(t *testi
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"2024-09-01 01:23:45,B,Test Account,XXX,123.45,,,"), 0, nil, nil, nil, nil, nil) "2024-09-01 01:23:45,B,Test Account,XXX,123.45,,,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message) 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,T,Test Account,USD,123.45,Test Account2,XXX,123.45"), 0, nil, nil, nil, nil, nil) "2024-09-01 01:23:45,T,Test Account,USD,123.45,Test Account2,XXX,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message) assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message)
} }
@@ -835,7 +859,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseValidAmount(t *testing.T) {
"E": models.TRANSACTION_TYPE_EXPENSE, "E": models.TRANSACTION_TYPE_EXPENSE,
"T": models.TRANSACTION_TYPE_TRANSFER, "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) assert.Nil(t, err)
context := core.NewNullContext() context := core.NewNullContext()
@@ -845,11 +869,11 @@ func TestCustomTransactionDataDsvFileImporter_ParseValidAmount(t *testing.T) {
DefaultCurrency: "CNY", 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 00:00:00,B,123.45000000,\n"+
"2024-09-01 01:23:45,I,0.12000000,\n"+ "2024-09-01 01:23:45,I,0.12000000,\n"+
"2024-09-01 12:34:56,E,1.00000000,\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) assert.Nil(t, err)
@@ -873,6 +897,62 @@ func TestCustomTransactionDataDsvFileImporter_ParseValidAmount(t *testing.T) {
assert.Equal(t, int64(35), allNewTransactions[3].RelatedAccountAmount) 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) { func TestCustomTransactionDataDsvFileImporter_ParseInvalidAmount(t *testing.T) {
columnIndexMapping := map[datatable.TransactionDataTableColumn]int{ columnIndexMapping := map[datatable.TransactionDataTableColumn]int{
datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME: 0, datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME: 0,
@@ -886,7 +966,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidAmount(t *testing.T) {
"E": models.TRANSACTION_TYPE_EXPENSE, "E": models.TRANSACTION_TYPE_EXPENSE,
"T": models.TRANSACTION_TYPE_TRANSFER, "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) assert.Nil(t, err)
context := core.NewNullContext() context := core.NewNullContext()
@@ -896,12 +976,12 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidAmount(t *testing.T) {
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,E,Test Account,123 45,,"), 0, nil, nil, nil, nil, nil) "2024-09-01 12:34:56,E,Test Account,123 45,,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAmountInvalid.Message) assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err = importer.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) "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) assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
} }
@@ -917,7 +997,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseNoAmount2(t *testing.T) {
"E": models.TRANSACTION_TYPE_EXPENSE, "E": models.TRANSACTION_TYPE_EXPENSE,
"T": models.TRANSACTION_TYPE_TRANSFER, "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) assert.Nil(t, err)
context := core.NewNullContext() context := core.NewNullContext()
@@ -927,15 +1007,15 @@ func TestCustomTransactionDataDsvFileImporter_ParseNoAmount2(t *testing.T) {
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,E,Test Account,123.45,"), 0, nil, nil, nil, nil, nil) "2024-09-01 12:34:56,E,Test Account,123.45,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, int64(12345), allNewTransactions[0].Amount) assert.Equal(t, int64(12345), allNewTransactions[0].Amount)
assert.Equal(t, int64(0), allNewTransactions[0].RelatedAccountAmount) assert.Equal(t, int64(0), allNewTransactions[0].RelatedAccountAmount)
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte( allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,T,Test Account,123.45,Test Account2"), 0, nil, nil, nil, nil, nil) "2024-09-01 12:34:56,T,Test Account,123.45,Test Account2"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, int64(12345), allNewTransactions[0].Amount) assert.Equal(t, int64(12345), allNewTransactions[0].Amount)
@@ -952,7 +1032,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseValidGeographicLocation(t *te
transactionTypeMapping := map[string]models.TransactionType{ transactionTypeMapping := map[string]models.TransactionType{
"E": models.TRANSACTION_TYPE_EXPENSE, "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) assert.Nil(t, err)
context := core.NewNullContext() context := core.NewNullContext()
@@ -962,8 +1042,8 @@ func TestCustomTransactionDataDsvFileImporter_ParseValidGeographicLocation(t *te
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,E,123.45,123.45;45.56"), 0, nil, nil, nil, nil, nil) "2024-09-01 12:34:56,E,123.45,123.45;45.56"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
@@ -981,7 +1061,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidGeographicLocation(t *
transactionTypeMapping := map[string]models.TransactionType{ transactionTypeMapping := map[string]models.TransactionType{
"E": models.TRANSACTION_TYPE_EXPENSE, "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) assert.Nil(t, err)
context := core.NewNullContext() context := core.NewNullContext()
@@ -991,15 +1071,15 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidGeographicLocation(t *
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,E,123.45,,,1"), 0, nil, nil, nil, nil, nil) "2024-09-01 12:34:56,E,123.45,,,1"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, float64(0), allNewTransactions[0].GeoLongitude) assert.Equal(t, float64(0), allNewTransactions[0].GeoLongitude)
assert.Equal(t, float64(0), allNewTransactions[0].GeoLatitude) assert.Equal(t, float64(0), allNewTransactions[0].GeoLatitude)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,E,123.45,a b"), 0, nil, nil, nil, nil, nil) "2024-09-01 12:34:56,E,123.45,a b"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrGeographicLocationInvalid.Message) assert.EqualError(t, err, errs.ErrGeographicLocationInvalid.Message)
} }
@@ -1013,7 +1093,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseTag(t *testing.T) {
transactionTypeMapping := map[string]models.TransactionType{ transactionTypeMapping := map[string]models.TransactionType{
"E": models.TRANSACTION_TYPE_EXPENSE, "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) assert.Nil(t, err)
context := core.NewNullContext() context := core.NewNullContext()
@@ -1023,8 +1103,8 @@ func TestCustomTransactionDataDsvFileImporter_ParseTag(t *testing.T) {
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
_, _, _, _, _, allNewTags, err := converter.ParseImportedData(context, user, []byte( _, _, _, _, _, allNewTags, err := importer.ParseImportedData(context, user, []byte(
"2024-09-01 00:00:00,E,123.45,foo;;bar.;#test;hello\tworld;;"), 0, nil, nil, nil, nil, nil) "2024-09-01 00:00:00,E,123.45,foo;;bar.;#test;hello\tworld;;"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err) assert.Nil(t, err)
@@ -1053,7 +1133,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseTagWithoutSeparator(t *testin
transactionTypeMapping := map[string]models.TransactionType{ transactionTypeMapping := map[string]models.TransactionType{
"E": models.TRANSACTION_TYPE_EXPENSE, "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) assert.Nil(t, err)
context := core.NewNullContext() context := core.NewNullContext()
@@ -1063,8 +1143,8 @@ func TestCustomTransactionDataDsvFileImporter_ParseTagWithoutSeparator(t *testin
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
_, _, _, _, _, allNewTags, err := converter.ParseImportedData(context, user, []byte( _, _, _, _, _, allNewTags, err := importer.ParseImportedData(context, user, []byte(
"2024-09-01 00:00:00,E,123.45,foo;;bar.;#test;hello\tworld;;"), 0, nil, nil, nil, nil, nil) "2024-09-01 00:00:00,E,123.45,foo;;bar.;#test;hello\tworld;;"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err) assert.Nil(t, err)
@@ -1084,7 +1164,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseDescription(t *testing.T) {
transactionTypeMapping := map[string]models.TransactionType{ transactionTypeMapping := map[string]models.TransactionType{
"T": models.TRANSACTION_TYPE_TRANSFER, "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) assert.Nil(t, err)
context := core.NewNullContext() context := core.NewNullContext()
@@ -1094,8 +1174,8 @@ func TestCustomTransactionDataDsvFileImporter_ParseDescription(t *testing.T) {
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,T,123.45,foo bar\t#test"), 0, nil, nil, nil, nil, nil) "2024-09-01 12:34:56,T,123.45,foo bar\t#test"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
@@ -0,0 +1,137 @@
package custom
import (
"strings"
"time"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter"
csvconverter "github.com/mayswind/ezbookkeeping/pkg/converters/csv"
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
"github.com/mayswind/ezbookkeeping/pkg/converters/excel"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/models"
)
const customOOXMLExcelFileType = "custom_xlsx"
const customMSCFBExcelFileType = "custom_xls"
// customTransactionDataExcelFileImporter defines the structure of custom excel importer for transaction data
type customTransactionDataExcelFileImporter struct {
fileType string
columnIndexMapping map[datatable.TransactionDataTableColumn]int
transactionTypeNameMapping map[string]models.TransactionType
hasHeaderLine bool
timeFormat string
timezoneFormat string
amountDecimalSeparator string
amountDigitGroupingSymbol string
geoLocationSeparator string
geoLocationOrder converter.TransactionGeoLocationOrder
transactionTagSeparator string
}
// ParseDataLines returns the parsed file lines for specified the excel file data
func (c *customTransactionDataExcelFileImporter) ParseDataLines(ctx core.Context, data []byte) ([][]string, error) {
var excelDataTable datatable.BasicDataTable
var err error
if c.fileType == customOOXMLExcelFileType {
excelDataTable, err = excel.CreateNewExcelOOXMLFileBasicDataTable(data, false)
} else if c.fileType == customMSCFBExcelFileType {
excelDataTable, err = excel.CreateNewExcelMSCFBFileBasicDataTable(data, false)
} else {
return nil, errs.ErrImportFileTypeNotSupported
}
if err != nil {
return nil, err
}
iterator := excelDataTable.DataRowIterator()
allLines := make([][]string, 0)
for iterator.HasNext() {
row := iterator.Next()
items := make([]string, row.ColumnCount())
for i := 0; i < row.ColumnCount(); i++ {
items[i] = strings.Trim(row.GetData(i), " ")
}
allLines = append(allLines, items)
}
return allLines, nil
}
// ParseImportedData returns the imported data by parsing the custom transaction dsv data
func (c *customTransactionDataExcelFileImporter) 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.ParseDataLines(ctx, data)
if err != nil {
return nil, nil, nil, nil, nil, nil, err
}
dataTable := csvconverter.CreateNewCustomCsvBasicDataTable(allLines, c.hasHeaderLine)
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, defaultTimezone, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
}
// IsCustomExcelFileType returns whether the file type is the custom excel file type
func IsCustomExcelFileType(fileType string) bool {
return fileType == customOOXMLExcelFileType || fileType == customMSCFBExcelFileType
}
// CreateNewCustomTransactionDataExcelFileParser returns a new custom transaction data parser
func CreateNewCustomTransactionDataExcelFileParser(fileType string) (CustomTransactionDataParser, error) {
if fileType != customOOXMLExcelFileType && fileType != customMSCFBExcelFileType {
return nil, errs.ErrImportFileTypeNotSupported
}
return &customTransactionDataExcelFileImporter{
fileType: fileType,
}, nil
}
// CreateNewCustomTransactionDataExcelFileImporter returns a new custom excel importer for transaction data
func CreateNewCustomTransactionDataExcelFileImporter(fileType string, columnIndexMapping map[datatable.TransactionDataTableColumn]int, transactionTypeNameMapping map[string]models.TransactionType, hasHeaderLine bool, timeFormat string, timezoneFormat string, amountDecimalSeparator string, amountDigitGroupingSymbol string, geoLocationSeparator string, geoLocationOrder string, transactionTagSeparator string) (converter.TransactionDataImporter, error) {
if fileType != customOOXMLExcelFileType && fileType != customMSCFBExcelFileType {
return nil, errs.ErrImportFileTypeNotSupported
}
if geoLocationOrder == "" {
geoLocationOrder = string(converter.TRANSACTION_GEO_LOCATION_ORDER_LONGITUDE_LATITUDE)
} else if geoLocationOrder != string(converter.TRANSACTION_GEO_LOCATION_ORDER_LONGITUDE_LATITUDE) &&
geoLocationOrder != string(converter.TRANSACTION_GEO_LOCATION_ORDER_LATITUDE_LONGITUDE) {
return nil, errs.ErrImportFileTypeNotSupported
}
if _, exists := columnIndexMapping[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME]; !exists {
return nil, errs.ErrMissingRequiredFieldInHeaderRow
}
if _, exists := columnIndexMapping[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE]; !exists {
return nil, errs.ErrMissingRequiredFieldInHeaderRow
}
if _, exists := columnIndexMapping[datatable.TRANSACTION_DATA_TABLE_AMOUNT]; !exists {
return nil, errs.ErrMissingRequiredFieldInHeaderRow
}
return &customTransactionDataExcelFileImporter{
fileType: fileType,
columnIndexMapping: columnIndexMapping,
transactionTypeNameMapping: transactionTypeNameMapping,
hasHeaderLine: hasHeaderLine,
timeFormat: timeFormat,
timezoneFormat: timezoneFormat,
amountDecimalSeparator: amountDecimalSeparator,
amountDigitGroupingSymbol: amountDigitGroupingSymbol,
geoLocationSeparator: geoLocationSeparator,
geoLocationOrder: converter.TransactionGeoLocationOrder(geoLocationOrder),
transactionTagSeparator: transactionTagSeparator,
}, nil
}
@@ -0,0 +1,254 @@
package custom
import (
"os"
"testing"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/stretchr/testify/assert"
)
func TestIsCustomExcelFileType(t *testing.T) {
assert.True(t, IsCustomExcelFileType("custom_xlsx"))
assert.True(t, IsCustomExcelFileType("custom_xls"))
assert.False(t, IsCustomExcelFileType("xlsx"))
assert.False(t, IsCustomExcelFileType("xls"))
assert.False(t, IsCustomExcelFileType("excel"))
}
func TestCustomTransactionDataParser_ParseOOXMLExcelDataLines_EmptyData(t *testing.T) {
importer, err := CreateNewCustomTransactionDataExcelFileParser("custom_xlsx")
assert.Nil(t, err)
context := core.NewNullContext()
testdata, err := os.ReadFile("../../../testdata/empty_excel_file.xlsx")
assert.Nil(t, err)
allLines, err := importer.ParseDataLines(context, testdata)
assert.Nil(t, err)
assert.Equal(t, 0, len(allLines))
}
func TestCustomTransactionDataParser_ParseOOXMLExcelDataLines_SingleSheet(t *testing.T) {
importer, err := CreateNewCustomTransactionDataExcelFileParser("custom_xlsx")
assert.Nil(t, err)
context := core.NewNullContext()
testdata, err := os.ReadFile("../../../testdata/simple_excel_file.xlsx")
assert.Nil(t, err)
allLines, err := importer.ParseDataLines(context, testdata)
assert.Nil(t, err)
assert.Equal(t, 3, len(allLines))
assert.Equal(t, 3, len(allLines[0]))
assert.Equal(t, "A1", allLines[0][0])
assert.Equal(t, "B1", allLines[0][1])
assert.Equal(t, "C1", allLines[0][2])
assert.Equal(t, 3, len(allLines[1]))
assert.Equal(t, "A2", allLines[1][0])
assert.Equal(t, "B2", allLines[1][1])
assert.Equal(t, "C2", allLines[1][2])
assert.Equal(t, 3, len(allLines[2]))
assert.Equal(t, "A3", allLines[2][0])
assert.Equal(t, "B3", allLines[2][1])
assert.Equal(t, "C3", allLines[2][2])
}
func TestCustomTransactionDataParser_ParseOOXMLExcelDataLines_MultipleSheet(t *testing.T) {
importer, err := CreateNewCustomTransactionDataExcelFileParser("custom_xlsx")
assert.Nil(t, err)
context := core.NewNullContext()
testdata, err := os.ReadFile("../../../testdata/multiple_sheets_excel_file.xlsx")
assert.Nil(t, err)
allLines, err := importer.ParseDataLines(context, testdata)
assert.Nil(t, err)
assert.Equal(t, 9, len(allLines))
assert.Equal(t, 3, len(allLines[0]))
assert.Equal(t, "A1", allLines[0][0])
assert.Equal(t, "B1", allLines[0][1])
assert.Equal(t, "C1", allLines[0][2])
assert.Equal(t, 3, len(allLines[1]))
assert.Equal(t, "1-A2", allLines[1][0])
assert.Equal(t, "1-B2", allLines[1][1])
assert.Equal(t, "1-C2", allLines[1][2])
assert.Equal(t, 3, len(allLines[2]))
assert.Equal(t, "1-A3", allLines[2][0])
assert.Equal(t, "1-B3", allLines[2][1])
assert.Equal(t, "1-C3", allLines[2][2])
assert.Equal(t, 3, len(allLines[3]))
assert.Equal(t, "A1", allLines[3][0])
assert.Equal(t, "B1", allLines[3][1])
assert.Equal(t, "C1", allLines[3][2])
assert.Equal(t, 2, len(allLines[4]))
assert.Equal(t, "3-A2", allLines[4][0])
assert.Equal(t, "3-B2", allLines[4][1])
assert.Equal(t, 3, len(allLines[5]))
assert.Equal(t, "A1", allLines[5][0])
assert.Equal(t, "B1", allLines[5][1])
assert.Equal(t, "C1", allLines[5][2])
assert.Equal(t, 3, len(allLines[6]))
assert.Equal(t, "A1", allLines[6][0])
assert.Equal(t, "B1", allLines[6][1])
assert.Equal(t, "C1", allLines[6][2])
assert.Equal(t, 3, len(allLines[7]))
assert.Equal(t, "5-A2", allLines[7][0])
assert.Equal(t, "5-B2", allLines[7][1])
assert.Equal(t, "5-C2", allLines[7][2])
assert.Equal(t, 3, len(allLines[8]))
assert.Equal(t, "5-A3", allLines[8][0])
assert.Equal(t, "5-B3", allLines[8][1])
assert.Equal(t, "5-C3", allLines[8][2])
}
func TestCustomTransactionDataParser_ParseOOXMLExcelDataLines_MultipleSheetWithDifferentColumnCount(t *testing.T) {
importer, err := CreateNewCustomTransactionDataExcelFileParser("custom_xlsx")
assert.Nil(t, err)
context := core.NewNullContext()
testdata, err := os.ReadFile("../../../testdata/multiple_sheets_with_different_header_row_excel_file.xlsx")
assert.Nil(t, err)
_, err = importer.ParseDataLines(context, testdata)
assert.EqualError(t, err, errs.ErrFieldsInMultiTableAreDifferent.Message)
}
func TestCustomTransactionDataParser_ParseMSCFBExcelDataLines_EmptyData(t *testing.T) {
importer, err := CreateNewCustomTransactionDataExcelFileParser("custom_xls")
assert.Nil(t, err)
context := core.NewNullContext()
testdata, err := os.ReadFile("../../../testdata/empty_excel_file.xls")
assert.Nil(t, err)
allLines, err := importer.ParseDataLines(context, testdata)
assert.Nil(t, err)
assert.Equal(t, 0, len(allLines))
}
func TestCustomTransactionDataParser_ParseMSCFBExcelDataLines_SingleSheet(t *testing.T) {
importer, err := CreateNewCustomTransactionDataExcelFileParser("custom_xls")
assert.Nil(t, err)
context := core.NewNullContext()
testdata, err := os.ReadFile("../../../testdata/simple_excel_file.xls")
assert.Nil(t, err)
allLines, err := importer.ParseDataLines(context, testdata)
assert.Nil(t, err)
assert.Equal(t, 3, len(allLines))
assert.Equal(t, 3, len(allLines[0]))
assert.Equal(t, "A1", allLines[0][0])
assert.Equal(t, "B1", allLines[0][1])
assert.Equal(t, "C1", allLines[0][2])
assert.Equal(t, 3, len(allLines[1]))
assert.Equal(t, "A2", allLines[1][0])
assert.Equal(t, "B2", allLines[1][1])
assert.Equal(t, "C2", allLines[1][2])
assert.Equal(t, 3, len(allLines[2]))
assert.Equal(t, "A3", allLines[2][0])
assert.Equal(t, "B3", allLines[2][1])
assert.Equal(t, "C3", allLines[2][2])
}
func TestCustomTransactionDataParser_ParseMSCFBExcelDataLines_MultipleSheet(t *testing.T) {
importer, err := CreateNewCustomTransactionDataExcelFileParser("custom_xls")
assert.Nil(t, err)
context := core.NewNullContext()
testdata, err := os.ReadFile("../../../testdata/multiple_sheets_excel_file.xls")
assert.Nil(t, err)
allLines, err := importer.ParseDataLines(context, testdata)
assert.Nil(t, err)
assert.Equal(t, 9, len(allLines))
assert.Equal(t, 3, len(allLines[0]))
assert.Equal(t, "A1", allLines[0][0])
assert.Equal(t, "B1", allLines[0][1])
assert.Equal(t, "C1", allLines[0][2])
assert.Equal(t, 3, len(allLines[1]))
assert.Equal(t, "1-A2", allLines[1][0])
assert.Equal(t, "1-B2", allLines[1][1])
assert.Equal(t, "1-C2", allLines[1][2])
assert.Equal(t, 3, len(allLines[2]))
assert.Equal(t, "1-A3", allLines[2][0])
assert.Equal(t, "1-B3", allLines[2][1])
assert.Equal(t, "1-C3", allLines[2][2])
assert.Equal(t, 3, len(allLines[3]))
assert.Equal(t, "A1", allLines[3][0])
assert.Equal(t, "B1", allLines[3][1])
assert.Equal(t, "C1", allLines[3][2])
assert.Equal(t, 3, len(allLines[4]))
assert.Equal(t, "3-A2", allLines[4][0])
assert.Equal(t, "3-B2", allLines[4][1])
assert.Equal(t, "", allLines[4][2])
assert.Equal(t, 3, len(allLines[5]))
assert.Equal(t, "A1", allLines[5][0])
assert.Equal(t, "B1", allLines[5][1])
assert.Equal(t, "C1", allLines[5][2])
assert.Equal(t, 3, len(allLines[6]))
assert.Equal(t, "A1", allLines[6][0])
assert.Equal(t, "B1", allLines[6][1])
assert.Equal(t, "C1", allLines[6][2])
assert.Equal(t, 3, len(allLines[7]))
assert.Equal(t, "5-A2", allLines[7][0])
assert.Equal(t, "5-B2", allLines[7][1])
assert.Equal(t, "5-C2", allLines[7][2])
assert.Equal(t, 3, len(allLines[8]))
assert.Equal(t, "5-A3", allLines[8][0])
assert.Equal(t, "5-B3", allLines[8][1])
assert.Equal(t, "5-C3", allLines[8][2])
}
func TestCustomTransactionDataParser_ParseMSCFBExcelDataLines_MultipleSheetWithDifferentColumnCount(t *testing.T) {
importer, err := CreateNewCustomTransactionDataExcelFileParser("custom_xls")
assert.Nil(t, err)
context := core.NewNullContext()
testdata, err := os.ReadFile("../../../testdata/multiple_sheets_with_different_header_row_excel_file.xls")
assert.Nil(t, err)
_, err = importer.ParseDataLines(context, testdata)
assert.EqualError(t, err, errs.ErrFieldsInMultiTableAreDifferent.Message)
}
@@ -1,4 +1,4 @@
package dsv package custom
import ( import (
"strings" "strings"
@@ -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) { 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)) rowData := make(map[datatable.TransactionDataTableColumn]string, len(t.transactionDataTable.columnIndexMapping))
var transactionTime *time.Time = nil
for column, columnIndex := range t.transactionDataTable.columnIndexMapping { for column, columnIndex := range t.transactionDataTable.columnIndexMapping {
if columnIndex < 0 || columnIndex >= row.ColumnCount() { if columnIndex < 0 || columnIndex >= row.ColumnCount() {
@@ -144,10 +145,11 @@ func (t *customPlainTextDataRowIterator) parseTransaction(ctx core.Context, user
return nil, false, errs.ErrTransactionTimeInvalid return nil, false, errs.ErrTransactionTimeInvalid
} }
transactionTime = &dateTime
rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME] = utils.FormatUnixTimeToLongDateTime(dateTime.Unix(), dateTime.Location()) rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME] = utils.FormatUnixTimeToLongDateTime(dateTime.Unix(), dateTime.Location())
if t.transactionDataTable.timeFormatIncludeTimezone { 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:] timezone = timezone[:3] + ":" + timezone[3:]
rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE] = timezone 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 { } else {
return nil, false, errs.ErrImportFileTransactionTimezoneFormatInvalid 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) { func (t *customPlainTextDataRowIterator) parseAmount(ctx core.Context, amountValue string) (string, error) {
if t.transactionDataTable.amountDigitGroupingSymbol != "" { if t.transactionDataTable.amountDigitGroupingSymbol != "" {
amountValue = strings.ReplaceAll(amountValue, 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 != "." { if t.transactionDataTable.amountDecimalSeparator != "" && t.transactionDataTable.amountDecimalSeparator != "." {
@@ -72,6 +72,10 @@ const (
TRANSACTION_DATA_TABLE_GEOGRAPHIC_LOCATION TransactionDataTableColumn = 12 TRANSACTION_DATA_TABLE_GEOGRAPHIC_LOCATION TransactionDataTableColumn = 12
TRANSACTION_DATA_TABLE_TAGS TransactionDataTableColumn = 13 TRANSACTION_DATA_TABLE_TAGS TransactionDataTableColumn = 13
TRANSACTION_DATA_TABLE_DESCRIPTION TransactionDataTableColumn = 14 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 // 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 // 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 var importRequest models.ImportTransactionRequest
if err := json.Unmarshal(data, &importRequest); err != nil { if err := json.Unmarshal(data, &importRequest); err != nil {
@@ -55,7 +55,7 @@ func (c *defaultTransactionDataJsonImporter) ParseImportedData(ctx core.Context,
ezbookkeepingTagSeparator, 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) { 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) timezone := time.FixedZone("Transaction Timezone", utcOffset*60)
timezoneOffset := utils.FormatTimezoneOffset(time.Now().Unix(), timezone)
row := make(map[datatable.TransactionDataTableColumn]string, len(allJsonDataSupportedColumns)) row := make(map[datatable.TransactionDataTableColumn]string, len(allJsonDataSupportedColumns))
row[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME] = transaction.Time 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_TRANSACTION_TYPE] = transaction.Type
row[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY] = transaction.CategoryName row[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY] = transaction.CategoryName
row[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = transaction.SourceAccountName row[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = transaction.SourceAccountName
@@ -1,6 +1,8 @@
package _default package _default
import ( import (
"time"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter" "github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable" "github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
"github.com/mayswind/ezbookkeeping/pkg/core" "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 // 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( dataTable, err := createNewDefaultPlainTextDataTable(
string(data), string(data),
c.columnSeparator, c.columnSeparator,
@@ -104,5 +106,5 @@ func (c *defaultTransactionDataPlainTextConverter) ParseImportedData(ctx core.Co
ezbookkeepingTagSeparator, 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 ( import (
"testing" "testing"
"time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/core" "github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs" "github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/models" "github.com/mayswind/ezbookkeeping/pkg/models"
@@ -12,7 +14,7 @@ import (
) )
func TestDefaultTransactionDataCSVFileConverterToExportedContent(t *testing.T) { func TestDefaultTransactionDataCSVFileConverterToExportedContent(t *testing.T) {
converter := DefaultTransactionDataCSVFileConverter exporter := DefaultTransactionDataCSVFileConverter
context := core.NewNullContext() context := core.NewNullContext()
transactions := make([]*models.Transaction, 3) 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,+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,+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" "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.Nil(t, err)
assert.Equal(t, expectedContent, string(actualContent)) assert.Equal(t, expectedContent, string(actualContent))
} }
func TestDefaultTransactionDataCSVFileConverterParseImportedData_MinimumValidData(t *testing.T) { func TestDefaultTransactionDataCSVFileConverterParseImportedData_MinimumValidData(t *testing.T) {
converter := DefaultTransactionDataCSVFileConverter importer := DefaultTransactionDataCSVFileConverter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -134,11 +136,11 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_MinimumValidDat
DefaultCurrency: "CNY", 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 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 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 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) assert.Nil(t, err)
@@ -197,7 +199,7 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_MinimumValidDat
} }
func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseInvalidTime(t *testing.T) { func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseInvalidTime(t *testing.T) {
converter := DefaultTransactionDataCSVFileConverter importer := DefaultTransactionDataCSVFileConverter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -205,17 +207,17 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseInvalidTim
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+ _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
"2024-09-01T12:34:56,Expense,Test Category,Test Account,123.45,,"), 0, nil, nil, nil, nil, nil) "2024-09-01T12:34:56,Expense,Test Category,Test Account,123.45,,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message) assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+ _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
"09/01/2024 12:34:56,Expense,Test Category,Test Account,123.45,,"), 0, nil, nil, nil, nil, nil) "09/01/2024 12:34:56,Expense,Test Category,Test Account,123.45,,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message) assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
} }
func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseInvalidType(t *testing.T) { func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseInvalidType(t *testing.T) {
converter := DefaultTransactionDataCSVFileConverter importer := DefaultTransactionDataCSVFileConverter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -223,13 +225,13 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseInvalidTyp
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+ _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
"2024-09-01 12:34:56,Type,Test Category,Test Account,123.45,,"), 0, nil, nil, nil, nil, nil) "2024-09-01 12:34:56,Type,Test Category,Test Account,123.45,,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrTransactionTypeInvalid.Message) assert.EqualError(t, err, errs.ErrTransactionTypeInvalid.Message)
} }
func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseValidTimezone(t *testing.T) { func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseValidTimezone(t *testing.T) {
converter := DefaultTransactionDataCSVFileConverter importer := DefaultTransactionDataCSVFileConverter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -237,27 +239,27 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseValidTimez
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("Time,Timezone,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+ allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("Time,Timezone,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
"2024-09-01 12:34:56,-10:00,Expense,Test Category,Test Account,123.45,,"), 0, nil, nil, nil, nil, nil) "2024-09-01 12:34:56,-10:00,Expense,Test Category,Test Account,123.45,,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, int64(1725230096), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime)) 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"+ allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("Time,Timezone,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
"2024-09-01 12:34:56,+00:00,Expense,Test Category,Test Account,123.45,,"), 0, nil, nil, nil, nil, nil) "2024-09-01 12:34:56,+00:00,Expense,Test Category,Test Account,123.45,,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, int64(1725194096), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime)) 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"+ allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("Time,Timezone,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
"2024-09-01 12:34:56,+12:45,Expense,Test Category,Test Account,123.45,,"), 0, nil, nil, nil, nil, nil) "2024-09-01 12:34:56,+12:45,Expense,Test Category,Test Account,123.45,,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, int64(1725148196), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime)) assert.Equal(t, int64(1725148196), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
} }
func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseInvalidTimezone(t *testing.T) { func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseInvalidTimezone(t *testing.T) {
converter := DefaultTransactionDataCSVFileConverter importer := DefaultTransactionDataCSVFileConverter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -265,13 +267,13 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseInvalidTim
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("Time,Timezone,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+ _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("Time,Timezone,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
"2024-09-01 12:34:56,Asia/Shanghai,Expense,Test Category,Test Account,123.45,,"), 0, nil, nil, nil, nil, nil) "2024-09-01 12:34:56,Asia/Shanghai,Expense,Test Category,Test Account,123.45,,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrTransactionTimeZoneInvalid.Message) assert.EqualError(t, err, errs.ErrTransactionTimeZoneInvalid.Message)
} }
func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseValidAccountCurrency(t *testing.T) { func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseValidAccountCurrency(t *testing.T) {
converter := DefaultTransactionDataCSVFileConverter importer := DefaultTransactionDataCSVFileConverter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -279,9 +281,9 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseValidAccou
DefaultCurrency: "CNY", 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 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) assert.Nil(t, err)
@@ -298,7 +300,7 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseValidAccou
} }
func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseInvalidAccountCurrency(t *testing.T) { func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseInvalidAccountCurrency(t *testing.T) {
converter := DefaultTransactionDataCSVFileConverter importer := DefaultTransactionDataCSVFileConverter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -306,19 +308,19 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseInvalidAcc
DefaultCurrency: "CNY", 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 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) 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 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) assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message)
} }
func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseNotSupportedCurrency(t *testing.T) { func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseNotSupportedCurrency(t *testing.T) {
converter := DefaultTransactionDataCSVFileConverter importer := DefaultTransactionDataCSVFileConverter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -326,17 +328,17 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseNotSupport
DefaultCurrency: "CNY", 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,XXX,123.45,,,"), 0, nil, nil, nil, nil, nil) "2024-09-01 01:23:45,Balance Modification,,Test Account,XXX,123.45,,,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message) 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,Transfer,Test Category,Test Account,USD,123.45,Test Account2,XXX,123.45"), 0, nil, nil, nil, nil, nil) "2024-09-01 01:23:45,Transfer,Test Category,Test Account,USD,123.45,Test Account2,XXX,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message) assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message)
} }
func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseInvalidAmount(t *testing.T) { func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseInvalidAmount(t *testing.T) {
converter := DefaultTransactionDataCSVFileConverter importer := DefaultTransactionDataCSVFileConverter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -344,17 +346,17 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseInvalidAmo
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+ _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
"2024-09-01 12:34:56,Expense,Test Category,Test Account,123 45,,"), 0, nil, nil, nil, nil, nil) "2024-09-01 12:34:56,Expense,Test Category,Test Account,123 45,,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAmountInvalid.Message) assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+ _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
"2024-09-01 12:34:56,Transfer,Test Category,Test Account,123.45,Test Account2,123 45"), 0, nil, nil, nil, nil, nil) "2024-09-01 12:34:56,Transfer,Test Category,Test Account,123.45,Test Account2,123 45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAmountInvalid.Message) assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
} }
func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseNoAmount2(t *testing.T) { func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseNoAmount2(t *testing.T) {
converter := DefaultTransactionDataCSVFileConverter importer := DefaultTransactionDataCSVFileConverter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -362,15 +364,15 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseNoAmount2(
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2\n"+ allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2\n"+
"2024-09-01 12:34:56,Expense,Test Category,Test Account,123.45,"), 0, nil, nil, nil, nil, nil) "2024-09-01 12:34:56,Expense,Test Category,Test Account,123.45,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, int64(12345), allNewTransactions[0].Amount) assert.Equal(t, int64(12345), allNewTransactions[0].Amount)
assert.Equal(t, int64(0), allNewTransactions[0].RelatedAccountAmount) assert.Equal(t, int64(0), allNewTransactions[0].RelatedAccountAmount)
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2\n"+ allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2\n"+
"2024-09-01 12:34:56,Transfer,Test Category,Test Account,123.45,Test Account2"), 0, nil, nil, nil, nil, nil) "2024-09-01 12:34:56,Transfer,Test Category,Test Account,123.45,Test Account2"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, int64(12345), allNewTransactions[0].Amount) assert.Equal(t, int64(12345), allNewTransactions[0].Amount)
@@ -378,7 +380,7 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseNoAmount2(
} }
func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseValidGeographicLocation(t *testing.T) { func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseValidGeographicLocation(t *testing.T) {
converter := DefaultTransactionDataCSVFileConverter importer := DefaultTransactionDataCSVFileConverter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -386,8 +388,8 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseValidGeogr
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount,Geographic Location\n"+ allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount,Geographic Location\n"+
"2024-09-01 12:34:56,Expense,Test Category,Test Account,123.45,,,123.45 45.56"), 0, nil, nil, nil, nil, nil) "2024-09-01 12:34:56,Expense,Test Category,Test Account,123.45,,,123.45 45.56"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
@@ -396,7 +398,7 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseValidGeogr
} }
func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseInvalidGeographicLocation(t *testing.T) { func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseInvalidGeographicLocation(t *testing.T) {
converter := DefaultTransactionDataCSVFileConverter importer := DefaultTransactionDataCSVFileConverter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -404,24 +406,24 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseInvalidGeo
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount,Geographic Location\n"+ allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount,Geographic Location\n"+
"2024-09-01 12:34:56,Expense,Test Category,Test Account,123.45,,,1"), 0, nil, nil, nil, nil, nil) "2024-09-01 12:34:56,Expense,Test Category,Test Account,123.45,,,1"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, float64(0), allNewTransactions[0].GeoLongitude) assert.Equal(t, float64(0), allNewTransactions[0].GeoLongitude)
assert.Equal(t, float64(0), allNewTransactions[0].GeoLatitude) 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"+ _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount,Geographic Location\n"+
"2024-09-01 12:34:56,Expense,Test Category,Test Account,123.45,,,a b"), 0, nil, nil, nil, nil, nil) "2024-09-01 12:34:56,Expense,Test Category,Test Account,123.45,,,a b"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrGeographicLocationInvalid.Message) 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"+ _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount,Geographic Location\n"+
"2024-09-01 12:34:56,Expense,Test Category,Test Account,123.45,,,1 "), 0, nil, nil, nil, nil, nil) "2024-09-01 12:34:56,Expense,Test Category,Test Account,123.45,,,1 "), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrGeographicLocationInvalid.Message) assert.EqualError(t, err, errs.ErrGeographicLocationInvalid.Message)
} }
func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseTag(t *testing.T) { func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseTag(t *testing.T) {
converter := DefaultTransactionDataCSVFileConverter importer := DefaultTransactionDataCSVFileConverter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -429,8 +431,8 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseTag(t *tes
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
_, _, _, _, _, allNewTags, err := converter.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount,Tags\n"+ _, _, _, _, _, allNewTags, err := importer.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount,Tags\n"+
"2024-09-01 00:00:00,Balance Modification,,Test Account,123.45,,,foo;;bar.;#test;hello\tworld;;"), 0, nil, nil, nil, nil, nil) "2024-09-01 00:00:00,Balance Modification,,Test Account,123.45,,,foo;;bar.;#test;hello\tworld;;"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err) assert.Nil(t, err)
@@ -450,7 +452,7 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseTag(t *tes
} }
func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseDescription(t *testing.T) { func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseDescription(t *testing.T) {
converter := DefaultTransactionDataCSVFileConverter importer := DefaultTransactionDataCSVFileConverter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -458,8 +460,8 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseDescriptio
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount,Description\n"+ allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount,Description\n"+
"2024-09-01 12:34:56,Expense,Test Category,Test Account,123.45,,,foo bar\t#test"), 0, nil, nil, nil, nil, nil) "2024-09-01 12:34:56,Expense,Test Category,Test Account,123.45,,,foo bar\t#test"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
@@ -467,7 +469,7 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseDescriptio
} }
func TestDefaultTransactionDataCSVFileConverterParseImportedData_MissingFileHeader(t *testing.T) { func TestDefaultTransactionDataCSVFileConverterParseImportedData_MissingFileHeader(t *testing.T) {
converter := DefaultTransactionDataCSVFileConverter importer := DefaultTransactionDataCSVFileConverter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -475,12 +477,12 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_MissingFileHead
DefaultCurrency: "CNY", 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) assert.EqualError(t, err, errs.ErrNotFoundTransactionDataInFile.Message)
} }
func TestDefaultTransactionDataCSVFileConverterParseImportedData_MissingRequiredColumn(t *testing.T) { func TestDefaultTransactionDataCSVFileConverterParseImportedData_MissingRequiredColumn(t *testing.T) {
converter := DefaultTransactionDataCSVFileConverter importer := DefaultTransactionDataCSVFileConverter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -489,32 +491,32 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_MissingRequired
} }
// Missing Time Column // 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"+ _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("Timezone,Type,Category,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount,Geographic Location,Tags,Description\n"+
"+08:00,Balance Modification,,Test Sub Category,Test Account,CNY,123.45,,,,,,"), 0, nil, nil, nil, nil, nil) "+08:00,Balance Modification,,Test Sub Category,Test Account,CNY,123.45,,,,,,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
// Missing Type Column // 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"+ _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("Time,Category,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount,Geographic Location,Tags,Description\n"+
"2024-09-01 00:00:00,+08:00,Test Category,Test Sub Category,Test Account,CNY,123.45,,,,,,"), 0, nil, nil, nil, nil, nil) "2024-09-01 00:00:00,+08:00,Test Category,Test Sub Category,Test Account,CNY,123.45,,,,,,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
// Missing Sub Category Column // 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"+ _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("Time,Type,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount,Geographic Location,Tags,Description\n"+
"2024-09-01 00:00:00,+08:00,Balance Modification,Test Account,CNY,123.45,,,,,,"), 0, nil, nil, nil, nil, nil) "2024-09-01 00:00:00,+08:00,Balance Modification,Test Account,CNY,123.45,,,,,,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
// Missing Account Name Column // 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"+ _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("Time,Timezone,Type,Category,Sub Category,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount,Geographic Location,Tags,Description\n"+
"2024-09-01 00:00:00,+08:00,Balance Modification,,Test Sub Category,CNY,123.45,,,,,,"), 0, nil, nil, nil, nil, nil) "2024-09-01 00:00:00,+08:00,Balance Modification,,Test Sub Category,CNY,123.45,,,,,,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
// Missing Amount Column // 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"+ _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("Time,Timezone,Type,Category,Sub Category,Account,Account Currency,Account2,Account2 Currency,Account2 Amount,Geographic Location,Tags,Description\n"+
"2024-09-01 00:00:00,+08:00,Balance Modification,,Test Sub Category,Test Account,CNY,,,,,,"), 0, nil, nil, nil, nil, nil) "2024-09-01 00:00:00,+08:00,Balance Modification,,Test Sub Category,Test Account,CNY,,,,,,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
// Missing Account2 Name Column // 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"+ _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("Time,Timezone,Type,Category,Sub Category,Account,Account Currency,Amount,Account2 Currency,Account2 Amount,Geographic Location,Tags,Description\n"+
"2024-09-01 00:00:00,+08:00,Balance Modification,,Test Sub Category,Test Account,CNY,123.45,,,,,"), 0, nil, nil, nil, nil, nil) "2024-09-01 00:00:00,+08:00,Balance Modification,,Test Sub Category,Test Account,CNY,123.45,,,,,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
} }
@@ -86,7 +86,7 @@ func (t *ExcelMSCFBFileBasicDataTable) DataRowIterator() datatable.BasicDataTabl
// ColumnCount returns the total count of column in this data row // ColumnCount returns the total count of column in this data row
func (r *ExcelMSCFBFileBasicDataTableRow) ColumnCount() int { func (r *ExcelMSCFBFileBasicDataTableRow) ColumnCount() int {
row := r.sheet.Row(r.rowIndex) row := r.sheet.Row(r.rowIndex)
return row.LastCol() + 1 return row.LastCol()
} }
// GetData returns the data in the specified column index // GetData returns the data in the specified column index
@@ -195,7 +195,10 @@ func CreateNewExcelMSCFBFileBasicDataTable(data []byte, hasTitleLine bool) (data
} }
if i == 0 { if i == 0 {
for j := 0; j <= row.LastCol(); j++ { // row.LastCol() returns "colMac" in the "Row" struct, that is an unsigned integer that specifies the one-based index of the last column.
// But row.FirstCol() returns "colMic" in the "Row" struct, that is an unsigned integer that specifies the zero-based index of the first column.
// Reference: https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-xls/4aab09eb-49ed-4d01-a3b1-1d726247d3c2
for j := 0; j < row.LastCol(); j++ {
headerItem := row.Col(j) headerItem := row.Col(j)
if headerItem == "" { if headerItem == "" {
@@ -205,7 +208,7 @@ func CreateNewExcelMSCFBFileBasicDataTable(data []byte, hasTitleLine bool) (data
firstRowItems = append(firstRowItems, headerItem) firstRowItems = append(firstRowItems, headerItem)
} }
} else { } else {
for j := 0; j <= min(row.LastCol(), len(firstRowItems)-1); j++ { for j := 0; j < min(row.LastCol(), len(firstRowItems)); j++ {
headerItem := row.Col(j) headerItem := row.Col(j)
if headerItem != firstRowItems[j] { if headerItem != firstRowItems[j] {
@@ -300,10 +300,10 @@ func TestExcelMSCFBFileBasicDataRowColumnCount(t *testing.T) {
iterator := datatable.DataRowIterator() iterator := datatable.DataRowIterator()
row1 := iterator.Next() row1 := iterator.Next()
assert.EqualValues(t, 4, row1.ColumnCount()) assert.EqualValues(t, 3, row1.ColumnCount())
row2 := iterator.Next() row2 := iterator.Next()
assert.EqualValues(t, 4, row2.ColumnCount()) assert.EqualValues(t, 3, row2.ColumnCount())
} }
func TestExcelMSCFBFileBasicDataRowGetData(t *testing.T) { func TestExcelMSCFBFileBasicDataRowGetData(t *testing.T) {
@@ -3,6 +3,7 @@ package feidee
import ( import (
"bytes" "bytes"
"strings" "strings"
"time"
"golang.org/x/text/encoding/unicode" "golang.org/x/text/encoding/unicode"
"golang.org/x/text/transform" "golang.org/x/text/transform"
@@ -27,6 +28,9 @@ const feideeMymoneyAppTransactionAccountCurrencyColumnName = "账户币种"
const feideeMymoneyAppTransactionAmountColumnName = "金额" const feideeMymoneyAppTransactionAmountColumnName = "金额"
const feideeMymoneyAppTransactionDescriptionColumnName = "备注" const feideeMymoneyAppTransactionDescriptionColumnName = "备注"
const feideeMymoneyAppTransactionRelatedIdColumnName = "关联Id" const feideeMymoneyAppTransactionRelatedIdColumnName = "关联Id"
const feideeMymoneyAppTransactionMemberColumnName = "成员"
const feideeMymoneyAppTransactionProjectColumnName = "项目"
const feideeMymoneyAppTransactionMerchantColumnName = "商家"
const feideeMymoneyAppTransactionTypeModifyBalanceText = "余额变更" const feideeMymoneyAppTransactionTypeModifyBalanceText = "余额变更"
const feideeMymoneyAppTransactionTypeModifyOutstandingBalanceText = "负债变更" const feideeMymoneyAppTransactionTypeModifyOutstandingBalanceText = "负债变更"
@@ -44,6 +48,9 @@ var feideeMymoneyAppDataColumnNameMapping = map[datatable.TransactionDataTableCo
datatable.TRANSACTION_DATA_TABLE_ACCOUNT_CURRENCY: feideeMymoneyAppTransactionAccountCurrencyColumnName, datatable.TRANSACTION_DATA_TABLE_ACCOUNT_CURRENCY: feideeMymoneyAppTransactionAccountCurrencyColumnName,
datatable.TRANSACTION_DATA_TABLE_AMOUNT: feideeMymoneyAppTransactionAmountColumnName, datatable.TRANSACTION_DATA_TABLE_AMOUNT: feideeMymoneyAppTransactionAmountColumnName,
datatable.TRANSACTION_DATA_TABLE_DESCRIPTION: feideeMymoneyAppTransactionDescriptionColumnName, 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 // 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 // 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() fallback := unicode.UTF8.NewDecoder()
reader := transform.NewReader(bytes.NewReader(data), unicode.BOMOverride(fallback)) reader := transform.NewReader(bytes.NewReader(data), unicode.BOMOverride(fallback))
@@ -91,7 +98,7 @@ func (c *feideeMymoneyAppTransactionDataCsvFileImporter) ParseImportedData(ctx c
dataTableImporter := converter.CreateNewSimpleImporterWithTypeNameMapping(feideeMymoneyTransactionTypeNameMapping) 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) { 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) 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() transactionRowParser := createFeideeMymoneyTransactionDataRowParser()
transactionDataTable := datatable.CreateNewWritableTransactionDataTableWithRowParser(newColumns, transactionRowParser) transactionDataTable := datatable.CreateNewWritableTransactionDataTableWithRowParser(newColumns, transactionRowParser)
transferTransactionsMap := make(map[string]map[datatable.TransactionDataTableColumn]string, 0) transferTransactionsMap := make(map[string]map[datatable.TransactionDataTableColumn]string, 0)
@@ -6,6 +6,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/core" "github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs" "github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/models" "github.com/mayswind/ezbookkeeping/pkg/models"
@@ -13,7 +14,7 @@ import (
) )
func TestFeideeMymoneyCsvFileImporterParseImportedData_MinimumValidData(t *testing.T) { func TestFeideeMymoneyCsvFileImporterParseImportedData_MinimumValidData(t *testing.T) {
converter := FeideeMymoneyAppTransactionDataCsvFileImporter importer := FeideeMymoneyAppTransactionDataCsvFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -21,7 +22,7 @@ func TestFeideeMymoneyCsvFileImporterParseImportedData_MinimumValidData(t *testi
DefaultCurrency: "CNY", 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"+ "\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+
"\"余额变更\",\"2024-09-01 00:00:00\",\"\",\"Test Account\",\"123.45\",\"\",\"\"\n"+ "\"余额变更\",\"2024-09-01 00:00:00\",\"\",\"Test Account\",\"123.45\",\"\",\"\"\n"+
"\"余额变更\",\"2024-09-01 01:00:00\",\"\",\"Test Account2\",\"-0.12\",\"\",\"\"\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 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-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 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) assert.Nil(t, err)
@@ -110,7 +111,7 @@ func TestFeideeMymoneyCsvFileImporterParseImportedData_MinimumValidData(t *testi
} }
func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseOutstandingBalanceModification(t *testing.T) { func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseOutstandingBalanceModification(t *testing.T) {
converter := FeideeMymoneyAppTransactionDataCsvFileImporter importer := FeideeMymoneyAppTransactionDataCsvFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -118,10 +119,10 @@ func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseOutstandingBalanceMo
DefaultCurrency: "CNY", 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"+ "\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+
"\"负债变更\",\"2024-09-01 00:00:00\",\"\",\"Test Account\",\"123.45\",\"\",\"\"\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) assert.Nil(t, err)
@@ -160,7 +161,7 @@ func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseOutstandingBalanceMo
} }
func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseInvalidTime(t *testing.T) { func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseInvalidTime(t *testing.T) {
converter := FeideeMymoneyAppTransactionDataCsvFileImporter importer := FeideeMymoneyAppTransactionDataCsvFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -168,19 +169,19 @@ func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseInvalidTime(t *testi
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+ _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
"\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\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) 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"+ "\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联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) assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
} }
func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseInvalidType(t *testing.T) { func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseInvalidType(t *testing.T) {
converter := FeideeMymoneyAppTransactionDataCsvFileImporter importer := FeideeMymoneyAppTransactionDataCsvFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -188,14 +189,14 @@ func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseInvalidType(t *testi
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+ _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
"\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\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) assert.EqualError(t, err, errs.ErrTransactionTypeInvalid.Message)
} }
func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseValidAccountCurrency(t *testing.T) { func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseValidAccountCurrency(t *testing.T) {
converter := FeideeMymoneyAppTransactionDataCsvFileImporter importer := FeideeMymoneyAppTransactionDataCsvFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -203,11 +204,11 @@ func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseValidAccountCurrency
DefaultCurrency: "CNY", 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"+ "\"交易类型\",\"日期\",\"子类别\",\"账户\",\"账户币种\",\"金额\",\"备注\",\"关联Id\"\n"+
"\"余额变更\",\"2024-09-01 01:23:45\",\"\",\"Test Account\",\"USD\",\"123.45\",\"\",\"\"\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 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) assert.Nil(t, err)
@@ -224,7 +225,7 @@ func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseValidAccountCurrency
} }
func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseInvalidAccountCurrency(t *testing.T) { func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseInvalidAccountCurrency(t *testing.T) {
converter := FeideeMymoneyAppTransactionDataCsvFileImporter importer := FeideeMymoneyAppTransactionDataCsvFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -232,23 +233,23 @@ func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseInvalidAccountCurren
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+ _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
"\"交易类型\",\"日期\",\"子类别\",\"账户\",\"账户币种\",\"金额\",\"备注\",\"关联Id\"\n"+ "\"交易类型\",\"日期\",\"子类别\",\"账户\",\"账户币种\",\"金额\",\"备注\",\"关联Id\"\n"+
"\"余额变更\",\"2024-09-01 01:23:45\",\"\",\"Test Account\",\"USD\",\"123.45\",\"\",\"\"\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 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) 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"+ "\"交易类型\",\"日期\",\"子类别\",\"账户\",\"账户币种\",\"金额\",\"备注\",\"关联Id\"\n"+
"\"余额变更\",\"2024-09-01 01:23:45\",\"\",\"Test Account\",\"USD\",\"123.45\",\"\",\"\"\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 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) assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message)
} }
func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseNotSupportedCurrency(t *testing.T) { func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseNotSupportedCurrency(t *testing.T) {
converter := FeideeMymoneyAppTransactionDataCsvFileImporter importer := FeideeMymoneyAppTransactionDataCsvFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -256,26 +257,26 @@ func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseNotSupportedCurrency
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+ _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
"\"交易类型\",\"日期\",\"子类别\",\"账户\",\"账户币种\",\"金额\",\"备注\",\"关联Id\"\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) 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"+ "\"交易类型\",\"日期\",\"子类别\",\"账户\",\"账户币种\",\"金额\",\"备注\",\"关联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 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) 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"+ "\"交易类型\",\"日期\",\"子类别\",\"账户\",\"账户币种\",\"金额\",\"备注\",\"关联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 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) assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message)
} }
func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseInvalidAmount(t *testing.T) { func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseInvalidAmount(t *testing.T) {
converter := FeideeMymoneyAppTransactionDataCsvFileImporter importer := FeideeMymoneyAppTransactionDataCsvFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -283,31 +284,31 @@ func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseInvalidAmount(t *tes
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+ _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
"\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\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) 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"+ "\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联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) 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"+ "\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联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 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) 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"+ "\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联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 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) assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
} }
func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseDescription(t *testing.T) { func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseDescription(t *testing.T) {
converter := FeideeMymoneyAppTransactionDataCsvFileImporter importer := FeideeMymoneyAppTransactionDataCsvFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -315,18 +316,18 @@ func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseDescription(t *testi
DefaultCurrency: "CNY", 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"+ "\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+
"\"支出\",\"2024-09-01 12:34:56\",\"Test Category\",\"Test Account\",\"123.45\",\"Test\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.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, "Test\nA new line break", allNewTransactions[0].Comment) assert.Equal(t, "Test\nA new line break", allNewTransactions[0].Comment)
} }
func TestFeideeMymoneyCsvFileImporterParseImportedData_InvalidRelatedId(t *testing.T) { func TestFeideeMymoneyCsvFileImporterParseImportedData_WithAdditionalOptions(t *testing.T) {
converter := FeideeMymoneyAppTransactionDataCsvFileImporter importer := FeideeMymoneyAppTransactionDataCsvFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -334,41 +335,70 @@ func TestFeideeMymoneyCsvFileImporterParseImportedData_InvalidRelatedId(t *testi
DefaultCurrency: "CNY", 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"+ "\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联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) 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"+ "\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联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) 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"+ "\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+
"\"转出\",\"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 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) assert.EqualError(t, err, errs.ErrFoundRecordNotHasRelatedRecord.Message)
} }
func TestFeideeMymoneyCsvFileImporterParseImportedData_MissingFileHeader(t *testing.T) { func TestFeideeMymoneyCsvFileImporterParseImportedData_MissingFileHeader(t *testing.T) {
converter := FeideeMymoneyAppTransactionDataCsvFileImporter importer := FeideeMymoneyAppTransactionDataCsvFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
Uid: 1, Uid: 1,
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+ _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联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.ErrInvalidFileHeader.Message) 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) assert.EqualError(t, err, errs.ErrInvalidFileHeader.Message)
} }
func TestFeideeMymoneyCsvFileImporterParseImportedData_MissingRequiredColumn(t *testing.T) { func TestFeideeMymoneyCsvFileImporterParseImportedData_MissingRequiredColumn(t *testing.T) {
converter := FeideeMymoneyAppTransactionDataCsvFileImporter importer := FeideeMymoneyAppTransactionDataCsvFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -377,38 +407,38 @@ func TestFeideeMymoneyCsvFileImporterParseImportedData_MissingRequiredColumn(t *
} }
// Missing Time Column // 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"+ "\"交易类型\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联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) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
// Missing Type Column // 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"+ "\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联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) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
// Missing Sub Category Column // 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"+ "\"交易类型\",\"日期\",\"账户\",\"金额\",\"备注\",\"关联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) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
// Missing Account Name Column // 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"+ "\"交易类型\",\"日期\",\"子类别\",\"金额\",\"备注\",\"关联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) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
// Missing Amount Column // 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"+ "\"交易类型\",\"日期\",\"子类别\",\"账户\",\"备注\",\"关联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) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
// Missing Related ID Column // 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"+ "\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\"\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) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
} }
@@ -1,6 +1,8 @@
package feidee package feidee
import ( import (
"time"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter" "github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable" "github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
"github.com/mayswind/ezbookkeeping/pkg/converters/excel" "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_AMOUNT: "金额",
datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME: "账户2", datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME: "账户2",
datatable.TRANSACTION_DATA_TABLE_DESCRIPTION: "备注", 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 // 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 // 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) dataTable, err := excel.CreateNewExcelOOXMLFileBasicDataTable(data, true)
if err != nil { if err != nil {
@@ -42,5 +47,5 @@ func (c *feideeMymoneyElecloudTransactionDataXlsxFileImporter) ParseImportedData
transactionDataTable := datatable.CreateNewTransactionDataTableFromBasicDataTableWithRowParser(dataTable, feideeMymoneyElecloudDataColumnNameMapping, transactionRowParser) transactionDataTable := datatable.CreateNewTransactionDataTableFromBasicDataTableWithRowParser(dataTable, feideeMymoneyElecloudDataColumnNameMapping, transactionRowParser)
dataTableImporter := converter.CreateNewSimpleImporter(feideeMymoneyElecloudTransactionTypeNameMapping) 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/stretchr/testify/assert"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/core" "github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/models" "github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/utils" "github.com/mayswind/ezbookkeeping/pkg/utils"
) )
func TestFeideeMymoneyElecloudTransactionDataXlsxImporterParseImportedData_MinimumValidData(t *testing.T) { func TestFeideeMymoneyElecloudTransactionDataXlsxImporterParseImportedData_MinimumValidData(t *testing.T) {
converter := FeideeMymoneyElecloudTransactionDataXlsxFileImporter importer := FeideeMymoneyElecloudTransactionDataXlsxFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -24,7 +25,7 @@ func TestFeideeMymoneyElecloudTransactionDataXlsxImporterParseImportedData_Minim
testdata, err := os.ReadFile("../../../testdata/feidee_mymoney_elecloud_test_file.xlsx") testdata, err := os.ReadFile("../../../testdata/feidee_mymoney_elecloud_test_file.xlsx")
assert.Nil(t, err) 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.Nil(t, err)
assert.Equal(t, 7, len(allNewTransactions)) assert.Equal(t, 7, len(allNewTransactions))
@@ -1,6 +1,8 @@
package feidee package feidee
import ( import (
"time"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter" "github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable" "github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
"github.com/mayswind/ezbookkeeping/pkg/converters/excel" "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_AMOUNT: "金额",
datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME: "账户2", datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME: "账户2",
datatable.TRANSACTION_DATA_TABLE_DESCRIPTION: "备注", 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 // 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 // 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) dataTable, err := excel.CreateNewExcelMSCFBFileBasicDataTable(data, true)
if err != nil { if err != nil {
@@ -41,5 +46,5 @@ func (c *feideeMymoneyWebTransactionDataXlsFileImporter) ParseImportedData(ctx c
transactionDataTable := datatable.CreateNewTransactionDataTableFromBasicDataTableWithRowParser(dataTable, feideeMymoneyWebDataColumnNameMapping, transactionRowParser) transactionDataTable := datatable.CreateNewTransactionDataTableFromBasicDataTableWithRowParser(dataTable, feideeMymoneyWebDataColumnNameMapping, transactionRowParser)
dataTableImporter := converter.CreateNewSimpleImporterWithTypeNameMapping(feideeMymoneyTransactionTypeNameMapping) 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/stretchr/testify/assert"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/core" "github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/models" "github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/utils" "github.com/mayswind/ezbookkeeping/pkg/utils"
) )
func TestFeideeMymoneyTransactionDataXlsImporterParseImportedData_MinimumValidData(t *testing.T) { func TestFeideeMymoneyTransactionDataXlsImporterParseImportedData_MinimumValidData(t *testing.T) {
converter := FeideeMymoneyWebTransactionDataXlsFileImporter importer := FeideeMymoneyWebTransactionDataXlsFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -24,7 +25,7 @@ func TestFeideeMymoneyTransactionDataXlsImporterParseImportedData_MinimumValidDa
testdata, err := os.ReadFile("../../../testdata/feidee_mymoney_test_file.xls") testdata, err := os.ReadFile("../../../testdata/feidee_mymoney_test_file.xls")
assert.Nil(t, err) 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.Nil(t, err)
assert.Equal(t, 7, len(allNewTransactions)) assert.Equal(t, 7, len(allNewTransactions))
@@ -2,6 +2,7 @@ package fireflyIII
import ( import (
"bytes" "bytes"
"time"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter" "github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/converters/csv" "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 // 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) reader := bytes.NewReader(data)
dataTable, err := csv.CreateNewCsvBasicDataTable(ctx, reader, true) dataTable, err := csv.CreateNewCsvBasicDataTable(ctx, reader, true)
@@ -52,5 +53,5 @@ func (c *fireflyIIITransactionDataCsvFileImporter) ParseImportedData(ctx core.Co
transactionDataTable := datatable.CreateNewTransactionDataTableFromBasicDataTableWithRowParser(dataTable, fireflyIIITransactionDataColumnNameMapping, transactionRowParser) transactionDataTable := datatable.CreateNewTransactionDataTableFromBasicDataTableWithRowParser(dataTable, fireflyIIITransactionDataColumnNameMapping, transactionRowParser)
dataTableImporter := converter.CreateNewImporterWithTypeNameMapping(fireflyIIITransactionTypeNameMapping, "", "", ",") 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 ( import (
"testing" "testing"
"time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/core" "github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs" "github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/models" "github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/utils" "github.com/mayswind/ezbookkeeping/pkg/utils"
) )
func TestFireFlyIIICsvFileConverterParseImportedData_MinimumValidData(t *testing.T) { func TestFireFlyIIICsvFileimporterParseImportedData_MinimumValidData(t *testing.T) {
converter := FireflyIIITransactionDataCsvFileImporter importer := FireflyIIITransactionDataCsvFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -20,11 +22,11 @@ func TestFireFlyIIICsvFileConverterParseImportedData_MinimumValidData(t *testing
DefaultCurrency: "CNY", 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"+ "\"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"+ "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"+ "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) assert.Nil(t, err)
@@ -82,8 +84,8 @@ func TestFireFlyIIICsvFileConverterParseImportedData_MinimumValidData(t *testing
assert.Equal(t, "Test Category3", allNewSubTransferCategories[0].Name) assert.Equal(t, "Test Category3", allNewSubTransferCategories[0].Name)
} }
func TestFireFlyIIICsvFileConverterParseImportedData_ParseInvalidTime(t *testing.T) { func TestFireFlyIIICsvFileimporterParseImportedData_ParseInvalidTime(t *testing.T) {
converter := FireflyIIITransactionDataCsvFileImporter importer := FireflyIIITransactionDataCsvFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -91,17 +93,17 @@ func TestFireFlyIIICsvFileConverterParseImportedData_ParseInvalidTime(t *testing
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name,category\n"+ _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name,category\n"+
"Withdrawal,-1.00,2024-09-01T12:34:56,\"Test Account\",\"A expense account\",\"Test Category\""), 0, nil, nil, nil, nil, nil) "Withdrawal,-1.00,2024-09-01T12:34:56,\"Test Account\",\"A expense account\",\"Test Category\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message) assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name,category\n"+ _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name,category\n"+
"Withdrawal,-1.00,2024-09-01 12:34:56+08:00,\"Test Account\",\"A expense account\",\"Test Category\""), 0, nil, nil, nil, nil, nil) "Withdrawal,-1.00,2024-09-01 12:34:56+08:00,\"Test Account\",\"A expense account\",\"Test Category\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message) assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
} }
func TestFireFlyIIICsvFileConverterParseImportedData_ParseInvalidType(t *testing.T) { func TestFireFlyIIICsvFileimporterParseImportedData_ParseInvalidType(t *testing.T) {
converter := FireflyIIITransactionDataCsvFileImporter importer := FireflyIIITransactionDataCsvFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -109,13 +111,13 @@ func TestFireFlyIIICsvFileConverterParseImportedData_ParseInvalidType(t *testing
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name,category\n"+ _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name,category\n"+
"Type,123.45,2024-09-01T12:34:56+08:00,\"Test Account\",\"A expense account\",\"Test Category\""), 0, nil, nil, nil, nil, nil) "Type,123.45,2024-09-01T12:34:56+08:00,\"Test Account\",\"A expense account\",\"Test Category\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrTransactionTypeInvalid.Message) assert.EqualError(t, err, errs.ErrTransactionTypeInvalid.Message)
} }
func TestFireFlyIIICsvFileConverterParseImportedData_ParseAccountNameAsCategoryName(t *testing.T) { func TestFireFlyIIICsvFileimporterParseImportedData_ParseAccountNameAsCategoryName(t *testing.T) {
converter := FireflyIIITransactionDataCsvFileImporter importer := FireflyIIITransactionDataCsvFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -123,23 +125,23 @@ func TestFireFlyIIICsvFileConverterParseImportedData_ParseAccountNameAsCategoryN
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name,category\n"+ allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name,category\n"+
"Withdrawal,-1.00,2024-09-01T12:34:56+08:00,\"Test Account\",\"A expense account\",\"\""), 0, nil, nil, nil, nil, nil) "Withdrawal,-1.00,2024-09-01T12:34:56+08:00,\"Test Account\",\"A expense account\",\"\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, "A expense account", allNewTransactions[0].OriginalCategoryName) 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"+ allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name,category\n"+
"Deposit,10.00,2024-09-01T12:34:56+08:00,\"A revenue account\",\"Test Account\",\"\""), 0, nil, nil, nil, nil, nil) "Deposit,10.00,2024-09-01T12:34:56+08:00,\"A revenue account\",\"Test Account\",\"\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, "A revenue account", allNewTransactions[0].OriginalCategoryName) assert.Equal(t, "A revenue account", allNewTransactions[0].OriginalCategoryName)
} }
func TestFireFlyIIICsvFileConverterParseImportedData_ParseValidTimezone(t *testing.T) { func TestFireFlyIIICsvFileimporterParseImportedData_ParseValidTimezone(t *testing.T) {
converter := FireflyIIITransactionDataCsvFileImporter importer := FireflyIIITransactionDataCsvFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -147,27 +149,27 @@ func TestFireFlyIIICsvFileConverterParseImportedData_ParseValidTimezone(t *testi
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name,category\n"+ allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name,category\n"+
"Withdrawal,-1.00,2024-09-01T12:34:56-10:00,\"Test Account\",\"A expense account\",\"Test Category\""), 0, nil, nil, nil, nil, nil) "Withdrawal,-1.00,2024-09-01T12:34:56-10:00,\"Test Account\",\"A expense account\",\"Test Category\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, int64(1725230096), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime)) 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"+ allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name,category\n"+
"Withdrawal,-1.00,2024-09-01T12:34:56+00:00,\"Test Account\",\"A expense account\",\"Test Category\""), 0, nil, nil, nil, nil, nil) "Withdrawal,-1.00,2024-09-01T12:34:56+00:00,\"Test Account\",\"A expense account\",\"Test Category\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, int64(1725194096), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime)) 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"+ allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name,category\n"+
"Withdrawal,-1.00,2024-09-01T12:34:56+12:45,\"Test Account\",\"A expense account\",\"Test Category\""), 0, nil, nil, nil, nil, nil) "Withdrawal,-1.00,2024-09-01T12:34:56+12:45,\"Test Account\",\"A expense account\",\"Test Category\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, int64(1725148196), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime)) assert.Equal(t, int64(1725148196), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
} }
func TestFireFlyIIICsvFileConverterParseImportedData_ParseValidAccountCurrency(t *testing.T) { func TestFireFlyIIICsvFileimporterParseImportedData_ParseValidAccountCurrency(t *testing.T) {
converter := FireflyIIITransactionDataCsvFileImporter importer := FireflyIIITransactionDataCsvFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -175,9 +177,9 @@ func TestFireFlyIIICsvFileConverterParseImportedData_ParseValidAccountCurrency(t
DefaultCurrency: "CNY", 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"+ "\"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) assert.Nil(t, err)
@@ -193,8 +195,8 @@ func TestFireFlyIIICsvFileConverterParseImportedData_ParseValidAccountCurrency(t
assert.Equal(t, "EUR", allNewAccounts[1].Currency) assert.Equal(t, "EUR", allNewAccounts[1].Currency)
} }
func TestFireFlyIIICsvFileConverterParseImportedData_ParseValidForeignAmountAndCurrency(t *testing.T) { func TestFireFlyIIICsvFileimporterParseImportedData_ParseValidForeignAmountAndCurrency(t *testing.T) {
converter := FireflyIIITransactionDataCsvFileImporter importer := FireflyIIITransactionDataCsvFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -202,8 +204,8 @@ func TestFireFlyIIICsvFileConverterParseImportedData_ParseValidForeignAmountAndC
DefaultCurrency: "CNY", 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"+ allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("type,amount,foreign_amount,date,currency_code,foreign_currency_code,source_name,destination_name,category\n"+
"Transfer,10.00,15.00,2024-09-01T12:34:56+08:00,USD,EUR,\"Test Account\",\"Test Account2\",\"Test Category\""), 0, nil, nil, nil, nil, nil) "Transfer,10.00,15.00,2024-09-01T12:34:56+08:00,USD,EUR,\"Test Account\",\"Test Account2\",\"Test Category\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
@@ -213,8 +215,8 @@ func TestFireFlyIIICsvFileConverterParseImportedData_ParseValidForeignAmountAndC
assert.Equal(t, "USD", allNewTransactions[0].OriginalSourceAccountCurrency) assert.Equal(t, "USD", allNewTransactions[0].OriginalSourceAccountCurrency)
assert.Equal(t, "EUR", allNewTransactions[0].OriginalDestinationAccountCurrency) 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"+ allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("type,amount,date,currency_code,foreign_currency_code,source_name,destination_name,category\n"+
"Transfer,10.00,2024-09-01T12:34:56+08:00,USD,EUR,\"Test Account\",\"Test Account2\",\"Test Category\""), 0, nil, nil, nil, nil, nil) "Transfer,10.00,2024-09-01T12:34:56+08:00,USD,EUR,\"Test Account\",\"Test Account2\",\"Test Category\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
@@ -223,8 +225,8 @@ func TestFireFlyIIICsvFileConverterParseImportedData_ParseValidForeignAmountAndC
assert.Equal(t, "USD", allNewTransactions[0].OriginalSourceAccountCurrency) assert.Equal(t, "USD", allNewTransactions[0].OriginalSourceAccountCurrency)
assert.Equal(t, "EUR", allNewTransactions[0].OriginalDestinationAccountCurrency) 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"+ allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("type,amount,date,currency_code,foreign_currency_code,source_name,destination_name,category\n"+
"Transfer,10.00,2024-09-01T12:34:56+08:00,USD,,\"Test Account\",\"Test Account2\",\"Test Category\""), 0, nil, nil, nil, nil, nil) "Transfer,10.00,2024-09-01T12:34:56+08:00,USD,,\"Test Account\",\"Test Account2\",\"Test Category\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
@@ -232,8 +234,8 @@ func TestFireFlyIIICsvFileConverterParseImportedData_ParseValidForeignAmountAndC
assert.Equal(t, "USD", allNewTransactions[0].OriginalDestinationAccountCurrency) assert.Equal(t, "USD", allNewTransactions[0].OriginalDestinationAccountCurrency)
} }
func TestFireFlyIIICsvFileConverterParseImportedData_ParseInvalidAccountCurrency(t *testing.T) { func TestFireFlyIIICsvFileimporterParseImportedData_ParseInvalidAccountCurrency(t *testing.T) {
converter := FireflyIIITransactionDataCsvFileImporter importer := FireflyIIITransactionDataCsvFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -241,19 +243,19 @@ func TestFireFlyIIICsvFileConverterParseImportedData_ParseInvalidAccountCurrency
DefaultCurrency: "CNY", 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"+ "\"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) 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"+ "\"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) assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message)
} }
func TestFireFlyIIICsvFileConverterParseImportedData_ParseNotSupportedCurrency(t *testing.T) { func TestFireFlyIIICsvFileimporterParseImportedData_ParseNotSupportedCurrency(t *testing.T) {
converter := FireflyIIITransactionDataCsvFileImporter importer := FireflyIIITransactionDataCsvFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -261,17 +263,17 @@ func TestFireFlyIIICsvFileConverterParseImportedData_ParseNotSupportedCurrency(t
DefaultCurrency: "CNY", 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,XXX,,\"Initial balance for \"\"Test Account\"\"\",\"Test Account\",\n"), 0, nil, nil, nil, nil, nil) "\"Opening balance\",123.45,,2024-09-01T00:00:00+08:00,XXX,,\"Initial balance for \"\"Test Account\"\"\",\"Test Account\",\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message) 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"+
"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) "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) assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message)
} }
func TestFireFlyIIICsvFileConverterParseImportedData_ParseInvalidAmount(t *testing.T) { func TestFireFlyIIICsvFileimporterParseImportedData_ParseInvalidAmount(t *testing.T) {
converter := FireflyIIITransactionDataCsvFileImporter importer := FireflyIIITransactionDataCsvFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -279,17 +281,17 @@ func TestFireFlyIIICsvFileConverterParseImportedData_ParseInvalidAmount(t *testi
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name,category\n"+ _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name,category\n"+
"Withdrawal,-123 45,2024-09-01T12:34:56+08:00,\"Test Account\",\"A expense account\",\"Test Category\"\n"), 0, nil, nil, nil, nil, nil) "Withdrawal,-123 45,2024-09-01T12:34:56+08:00,\"Test Account\",\"A expense account\",\"Test Category\"\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAmountInvalid.Message) assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("type,amount,foreign_amount,date,source_name,destination_name,category\n"+ _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("type,amount,foreign_amount,date,source_name,destination_name,category\n"+
"Transfer,123.45,123 45,2024-09-01T23:59:59+08:00,\"Test Account\",\"Test Account2\",\"Test Category2\""), 0, nil, nil, nil, nil, nil) "Transfer,123.45,123 45,2024-09-01T23:59:59+08:00,\"Test Account\",\"Test Account2\",\"Test Category2\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAmountInvalid.Message) assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
} }
func TestFireFlyIIICsvFileConverterParseImportedData_ParseDescription(t *testing.T) { func TestFireFlyIIICsvFileimporterParseImportedData_ParseDescription(t *testing.T) {
converter := FireflyIIITransactionDataCsvFileImporter importer := FireflyIIITransactionDataCsvFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -297,16 +299,16 @@ func TestFireFlyIIICsvFileConverterParseImportedData_ParseDescription(t *testing
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("type,amount,description,date,source_name,destination_name,category\n"+ allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("type,amount,description,date,source_name,destination_name,category\n"+
"Withdrawal,-123.45,\"foo bar\t#test\",2024-09-01T12:34:56+08:00,\"Test Account\",\"A expense account\",\"Test Category\"\n"), 0, nil, nil, nil, nil, nil) "Withdrawal,-123.45,\"foo bar\t#test\",2024-09-01T12:34:56+08:00,\"Test Account\",\"A expense account\",\"Test Category\"\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, "foo bar\t#test", allNewTransactions[0].Comment) assert.Equal(t, "foo bar\t#test", allNewTransactions[0].Comment)
} }
func TestFireFlyIIICsvFileConverterParseImportedData_ParseTags(t *testing.T) { func TestFireFlyIIICsvFileimporterParseImportedData_ParseTags(t *testing.T) {
converter := FireflyIIITransactionDataCsvFileImporter importer := FireflyIIITransactionDataCsvFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -314,8 +316,8 @@ func TestFireFlyIIICsvFileConverterParseImportedData_ParseTags(t *testing.T) {
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
allNewTransactions, _, _, _, _, allNewTags, err := converter.ParseImportedData(context, user, []byte("type,amount,tags,date,source_name,destination_name,category\n"+ allNewTransactions, _, _, _, _, allNewTags, err := importer.ParseImportedData(context, user, []byte("type,amount,tags,date,source_name,destination_name,category\n"+
"Withdrawal,-123.45,\"tag1,tag2,tag3\",2024-09-01T12:34:56+08:00,\"Test Account\",\"A expense account\",\"Test Category\"\n"), 0, nil, nil, nil, nil, nil) "Withdrawal,-123.45,\"tag1,tag2,tag3\",2024-09-01T12:34:56+08:00,\"Test Account\",\"A expense account\",\"Test Category\"\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
@@ -328,8 +330,8 @@ func TestFireFlyIIICsvFileConverterParseImportedData_ParseTags(t *testing.T) {
assert.Equal(t, "tag3", allNewTags[2].Name) assert.Equal(t, "tag3", allNewTags[2].Name)
} }
func TestFireFlyIIICsvFileConverterParseImportedData_MissingFileHeader(t *testing.T) { func TestFireFlyIIICsvFileimporterParseImportedData_MissingFileHeader(t *testing.T) {
converter := FireflyIIITransactionDataCsvFileImporter importer := FireflyIIITransactionDataCsvFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -337,12 +339,12 @@ func TestFireFlyIIICsvFileConverterParseImportedData_MissingFileHeader(t *testin
DefaultCurrency: "CNY", 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) assert.EqualError(t, err, errs.ErrNotFoundTransactionDataInFile.Message)
} }
func TestFireFlyIIICsvFileConverterParseImportedData_MissingRequiredColumn(t *testing.T) { func TestFireFlyIIICsvFileimporterParseImportedData_MissingRequiredColumn(t *testing.T) {
converter := FireflyIIITransactionDataCsvFileImporter importer := FireflyIIITransactionDataCsvFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -351,32 +353,32 @@ func TestFireFlyIIICsvFileConverterParseImportedData_MissingRequiredColumn(t *te
} }
// Missing Time Column // Missing Time Column
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("type,amount,source_name,destination_name,category\n"+ _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("type,amount,source_name,destination_name,category\n"+
"\"Opening balance\",123.45,\"Initial balance for \"\"Test Account\"\"\",\"Test Account\",\n"), 0, nil, nil, nil, nil, nil) "\"Opening balance\",123.45,\"Initial balance for \"\"Test Account\"\"\",\"Test Account\",\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
// Missing Type Column // Missing Type Column
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("amount,date,source_name,destination_name,category\n"+ _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("amount,date,source_name,destination_name,category\n"+
"123.45,2024-09-01T00:00:00+08:00,\"Initial balance for \"\"Test Account\"\"\",\"Test Account\",\n"), 0, nil, nil, nil, nil, nil) "123.45,2024-09-01T00:00:00+08:00,\"Initial balance for \"\"Test Account\"\"\",\"Test Account\",\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
// Missing Sub Category Column // Missing Sub Category Column
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name\n"+ _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name\n"+
"\"Opening balance\",123.45,2024-09-01T00:00:00+08:00,\"Initial balance for \"\"Test Account\"\"\",\"Test Account\"\n"), 0, nil, nil, nil, nil, nil) "\"Opening balance\",123.45,2024-09-01T00:00:00+08:00,\"Initial balance for \"\"Test Account\"\"\",\"Test Account\"\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
// Missing Account Name Column // Missing Account Name Column
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("type,amount,date,destination_name,category\n"+ _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("type,amount,date,destination_name,category\n"+
"\"Opening balance\",123.45,2024-09-01T00:00:00+08:00,\"Test Account\",\n"), 0, nil, nil, nil, nil, nil) "\"Opening balance\",123.45,2024-09-01T00:00:00+08:00,\"Test Account\",\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
// Missing Amount Column // Missing Amount Column
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("type,date,source_name,destination_name,category\n"+ _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("type,date,source_name,destination_name,category\n"+
"\"Opening balance\",2024-09-01T00:00:00+08:00,\"Initial balance for \"\"Test Account\"\"\",\"Test Account\",\n"), 0, nil, nil, nil, nil, nil) "\"Opening balance\",2024-09-01T00:00:00+08:00,\"Initial balance for \"\"Test Account\"\"\",\"Test Account\",\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
// Missing Account2 Name Column // Missing Account2 Name Column
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("type,amount,date,source_name,category\n"+ _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("type,amount,date,source_name,category\n"+
"\"Opening balance\",123.45,2024-09-01T00:00:00+08:00,\"Initial balance for \"\"Test Account\"\"\",\n"), 0, nil, nil, nil, nil, nil) "\"Opening balance\",123.45,2024-09-01T00:00:00+08:00,\"Initial balance for \"\"Test Account\"\"\",\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message) 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_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 // trim trailing zero in decimal
@@ -1,6 +1,8 @@
package gnucash package gnucash
import ( import (
"time"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter" "github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/core" "github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/models" "github.com/mayswind/ezbookkeeping/pkg/models"
@@ -24,7 +26,7 @@ var (
) )
// ParseImportedData returns the imported data by parsing the gnucash transaction data // 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) gnucashDataReader, err := createNewGnuCashDatabaseReader(data)
if err != nil { if err != nil {
@@ -45,5 +47,5 @@ func (c *gnucashTransactionDataImporter) ParseImportedData(ctx core.Context, use
dataTableImporter := converter.CreateNewSimpleImporterWithTypeNameMapping(gnucashTransactionTypeNameMapping) 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" "bytes"
"compress/gzip" "compress/gzip"
"testing" "testing"
"time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/core" "github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs" "github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/models" "github.com/mayswind/ezbookkeeping/pkg/models"
@@ -185,7 +187,7 @@ const gnucashCommonValidDataCaseFooter = "</gnc:book>\n" +
"</gnc-v2>\n" "</gnc-v2>\n"
func TestGnuCashTransactionDatabaseFileParseImportedData_MinimumValidData(t *testing.T) { func TestGnuCashTransactionDatabaseFileParseImportedData_MinimumValidData(t *testing.T) {
converter := GnuCashTransactionDataImporter importer := GnuCashTransactionDataImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -193,14 +195,14 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_MinimumValidData(t *tes
DefaultCurrency: "CNY", 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) assert.Nil(t, err)
checkParsedMinimumValidData(t, allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags) checkParsedMinimumValidData(t, allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags)
} }
func TestGnuCashTransactionDatabaseFileParseImportedData_GzippedMinimumValidData(t *testing.T) { func TestGnuCashTransactionDatabaseFileParseImportedData_GzippedMinimumValidData(t *testing.T) {
converter := GnuCashTransactionDataImporter importer := GnuCashTransactionDataImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -217,14 +219,14 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_GzippedMinimumValidData
assert.Nil(t, err) assert.Nil(t, err)
gzippedData := buffer.Bytes() 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) assert.Nil(t, err)
checkParsedMinimumValidData(t, allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags) checkParsedMinimumValidData(t, allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags)
} }
func TestGnuCashTransactionDatabaseFileParseImportedData_MinimumValidDataWithReversedSplitOrder(t *testing.T) { func TestGnuCashTransactionDatabaseFileParseImportedData_MinimumValidDataWithReversedSplitOrder(t *testing.T) {
converter := GnuCashTransactionDataImporter importer := GnuCashTransactionDataImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -232,7 +234,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_MinimumValidDataWithRev
DefaultCurrency: "CNY", 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"+ "<gnc-v2\n"+
" xmlns:gnc=\"http://www.gnucash.org/XML/gnc\"\n"+ " xmlns:gnc=\"http://www.gnucash.org/XML/gnc\"\n"+
" xmlns:act=\"http://www.gnucash.org/XML/act\"\n"+ " xmlns:act=\"http://www.gnucash.org/XML/act\"\n"+
@@ -356,14 +358,14 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_MinimumValidDataWithRev
" </trn:splits>\n"+ " </trn:splits>\n"+
"</gnc:transaction>\n"+ "</gnc:transaction>\n"+
"</gnc:book>\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) assert.Nil(t, err)
checkParsedMinimumValidData(t, allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags) checkParsedMinimumValidData(t, allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags)
} }
func TestGnuCashTransactionDatabaseFileParseImportedData_ParseInvalidTime(t *testing.T) { func TestGnuCashTransactionDatabaseFileParseImportedData_ParseInvalidTime(t *testing.T) {
converter := GnuCashTransactionDataImporter importer := GnuCashTransactionDataImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -371,7 +373,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseInvalidTime(t *tes
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
gnucashCommonValidDataCaseHeader+ gnucashCommonValidDataCaseHeader+
"<gnc:transaction version=\"2.0.0\">\n"+ "<gnc:transaction version=\"2.0.0\">\n"+
" <trn:date-posted>\n"+ " <trn:date-posted>\n"+
@@ -388,10 +390,10 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseInvalidTime(t *tes
" </trn:split>\n"+ " </trn:split>\n"+
" </trn:splits>\n"+ " </trn:splits>\n"+
"</gnc:transaction>\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) assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
gnucashCommonValidDataCaseHeader+ gnucashCommonValidDataCaseHeader+
"<gnc:transaction version=\"2.0.0\">\n"+ "<gnc:transaction version=\"2.0.0\">\n"+
" <trn:date-posted>\n"+ " <trn:date-posted>\n"+
@@ -408,12 +410,12 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseInvalidTime(t *tes
" </trn:split>\n"+ " </trn:split>\n"+
" </trn:splits>\n"+ " </trn:splits>\n"+
"</gnc:transaction>\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) assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
} }
func TestGnuCashTransactionDatabaseFileParseImportedData_ParseValidTimezone(t *testing.T) { func TestGnuCashTransactionDatabaseFileParseImportedData_ParseValidTimezone(t *testing.T) {
converter := GnuCashTransactionDataImporter importer := GnuCashTransactionDataImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -421,7 +423,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseValidTimezone(t *t
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
gnucashCommonValidDataCaseHeader+ gnucashCommonValidDataCaseHeader+
"<gnc:transaction version=\"2.0.0\">\n"+ "<gnc:transaction version=\"2.0.0\">\n"+
" <trn:date-posted>\n"+ " <trn:date-posted>\n"+
@@ -438,12 +440,12 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseValidTimezone(t *t
" </trn:split>\n"+ " </trn:split>\n"+
" </trn:splits>\n"+ " </trn:splits>\n"+
"</gnc:transaction>\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.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, int64(1725230096), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime)) 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+ gnucashCommonValidDataCaseHeader+
"<gnc:transaction version=\"2.0.0\">\n"+ "<gnc:transaction version=\"2.0.0\">\n"+
" <trn:date-posted>\n"+ " <trn:date-posted>\n"+
@@ -460,14 +462,14 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseValidTimezone(t *t
" </trn:split>\n"+ " </trn:split>\n"+
" </trn:splits>\n"+ " </trn:splits>\n"+
"</gnc:transaction>\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.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, int64(1725148196), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime)) assert.Equal(t, int64(1725148196), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
} }
func TestGnuCashTransactionDatabaseFileParseImportedData_ParseValidAccountCurrency(t *testing.T) { func TestGnuCashTransactionDatabaseFileParseImportedData_ParseValidAccountCurrency(t *testing.T) {
converter := GnuCashTransactionDataImporter importer := GnuCashTransactionDataImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -475,7 +477,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseValidAccountCurren
DefaultCurrency: "CNY", 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"+ "<gnc-v2\n"+
" xmlns:gnc=\"http://www.gnucash.org/XML/gnc\"\n"+ " xmlns:gnc=\"http://www.gnucash.org/XML/gnc\"\n"+
" xmlns:act=\"http://www.gnucash.org/XML/act\"\n"+ " xmlns:act=\"http://www.gnucash.org/XML/act\"\n"+
@@ -557,7 +559,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseValidAccountCurren
" </trn:splits>\n"+ " </trn:splits>\n"+
"</gnc:transaction>\n"+ "</gnc:transaction>\n"+
"</gnc:book>\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) assert.Nil(t, err)
@@ -574,7 +576,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseValidAccountCurren
} }
func TestGnuCashTransactionDatabaseFileParseImportedData_ParseValidAmount(t *testing.T) { func TestGnuCashTransactionDatabaseFileParseImportedData_ParseValidAmount(t *testing.T) {
converter := GnuCashTransactionDataImporter importer := GnuCashTransactionDataImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -582,7 +584,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseValidAmount(t *tes
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
gnucashCommonValidDataCaseHeader+ gnucashCommonValidDataCaseHeader+
"<gnc:transaction version=\"2.0.0\">\n"+ "<gnc:transaction version=\"2.0.0\">\n"+
" <trn:date-posted>\n"+ " <trn:date-posted>\n"+
@@ -599,13 +601,13 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseValidAmount(t *tes
" </trn:split>\n"+ " </trn:split>\n"+
" </trn:splits>\n"+ " </trn:splits>\n"+
"</gnc:transaction>\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.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, int64(1234500), allNewTransactions[0].Amount) assert.Equal(t, int64(1234500), allNewTransactions[0].Amount)
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte( allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
gnucashCommonValidDataCaseHeader+ gnucashCommonValidDataCaseHeader+
"<gnc:transaction version=\"2.0.0\">\n"+ "<gnc:transaction version=\"2.0.0\">\n"+
" <trn:date-posted>\n"+ " <trn:date-posted>\n"+
@@ -622,7 +624,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseValidAmount(t *tes
" </trn:split>\n"+ " </trn:split>\n"+
" </trn:splits>\n"+ " </trn:splits>\n"+
"</gnc:transaction>\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.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
@@ -630,7 +632,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseValidAmount(t *tes
} }
func TestGnuCashTransactionDatabaseFileParseImportedData_ParseInvalidAmount(t *testing.T) { func TestGnuCashTransactionDatabaseFileParseImportedData_ParseInvalidAmount(t *testing.T) {
converter := GnuCashTransactionDataImporter importer := GnuCashTransactionDataImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -638,7 +640,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseInvalidAmount(t *t
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
gnucashCommonValidDataCaseHeader+ gnucashCommonValidDataCaseHeader+
"<gnc:transaction version=\"2.0.0\">\n"+ "<gnc:transaction version=\"2.0.0\">\n"+
" <trn:date-posted>\n"+ " <trn:date-posted>\n"+
@@ -655,10 +657,10 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseInvalidAmount(t *t
" </trn:split>\n"+ " </trn:split>\n"+
" </trn:splits>\n"+ " </trn:splits>\n"+
"</gnc:transaction>\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) assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
gnucashCommonValidDataCaseHeader+ gnucashCommonValidDataCaseHeader+
"<gnc:transaction version=\"2.0.0\">\n"+ "<gnc:transaction version=\"2.0.0\">\n"+
" <trn:date-posted>\n"+ " <trn:date-posted>\n"+
@@ -675,10 +677,10 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseInvalidAmount(t *t
" </trn:split>\n"+ " </trn:split>\n"+
" </trn:splits>\n"+ " </trn:splits>\n"+
"</gnc:transaction>\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) assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
gnucashCommonValidDataCaseHeader+ gnucashCommonValidDataCaseHeader+
"<gnc:transaction version=\"2.0.0\">\n"+ "<gnc:transaction version=\"2.0.0\">\n"+
" <trn:date-posted>\n"+ " <trn:date-posted>\n"+
@@ -695,12 +697,12 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseInvalidAmount(t *t
" </trn:split>\n"+ " </trn:split>\n"+
" </trn:splits>\n"+ " </trn:splits>\n"+
"</gnc:transaction>\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) assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
} }
func TestGnuCashTransactionDatabaseFileParseImportedData_ParseDescription(t *testing.T) { func TestGnuCashTransactionDatabaseFileParseImportedData_ParseDescription(t *testing.T) {
converter := GnuCashTransactionDataImporter importer := GnuCashTransactionDataImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -708,7 +710,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseDescription(t *tes
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
gnucashCommonValidDataCaseHeader+ gnucashCommonValidDataCaseHeader+
"<gnc:transaction version=\"2.0.0\">\n"+ "<gnc:transaction version=\"2.0.0\">\n"+
" <trn:date-posted>\n"+ " <trn:date-posted>\n"+
@@ -726,7 +728,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseDescription(t *tes
" </trn:split>\n"+ " </trn:split>\n"+
" </trn:splits>\n"+ " </trn:splits>\n"+
"</gnc:transaction>\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.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
@@ -734,7 +736,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseDescription(t *tes
} }
func TestGnuCashTransactionDatabaseFileParseImportedData_SkipZeroAmountTransaction(t *testing.T) { func TestGnuCashTransactionDatabaseFileParseImportedData_SkipZeroAmountTransaction(t *testing.T) {
converter := GnuCashTransactionDataImporter importer := GnuCashTransactionDataImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -742,7 +744,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_SkipZeroAmountTransacti
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
gnucashCommonValidDataCaseHeader+ gnucashCommonValidDataCaseHeader+
"<gnc:transaction version=\"2.0.0\">\n"+ "<gnc:transaction version=\"2.0.0\">\n"+
" <trn:date-posted>\n"+ " <trn:date-posted>\n"+
@@ -755,12 +757,12 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_SkipZeroAmountTransacti
" </trn:split>\n"+ " </trn:split>\n"+
" </trn:splits>\n"+ " </trn:splits>\n"+
"</gnc:transaction>\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) assert.EqualError(t, err, errs.ErrNotFoundTransactionDataInFile.Message)
} }
func TestGnuCashTransactionDatabaseFileParseImportedData_NotSupportedToParseSplitTransaction(t *testing.T) { func TestGnuCashTransactionDatabaseFileParseImportedData_NotSupportedToParseSplitTransaction(t *testing.T) {
converter := GnuCashTransactionDataImporter importer := GnuCashTransactionDataImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -768,7 +770,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_NotSupportedToParseSpli
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
gnucashCommonValidDataCaseHeader+ gnucashCommonValidDataCaseHeader+
"<gnc:account version=\"2.0.0\">\n"+ "<gnc:account version=\"2.0.0\">\n"+
" <act:name>Test Category2</act:name>\n"+ " <act:name>Test Category2</act:name>\n"+
@@ -805,12 +807,12 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_NotSupportedToParseSpli
" </trn:split>\n"+ " </trn:split>\n"+
" </trn:splits>\n"+ " </trn:splits>\n"+
"</gnc:transaction>\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) assert.EqualError(t, err, errs.ErrNotSupportedSplitTransactions.Message)
} }
func TestGnuCashTransactionDatabaseFileParseImportedData_MissingAccountRequiredNode(t *testing.T) { func TestGnuCashTransactionDatabaseFileParseImportedData_MissingAccountRequiredNode(t *testing.T) {
converter := GnuCashTransactionDataImporter importer := GnuCashTransactionDataImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -819,7 +821,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_MissingAccountRequiredN
} }
// Missing Account Currency Node // Missing Account Currency Node
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n"+ "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n"+
"<gnc-v2\n"+ "<gnc-v2\n"+
" xmlns:gnc=\"http://www.gnucash.org/XML/gnc\"\n"+ " xmlns:gnc=\"http://www.gnucash.org/XML/gnc\"\n"+
@@ -872,12 +874,12 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_MissingAccountRequiredN
" </trn:split>\n"+ " </trn:split>\n"+
" </trn:splits>\n"+ " </trn:splits>\n"+
"</gnc:transaction>\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) assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message)
} }
func TestGnuCashTransactionDatabaseFileParseImportedData_MissingTransactionRequiredNode(t *testing.T) { func TestGnuCashTransactionDatabaseFileParseImportedData_MissingTransactionRequiredNode(t *testing.T) {
converter := GnuCashTransactionDataImporter importer := GnuCashTransactionDataImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -886,7 +888,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_MissingTransactionRequi
} }
// Missing Transaction Time Node // Missing Transaction Time Node
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
gnucashCommonValidDataCaseHeader+ gnucashCommonValidDataCaseHeader+
"<gnc:transaction version=\"2.0.0\">\n"+ "<gnc:transaction version=\"2.0.0\">\n"+
" <trn:splits>\n"+ " <trn:splits>\n"+
@@ -900,22 +902,22 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_MissingTransactionRequi
" </trn:split>\n"+ " </trn:split>\n"+
" </trn:splits>\n"+ " </trn:splits>\n"+
"</gnc:transaction>\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) assert.EqualError(t, err, errs.ErrMissingTransactionTime.Message)
// Missing Transaction Splits Node // Missing Transaction Splits Node
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
gnucashCommonValidDataCaseHeader+ gnucashCommonValidDataCaseHeader+
"<gnc:transaction version=\"2.0.0\">\n"+ "<gnc:transaction version=\"2.0.0\">\n"+
" <trn:date-posted>\n"+ " <trn:date-posted>\n"+
" <ts:date>2024-09-01 00:00:00 +0000</ts:date>\n"+ " <ts:date>2024-09-01 00:00:00 +0000</ts:date>\n"+
" </trn:date-posted>\n"+ " </trn:date-posted>\n"+
"</gnc:transaction>\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) assert.EqualError(t, err, errs.ErrInvalidGnuCashFile.Message)
// Missing Transaction Split Quantity Node // Missing Transaction Split Quantity Node
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
gnucashCommonValidDataCaseHeader+ gnucashCommonValidDataCaseHeader+
"<gnc:transaction version=\"2.0.0\">\n"+ "<gnc:transaction version=\"2.0.0\">\n"+
" <trn:date-posted>\n"+ " <trn:date-posted>\n"+
@@ -930,11 +932,11 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_MissingTransactionRequi
" </trn:split>\n"+ " </trn:split>\n"+
" </trn:splits>\n"+ " </trn:splits>\n"+
"</gnc:transaction>\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) assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
// Missing Transaction Split Account Node // Missing Transaction Split Account Node
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
gnucashCommonValidDataCaseHeader+ gnucashCommonValidDataCaseHeader+
"<gnc:transaction version=\"2.0.0\">\n"+ "<gnc:transaction version=\"2.0.0\">\n"+
" <trn:date-posted>\n"+ " <trn:date-posted>\n"+
@@ -950,7 +952,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_MissingTransactionRequi
" </trn:split>\n"+ " </trn:split>\n"+
" </trn:splits>\n"+ " </trn:splits>\n"+
"</gnc:transaction>\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) 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_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 { if len(gnucashTransaction.Splits) == 2 {
splitData1 := gnucashTransaction.Splits[0] splitData1 := gnucashTransaction.Splits[0]
@@ -1,6 +1,8 @@
package iif package iif
import ( import (
"time"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter" "github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/core" "github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/models" "github.com/mayswind/ezbookkeeping/pkg/models"
@@ -23,7 +25,7 @@ var (
) )
// ParseImportedData returns the imported data by parsing the intuit interchange format (iif) data // 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) iifDataReader := createNewIifDataReader(data)
accountDatasets, transactionDatasets, err := iifDataReader.read(ctx) accountDatasets, transactionDatasets, err := iifDataReader.read(ctx)
@@ -39,5 +41,5 @@ func (c *iifTransactionDataFileImporter) ParseImportedData(ctx core.Context, use
dataTableImporter := converter.CreateNewSimpleImporterWithTypeNameMapping(iifTransactionTypeNameMapping) 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 ( import (
"testing" "testing"
"time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/core" "github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs" "github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/models" "github.com/mayswind/ezbookkeeping/pkg/models"
@@ -12,7 +14,7 @@ import (
) )
func TestIIFTransactionDataFileParseImportedData_MinimumValidData(t *testing.T) { func TestIIFTransactionDataFileParseImportedData_MinimumValidData(t *testing.T) {
converter := IifTransactionDataFileImporter importer := IifTransactionDataFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -20,7 +22,7 @@ func TestIIFTransactionDataFileParseImportedData_MinimumValidData(t *testing.T)
DefaultCurrency: "CNY", 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\tNAME\tACCNTTYPE\n"+
"ACCNT\tTest Account\tBANK\n"+ "ACCNT\tTest Account\tBANK\n"+
"ACCNT\tTest Account2\tBANK\n"+ "ACCNT\tTest Account2\tBANK\n"+
@@ -49,7 +51,7 @@ func TestIIFTransactionDataFileParseImportedData_MinimumValidData(t *testing.T)
"ENDTRNS\t\t\t\t\n"+ "ENDTRNS\t\t\t\t\n"+
"TRNS\tCREDIT CARD\t09/07/2024\tTest Category2\t34.56\n"+ "TRNS\tCREDIT CARD\t09/07/2024\tTest Category2\t34.56\n"+
"SPL\tCREDIT CARD\t09/07/2024\tTest Account2\t-34.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) assert.Nil(t, err)
@@ -132,7 +134,7 @@ func TestIIFTransactionDataFileParseImportedData_MinimumValidData(t *testing.T)
} }
func TestIIFTransactionDataFileParseImportedData_MinimumValidDataWithoutAccountData(t *testing.T) { func TestIIFTransactionDataFileParseImportedData_MinimumValidDataWithoutAccountData(t *testing.T) {
converter := IifTransactionDataFileImporter importer := IifTransactionDataFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -140,13 +142,13 @@ func TestIIFTransactionDataFileParseImportedData_MinimumValidDataWithoutAccountD
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+ "!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!SPL\tDATE\tACCNT\tAMOUNT\n"+ "!SPL\tDATE\tACCNT\tAMOUNT\n"+
"!ENDTRNS\t\t\t\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 Category\t-123.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) assert.Nil(t, err)
@@ -162,7 +164,7 @@ func TestIIFTransactionDataFileParseImportedData_MinimumValidDataWithoutAccountD
} }
func TestIIFTransactionDataFileParseImportedData_MultipleDataset(t *testing.T) { func TestIIFTransactionDataFileParseImportedData_MultipleDataset(t *testing.T) {
converter := IifTransactionDataFileImporter importer := IifTransactionDataFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -170,7 +172,7 @@ func TestIIFTransactionDataFileParseImportedData_MultipleDataset(t *testing.T) {
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"!ACCNT\tNAME\tACCNTTYPE\n"+ "!ACCNT\tNAME\tACCNTTYPE\n"+
"ACCNT\tTest Account3\tBANK\n"+ "ACCNT\tTest Account3\tBANK\n"+
"ACCNT\tTest Account4\tBANK\n"+ "ACCNT\tTest Account4\tBANK\n"+
@@ -202,7 +204,7 @@ func TestIIFTransactionDataFileParseImportedData_MultipleDataset(t *testing.T) {
"ENDTRNS\t\t\t\t\n"+ "ENDTRNS\t\t\t\t\n"+
"!ACCNT\tTEST\tNAME\tACCNTTYPE\n"+ "!ACCNT\tTEST\tNAME\tACCNTTYPE\n"+
"ACCNT\t\tTest Category\tINC\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) assert.Nil(t, err)
@@ -247,7 +249,7 @@ func TestIIFTransactionDataFileParseImportedData_MultipleDataset(t *testing.T) {
} }
func TestIIFTransactionDataFileParseImportedData_ParseCategoryAndSubCategory(t *testing.T) { func TestIIFTransactionDataFileParseImportedData_ParseCategoryAndSubCategory(t *testing.T) {
converter := IifTransactionDataFileImporter importer := IifTransactionDataFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -255,7 +257,7 @@ func TestIIFTransactionDataFileParseImportedData_ParseCategoryAndSubCategory(t *
DefaultCurrency: "CNY", 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\tNAME\tACCNTTYPE\n"+
"ACCNT\tTest Parent Category:Test Category\tINC\n"+ "ACCNT\tTest Parent Category:Test Category\tINC\n"+
"ACCNT\tTest Parent Category2:Test Category2\tEXP\n"+ "ACCNT\tTest Parent Category2:Test Category2\tEXP\n"+
@@ -267,7 +269,7 @@ func TestIIFTransactionDataFileParseImportedData_ParseCategoryAndSubCategory(t *
"ENDTRNS\t\t\t\n"+ "ENDTRNS\t\t\t\n"+
"TRNS\t09/02/2024\tTest Account2\t-123.45\n"+ "TRNS\t09/02/2024\tTest Account2\t-123.45\n"+
"SPL\t09/02/2024\tTest Parent Category2:Test Category2\t123.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) assert.Nil(t, err)
@@ -299,7 +301,7 @@ func TestIIFTransactionDataFileParseImportedData_ParseCategoryAndSubCategory(t *
} }
func TestIIFTransactionDataFileParseImportedData_ParseYearMonthDayFormatTime(t *testing.T) { func TestIIFTransactionDataFileParseImportedData_ParseYearMonthDayFormatTime(t *testing.T) {
converter := IifTransactionDataFileImporter importer := IifTransactionDataFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -307,7 +309,7 @@ func TestIIFTransactionDataFileParseImportedData_ParseYearMonthDayFormatTime(t *
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+ "!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!SPL\tDATE\tACCNT\tAMOUNT\n"+ "!SPL\tDATE\tACCNT\tAMOUNT\n"+
"!ENDTRNS\t\t\t\n"+ "!ENDTRNS\t\t\t\n"+
@@ -322,7 +324,7 @@ func TestIIFTransactionDataFileParseImportedData_ParseYearMonthDayFormatTime(t *
"ENDTRNS\t\t\t\n"+ "ENDTRNS\t\t\t\n"+
"TRNS\t2024/9/4\tTest Account\t123.45\n"+ "TRNS\t2024/9/4\tTest Account\t123.45\n"+
"SPL\t2024/9/4\tTest Account2\t-123.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) assert.Nil(t, err)
@@ -334,7 +336,7 @@ func TestIIFTransactionDataFileParseImportedData_ParseYearMonthDayFormatTime(t *
} }
func TestIIFTransactionDataFileParseImportedData_ParseShortMonthDayYearFormatTime(t *testing.T) { func TestIIFTransactionDataFileParseImportedData_ParseShortMonthDayYearFormatTime(t *testing.T) {
converter := IifTransactionDataFileImporter importer := IifTransactionDataFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -342,7 +344,7 @@ func TestIIFTransactionDataFileParseImportedData_ParseShortMonthDayYearFormatTim
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+ "!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!SPL\tDATE\tACCNT\tAMOUNT\n"+ "!SPL\tDATE\tACCNT\tAMOUNT\n"+
"!ENDTRNS\t\t\t\n"+ "!ENDTRNS\t\t\t\n"+
@@ -354,7 +356,7 @@ func TestIIFTransactionDataFileParseImportedData_ParseShortMonthDayYearFormatTim
"ENDTRNS\t\t\t\n"+ "ENDTRNS\t\t\t\n"+
"TRNS\t9/3/2024\tTest Account\t123.45\n"+ "TRNS\t9/3/2024\tTest Account\t123.45\n"+
"SPL\t9/3/2024\tTest Account2\t-123.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) assert.Nil(t, err)
@@ -365,7 +367,7 @@ func TestIIFTransactionDataFileParseImportedData_ParseShortMonthDayYearFormatTim
} }
func TestIIFTransactionDataFileParseImportedData_ParseShortMonthDayTwoDigitsYearFormatTime(t *testing.T) { func TestIIFTransactionDataFileParseImportedData_ParseShortMonthDayTwoDigitsYearFormatTime(t *testing.T) {
converter := IifTransactionDataFileImporter importer := IifTransactionDataFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -373,7 +375,7 @@ func TestIIFTransactionDataFileParseImportedData_ParseShortMonthDayTwoDigitsYear
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+ "!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!SPL\tDATE\tACCNT\tAMOUNT\n"+ "!SPL\tDATE\tACCNT\tAMOUNT\n"+
"!ENDTRNS\t\t\t\n"+ "!ENDTRNS\t\t\t\n"+
@@ -385,7 +387,7 @@ func TestIIFTransactionDataFileParseImportedData_ParseShortMonthDayTwoDigitsYear
"ENDTRNS\t\t\t\n"+ "ENDTRNS\t\t\t\n"+
"TRNS\t24/9/3\tTest Account\t123.45\n"+ "TRNS\t24/9/3\tTest Account\t123.45\n"+
"SPL\t24/9/3\tTest Account2\t-123.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) assert.Nil(t, err)
@@ -396,7 +398,7 @@ func TestIIFTransactionDataFileParseImportedData_ParseShortMonthDayTwoDigitsYear
} }
func TestIIFTransactionDataFileParseImportedData_ParseInvalidTime(t *testing.T) { func TestIIFTransactionDataFileParseImportedData_ParseInvalidTime(t *testing.T) {
converter := IifTransactionDataFileImporter importer := IifTransactionDataFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -404,36 +406,36 @@ func TestIIFTransactionDataFileParseImportedData_ParseInvalidTime(t *testing.T)
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+ "!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!SPL\tDATE\tACCNT\tAMOUNT\n"+ "!SPL\tDATE\tACCNT\tAMOUNT\n"+
"!ENDTRNS\t\t\t\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"+ "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) assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+ "!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!SPL\tDATE\tACCNT\tAMOUNT\n"+ "!SPL\tDATE\tACCNT\tAMOUNT\n"+
"!ENDTRNS\t\t\t\n"+ "!ENDTRNS\t\t\t\n"+
"TRNS\t2024-09-01\tTest Account\t123.45\n"+ "TRNS\t2024-09-01\tTest Account\t123.45\n"+
"SPL\t2024-09-01\tTest Account2\t-123.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) assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+ "!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!SPL\tDATE\tACCNT\tAMOUNT\n"+ "!SPL\tDATE\tACCNT\tAMOUNT\n"+
"!ENDTRNS\t\t\t\n"+ "!ENDTRNS\t\t\t\n"+
"TRNS\t9/24\tTest Account\t123.45\n"+ "TRNS\t9/24\tTest Account\t123.45\n"+
"SPL\t9/24\tTest Account2\t-123.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) assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
} }
func TestIIFTransactionDataFileParseImportedData_ParseAmountWithThousandsSeparator(t *testing.T) { func TestIIFTransactionDataFileParseImportedData_ParseAmountWithThousandsSeparator(t *testing.T) {
converter := IifTransactionDataFileImporter importer := IifTransactionDataFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -441,13 +443,13 @@ func TestIIFTransactionDataFileParseImportedData_ParseAmountWithThousandsSeparat
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+ "!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!SPL\tDATE\tACCNT\tAMOUNT\n"+ "!SPL\tDATE\tACCNT\tAMOUNT\n"+
"!ENDTRNS\t\t\t\n"+ "!ENDTRNS\t\t\t\n"+
"TRNS\t9/01/2024\tTest Account\t123,456.78\n"+ "TRNS\t9/01/2024\tTest Account\t123,456.78\n"+
"SPL\t9/01/2024\tTest Account2\t-123,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) assert.Nil(t, err)
@@ -456,7 +458,7 @@ func TestIIFTransactionDataFileParseImportedData_ParseAmountWithThousandsSeparat
} }
func TestIIFTransactionDataFileParseImportedData_ParseInvalidAmount(t *testing.T) { func TestIIFTransactionDataFileParseImportedData_ParseInvalidAmount(t *testing.T) {
converter := IifTransactionDataFileImporter importer := IifTransactionDataFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -464,27 +466,27 @@ func TestIIFTransactionDataFileParseImportedData_ParseInvalidAmount(t *testing.T
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+ "!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!SPL\tDATE\tACCNT\tAMOUNT\n"+ "!SPL\tDATE\tACCNT\tAMOUNT\n"+
"!ENDTRNS\t\t\t\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"+ "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) assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+ "!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!SPL\tDATE\tACCNT\tAMOUNT\n"+ "!SPL\tDATE\tACCNT\tAMOUNT\n"+
"!ENDTRNS\t\t\t\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"+ "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) assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
} }
func TestIIFTransactionDataFileParseImportedData_ParseDescription(t *testing.T) { func TestIIFTransactionDataFileParseImportedData_ParseDescription(t *testing.T) {
converter := IifTransactionDataFileImporter importer := IifTransactionDataFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -492,25 +494,25 @@ func TestIIFTransactionDataFileParseImportedData_ParseDescription(t *testing.T)
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tNAME\tAMOUNT\tMEMO\n"+ "!TRNS\tDATE\tACCNT\tNAME\tAMOUNT\tMEMO\n"+
"!SPL\tDATE\tACCNT\tNAME\tAMOUNT\tMEMO\n"+ "!SPL\tDATE\tACCNT\tNAME\tAMOUNT\tMEMO\n"+
"!ENDTRNS\t\t\t\t\t\n"+ "!ENDTRNS\t\t\t\t\t\n"+
"TRNS\t09/01/2024\tTest Account\t\"Test\"\t123.45\t\"foo bar\t#test\"\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"+ "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.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, "foo bar\t#test", allNewTransactions[0].Comment) 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"+ "!TRNS\tDATE\tACCNT\tNAME\tAMOUNT\tMEMO\n"+
"!SPL\tDATE\tACCNT\tNAME\tAMOUNT\tMEMO\n"+ "!SPL\tDATE\tACCNT\tNAME\tAMOUNT\tMEMO\n"+
"!ENDTRNS\t\t\t\t\t\n"+ "!ENDTRNS\t\t\t\t\t\n"+
"TRNS\t09/01/2024\tTest Account\tTest\t123.45\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"+ "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.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
@@ -518,7 +520,7 @@ func TestIIFTransactionDataFileParseImportedData_ParseDescription(t *testing.T)
} }
func TestIIFTransactionDataFileParseImportedData_ParseSplitTransaction(t *testing.T) { func TestIIFTransactionDataFileParseImportedData_ParseSplitTransaction(t *testing.T) {
converter := IifTransactionDataFileImporter importer := IifTransactionDataFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -526,7 +528,7 @@ func TestIIFTransactionDataFileParseImportedData_ParseSplitTransaction(t *testin
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
allNewTransactions, allNewAccounts, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( allNewTransactions, allNewAccounts, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"!ACCNT\tNAME\tACCNTTYPE\n"+ "!ACCNT\tNAME\tACCNTTYPE\n"+
"ACCNT\tTest Category\tINC\n"+ "ACCNT\tTest Category\tINC\n"+
"ACCNT\tTest Category2\tEXP\n"+ "ACCNT\tTest Category2\tEXP\n"+
@@ -552,7 +554,7 @@ func TestIIFTransactionDataFileParseImportedData_ParseSplitTransaction(t *testin
"TRNS\t09/05/2024\tTest Category2\t100.00\n"+ "TRNS\t09/05/2024\tTest Category2\t100.00\n"+
"SPL\t09/05/2024\tTest Account3\t-40.00\n"+ "SPL\t09/05/2024\tTest Account3\t-40.00\n"+
"SPL\t09/05/2024\tTest Account4\t-60.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) assert.Nil(t, err)
@@ -651,7 +653,7 @@ func TestIIFTransactionDataFileParseImportedData_ParseSplitTransaction(t *testin
} }
func TestIIFTransactionDataFileParseImportedData_ParseSplitTransactionDescription(t *testing.T) { func TestIIFTransactionDataFileParseImportedData_ParseSplitTransactionDescription(t *testing.T) {
converter := IifTransactionDataFileImporter importer := IifTransactionDataFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -659,21 +661,21 @@ func TestIIFTransactionDataFileParseImportedData_ParseSplitTransactionDescriptio
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tNAME\tAMOUNT\tMEMO\n"+ "!TRNS\tDATE\tACCNT\tNAME\tAMOUNT\tMEMO\n"+
"!SPL\tDATE\tACCNT\tNAME\tAMOUNT\tMEMO\n"+ "!SPL\tDATE\tACCNT\tNAME\tAMOUNT\tMEMO\n"+
"!ENDTRNS\t\t\t\t\t\n"+ "!ENDTRNS\t\t\t\t\t\n"+
"TRNS\t09/01/2024\tTest Account\t\"Test\"\t123.45\t\"foo bar\t#test\"\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 Account2\t\t-100.00\t\"foo\ttest#bar\"\n"+
"SPL\t09/01/2024\tTest Account3\t\t-23.45\t\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.Nil(t, err)
assert.Equal(t, 2, len(allNewTransactions)) assert.Equal(t, 2, len(allNewTransactions))
assert.Equal(t, "foo\ttest#bar", allNewTransactions[0].Comment) assert.Equal(t, "foo\ttest#bar", allNewTransactions[0].Comment)
assert.Equal(t, "foo bar\t#test", allNewTransactions[1].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"+ "!TRNS\tDATE\tACCNT\tNAME\tAMOUNT\tMEMO\n"+
"!SPL\tDATE\tACCNT\tNAME\tAMOUNT\tMEMO\n"+ "!SPL\tDATE\tACCNT\tNAME\tAMOUNT\tMEMO\n"+
"!ENDTRNS\t\t\t\t\t\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 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 Account3\tfoo\t-12.34\t\n"+
"SPL\t09/01/2024\tTest Account4\t\t-11.11\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.Nil(t, err)
assert.Equal(t, 3, len(allNewTransactions)) assert.Equal(t, 3, len(allNewTransactions))
@@ -691,7 +693,7 @@ func TestIIFTransactionDataFileParseImportedData_ParseSplitTransactionDescriptio
} }
func TestIIFTransactionDataFileParseImportedData_NotSupportedSplitTransaction(t *testing.T) { func TestIIFTransactionDataFileParseImportedData_NotSupportedSplitTransaction(t *testing.T) {
converter := IifTransactionDataFileImporter importer := IifTransactionDataFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -700,52 +702,52 @@ func TestIIFTransactionDataFileParseImportedData_NotSupportedSplitTransaction(t
} }
// Opening balance transaction // Opening balance transaction
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"!TRNS\tTRNSTYPE\tDATE\tACCNT\tAMOUNT\n"+ "!TRNS\tTRNSTYPE\tDATE\tACCNT\tAMOUNT\n"+
"!SPL\tTRNSTYPE\tDATE\tACCNT\tAMOUNT\n"+ "!SPL\tTRNSTYPE\tDATE\tACCNT\tAMOUNT\n"+
"!ENDTRNS\t\t\t\t\n"+ "!ENDTRNS\t\t\t\t\n"+
"TRNS\tBEGINBALCHECK\t09/01/2024\tTest Account\t123.45\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 Account2\t-100.00\n"+
"SPL\tBEGINBALCHECK\t09/01/2024\tTest Account3\t-23.45\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) assert.EqualError(t, err, errs.ErrNotSupportedSplitTransactions.Message)
// Transaction with invalid amount // Transaction with invalid amount
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+ "!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!SPL\tDATE\tACCNT\tAMOUNT\n"+ "!SPL\tDATE\tACCNT\tAMOUNT\n"+
"!ENDTRNS\t\t\t\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-100.00\n"+ "SPL\t09/01/2024\tTest Account2\t-100.00\n"+
"SPL\t09/01/2024\tTest Account3\t-23.45\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) assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
// Transaction split data with invalid amount // Transaction split data with invalid amount
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+ "!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!SPL\tDATE\tACCNT\tAMOUNT\n"+ "!SPL\tDATE\tACCNT\tAMOUNT\n"+
"!ENDTRNS\t\t\t\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-100 00\n"+ "SPL\t09/01/2024\tTest Account2\t-100 00\n"+
"SPL\t09/01/2024\tTest Account3\t-23.45\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) assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
// Transaction amount not equal to sum of split data amount // 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"+ "!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!SPL\tDATE\tACCNT\tAMOUNT\n"+ "!SPL\tDATE\tACCNT\tAMOUNT\n"+
"!ENDTRNS\t\t\t\n"+ "!ENDTRNS\t\t\t\n"+
"TRNS\t09/01/2024\tTest Account\t123.00\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 Account2\t-100.00\n"+
"SPL\t09/01/2024\tTest Account3\t-23.45\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) assert.EqualError(t, err, errs.ErrNotSupportedSplitTransactions.Message)
} }
func TestIIFTransactionDataFileParseImportedData_InvalidDataLines(t *testing.T) { func TestIIFTransactionDataFileParseImportedData_InvalidDataLines(t *testing.T) {
converter := IifTransactionDataFileImporter importer := IifTransactionDataFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -754,75 +756,75 @@ func TestIIFTransactionDataFileParseImportedData_InvalidDataLines(t *testing.T)
} }
//Missing Transaction Line //Missing Transaction Line
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+ "!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!SPL\tDATE\tACCNT\tAMOUNT\n"+ "!SPL\tDATE\tACCNT\tAMOUNT\n"+
"!ENDTRNS\t\t\t\n"+ "!ENDTRNS\t\t\t\n"+
"SPL\t09/01/2024\tTest Account2\t-123.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) assert.EqualError(t, err, errs.ErrInvalidIIFFile.Message)
// Missing Transaction And Split Line // Missing Transaction And Split Line
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+ "!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!SPL\tDATE\tACCNT\tAMOUNT\n"+ "!SPL\tDATE\tACCNT\tAMOUNT\n"+
"!ENDTRNS\t\t\t\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) assert.EqualError(t, err, errs.ErrInvalidIIFFile.Message)
// Missing Split Line // Missing Split Line
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+ "!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!SPL\tDATE\tACCNT\tAMOUNT\n"+ "!SPL\tDATE\tACCNT\tAMOUNT\n"+
"!ENDTRNS\t\t\t\n"+ "!ENDTRNS\t\t\t\n"+
"TRNS\t09/01/2024\tTest Account\t123.45\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) assert.EqualError(t, err, errs.ErrInvalidIIFFile.Message)
// Missing Transaction End Line // Missing Transaction End Line
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+ "!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!SPL\tDATE\tACCNT\tAMOUNT\n"+ "!SPL\tDATE\tACCNT\tAMOUNT\n"+
"!ENDTRNS\t\t\t\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"), 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) assert.EqualError(t, err, errs.ErrInvalidIIFFile.Message)
// Missing Transaction End Line (following is another header) // 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"+ "!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!SPL\tDATE\tACCNT\tAMOUNT\n"+ "!SPL\tDATE\tACCNT\tAMOUNT\n"+
"!ENDTRNS\t\t\t\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"+ "SPL\t09/01/2024\tTest Account2\t-123.45\n"+
"!ACCNT\tNAME\tACCNTTYPE\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) assert.EqualError(t, err, errs.ErrInvalidIIFFile.Message)
// Invalid Line // Invalid Line
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+ "!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!SPL\tDATE\tACCNT\tAMOUNT\n"+ "!SPL\tDATE\tACCNT\tAMOUNT\n"+
"!ENDTRNS\t\t\t\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"+ "SPL\t09/01/2024\tTest Account2\t-123.45\n"+
"TEST\t\t\t\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) assert.EqualError(t, err, errs.ErrInvalidIIFFile.Message)
// Repeat Transaction Line // Repeat Transaction Line
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+ "!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!SPL\tDATE\tACCNT\tAMOUNT\n"+ "!SPL\tDATE\tACCNT\tAMOUNT\n"+
"!ENDTRNS\t\t\t\n"+ "!ENDTRNS\t\t\t\n"+
"TRNS\t09/01/2024\tTest Account\t123.45\n"+ "TRNS\t09/01/2024\tTest Account\t123.45\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"+ "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) assert.EqualError(t, err, errs.ErrInvalidIIFFile.Message)
// Repeat Transaction End Line // Repeat Transaction End Line
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\t\n"+ "!TRNS\tDATE\tACCNT\tAMOUNT\t\n"+
"!SPL\tDATE\tACCNT\tAMOUNT\t\n"+ "!SPL\tDATE\tACCNT\tAMOUNT\t\n"+
"!ENDTRNS\t\t\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"+ "TRNS\t09/01/2024\tTest Account\t123.45\n"+
"SPL\t09/01/2024\tTest Account2\t-123.45\n"+ "SPL\t09/01/2024\tTest Account2\t-123.45\n"+
"ENDTRNS\t\t\t\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) assert.EqualError(t, err, errs.ErrInvalidIIFFile.Message)
} }
func TestIIFTransactionDataFileParseImportedData_InvalidHeaderLines(t *testing.T) { func TestIIFTransactionDataFileParseImportedData_InvalidHeaderLines(t *testing.T) {
converter := IifTransactionDataFileImporter importer := IifTransactionDataFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -844,49 +846,49 @@ func TestIIFTransactionDataFileParseImportedData_InvalidHeaderLines(t *testing.T
} }
// Missing All Sample Lines // 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"+ "TRNS\t09/01/2024\tTest Account\t123.45\n"+
"SPL\t09/01/2024\tTest Account2\t-123.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) assert.EqualError(t, err, errs.ErrInvalidIIFFile.Message)
// Missing Transaction Sample Line // Missing Transaction Sample Line
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"!SPL\tDATE\tACCNT\tAMOUNT\n"+ "!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) assert.EqualError(t, err, errs.ErrInvalidIIFFile.Message)
// Missing Split Sample Line // Missing Split Sample Line
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+ "!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) assert.EqualError(t, err, errs.ErrInvalidIIFFile.Message)
// Missing Transaction End Sample Line // Missing Transaction End Sample Line
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+ "!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) assert.EqualError(t, err, errs.ErrInvalidIIFFile.Message)
// Missing Transaction End Sample Line (following is data line) // 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"+ "!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!SPL\tDATE\tACCNT\tAMOUNT\n"+ "!SPL\tDATE\tACCNT\tAMOUNT\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"+ "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) assert.EqualError(t, err, errs.ErrInvalidIIFFile.Message)
// Invalid Sample Line // Invalid Sample Line
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+ "!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!TEST\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) assert.EqualError(t, err, errs.ErrInvalidIIFFile.Message)
} }
func TestIIFTransactionDataFileParseImportedData_MissingRequiredColumn(t *testing.T) { func TestIIFTransactionDataFileParseImportedData_MissingRequiredColumn(t *testing.T) {
converter := IifTransactionDataFileImporter importer := IifTransactionDataFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -895,32 +897,32 @@ func TestIIFTransactionDataFileParseImportedData_MissingRequiredColumn(t *testin
} }
// Missing Date Column // Missing Date Column
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"!TRNS\tACCNT\tAMOUNT\t\n"+ "!TRNS\tACCNT\tAMOUNT\t\n"+
"!SPL\tACCNT\tAMOUNT\t\n"+ "!SPL\tACCNT\tAMOUNT\t\n"+
"!ENDTRNS\t\t\t\n"+ "!ENDTRNS\t\t\t\n"+
"TRNS\tTest Account\t123.45\n"+ "TRNS\tTest Account\t123.45\n"+
"SPL\tTest Account2\t-123.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) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
// Missing Account Column // Missing Account Column
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tAMOUNT\t\n"+ "!TRNS\tDATE\tAMOUNT\t\n"+
"!SPL\tDATE\tAMOUNT\t\n"+ "!SPL\tDATE\tAMOUNT\t\n"+
"!ENDTRNS\t\t\t\n"+ "!ENDTRNS\t\t\t\n"+
"TRNS\t09/01/2024\t123.45\n"+ "TRNS\t09/01/2024\t123.45\n"+
"SPL\t09/01/2024\t-123.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) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
// Missing Amount Column // Missing Amount Column
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\t\n"+ "!TRNS\tDATE\tACCNT\t\n"+
"!SPL\tDATE\tACCNT\t\n"+ "!SPL\tDATE\tACCNT\t\n"+
"!ENDTRNS\t\t\t\n"+ "!ENDTRNS\t\t\t\n"+
"TRNS\t09/01/2024\tTest Account\n"+ "TRNS\t09/01/2024\tTest Account\n"+
"SPL\t09/01/2024\tTest Account2\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) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
} }
@@ -2,6 +2,7 @@ package jdcom
import ( import (
"bytes" "bytes"
"time"
"golang.org/x/text/encoding/unicode" "golang.org/x/text/encoding/unicode"
"golang.org/x/text/transform" "golang.org/x/text/transform"
@@ -27,7 +28,7 @@ var (
) )
// ParseImportedData returns the imported data by parsing the jd.com finance transaction csv data // 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() fallback := unicode.UTF8.NewDecoder()
reader := transform.NewReader(bytes.NewReader(data), unicode.BOMOverride(fallback)) 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) transactionDataTable := datatable.CreateNewTransactionDataTableFromCommonDataTable(commonDataTable, jdComFinanceTransactionSupportedColumns, transactionRowParser)
dataTableImporter := converter.CreateNewSimpleImporterWithTypeNameMapping(jdComFinanceTransactionTypeNameMapping) 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/stretchr/testify/assert"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/core" "github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs" "github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/models" "github.com/mayswind/ezbookkeeping/pkg/models"
@@ -13,7 +14,7 @@ import (
) )
func TestJDComFinanceCsvFileImporterParseImportedData_MinimumValidData(t *testing.T) { func TestJDComFinanceCsvFileImporterParseImportedData_MinimumValidData(t *testing.T) {
converter := JDComFinanceTransactionDataCsvFileImporter importer := JDComFinanceTransactionDataCsvFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ 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 12:34:56,xxx,xxx,123.45,银行卡,交易成功,支出,其他网购\n" +
"2025-09-01 23:59:59,xxx,京东钱包余额充值,0.05,银行卡,交易成功,不计收支,余额\n" + "2025-09-01 23:59:59,xxx,京东钱包余额充值,0.05,银行卡,交易成功,不计收支,余额\n" +
"2025-09-02 23:59:59,xxx,京东余额提现,0.03,银行卡,交易成功,不计收支,余额\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.Nil(t, err)
assert.Equal(t, 4, len(allNewTransactions)) assert.Equal(t, 4, len(allNewTransactions))
@@ -93,7 +94,7 @@ func TestJDComFinanceCsvFileImporterParseImportedData_MinimumValidData(t *testin
} }
func TestJDComFinanceCsvFileImporterParseImportedData_ParseRefundTransaction(t *testing.T) { func TestJDComFinanceCsvFileImporterParseImportedData_ParseRefundTransaction(t *testing.T) {
converter := JDComFinanceTransactionDataCsvFileImporter importer := JDComFinanceTransactionDataCsvFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -110,7 +111,7 @@ func TestJDComFinanceCsvFileImporterParseImportedData_ParseRefundTransaction(t *
"2025-09-01 02:34:56,xxx,xxx,0.12(已全额退款),银行卡,交易成功,不计收支\n" + "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 01:23:45,xxx,xxx,3.45,银行卡,退款成功,不计收支\n" +
"2025-09-02 02:34:56,xxx,xxx,123.45(已退款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.Nil(t, err)
assert.Equal(t, int64(1234567890), allNewTransactions[0].Uid) assert.Equal(t, int64(1234567890), allNewTransactions[0].Uid)
@@ -139,7 +140,7 @@ func TestJDComFinanceCsvFileImporterParseImportedData_ParseRefundTransaction(t *
} }
func TestJDComFinanceCsvFileImporterParseImportedData_ParseInvalidTime(t *testing.T) { func TestJDComFinanceCsvFileImporterParseImportedData_ParseInvalidTime(t *testing.T) {
converter := JDComFinanceTransactionDataCsvFileImporter importer := JDComFinanceTransactionDataCsvFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -153,7 +154,7 @@ func TestJDComFinanceCsvFileImporterParseImportedData_ParseInvalidTime(t *testin
"\n" + "\n" +
"交易时间,商户名称,交易说明,金额,收/付款方式,交易状态,收/支\n" + "交易时间,商户名称,交易说明,金额,收/付款方式,交易状态,收/支\n" +
"2025-09-01T01:23:45,xxx,xxx,0.12,银行卡,交易成功,支出\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) assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
data2 := "导出信息:\n" + data2 := "导出信息:\n" +
@@ -162,12 +163,12 @@ func TestJDComFinanceCsvFileImporterParseImportedData_ParseInvalidTime(t *testin
"\n" + "\n" +
"交易时间,商户名称,交易说明,金额,收/付款方式,交易状态,收/支\n" + "交易时间,商户名称,交易说明,金额,收/付款方式,交易状态,收/支\n" +
"09/01/2025 01:23:45,xxx,xxx,0.12,银行卡,交易成功,支出\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) assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
} }
func TestJDComFinanceCsvFileImporterParseImportedData_ParseInvalidType(t *testing.T) { func TestJDComFinanceCsvFileImporterParseImportedData_ParseInvalidType(t *testing.T) {
converter := JDComFinanceTransactionDataCsvFileImporter importer := JDComFinanceTransactionDataCsvFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -181,12 +182,12 @@ func TestJDComFinanceCsvFileImporterParseImportedData_ParseInvalidType(t *testin
"\n" + "\n" +
"交易时间,商户名称,交易说明,金额,收/付款方式,交易状态,收/支\n" + "交易时间,商户名称,交易说明,金额,收/付款方式,交易状态,收/支\n" +
"2025-09-01 01:23:45,xxx,xxx,0.12,银行卡,交易成功,转账\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) assert.EqualError(t, err, errs.ErrNotFoundTransactionDataInFile.Message)
} }
func TestJDComFinanceCsvFileImporterParseImportedData_ParseInvalidAmount(t *testing.T) { func TestJDComFinanceCsvFileImporterParseImportedData_ParseInvalidAmount(t *testing.T) {
converter := JDComFinanceTransactionDataCsvFileImporter importer := JDComFinanceTransactionDataCsvFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -200,12 +201,12 @@ func TestJDComFinanceCsvFileImporterParseImportedData_ParseInvalidAmount(t *test
"\n" + "\n" +
"交易时间,商户名称,交易说明,金额,收/付款方式,交易状态,收/支\n" + "交易时间,商户名称,交易说明,金额,收/付款方式,交易状态,收/支\n" +
"2025-09-01 01:23:45,xxx,xxx,¥0.12,银行卡,交易成功,支出\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) assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
} }
func TestJDComFinanceCsvFileImporterParseImportedData_ParseAccountName(t *testing.T) { func TestJDComFinanceCsvFileImporterParseImportedData_ParseAccountName(t *testing.T) {
converter := JDComFinanceTransactionDataCsvFileImporter importer := JDComFinanceTransactionDataCsvFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -220,7 +221,7 @@ func TestJDComFinanceCsvFileImporterParseImportedData_ParseAccountName(t *testin
"\n" + "\n" +
"交易时间,商户名称,交易说明,金额,收/付款方式,交易状态,收/支,交易分类\n" + "交易时间,商户名称,交易说明,金额,收/付款方式,交易状态,收/支,交易分类\n" +
"2025-09-01 01:23:45,xxx,京东钱包余额充值,0.05,银行卡,交易成功,不计收支,余额\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.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
@@ -235,7 +236,7 @@ func TestJDComFinanceCsvFileImporterParseImportedData_ParseAccountName(t *testin
"\n" + "\n" +
"交易时间,商户名称,交易说明,金额,收/付款方式,交易状态,收/支,交易分类\n" + "交易时间,商户名称,交易说明,金额,收/付款方式,交易状态,收/支,交易分类\n" +
"2025-09-01 01:23:45,xxx,京东余额提现,0.05,银行卡,交易成功,不计收支,余额\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.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) 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" "2025-09-01 01:23:45,xxx,京东小金库-转入,0.05,余额,交易成功,不计收支,小金库\n"
assert.Nil(t, err) 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.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) 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" "2025-09-01 01:23:45,xxx,京东小金库-转出,0.05,余额,交易成功,不计收支,小金库\n"
assert.Nil(t, err) 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.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) 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" "2025-09-01 01:23:45,xxx,价保退款,0.05,银行卡,交易成功,不计收支,其他\n"
assert.Nil(t, err) 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.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) 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" "2025-09-01 01:23:45,xxx,白条主动还款,0.05,银行卡,交易成功,不计收支,白条\n"
assert.Nil(t, err) 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.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
@@ -312,7 +313,7 @@ func TestJDComFinanceCsvFileImporterParseImportedData_ParseAccountName(t *testin
} }
func TestJDComFinanceCsvFileImporterParseImportedData_ParseDescription(t *testing.T) { func TestJDComFinanceCsvFileImporterParseImportedData_ParseDescription(t *testing.T) {
converter := JDComFinanceTransactionDataCsvFileImporter importer := JDComFinanceTransactionDataCsvFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -326,7 +327,7 @@ func TestJDComFinanceCsvFileImporterParseImportedData_ParseDescription(t *testin
"\n" + "\n" +
"交易时间,商户名称,交易说明,金额,收/付款方式,交易状态,收/支\n" + "交易时间,商户名称,交易说明,金额,收/付款方式,交易状态,收/支\n" +
"2025-09-01 01:23:45,xxx,,0.12,银行卡,交易成功,支出\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.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
@@ -338,7 +339,7 @@ func TestJDComFinanceCsvFileImporterParseImportedData_ParseDescription(t *testin
"\n" + "\n" +
"交易时间,商户名称,交易说明,交易说明,金额,收/付款方式,交易状态,收/支,备注\n" + "交易时间,商户名称,交易说明,交易说明,金额,收/付款方式,交易状态,收/支,备注\n" +
"2025-09-01 01:23:45,xxx,xxx,Test,0.12,银行卡,交易成功,支出,\"foo\"\"bar,\ntest\"\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, 1, len(allNewTransactions))
assert.Equal(t, "foo\"bar,\ntest", allNewTransactions[0].Comment) assert.Equal(t, "foo\"bar,\ntest", allNewTransactions[0].Comment)
@@ -348,13 +349,13 @@ func TestJDComFinanceCsvFileImporterParseImportedData_ParseDescription(t *testin
"\n" + "\n" +
"交易时间,商户名称,交易说明,交易说明,金额,收/付款方式,交易状态,收/支,备注\n" + "交易时间,商户名称,交易说明,交易说明,金额,收/付款方式,交易状态,收/支,备注\n" +
"2025-09-01 01:23:45,xxx,xxx,Test,0.12,银行卡,交易成功,支出,\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, 1, len(allNewTransactions))
assert.Equal(t, "Test", allNewTransactions[0].Comment) assert.Equal(t, "Test", allNewTransactions[0].Comment)
} }
func TestJDComFinanceCsvFileImporterParseImportedData_SkipUnknownStatusTransaction(t *testing.T) { func TestJDComFinanceCsvFileImporterParseImportedData_SkipUnknownStatusTransaction(t *testing.T) {
converter := JDComFinanceTransactionDataCsvFileImporter importer := JDComFinanceTransactionDataCsvFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -368,12 +369,12 @@ func TestJDComFinanceCsvFileImporterParseImportedData_SkipUnknownStatusTransacti
"\n" + "\n" +
"交易时间,商户名称,交易说明,金额,收/付款方式,交易状态,收/支\n" + "交易时间,商户名称,交易说明,金额,收/付款方式,交易状态,收/支\n" +
"2025-09-01 01:23:45,xxx,xxx,0.12,银行卡,xxxx,支出\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) assert.EqualError(t, err, errs.ErrNotFoundTransactionDataInFile.Message)
} }
func TestJDComFinanceCsvFileImporterParseImportedData_SkipUnknownMemoTransferTransaction(t *testing.T) { func TestJDComFinanceCsvFileImporterParseImportedData_SkipUnknownMemoTransferTransaction(t *testing.T) {
converter := JDComFinanceTransactionDataCsvFileImporter importer := JDComFinanceTransactionDataCsvFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -387,12 +388,12 @@ func TestJDComFinanceCsvFileImporterParseImportedData_SkipUnknownMemoTransferTra
"\n" + "\n" +
"交易时间,商户名称,交易说明,金额,收/付款方式,交易状态,收/支\n" + "交易时间,商户名称,交易说明,金额,收/付款方式,交易状态,收/支\n" +
"2025-09-01 01:23:45,xxx,xxx,0.12,银行卡,交易成功,不计收支\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) assert.EqualError(t, err, errs.ErrNotFoundTransactionDataInFile.Message)
} }
func TestJDComFinanceCsvFileImporterParseImportedData_MissingFileHeader(t *testing.T) { func TestJDComFinanceCsvFileImporterParseImportedData_MissingFileHeader(t *testing.T) {
converter := JDComFinanceTransactionDataCsvFileImporter importer := JDComFinanceTransactionDataCsvFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -402,15 +403,15 @@ func TestJDComFinanceCsvFileImporterParseImportedData_MissingFileHeader(t *testi
data := "交易时间,商户名称,交易说明,金额,收/付款方式,交易状态,收/支\n" + data := "交易时间,商户名称,交易说明,金额,收/付款方式,交易状态,收/支\n" +
"2025-09-01 01:23:45,xxx,xxx,0.12,银行卡,交易成功,支出\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) 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) assert.EqualError(t, err, errs.ErrInvalidFileHeader.Message)
} }
func TestJDComFinanceCsvFileImporterParseImportedData_MissingRequiredColumn(t *testing.T) { func TestJDComFinanceCsvFileImporterParseImportedData_MissingRequiredColumn(t *testing.T) {
converter := JDComFinanceTransactionDataCsvFileImporter importer := JDComFinanceTransactionDataCsvFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -425,7 +426,7 @@ func TestJDComFinanceCsvFileImporterParseImportedData_MissingRequiredColumn(t *t
"\n" + "\n" +
"商户名称,交易说明,金额,收/付款方式,交易状态,收/支\n" + "商户名称,交易说明,金额,收/付款方式,交易状态,收/支\n" +
"xxx,xxx,0.12,银行卡,交易成功,支出\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) assert.EqualError(t, err, errs.ErrInvalidFileHeader.Message)
// Missing Merchant Name Column // Missing Merchant Name Column
@@ -435,7 +436,7 @@ func TestJDComFinanceCsvFileImporterParseImportedData_MissingRequiredColumn(t *t
"\n" + "\n" +
"交易时间,交易说明,金额,收/付款方式,交易状态,收/支\n" + "交易时间,交易说明,金额,收/付款方式,交易状态,收/支\n" +
"2025-09-01 01:23:45,xxx,0.12,银行卡,交易成功,支出\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) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
// Missing Transaction Memo Column // Missing Transaction Memo Column
@@ -445,7 +446,7 @@ func TestJDComFinanceCsvFileImporterParseImportedData_MissingRequiredColumn(t *t
"\n" + "\n" +
"交易时间,商户名称,金额,收/付款方式,交易状态,收/支\n" + "交易时间,商户名称,金额,收/付款方式,交易状态,收/支\n" +
"2025-09-01 01:23:45,xxx,0.12,银行卡,交易成功,支出\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) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
// Missing Amount Column // Missing Amount Column
@@ -455,7 +456,7 @@ func TestJDComFinanceCsvFileImporterParseImportedData_MissingRequiredColumn(t *t
"\n" + "\n" +
"交易时间,商户名称,交易说明,收/付款方式,交易状态,收/支\n" + "交易时间,商户名称,交易说明,收/付款方式,交易状态,收/支\n" +
"2025-09-01 01:23:45,xxx,xxx,银行卡,交易成功,支出\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) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
// Missing Related Account Column // Missing Related Account Column
@@ -465,7 +466,7 @@ func TestJDComFinanceCsvFileImporterParseImportedData_MissingRequiredColumn(t *t
"\n" + "\n" +
"交易时间,商户名称,交易说明,金额,交易状态,收/支\n" + "交易时间,商户名称,交易说明,金额,交易状态,收/支\n" +
"2025-09-01 01:23:45,xxx,xxx,0.12,交易成功,支出\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) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
// Missing Status Column // Missing Status Column
@@ -475,7 +476,7 @@ func TestJDComFinanceCsvFileImporterParseImportedData_MissingRequiredColumn(t *t
"\n" + "\n" +
"交易时间,商户名称,交易说明,金额,收/付款方式,收/支\n" + "交易时间,商户名称,交易说明,金额,收/付款方式,收/支\n" +
"2025-09-01 01:23:45,xxx,xxx,0.12,银行卡,支出\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) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
// Missing Type Column // Missing Type Column
@@ -485,12 +486,12 @@ func TestJDComFinanceCsvFileImporterParseImportedData_MissingRequiredColumn(t *t
"\n" + "\n" +
"交易时间,商户名称,交易说明,金额,收/付款方式,交易状态\n" + "交易时间,商户名称,交易说明,金额,收/付款方式,交易状态\n" +
"2025-09-01 01:23:45,xxx,xxx,0.12,银行卡,交易成功\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) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
} }
func TestJDComFinanceCsvFileImporterParseImportedData_NoTransactionData(t *testing.T) { func TestJDComFinanceCsvFileImporterParseImportedData_NoTransactionData(t *testing.T) {
converter := JDComFinanceTransactionDataCsvFileImporter importer := JDComFinanceTransactionDataCsvFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -503,6 +504,6 @@ func TestJDComFinanceCsvFileImporterParseImportedData_NoTransactionData(t *testi
"日期区间:2025-01-01 至 2025-09-01\n" + "日期区间:2025-01-01 至 2025-09-01\n" +
"\n" + "\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) assert.EqualError(t, err, errs.ErrNotFoundTransactionDataInFile.Message)
} }
@@ -1,6 +1,8 @@
package mt package mt
import ( import (
"time"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter" "github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/core" "github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/models" "github.com/mayswind/ezbookkeeping/pkg/models"
@@ -22,7 +24,7 @@ var (
) )
// ParseImportedData returns the imported data by parsing the mt940 file statement data // 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) mt940DataReader := createNewMT940FileReader(data)
mt940Data, err := mt940DataReader.read(ctx) mt940Data, err := mt940DataReader.read(ctx)
@@ -38,5 +40,5 @@ func (c *mt940TransactionDataFileImporter) ParseImportedData(ctx core.Context, u
dataTableImporter := converter.CreateNewSimpleImporterWithTypeNameMapping(mt940TransactionTypeNameMapping) 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 ( import (
"testing" "testing"
"time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/core" "github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs" "github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/models" "github.com/mayswind/ezbookkeeping/pkg/models"
@@ -12,7 +14,7 @@ import (
) )
func TestMT940TransactionDataFileParseImportedData_MinimumValidData(t *testing.T) { func TestMT940TransactionDataFileParseImportedData_MinimumValidData(t *testing.T) {
converter := MT940TransactionDataFileImporter importer := MT940TransactionDataFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -20,7 +22,7 @@ func TestMT940TransactionDataFileParseImportedData_MinimumValidData(t *testing.T
DefaultCurrency: "CNY", 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: `{1:F01TESTBANK123456789}{2:I940TESTBANK}{4:
:20:123456789 :20:123456789
:25:12345678 :25:12345678
@@ -31,7 +33,7 @@ func TestMT940TransactionDataFileParseImportedData_MinimumValidData(t *testing.T
:61:2506020603D234,56NTRFFOOBAR :61:2506020603D234,56NTRFFOOBAR
:86:Transaction 2 :86:Transaction 2
:62F:C250601CNY123,45 :62F:C250601CNY123,45
-}`), 0, nil, nil, nil, nil, nil) -}`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err) assert.Nil(t, err)
@@ -70,7 +72,7 @@ func TestMT940TransactionDataFileParseImportedData_MinimumValidData(t *testing.T
} }
func TestMT940TransactionDataFileParseImportedData_ParseTransactionValidAmountAndCurrency(t *testing.T) { func TestMT940TransactionDataFileParseImportedData_ParseTransactionValidAmountAndCurrency(t *testing.T) {
converter := MT940TransactionDataFileImporter importer := MT940TransactionDataFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -78,7 +80,7 @@ func TestMT940TransactionDataFileParseImportedData_ParseTransactionValidAmountAn
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
`{1:F01TESTBANK123456789}{2:I940TESTBANK}{4: `{1:F01TESTBANK123456789}{2:I940TESTBANK}{4:
:20:123456789 :20:123456789
:25:12345678 :25:12345678
@@ -91,7 +93,7 @@ func TestMT940TransactionDataFileParseImportedData_ParseTransactionValidAmountAn
:61:250603C1,NTRFTEST :61:250603C1,NTRFTEST
:86:Transaction 3 :86:Transaction 3
:62F:C250601CNY123,45 :62F:C250601CNY123,45
-}`), 0, nil, nil, nil, nil, nil) -}`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 3, len(allNewTransactions)) assert.Equal(t, 3, len(allNewTransactions))
@@ -101,7 +103,7 @@ func TestMT940TransactionDataFileParseImportedData_ParseTransactionValidAmountAn
} }
func TestMT940TransactionDataFileParseImportedData_ParseTransactionInvalidAmountAndCurrency(t *testing.T) { func TestMT940TransactionDataFileParseImportedData_ParseTransactionInvalidAmountAndCurrency(t *testing.T) {
converter := MT940TransactionDataFileImporter importer := MT940TransactionDataFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -109,7 +111,7 @@ func TestMT940TransactionDataFileParseImportedData_ParseTransactionInvalidAmount
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
`{1:F01TESTBANK123456789}{2:I940TESTBANK}{4: `{1:F01TESTBANK123456789}{2:I940TESTBANK}{4:
:20:123456789 :20:123456789
:25:12345678 :25:12345678
@@ -117,10 +119,10 @@ func TestMT940TransactionDataFileParseImportedData_ParseTransactionInvalidAmount
:60F:C250601CNY123,45 :60F:C250601CNY123,45
:61:2506010602C123 45NTRFTEST :61:2506010602C123 45NTRFTEST
:62F:C250601CNY123,45 :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) assert.EqualError(t, err, errs.ErrInvalidMT940File.Message)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
`{1:F01TESTBANK123456789}{2:I940TESTBANK}{4: `{1:F01TESTBANK123456789}{2:I940TESTBANK}{4:
:20:123456789 :20:123456789
:25:12345678 :25:12345678
@@ -128,12 +130,12 @@ func TestMT940TransactionDataFileParseImportedData_ParseTransactionInvalidAmount
:60F:C250601CNY123,45 :60F:C250601CNY123,45
:61:2506010602C12.45NTRFTEST :61:2506010602C12.45NTRFTEST
:62F:C250601CNY123,45 :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) assert.EqualError(t, err, errs.ErrInvalidMT940File.Message)
} }
func TestMT940TransactionDataFileParseImportedData_ParseTransactionType(t *testing.T) { func TestMT940TransactionDataFileParseImportedData_ParseTransactionType(t *testing.T) {
converter := MT940TransactionDataFileImporter importer := MT940TransactionDataFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -141,7 +143,7 @@ func TestMT940TransactionDataFileParseImportedData_ParseTransactionType(t *testi
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
`{1:F01TESTBANK123456789}{2:I940TESTBANK}{4: `{1:F01TESTBANK123456789}{2:I940TESTBANK}{4:
:20:123456789 :20:123456789
:25:12345678 :25:12345678
@@ -156,7 +158,7 @@ func TestMT940TransactionDataFileParseImportedData_ParseTransactionType(t *testi
:61:250604RD123,45NTRFTEST :61:250604RD123,45NTRFTEST
:86:Transaction 4 :86:Transaction 4
:62F:C250601CNY123,45 :62F:C250601CNY123,45
-}`), 0, nil, nil, nil, nil, nil) -}`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 4, len(allNewTransactions)) assert.Equal(t, 4, len(allNewTransactions))
@@ -167,7 +169,7 @@ func TestMT940TransactionDataFileParseImportedData_ParseTransactionType(t *testi
} }
func TestMT940TransactionDataFileParseImportedData_ParseDescription(t *testing.T) { func TestMT940TransactionDataFileParseImportedData_ParseDescription(t *testing.T) {
converter := MT940TransactionDataFileImporter importer := MT940TransactionDataFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -175,7 +177,7 @@ func TestMT940TransactionDataFileParseImportedData_ParseDescription(t *testing.T
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
`{1:F01TESTBANK123456789}{2:I940TESTBANK}{4: `{1:F01TESTBANK123456789}{2:I940TESTBANK}{4:
:20:123456789 :20:123456789
:25:12345678 :25:12345678
@@ -186,7 +188,7 @@ func TestMT940TransactionDataFileParseImportedData_ParseDescription(t *testing.T
Part 2 Part 2
Part 3 Part 3
:62F:C250601CNY123,45 :62F:C250601CNY123,45
-}`), 0, nil, nil, nil, nil, nil) -}`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
@@ -194,7 +196,7 @@ func TestMT940TransactionDataFileParseImportedData_ParseDescription(t *testing.T
} }
func TestMT940TransactionDataFileParseImportedData_MissingRequiredField(t *testing.T) { func TestMT940TransactionDataFileParseImportedData_MissingRequiredField(t *testing.T) {
converter := MT940TransactionDataFileImporter importer := MT940TransactionDataFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -203,12 +205,12 @@ func TestMT940TransactionDataFileParseImportedData_MissingRequiredField(t *testi
} }
// Missing opening balance and closing balance // Missing opening balance and closing balance
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
`{1:F01TESTBANK123456789}{2:I940TESTBANK}{4: `{1:F01TESTBANK123456789}{2:I940TESTBANK}{4:
:20:123456789 :20:123456789
:28C:123/1 :28C:123/1
:61:250601C123,45NTRFTEST :61:250601C123,45NTRFTEST
:86:Transaction 1 :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) assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message)
} }
+1 -1
View File
@@ -23,7 +23,7 @@ import (
const ofx1USAsciiEncoding = "usascii" const ofx1USAsciiEncoding = "usascii"
const ofx1UnicodeEncoding = "unicode" 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" const ofx1SGMLDataFormat = "OFXSGML"
var ofx2HeaderPattern = regexp.MustCompile("<\\?OFX( +[A-Z]+=\"[^=]*\")* *\\?>") var ofx2HeaderPattern = regexp.MustCompile("<\\?OFX( +[A-Z]+=\"[^=]*\")* *\\?>")
@@ -1,6 +1,8 @@
package ofx package ofx
import ( import (
"time"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter" "github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/core" "github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/models" "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 // 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) ofxDataReader, err := createNewOFXFileReader(ctx, data)
if err != nil { if err != nil {
@@ -44,5 +46,5 @@ func (c *ofxTransactionDataImporter) ParseImportedData(ctx core.Context, user *m
dataTableImporter := converter.CreateNewSimpleImporterWithTypeNameMapping(ofxTransactionTypeNameMapping) 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 ( import (
"testing" "testing"
"time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/core" "github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs" "github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/models" "github.com/mayswind/ezbookkeeping/pkg/models"
@@ -12,7 +14,7 @@ import (
) )
func TestOFXTransactionDataFileParseImportedData_MinimumValidData(t *testing.T) { func TestOFXTransactionDataFileParseImportedData_MinimumValidData(t *testing.T) {
converter := OFXTransactionDataImporter importer := OFXTransactionDataImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -20,7 +22,7 @@ func TestOFXTransactionDataFileParseImportedData_MinimumValidData(t *testing.T)
DefaultCurrency: "CNY", 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"+ "<OFX>\n"+
" <BANKMSGSRSV1>\n"+ " <BANKMSGSRSV1>\n"+
" <STMTTRNRS>\n"+ " <STMTTRNRS>\n"+
@@ -76,7 +78,7 @@ func TestOFXTransactionDataFileParseImportedData_MinimumValidData(t *testing.T)
" </CCSTMTRS>\n"+ " </CCSTMTRS>\n"+
" </CCSTMTTRNRS>\n"+ " </CCSTMTTRNRS>\n"+
" </CREDITCARDMSGSRSV1>\n"+ " </CREDITCARDMSGSRSV1>\n"+
"</OFX>"), 0, nil, nil, nil, nil, nil) "</OFX>"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err) assert.Nil(t, err)
@@ -160,7 +162,7 @@ func TestOFXTransactionDataFileParseImportedData_MinimumValidData(t *testing.T)
} }
func TestOFXTransactionDataFileParseImportedData_ParseAccountTo(t *testing.T) { func TestOFXTransactionDataFileParseImportedData_ParseAccountTo(t *testing.T) {
converter := OFXTransactionDataImporter importer := OFXTransactionDataImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -168,7 +170,7 @@ func TestOFXTransactionDataFileParseImportedData_ParseAccountTo(t *testing.T) {
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
allNewTransactions, allNewAccounts, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( allNewTransactions, allNewAccounts, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"<OFX>\n"+ "<OFX>\n"+
" <BANKMSGSRSV1>\n"+ " <BANKMSGSRSV1>\n"+
" <STMTTRNRS>\n"+ " <STMTTRNRS>\n"+
@@ -210,7 +212,7 @@ func TestOFXTransactionDataFileParseImportedData_ParseAccountTo(t *testing.T) {
" </CCSTMTRS>\n"+ " </CCSTMTRS>\n"+
" </CCSTMTTRNRS>\n"+ " </CCSTMTTRNRS>\n"+
" </CREDITCARDMSGSRSV1>\n"+ " </CREDITCARDMSGSRSV1>\n"+
"</OFX>"), 0, nil, nil, nil, nil, nil) "</OFX>"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err) assert.Nil(t, err)
@@ -243,7 +245,7 @@ func TestOFXTransactionDataFileParseImportedData_ParseAccountTo(t *testing.T) {
} }
func TestOFXTransactionDataFileParseImportedData_ParseValidTransactionTime(t *testing.T) { func TestOFXTransactionDataFileParseImportedData_ParseValidTransactionTime(t *testing.T) {
converter := OFXTransactionDataImporter importer := OFXTransactionDataImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -251,7 +253,7 @@ func TestOFXTransactionDataFileParseImportedData_ParseValidTransactionTime(t *te
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"<OFX>\n"+ "<OFX>\n"+
" <BANKMSGSRSV1>\n"+ " <BANKMSGSRSV1>\n"+
" <STMTTRNRS>\n"+ " <STMTTRNRS>\n"+
@@ -295,7 +297,7 @@ func TestOFXTransactionDataFileParseImportedData_ParseValidTransactionTime(t *te
" </STMTRS>\n"+ " </STMTRS>\n"+
" </STMTTRNRS>\n"+ " </STMTTRNRS>\n"+
" </BANKMSGSRSV1>\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.Nil(t, err)
@@ -310,7 +312,7 @@ func TestOFXTransactionDataFileParseImportedData_ParseValidTransactionTime(t *te
} }
func TestOFXTransactionDataFileParseImportedData_ParseInvalidTransactionTime(t *testing.T) { func TestOFXTransactionDataFileParseImportedData_ParseInvalidTransactionTime(t *testing.T) {
converter := OFXTransactionDataImporter importer := OFXTransactionDataImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -318,7 +320,7 @@ func TestOFXTransactionDataFileParseImportedData_ParseInvalidTransactionTime(t *
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"<OFX>\n"+ "<OFX>\n"+
" <BANKMSGSRSV1>\n"+ " <BANKMSGSRSV1>\n"+
" <STMTTRNRS>\n"+ " <STMTTRNRS>\n"+
@@ -337,10 +339,10 @@ func TestOFXTransactionDataFileParseImportedData_ParseInvalidTransactionTime(t *
" </STMTRS>\n"+ " </STMTRS>\n"+
" </STMTTRNRS>\n"+ " </STMTTRNRS>\n"+
" </BANKMSGSRSV1>\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) assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"<OFX>\n"+ "<OFX>\n"+
" <BANKMSGSRSV1>\n"+ " <BANKMSGSRSV1>\n"+
" <STMTTRNRS>\n"+ " <STMTTRNRS>\n"+
@@ -359,10 +361,10 @@ func TestOFXTransactionDataFileParseImportedData_ParseInvalidTransactionTime(t *
" </STMTRS>\n"+ " </STMTRS>\n"+
" </STMTTRNRS>\n"+ " </STMTTRNRS>\n"+
" </BANKMSGSRSV1>\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) assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"<OFX>\n"+ "<OFX>\n"+
" <BANKMSGSRSV1>\n"+ " <BANKMSGSRSV1>\n"+
" <STMTTRNRS>\n"+ " <STMTTRNRS>\n"+
@@ -381,10 +383,10 @@ func TestOFXTransactionDataFileParseImportedData_ParseInvalidTransactionTime(t *
" </STMTRS>\n"+ " </STMTRS>\n"+
" </STMTTRNRS>\n"+ " </STMTTRNRS>\n"+
" </BANKMSGSRSV1>\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) assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"<OFX>\n"+ "<OFX>\n"+
" <BANKMSGSRSV1>\n"+ " <BANKMSGSRSV1>\n"+
" <STMTTRNRS>\n"+ " <STMTTRNRS>\n"+
@@ -403,12 +405,12 @@ func TestOFXTransactionDataFileParseImportedData_ParseInvalidTransactionTime(t *
" </STMTRS>\n"+ " </STMTRS>\n"+
" </STMTTRNRS>\n"+ " </STMTTRNRS>\n"+
" </BANKMSGSRSV1>\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) assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
} }
func TestOFXTransactionDataFileParseImportedData_ParseAmount_CommaAsDecimalPoint(t *testing.T) { func TestOFXTransactionDataFileParseImportedData_ParseAmount_CommaAsDecimalPoint(t *testing.T) {
converter := OFXTransactionDataImporter importer := OFXTransactionDataImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -416,7 +418,7 @@ func TestOFXTransactionDataFileParseImportedData_ParseAmount_CommaAsDecimalPoint
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"<OFX>\n"+ "<OFX>\n"+
" <BANKMSGSRSV1>\n"+ " <BANKMSGSRSV1>\n"+
" <STMTTRNRS>\n"+ " <STMTTRNRS>\n"+
@@ -435,7 +437,7 @@ func TestOFXTransactionDataFileParseImportedData_ParseAmount_CommaAsDecimalPoint
" </STMTRS>\n"+ " </STMTRS>\n"+
" </STMTTRNRS>\n"+ " </STMTTRNRS>\n"+
" </BANKMSGSRSV1>\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.Nil(t, err)
@@ -444,7 +446,7 @@ func TestOFXTransactionDataFileParseImportedData_ParseAmount_CommaAsDecimalPoint
} }
func TestOFXTransactionDataFileParseImportedData_ParseInvalidAmount(t *testing.T) { func TestOFXTransactionDataFileParseImportedData_ParseInvalidAmount(t *testing.T) {
converter := OFXTransactionDataImporter importer := OFXTransactionDataImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -452,7 +454,7 @@ func TestOFXTransactionDataFileParseImportedData_ParseInvalidAmount(t *testing.T
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"<OFX>\n"+ "<OFX>\n"+
" <BANKMSGSRSV1>\n"+ " <BANKMSGSRSV1>\n"+
" <STMTTRNRS>\n"+ " <STMTTRNRS>\n"+
@@ -471,12 +473,12 @@ func TestOFXTransactionDataFileParseImportedData_ParseInvalidAmount(t *testing.T
" </STMTRS>\n"+ " </STMTRS>\n"+
" </STMTTRNRS>\n"+ " </STMTTRNRS>\n"+
" </BANKMSGSRSV1>\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) assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
} }
func TestOFXTransactionDataFileParseImportedData_ParseTransactionCurrency(t *testing.T) { func TestOFXTransactionDataFileParseImportedData_ParseTransactionCurrency(t *testing.T) {
converter := OFXTransactionDataImporter importer := OFXTransactionDataImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -484,7 +486,7 @@ func TestOFXTransactionDataFileParseImportedData_ParseTransactionCurrency(t *tes
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"<OFX>\n"+ "<OFX>\n"+
" <BANKMSGSRSV1>\n"+ " <BANKMSGSRSV1>\n"+
" <STMTTRNRS>\n"+ " <STMTTRNRS>\n"+
@@ -504,7 +506,7 @@ func TestOFXTransactionDataFileParseImportedData_ParseTransactionCurrency(t *tes
" </STMTRS>\n"+ " </STMTRS>\n"+
" </STMTTRNRS>\n"+ " </STMTTRNRS>\n"+
" </BANKMSGSRSV1>\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.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
@@ -512,7 +514,7 @@ func TestOFXTransactionDataFileParseImportedData_ParseTransactionCurrency(t *tes
} }
func TestOFXTransactionDataFileParseImportedData_ParseDescription(t *testing.T) { func TestOFXTransactionDataFileParseImportedData_ParseDescription(t *testing.T) {
converter := OFXTransactionDataImporter importer := OFXTransactionDataImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -520,7 +522,7 @@ func TestOFXTransactionDataFileParseImportedData_ParseDescription(t *testing.T)
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"<OFX>\n"+ "<OFX>\n"+
" <BANKMSGSRSV1>\n"+ " <BANKMSGSRSV1>\n"+
" <STMTTRNRS>\n"+ " <STMTTRNRS>\n"+
@@ -541,13 +543,13 @@ func TestOFXTransactionDataFileParseImportedData_ParseDescription(t *testing.T)
" </STMTRS>\n"+ " </STMTRS>\n"+
" </STMTTRNRS>\n"+ " </STMTTRNRS>\n"+
" </BANKMSGSRSV1>\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.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, "foo bar\t#test", allNewTransactions[0].Comment) 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"+ "<OFX>\n"+
" <BANKMSGSRSV1>\n"+ " <BANKMSGSRSV1>\n"+
" <STMTTRNRS>\n"+ " <STMTTRNRS>\n"+
@@ -567,13 +569,13 @@ func TestOFXTransactionDataFileParseImportedData_ParseDescription(t *testing.T)
" </STMTRS>\n"+ " </STMTRS>\n"+
" </STMTTRNRS>\n"+ " </STMTTRNRS>\n"+
" </BANKMSGSRSV1>\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.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, "Test", allNewTransactions[0].Comment) assert.Equal(t, "Test", allNewTransactions[0].Comment)
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte( allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"<OFX>\n"+ "<OFX>\n"+
" <BANKMSGSRSV1>\n"+ " <BANKMSGSRSV1>\n"+
" <STMTTRNRS>\n"+ " <STMTTRNRS>\n"+
@@ -595,7 +597,7 @@ func TestOFXTransactionDataFileParseImportedData_ParseDescription(t *testing.T)
" </STMTRS>\n"+ " </STMTRS>\n"+
" </STMTTRNRS>\n"+ " </STMTTRNRS>\n"+
" </BANKMSGSRSV1>\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.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
@@ -603,7 +605,7 @@ func TestOFXTransactionDataFileParseImportedData_ParseDescription(t *testing.T)
} }
func TestOFXTransactionDataFileParseImportedData_MissingAccountFromNode(t *testing.T) { func TestOFXTransactionDataFileParseImportedData_MissingAccountFromNode(t *testing.T) {
converter := OFXTransactionDataImporter importer := OFXTransactionDataImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -612,7 +614,7 @@ func TestOFXTransactionDataFileParseImportedData_MissingAccountFromNode(t *testi
} }
// Missing Posted Date Node // Missing Posted Date Node
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"<OFX>\n"+ "<OFX>\n"+
" <BANKMSGSRSV1>\n"+ " <BANKMSGSRSV1>\n"+
" <STMTTRNRS>\n"+ " <STMTTRNRS>\n"+
@@ -628,12 +630,12 @@ func TestOFXTransactionDataFileParseImportedData_MissingAccountFromNode(t *testi
" </STMTRS>\n"+ " </STMTRS>\n"+
" </STMTTRNRS>\n"+ " </STMTTRNRS>\n"+
" </BANKMSGSRSV1>\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) assert.EqualError(t, err, errs.ErrMissingAccountData.Message)
} }
func TestOFXTransactionDataFileParseImportedData_MissingCurrencyNode(t *testing.T) { func TestOFXTransactionDataFileParseImportedData_MissingCurrencyNode(t *testing.T) {
converter := OFXTransactionDataImporter importer := OFXTransactionDataImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -642,7 +644,7 @@ func TestOFXTransactionDataFileParseImportedData_MissingCurrencyNode(t *testing.
} }
// Missing Default Currency Node // Missing Default Currency Node
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"<OFX>\n"+ "<OFX>\n"+
" <BANKMSGSRSV1>\n"+ " <BANKMSGSRSV1>\n"+
" <STMTTRNRS>\n"+ " <STMTTRNRS>\n"+
@@ -660,12 +662,12 @@ func TestOFXTransactionDataFileParseImportedData_MissingCurrencyNode(t *testing.
" </STMTRS>\n"+ " </STMTRS>\n"+
" </STMTTRNRS>\n"+ " </STMTTRNRS>\n"+
" </BANKMSGSRSV1>\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) assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message)
} }
func TestOFXTransactionDataFileParseImportedData_MissingTransactionRequiredNode(t *testing.T) { func TestOFXTransactionDataFileParseImportedData_MissingTransactionRequiredNode(t *testing.T) {
converter := OFXTransactionDataImporter importer := OFXTransactionDataImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -674,7 +676,7 @@ func TestOFXTransactionDataFileParseImportedData_MissingTransactionRequiredNode(
} }
// Missing Posted Date Node // Missing Posted Date Node
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"<OFX>\n"+ "<OFX>\n"+
" <BANKMSGSRSV1>\n"+ " <BANKMSGSRSV1>\n"+
" <STMTTRNRS>\n"+ " <STMTTRNRS>\n"+
@@ -692,11 +694,11 @@ func TestOFXTransactionDataFileParseImportedData_MissingTransactionRequiredNode(
" </STMTRS>\n"+ " </STMTRS>\n"+
" </STMTTRNRS>\n"+ " </STMTTRNRS>\n"+
" </BANKMSGSRSV1>\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) assert.EqualError(t, err, errs.ErrMissingTransactionTime.Message)
// Missing Transaction Type Node // Missing Transaction Type Node
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"<OFX>\n"+ "<OFX>\n"+
" <BANKMSGSRSV1>\n"+ " <BANKMSGSRSV1>\n"+
" <STMTTRNRS>\n"+ " <STMTTRNRS>\n"+
@@ -714,11 +716,11 @@ func TestOFXTransactionDataFileParseImportedData_MissingTransactionRequiredNode(
" </STMTRS>\n"+ " </STMTRS>\n"+
" </STMTTRNRS>\n"+ " </STMTTRNRS>\n"+
" </BANKMSGSRSV1>\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) assert.EqualError(t, err, errs.ErrTransactionTypeInvalid.Message)
// Missing Amount Node // Missing Amount Node
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"<OFX>\n"+ "<OFX>\n"+
" <BANKMSGSRSV1>\n"+ " <BANKMSGSRSV1>\n"+
" <STMTTRNRS>\n"+ " <STMTTRNRS>\n"+
@@ -736,6 +738,6 @@ func TestOFXTransactionDataFileParseImportedData_MissingTransactionRequiredNode(
" </STMTRS>\n"+ " </STMTRS>\n"+
" </STMTTRNRS>\n"+ " </STMTTRNRS>\n"+
" </BANKMSGSRSV1>\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) assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
} }
@@ -1,6 +1,8 @@
package qif package qif
import ( import (
"time"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter" "github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/core" "github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/models" "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 // 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) qifDataReader := createNewQifDataReader(data)
qifData, err := qifDataReader.read(ctx) qifData, err := qifDataReader.read(ctx)
@@ -51,5 +53,5 @@ func (c *qifTransactionDataImporter) ParseImportedData(ctx core.Context, user *m
dataTableImporter := converter.CreateNewSimpleImporterWithTypeNameMapping(qifTransactionTypeNameMapping) 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 ( import (
"testing" "testing"
"time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/core" "github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs" "github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/models" "github.com/mayswind/ezbookkeeping/pkg/models"
@@ -12,7 +14,7 @@ import (
) )
func TestQIFTransactionDataFileParseImportedData_MinimumValidData(t *testing.T) { func TestQIFTransactionDataFileParseImportedData_MinimumValidData(t *testing.T) {
converter := QifYearMonthDayTransactionDataImporter importer := QifYearMonthDayTransactionDataImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -20,7 +22,7 @@ func TestQIFTransactionDataFileParseImportedData_MinimumValidData(t *testing.T)
DefaultCurrency: "CNY", 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"+ "!Type:Bank\n"+
"D2024-09-01\n"+ "D2024-09-01\n"+
"T123.45\n"+ "T123.45\n"+
@@ -42,7 +44,7 @@ func TestQIFTransactionDataFileParseImportedData_MinimumValidData(t *testing.T)
"D2024-09-05\n"+ "D2024-09-05\n"+
"T0.06\n"+ "T0.06\n"+
"L[Test Account2]\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) assert.Nil(t, err)
@@ -113,7 +115,7 @@ func TestQIFTransactionDataFileParseImportedData_MinimumValidData(t *testing.T)
} }
func TestQIFTransactionDataFileParseImportedData_ParseYearMonthDayDateFormatTime(t *testing.T) { func TestQIFTransactionDataFileParseImportedData_ParseYearMonthDayDateFormatTime(t *testing.T) {
converter := QifYearMonthDayTransactionDataImporter importer := QifYearMonthDayTransactionDataImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -121,7 +123,7 @@ func TestQIFTransactionDataFileParseImportedData_ParseYearMonthDayDateFormatTime
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"!Type:Bank\n"+ "!Type:Bank\n"+
"D2024-09-01\n"+ "D2024-09-01\n"+
"T-123.45\n"+ "T-123.45\n"+
@@ -137,7 +139,7 @@ func TestQIFTransactionDataFileParseImportedData_ParseYearMonthDayDateFormatTime
"^\n"+ "^\n"+
"D2024'9.5\n"+ "D2024'9.5\n"+
"T-123.45\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.Nil(t, err)
@@ -151,7 +153,7 @@ func TestQIFTransactionDataFileParseImportedData_ParseYearMonthDayDateFormatTime
} }
func TestQIFTransactionDataFileParseImportedData_ParseMonthDayYearDateFormatTime(t *testing.T) { func TestQIFTransactionDataFileParseImportedData_ParseMonthDayYearDateFormatTime(t *testing.T) {
converter := QifMonthDayYearTransactionDataImporter importer := QifMonthDayYearTransactionDataImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -159,7 +161,7 @@ func TestQIFTransactionDataFileParseImportedData_ParseMonthDayYearDateFormatTime
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"!Type:Bank\n"+ "!Type:Bank\n"+
"D09-01-2024\n"+ "D09-01-2024\n"+
"T-123.45\n"+ "T-123.45\n"+
@@ -175,7 +177,7 @@ func TestQIFTransactionDataFileParseImportedData_ParseMonthDayYearDateFormatTime
"^\n"+ "^\n"+
"D9.5'2024\n"+ "D9.5'2024\n"+
"T-123.45\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.Nil(t, err)
@@ -189,7 +191,7 @@ func TestQIFTransactionDataFileParseImportedData_ParseMonthDayYearDateFormatTime
} }
func TestQIFTransactionDataFileParseImportedData_ParseDayYearMonthDateFormatTime(t *testing.T) { func TestQIFTransactionDataFileParseImportedData_ParseDayYearMonthDateFormatTime(t *testing.T) {
converter := QifDayMonthYearTransactionDataImporter importer := QifDayMonthYearTransactionDataImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -197,7 +199,7 @@ func TestQIFTransactionDataFileParseImportedData_ParseDayYearMonthDateFormatTime
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"!Type:Bank\n"+ "!Type:Bank\n"+
"D01-09-2024\n"+ "D01-09-2024\n"+
"T-123.45\n"+ "T-123.45\n"+
@@ -213,7 +215,7 @@ func TestQIFTransactionDataFileParseImportedData_ParseDayYearMonthDateFormatTime
"^\n"+ "^\n"+
"D5'9.2024\n"+ "D5'9.2024\n"+
"T-123.45\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.Nil(t, err)
@@ -227,7 +229,7 @@ func TestQIFTransactionDataFileParseImportedData_ParseDayYearMonthDateFormatTime
} }
func TestQIFTransactionDataFileParseImportedData_ParseShortYearMonthDayDateFormatTime(t *testing.T) { func TestQIFTransactionDataFileParseImportedData_ParseShortYearMonthDayDateFormatTime(t *testing.T) {
converter := QifYearMonthDayTransactionDataImporter importer := QifYearMonthDayTransactionDataImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -235,7 +237,7 @@ func TestQIFTransactionDataFileParseImportedData_ParseShortYearMonthDayDateForma
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"!Type:Bank\n"+ "!Type:Bank\n"+
"D24-09-01\n"+ "D24-09-01\n"+
"T-123.45\n"+ "T-123.45\n"+
@@ -251,7 +253,7 @@ func TestQIFTransactionDataFileParseImportedData_ParseShortYearMonthDayDateForma
"^\n"+ "^\n"+
"D24'9.5\n"+ "D24'9.5\n"+
"T-123.45\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.Nil(t, err)
@@ -265,7 +267,7 @@ func TestQIFTransactionDataFileParseImportedData_ParseShortYearMonthDayDateForma
} }
func TestQIFTransactionDataFileParseImportedData_ParseInvalidTime(t *testing.T) { func TestQIFTransactionDataFileParseImportedData_ParseInvalidTime(t *testing.T) {
converter := QifYearMonthDayTransactionDataImporter importer := QifYearMonthDayTransactionDataImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -273,16 +275,16 @@ func TestQIFTransactionDataFileParseImportedData_ParseInvalidTime(t *testing.T)
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"!Type:Bank\n"+ "!Type:Bank\n"+
"D2024 09 01\n"+ "D2024 09 01\n"+
"T-123.45\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) assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
} }
func TestQIFTransactionDataFileParseImportedData_ParseAmountWithThousandsSeparator(t *testing.T) { func TestQIFTransactionDataFileParseImportedData_ParseAmountWithThousandsSeparator(t *testing.T) {
converter := QifYearMonthDayTransactionDataImporter importer := QifYearMonthDayTransactionDataImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -290,11 +292,11 @@ func TestQIFTransactionDataFileParseImportedData_ParseAmountWithThousandsSeparat
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"!Type:Bank\n"+ "!Type:Bank\n"+
"D2024-09-01\n"+ "D2024-09-01\n"+
"T-123,456.78\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) assert.Nil(t, err)
@@ -304,7 +306,7 @@ func TestQIFTransactionDataFileParseImportedData_ParseAmountWithThousandsSeparat
} }
func TestQIFTransactionDataFileParseImportedData_ParseInvalidAmount(t *testing.T) { func TestQIFTransactionDataFileParseImportedData_ParseInvalidAmount(t *testing.T) {
converter := QifYearMonthDayTransactionDataImporter importer := QifYearMonthDayTransactionDataImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -312,16 +314,16 @@ func TestQIFTransactionDataFileParseImportedData_ParseInvalidAmount(t *testing.T
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"!Type:Bank\n"+ "!Type:Bank\n"+
"D2024-09-01\n"+ "D2024-09-01\n"+
"T-123 45\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) assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
} }
func TestQIFTransactionDataFileParseImportedData_ParseAccountType(t *testing.T) { func TestQIFTransactionDataFileParseImportedData_ParseAccountType(t *testing.T) {
converter := QifYearMonthDayTransactionDataImporter importer := QifYearMonthDayTransactionDataImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -329,11 +331,11 @@ func TestQIFTransactionDataFileParseImportedData_ParseAccountType(t *testing.T)
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"!Type:Cash\n"+ "!Type:Cash\n"+
"D2024-09-01\n"+ "D2024-09-01\n"+
"T-123.45\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.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) 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(1725148800), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
assert.Equal(t, int64(12345), allNewTransactions[0].Amount) 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"+ "!Type:CCard\n"+
"D2024-09-01\n"+ "D2024-09-01\n"+
"T-123.45\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.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) 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(1725148800), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
assert.Equal(t, int64(12345), allNewTransactions[0].Amount) 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"+ "!Type:Oth A\n"+
"D2024-09-01\n"+ "D2024-09-01\n"+
"T-123.45\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.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) 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(1725148800), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
assert.Equal(t, int64(12345), allNewTransactions[0].Amount) 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"+ "!Type:Oth L\n"+
"D2024-09-01\n"+ "D2024-09-01\n"+
"T-123.45\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.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
@@ -383,7 +385,7 @@ func TestQIFTransactionDataFileParseImportedData_ParseAccountType(t *testing.T)
} }
func TestQIFTransactionDataFileParseImportedData_ParseAccount(t *testing.T) { func TestQIFTransactionDataFileParseImportedData_ParseAccount(t *testing.T) {
converter := QifYearMonthDayTransactionDataImporter importer := QifYearMonthDayTransactionDataImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -391,14 +393,14 @@ func TestQIFTransactionDataFileParseImportedData_ParseAccount(t *testing.T) {
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
allNewTransactions, allNewAccounts, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( allNewTransactions, allNewAccounts, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"!Account\n"+ "!Account\n"+
"NTest Account\n"+ "NTest Account\n"+
"^\n"+ "^\n"+
"!Type:Bank\n"+ "!Type:Bank\n"+
"D2024-09-01\n"+ "D2024-09-01\n"+
"T-123.45\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.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
@@ -412,7 +414,7 @@ func TestQIFTransactionDataFileParseImportedData_ParseAccount(t *testing.T) {
} }
func TestQIFTransactionDataFileParseImportedData_ParseAmountWithLeadingPlusSign(t *testing.T) { func TestQIFTransactionDataFileParseImportedData_ParseAmountWithLeadingPlusSign(t *testing.T) {
converter := QifYearMonthDayTransactionDataImporter importer := QifYearMonthDayTransactionDataImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -420,11 +422,11 @@ func TestQIFTransactionDataFileParseImportedData_ParseAmountWithLeadingPlusSign(
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"!Type:Bank\n"+ "!Type:Bank\n"+
"D2024-09-01\n"+ "D2024-09-01\n"+
"T+123.45\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.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
@@ -432,7 +434,7 @@ func TestQIFTransactionDataFileParseImportedData_ParseAmountWithLeadingPlusSign(
} }
func TestQIFTransactionDataFileParseImportedData_ParseSubCategory(t *testing.T) { func TestQIFTransactionDataFileParseImportedData_ParseSubCategory(t *testing.T) {
converter := QifYearMonthDayTransactionDataImporter importer := QifYearMonthDayTransactionDataImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -440,12 +442,12 @@ func TestQIFTransactionDataFileParseImportedData_ParseSubCategory(t *testing.T)
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
allNewTransactions, _, allNewSubExpenseCategories, _, _, _, err := converter.ParseImportedData(context, user, []byte( allNewTransactions, _, allNewSubExpenseCategories, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"!Type:Bank\n"+ "!Type:Bank\n"+
"D2024-09-01\n"+ "D2024-09-01\n"+
"T-123.45\n"+ "T-123.45\n"+
"LTest Category:Sub Category\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.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
@@ -458,7 +460,7 @@ func TestQIFTransactionDataFileParseImportedData_ParseSubCategory(t *testing.T)
} }
func TestQIFTransactionDataFileParseImportedData_ParseDescription(t *testing.T) { func TestQIFTransactionDataFileParseImportedData_ParseDescription(t *testing.T) {
converter := QifYearMonthDayTransactionDataImporter importer := QifYearMonthDayTransactionDataImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -466,7 +468,7 @@ func TestQIFTransactionDataFileParseImportedData_ParseDescription(t *testing.T)
DefaultCurrency: "CNY", DefaultCurrency: "CNY",
} }
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"!Type:Bank\n"+ "!Type:Bank\n"+
"D2024-09-01\n"+ "D2024-09-01\n"+
"T-123.45\n"+ "T-123.45\n"+
@@ -476,7 +478,24 @@ func TestQIFTransactionDataFileParseImportedData_ParseDescription(t *testing.T)
"D2024-09-02\n"+ "D2024-09-02\n"+
"T-234.56\n"+ "T-234.56\n"+
"PTest2\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.Nil(t, err)
assert.Equal(t, 2, len(allNewTransactions)) assert.Equal(t, 2, len(allNewTransactions))
@@ -484,8 +503,41 @@ func TestQIFTransactionDataFileParseImportedData_ParseDescription(t *testing.T)
assert.Equal(t, "Test2", allNewTransactions[1].Comment) 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) { func TestQIFTransactionDataFileParseImportedData_MissingRequiredFields(t *testing.T) {
converter := QifYearMonthDayTransactionDataImporter importer := QifYearMonthDayTransactionDataImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -494,16 +546,16 @@ func TestQIFTransactionDataFileParseImportedData_MissingRequiredFields(t *testin
} }
// Missing Time Field // Missing Time Field
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"!Type:Bank\n"+ "!Type:Bank\n"+
"T-123.45\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) assert.EqualError(t, err, errs.ErrMissingTransactionTime.Message)
// Missing Amount Field // Missing Amount Field
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte( _, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"!Type:Bank\n"+ "!Type:Bank\n"+
"D2024-09-01\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) 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_AMOUNT: true,
datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME: true, datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME: true,
datatable.TRANSACTION_DATA_TABLE_DESCRIPTION: true, datatable.TRANSACTION_DATA_TABLE_DESCRIPTION: true,
datatable.TRANSACTION_DATA_TABLE_PAYEE: true,
} }
// qifDateFormatType represents the quicken interchange format (qif) date format type // 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 != "" { if qifTransaction.Memo != "" {
data[datatable.TRANSACTION_DATA_TABLE_DESCRIPTION] = 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 return data, nil
+24 -10
View File
@@ -5,9 +5,9 @@ import (
"github.com/mayswind/ezbookkeeping/pkg/converters/beancount" "github.com/mayswind/ezbookkeeping/pkg/converters/beancount"
"github.com/mayswind/ezbookkeeping/pkg/converters/camt" "github.com/mayswind/ezbookkeeping/pkg/converters/camt"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter" "github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/converters/custom"
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable" "github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
"github.com/mayswind/ezbookkeeping/pkg/converters/default" "github.com/mayswind/ezbookkeeping/pkg/converters/default"
"github.com/mayswind/ezbookkeeping/pkg/converters/dsv"
"github.com/mayswind/ezbookkeeping/pkg/converters/feidee" "github.com/mayswind/ezbookkeeping/pkg/converters/feidee"
"github.com/mayswind/ezbookkeeping/pkg/converters/fireflyIII" "github.com/mayswind/ezbookkeeping/pkg/converters/fireflyIII"
"github.com/mayswind/ezbookkeeping/pkg/converters/gnucash" "github.com/mayswind/ezbookkeeping/pkg/converters/gnucash"
@@ -52,6 +52,8 @@ func GetTransactionDataImporter(fileType string) (converter.TransactionDataImpor
return qif.QifDayMonthYearTransactionDataImporter, nil return qif.QifDayMonthYearTransactionDataImporter, nil
} else if fileType == "iif" { } else if fileType == "iif" {
return iif.IifTransactionDataFileImporter, nil return iif.IifTransactionDataFileImporter, nil
} else if fileType == "camt052" {
return camt.Camt052TransactionDataImporter, nil
} else if fileType == "camt053" { } else if fileType == "camt053" {
return camt.Camt053TransactionDataImporter, nil return camt.Camt053TransactionDataImporter, nil
} else if fileType == "mt940" { } else if fileType == "mt940" {
@@ -83,17 +85,29 @@ func GetTransactionDataImporter(fileType string) (converter.TransactionDataImpor
} }
} }
// IsCustomDelimiterSeparatedValuesFileType returns whether the file type is the delimiter-separated values file type // IsCustomFileFormatFileType returns whether the file type is the custom file format
func IsCustomDelimiterSeparatedValuesFileType(fileType string) bool { func IsCustomFileFormatFileType(fileType string) bool {
return dsv.IsDelimiterSeparatedValuesFileType(fileType) return custom.IsDelimiterSeparatedValuesFileType(fileType) || custom.IsCustomExcelFileType(fileType)
} }
// CreateNewDelimiterSeparatedValuesDataParser returns a new delimiter-separated values data parser according to the file type and encoding // CreateNewCustomFileFormatTransactionDataParser returns a new custom transaction data parser according to the file type and encoding
func CreateNewDelimiterSeparatedValuesDataParser(fileType string, fileEncoding string) (dsv.CustomTransactionDataDsvFileParser, error) { func CreateNewCustomFileFormatTransactionDataParser(fileType string, fileEncoding string) (custom.CustomTransactionDataParser, error) {
return dsv.CreateNewCustomTransactionDataDsvFileParser(fileType, fileEncoding) if custom.IsDelimiterSeparatedValuesFileType(fileType) {
return custom.CreateNewCustomTransactionDataDsvFileParser(fileType, fileEncoding)
} else if custom.IsCustomExcelFileType(fileType) {
return custom.CreateNewCustomTransactionDataExcelFileParser(fileType)
} else {
return nil, errs.ErrImportFileTypeNotSupported
}
} }
// CreateNewDelimiterSeparatedValuesDataImporter returns a new delimiter-separated values data importer according to the file type and encoding // CreateNewCustomTransactionDataImporter returns a new custom transaction data importer according to the file type and encoding
func CreateNewDelimiterSeparatedValuesDataImporter(fileType string, fileEncoding string, columnIndexMapping map[datatable.TransactionDataTableColumn]int, transactionTypeNameMapping map[string]models.TransactionType, hasHeaderLine bool, timeFormat string, timezoneFormat string, amountDecimalSeparator string, amountDigitGroupingSymbol string, geoLocationSeparator string, geoLocationOrder string, transactionTagSeparator string) (converter.TransactionDataImporter, error) { func CreateNewCustomTransactionDataImporter(fileType string, fileEncoding string, columnIndexMapping map[datatable.TransactionDataTableColumn]int, transactionTypeNameMapping map[string]models.TransactionType, hasHeaderLine bool, timeFormat string, timezoneFormat string, amountDecimalSeparator string, amountDigitGroupingSymbol string, geoLocationSeparator string, geoLocationOrder string, transactionTagSeparator string) (converter.TransactionDataImporter, error) {
return dsv.CreateNewCustomTransactionDataDsvFileImporter(fileType, fileEncoding, columnIndexMapping, transactionTypeNameMapping, hasHeaderLine, timeFormat, timezoneFormat, amountDecimalSeparator, amountDigitGroupingSymbol, geoLocationSeparator, geoLocationOrder, transactionTagSeparator) if custom.IsDelimiterSeparatedValuesFileType(fileType) {
return custom.CreateNewCustomTransactionDataDsvFileImporter(fileType, fileEncoding, columnIndexMapping, transactionTypeNameMapping, hasHeaderLine, timeFormat, timezoneFormat, amountDecimalSeparator, amountDigitGroupingSymbol, geoLocationSeparator, geoLocationOrder, transactionTagSeparator)
} else if custom.IsCustomExcelFileType(fileType) {
return custom.CreateNewCustomTransactionDataExcelFileImporter(fileType, columnIndexMapping, transactionTypeNameMapping, hasHeaderLine, timeFormat, timezoneFormat, amountDecimalSeparator, amountDigitGroupingSymbol, geoLocationSeparator, geoLocationOrder, transactionTagSeparator)
} else {
return nil, errs.ErrImportFileTypeNotSupported
}
} }
@@ -2,6 +2,7 @@ package wechat
import ( import (
"bytes" "bytes"
"time"
"golang.org/x/text/encoding/unicode" "golang.org/x/text/encoding/unicode"
"golang.org/x/text/transform" "golang.org/x/text/transform"
@@ -27,7 +28,7 @@ var (
) )
// ParseImportedData returns the imported data by parsing the wechat pay transaction csv data // 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() fallback := unicode.UTF8.NewDecoder()
reader := transform.NewReader(bytes.NewReader(data), unicode.BOMOverride(fallback)) 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) transactionDataTable := datatable.CreateNewTransactionDataTableFromCommonDataTable(commonDataTable, wechatPayTransactionSupportedColumns, transactionRowParser)
dataTableImporter := converter.CreateNewSimpleImporterWithTypeNameMapping(wechatPayTransactionTypeNameMapping) 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/stretchr/testify/assert"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/core" "github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs" "github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/models" "github.com/mayswind/ezbookkeeping/pkg/models"
@@ -13,7 +14,7 @@ import (
) )
func TestWeChatPayCsvFileImporterParseImportedData_MinimumValidData(t *testing.T) { func TestWeChatPayCsvFileImporterParseImportedData_MinimumValidData(t *testing.T) {
converter := WeChatPayTransactionDataCsvFileImporter importer := WeChatPayTransactionDataCsvFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ 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 12:34:56,商户消费,支出,¥123.45,支付成功\n" +
"2024-09-01 23:59:59,零钱充值,/,¥0.05,充值完成\n" + "2024-09-01 23:59:59,零钱充值,/,¥0.05,充值完成\n" +
"2024-09-02 23:59:59,零钱提现,/,¥0.03,提现已到账\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.Nil(t, err)
assert.Equal(t, 4, len(allNewTransactions)) assert.Equal(t, 4, len(allNewTransactions))
@@ -93,7 +94,7 @@ func TestWeChatPayCsvFileImporterParseImportedData_MinimumValidData(t *testing.T
} }
func TestWeChatPayCsvFileImporterParseImportedData_ParseRefundTransaction(t *testing.T) { func TestWeChatPayCsvFileImporterParseImportedData_ParseRefundTransaction(t *testing.T) {
converter := WeChatPayTransactionDataCsvFileImporter importer := WeChatPayTransactionDataCsvFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -108,7 +109,7 @@ func TestWeChatPayCsvFileImporterParseImportedData_ParseRefundTransaction(t *tes
"----------------------微信支付账单明细列表--------------------,,,,\n" + "----------------------微信支付账单明细列表--------------------,,,,\n" +
"交易时间,交易类型,收/支,金额(元),当前状态\n" + "交易时间,交易类型,收/支,金额(元),当前状态\n" +
"2024-09-01 01:23:45,xxx-退款,收入,¥0.12,已全额退款\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.Nil(t, err)
assert.Equal(t, int64(1234567890), allNewTransactions[0].Uid) assert.Equal(t, int64(1234567890), allNewTransactions[0].Uid)
@@ -120,7 +121,7 @@ func TestWeChatPayCsvFileImporterParseImportedData_ParseRefundTransaction(t *tes
} }
func TestWeChatPayCsvFileImporterParseImportedData_ParseInvalidTime(t *testing.T) { func TestWeChatPayCsvFileImporterParseImportedData_ParseInvalidTime(t *testing.T) {
converter := WeChatPayTransactionDataCsvFileImporter importer := WeChatPayTransactionDataCsvFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -135,7 +136,7 @@ func TestWeChatPayCsvFileImporterParseImportedData_ParseInvalidTime(t *testing.T
"----------------------微信支付账单明细列表--------------------,,,,\n" + "----------------------微信支付账单明细列表--------------------,,,,\n" +
"交易时间,交易类型,收/支,金额(元),当前状态\n" + "交易时间,交易类型,收/支,金额(元),当前状态\n" +
"2024-09-01T01:23:45,二维码收款,收入,¥0.12,已收钱\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) assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
data2 := "微信支付账单明细,,,,\n" + data2 := "微信支付账单明细,,,,\n" +
@@ -145,12 +146,12 @@ func TestWeChatPayCsvFileImporterParseImportedData_ParseInvalidTime(t *testing.T
"----------------------微信支付账单明细列表--------------------,,,,\n" + "----------------------微信支付账单明细列表--------------------,,,,\n" +
"交易时间,交易类型,收/支,金额(元),当前状态\n" + "交易时间,交易类型,收/支,金额(元),当前状态\n" +
"09/01/2024 12:34:56,二维码收款,收入,¥0.12,已收钱\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) assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
} }
func TestWeChatPayCsvFileImporterParseImportedData_ParseInvalidType(t *testing.T) { func TestWeChatPayCsvFileImporterParseImportedData_ParseInvalidType(t *testing.T) {
converter := WeChatPayTransactionDataCsvFileImporter importer := WeChatPayTransactionDataCsvFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -165,12 +166,35 @@ func TestWeChatPayCsvFileImporterParseImportedData_ParseInvalidType(t *testing.T
"----------------------微信支付账单明细列表--------------------,,,,\n" + "----------------------微信支付账单明细列表--------------------,,,,\n" +
"交易时间,交易类型,收/支,金额(元),当前状态\n" + "交易时间,交易类型,收/支,金额(元),当前状态\n" +
"2024-09-01T01:23:45,xxx,,¥0.12,支付成功\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) assert.EqualError(t, err, errs.ErrNotFoundTransactionDataInFile.Message)
} }
func TestWeChatPayCsvFileImporterParseImportedData_ParseAmountWithThousandSeparator(t *testing.T) {
importer := WeChatPayTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
Uid: 1234567890,
DefaultCurrency: "CNY",
}
data1 := "微信支付账单明细,,,,\n" +
"微信昵称:[xxx],,,,\n" +
"起始时间:[2024-01-01 00:00:00] 终止时间:[2024-09-01 23:59:59],,,,\n" +
",,,,\n" +
"----------------------微信支付账单明细列表--------------------,,,,\n" +
"交易时间,交易类型,收/支,金额(元),支付方式,当前状态\n" +
"2024-09-01 01:23:45,二维码收款,收入,\"¥1,234.56\",/,已收钱\n"
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))
assert.Equal(t, int64(123456), allNewTransactions[0].Amount)
}
func TestWeChatPayCsvFileImporterParseImportedData_ParseInvalidAmount(t *testing.T) { func TestWeChatPayCsvFileImporterParseImportedData_ParseInvalidAmount(t *testing.T) {
converter := WeChatPayTransactionDataCsvFileImporter importer := WeChatPayTransactionDataCsvFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -185,12 +209,12 @@ func TestWeChatPayCsvFileImporterParseImportedData_ParseInvalidAmount(t *testing
"----------------------微信支付账单明细列表--------------------,,,,\n" + "----------------------微信支付账单明细列表--------------------,,,,\n" +
"交易时间,交易类型,收/支,金额(元),当前状态\n" + "交易时间,交易类型,收/支,金额(元),当前状态\n" +
"2024-09-01 01:23:45,二维码收款,收入,¥,已收钱\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) assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
} }
func TestWeChatPayCsvFileImporterParseImportedData_ParseAccountName(t *testing.T) { func TestWeChatPayCsvFileImporterParseImportedData_ParseAccountName(t *testing.T) {
converter := WeChatPayTransactionDataCsvFileImporter importer := WeChatPayTransactionDataCsvFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -206,7 +230,7 @@ func TestWeChatPayCsvFileImporterParseImportedData_ParseAccountName(t *testing.T
"----------------------微信支付账单明细列表--------------------,,,,\n" + "----------------------微信支付账单明细列表--------------------,,,,\n" +
"交易时间,交易类型,收/支,金额(元),支付方式,当前状态\n" + "交易时间,交易类型,收/支,金额(元),支付方式,当前状态\n" +
"2024-09-01 01:23:45,二维码收款,收入,¥0.12,/,已收钱\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.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
@@ -222,7 +246,7 @@ func TestWeChatPayCsvFileImporterParseImportedData_ParseAccountName(t *testing.T
"2024-09-01 01:23:45,xxx-退款,收入,¥0.12,test,已全额退款\n" "2024-09-01 01:23:45,xxx-退款,收入,¥0.12,test,已全额退款\n"
assert.Nil(t, err) 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.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
@@ -238,7 +262,7 @@ func TestWeChatPayCsvFileImporterParseImportedData_ParseAccountName(t *testing.T
"2024-09-01 23:59:59,零钱充值,/,¥0.05,test,充值完成\n" "2024-09-01 23:59:59,零钱充值,/,¥0.05,test,充值完成\n"
assert.Nil(t, err) 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.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
@@ -255,7 +279,7 @@ func TestWeChatPayCsvFileImporterParseImportedData_ParseAccountName(t *testing.T
"2024-09-02 23:59:59,零钱提现,/,¥0.03,test,提现已到账\n" "2024-09-02 23:59:59,零钱提现,/,¥0.03,test,提现已到账\n"
assert.Nil(t, err) 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.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
@@ -272,7 +296,7 @@ func TestWeChatPayCsvFileImporterParseImportedData_ParseAccountName(t *testing.T
"2024-09-03 23:59:59,信用卡还款,/,¥0.01,零钱,支付成功\n" "2024-09-03 23:59:59,信用卡还款,/,¥0.01,零钱,支付成功\n"
assert.Nil(t, err) 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.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
@@ -281,7 +305,7 @@ func TestWeChatPayCsvFileImporterParseImportedData_ParseAccountName(t *testing.T
} }
func TestWeChatPayCsvFileImporterParseImportedData_ParseDescription(t *testing.T) { func TestWeChatPayCsvFileImporterParseImportedData_ParseDescription(t *testing.T) {
converter := WeChatPayTransactionDataCsvFileImporter importer := WeChatPayTransactionDataCsvFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -296,7 +320,7 @@ func TestWeChatPayCsvFileImporterParseImportedData_ParseDescription(t *testing.T
"----------------------微信支付账单明细列表--------------------,,,,\n" + "----------------------微信支付账单明细列表--------------------,,,,\n" +
"交易时间,交易类型,收/支,金额(元),当前状态,备注\n" + "交易时间,交易类型,收/支,金额(元),当前状态,备注\n" +
"2024-09-01 01:23:45,二维码收款,收入,¥0.12,已收钱,\"/\"\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.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions)) assert.Equal(t, 1, len(allNewTransactions))
@@ -309,7 +333,7 @@ func TestWeChatPayCsvFileImporterParseImportedData_ParseDescription(t *testing.T
"----------------------微信支付账单明细列表--------------------,,,,\n" + "----------------------微信支付账单明细列表--------------------,,,,\n" +
"交易时间,交易类型,商品,收/支,金额(元),当前状态,备注\n" + "交易时间,交易类型,商品,收/支,金额(元),当前状态,备注\n" +
"2024-09-01 01:23:45,二维码收款,Test,收入,¥0.12,已收钱,\"foo\"\"bar,\ntest\"\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, 1, len(allNewTransactions))
assert.Equal(t, "foo\"bar,\ntest", allNewTransactions[0].Comment) assert.Equal(t, "foo\"bar,\ntest", allNewTransactions[0].Comment)
@@ -320,13 +344,13 @@ func TestWeChatPayCsvFileImporterParseImportedData_ParseDescription(t *testing.T
"----------------------微信支付账单明细列表--------------------,,,,\n" + "----------------------微信支付账单明细列表--------------------,,,,\n" +
"交易时间,交易类型,商品,收/支,金额(元),当前状态,备注\n" + "交易时间,交易类型,商品,收/支,金额(元),当前状态,备注\n" +
"2024-09-01 01:23:45,二维码收款,Test,收入,¥0.12,已收钱,\"\"\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, 1, len(allNewTransactions))
assert.Equal(t, "Test", allNewTransactions[0].Comment) assert.Equal(t, "Test", allNewTransactions[0].Comment)
} }
func TestWeChatPayCsvFileImporterParseImportedData_SkipUnknownTransferTransaction(t *testing.T) { func TestWeChatPayCsvFileImporterParseImportedData_SkipUnknownTransferTransaction(t *testing.T) {
converter := WeChatPayTransactionDataCsvFileImporter importer := WeChatPayTransactionDataCsvFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -341,12 +365,12 @@ func TestWeChatPayCsvFileImporterParseImportedData_SkipUnknownTransferTransactio
"----------------------微信支付账单明细列表--------------------,,,,\n" + "----------------------微信支付账单明细列表--------------------,,,,\n" +
"交易时间,交易类型,收/支,金额(元),当前状态\n" + "交易时间,交易类型,收/支,金额(元),当前状态\n" +
"2024-09-01 23:59:59,/,/,¥0.05,充值完成\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) assert.EqualError(t, err, errs.ErrNotFoundTransactionDataInFile.Message)
} }
func TestWeChatPayCsvFileImporterParseImportedData_MissingFileHeader(t *testing.T) { func TestWeChatPayCsvFileImporterParseImportedData_MissingFileHeader(t *testing.T) {
converter := WeChatPayTransactionDataCsvFileImporter importer := WeChatPayTransactionDataCsvFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -356,15 +380,15 @@ func TestWeChatPayCsvFileImporterParseImportedData_MissingFileHeader(t *testing.
data := "交易时间,交易类型,收/支,金额(元),当前状态\n" + data := "交易时间,交易类型,收/支,金额(元),当前状态\n" +
"2024-09-01 01:23:45,二维码收款,收入,¥0.12,已收钱\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) 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) assert.EqualError(t, err, errs.ErrInvalidFileHeader.Message)
} }
func TestWeChatPayCsvFileImporterParseImportedData_MissingRequiredColumn(t *testing.T) { func TestWeChatPayCsvFileImporterParseImportedData_MissingRequiredColumn(t *testing.T) {
converter := WeChatPayTransactionDataCsvFileImporter importer := WeChatPayTransactionDataCsvFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -380,7 +404,7 @@ func TestWeChatPayCsvFileImporterParseImportedData_MissingRequiredColumn(t *test
"----------------------微信支付账单明细列表--------------------,,,,\n" + "----------------------微信支付账单明细列表--------------------,,,,\n" +
"交易类型,收/支,金额(元),当前状态\n" + "交易类型,收/支,金额(元),当前状态\n" +
"二维码收款,收入,¥0.12,已收钱\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) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
// Missing Category Column // Missing Category Column
@@ -391,7 +415,7 @@ func TestWeChatPayCsvFileImporterParseImportedData_MissingRequiredColumn(t *test
"----------------------微信支付账单明细列表--------------------,,,,\n" + "----------------------微信支付账单明细列表--------------------,,,,\n" +
"交易时间,收/支,金额(元),当前状态\n" + "交易时间,收/支,金额(元),当前状态\n" +
"2024-09-01 01:23:45,收入,¥0.12,已收钱\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) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
// Missing Type Column // Missing Type Column
@@ -402,7 +426,7 @@ func TestWeChatPayCsvFileImporterParseImportedData_MissingRequiredColumn(t *test
"----------------------微信支付账单明细列表--------------------,,,,\n" + "----------------------微信支付账单明细列表--------------------,,,,\n" +
"交易时间,交易类型,金额(元),当前状态\n" + "交易时间,交易类型,金额(元),当前状态\n" +
"2024-09-01 01:23:45,二维码收款,¥0.12,已收钱\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) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
// Missing Amount Column // Missing Amount Column
@@ -413,7 +437,7 @@ func TestWeChatPayCsvFileImporterParseImportedData_MissingRequiredColumn(t *test
"----------------------微信支付账单明细列表--------------------,,,,\n" + "----------------------微信支付账单明细列表--------------------,,,,\n" +
"交易时间,交易类型,收/支,当前状态\n" + "交易时间,交易类型,收/支,当前状态\n" +
"2024-09-01 01:23:45,二维码收款,收入,已收钱\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) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
// Missing Status Column // Missing Status Column
@@ -424,12 +448,12 @@ func TestWeChatPayCsvFileImporterParseImportedData_MissingRequiredColumn(t *test
"----------------------微信支付账单明细列表--------------------,,,,\n" + "----------------------微信支付账单明细列表--------------------,,,,\n" +
"交易时间,交易类型,收/支,金额(元)\n" + "交易时间,交易类型,收/支,金额(元)\n" +
"2024-09-01 01:23:45,二维码收款,收入,¥0.12\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) assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
} }
func TestWeChatPayCsvFileImporterParseImportedData_NoTransactionData(t *testing.T) { func TestWeChatPayCsvFileImporterParseImportedData_NoTransactionData(t *testing.T) {
converter := WeChatPayTransactionDataCsvFileImporter importer := WeChatPayTransactionDataCsvFileImporter
context := core.NewNullContext() context := core.NewNullContext()
user := &models.User{ user := &models.User{
@@ -443,6 +467,6 @@ func TestWeChatPayCsvFileImporterParseImportedData_NoTransactionData(t *testing.
",,,,\n" + ",,,,\n" +
"----------------------微信支付账单明细列表--------------------,,,,\n" + "----------------------微信支付账单明细列表--------------------,,,,\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) assert.EqualError(t, err, errs.ErrNotFoundTransactionDataInFile.Message)
} }

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