Compare commits

..

2 Commits

Author SHA1 Message Date
MaysWind e9503d6ae2 fix the mobile transaction list page displayed incorrectly when loading more transactions 2025-10-02 21:37:00 +08:00
MaysWind d8736eebf8 bump version to 1.1.1 2025-10-02 21:35:40 +08:00
486 changed files with 10231 additions and 49415 deletions
+3 -6
View File
@@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Docker meta
id: meta
@@ -25,15 +25,14 @@ jobs:
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
image: tonistiigi/binfmt:qemu-v8.1.5
- 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
@@ -56,8 +55,6 @@ jobs:
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 }}
+3 -6
View File
@@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Docker meta
id: meta
@@ -25,15 +25,14 @@ jobs:
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
image: tonistiigi/binfmt:qemu-v8.1.5
- 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
@@ -55,8 +54,6 @@ jobs:
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 }}
+1
View File
@@ -1,5 +1,6 @@
name: Bug Report
description: Report a bug in ezBookkeeping
labels: bug
body:
- type: checkboxes
id: checkboxes
+1 -8
View File
@@ -1,8 +1 @@
blank_issues_enabled: false
contact_links:
- name: Frequently Asked Questions
url: https://ezbookkeeping.mayswind.net/faq
about: Please check whether your question has already been mentioned here.
- name: Usage Questions
url: https://github.com/mayswind/ezbookkeeping/discussions/categories/q-a
about: Questions about using ezBookkeeping can be discussed in GitHub Discussions.
blank_issues_enabled: false
@@ -1,5 +1,6 @@
name: Feature Request
description: Request a feature or enhancement for ezBookkeeping
labels: enhancement
body:
- type: checkboxes
id: checkboxes
@@ -1,124 +0,0 @@
name: Build docker image and package for linux
inputs:
release-build:
required: false
type: string
build-unix-time:
required: false
type: string
build-date:
required: false
type: string
check-3rd-api:
required: false
type: string
skip-tests:
required: false
type: string
platform:
required: true
type: string
platform-name:
required: true
type: string
docker-push:
required: true
type: boolean
docker-image-name:
required: true
type: string
docker-username:
required: false
type: string
docker-password:
required: false
type: string
docker-bake-meta-file-path:
required: true
type: string
docker-bake-meta-artifact-name:
required: true
type: string
docker-bake-digests-file-path:
required: true
type: string
docker-bake-digests-artifact-name-prefix:
required: true
type: string
package-file-name-prefix:
required: true
type: string
package-artifact-name-prefix:
required: true
type: string
runs:
using: "composite"
steps:
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Download docker bake meta artifact
uses: actions/download-artifact@v4
with:
name: ${{ inputs.docker-bake-meta-artifact-name }}
path: ${{ runner.temp }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
if: ${{ inputs.docker-username != '' && inputs.docker-password != '' }}
uses: docker/login-action@v3
with:
username: ${{ inputs.docker-username }}
password: ${{ inputs.docker-password }}
- name: Build docker for ${{ inputs.platform-name }}
id: bake
uses: docker/bake-action@v6
with:
files: |
./docker-bake.hcl
cwd://${{ inputs.docker-bake-meta-file-path }}
source: .
targets: image
set: |
*.tags=${{ inputs.docker-image-name }}
*.platform=${{ inputs.platform }}
*.args.RELEASE_BUILD=${{ inputs.release-build }}
*.args.BUILD_PIPELINE=1
*.args.BUILD_UNIXTIME=${{ inputs.build-unix-time }}
*.args.BUILD_DATE=${{ inputs.build-date }}
*.args.CHECK_3RD_API=${{ inputs.check-3rd-api }}
*.args.SKIP_TESTS=${{ inputs.skip-tests }}
*.output=type=image,push-by-digest=true,name-canonical=true,push=${{ inputs.docker-push }}
*.output+=type=local,dest=${{ runner.temp }}/package
- name: Export digests file for ${{ inputs.platform-name }}
shell: bash
run: |
digest="${{ fromJSON(steps.bake.outputs.metadata).image['containerimage.digest'] }}"
mkdir -p ${{ inputs.docker-bake-digests-file-path }}
touch "${{ inputs.docker-bake-digests-file-path }}/${digest#sha256:}"
- name: Build package file for ${{ inputs.platform-name }}
shell: bash
run: |
cd ${{ runner.temp }}/package/ezbookkeeping
tar -czf ${{ runner.temp }}/${{ inputs.package-file-name-prefix }}-${{ inputs.platform-name }}.tar.gz *
- name: Upload ${{ inputs.platform-name }} digests artifact
uses: actions/upload-artifact@v4
with:
name: ${{ inputs.docker-bake-digests-artifact-name-prefix }}-${{ inputs.platform-name }}
path: ${{ inputs.docker-bake-digests-file-path }}/*
if-no-files-found: error
- name: Upload artifact for ${{ inputs.platform-name }}
uses: actions/upload-artifact@v4
with:
name: ${{ inputs.package-artifact-name-prefix }}-${{ inputs.platform-name }}
path: ${{ runner.temp }}/${{ inputs.package-file-name-prefix }}-${{ inputs.platform-name }}.tar.gz
if-no-files-found: error
@@ -1,76 +0,0 @@
name: Build backend file for windows
inputs:
go-version:
required: false
default: "1.25.5"
mingw-version:
required: false
default: "15.2.0"
mingw-revison:
required: false
default: "v13-rev0"
release-build:
required: false
type: string
build-unix-time:
required: false
type: string
build-date:
required: false
type: string
check-3rd-api:
required: false
type: string
skip-tests:
required: false
type: string
backend-artifact-name-prefix:
required: true
type: string
runs:
using: "composite"
steps:
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: ${{ inputs.go-version }}
- name: Install MinGW
shell: pwsh
run: |
$mingwVersion = "${{ inputs.mingw-version }}"
$mingwRevision = "${{ inputs.mingw-revison }}"
$url = "https://github.com/niXman/mingw-builds-binaries/releases/download/${mingwVersion}-rt_${mingwRevision}/x86_64-${mingwVersion}-release-posix-seh-ucrt-rt_${mingwRevision}.7z"
$archive = "C:\mingw.7z"
$mingwDir = "C:\mingw64"
Write-Host "Downloading MinGW from ${url}"
Invoke-WebRequest -Uri ${url} -OutFile ${archive}
Remove-Item -Recurse -Force ${mingwDir}
New-Item -ItemType Directory -Path ${mingwDir}
Write-Host "Extracting MinGW to ${mingwDir}"
7z x ${archive} -oC:\
"${mingwDir}\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
- name: Build backend for windows-x64
shell: pwsh
env:
RELEASE_BUILD: "${{ inputs.release-build }}"
BUILD_PIPELINE: "1"
BUILD_UNIXTIME: "${{ inputs.build-unix-time }}"
BUILD_DATE: "${{ inputs.build-date }}"
CHECK_3RD_API: "${{ inputs.check-3rd-api }}"
SKIP_TESTS: "${{ inputs.skip-tests }}"
run: |
.\build.ps1 backend
- name: Upload windows backend artifact
uses: actions/upload-artifact@v4
with:
name: ${{ inputs.backend-artifact-name-prefix }}-windows-x64
path: ezbookkeeping.exe
if-no-files-found: error
@@ -1,57 +0,0 @@
name: Build packages for windows
inputs:
package-file-name-prefix:
required: true
type: string
package-artifact-name-prefix:
required: true
type: string
backend-artifact-name-prefix:
required: true
type: string
runs:
using: "composite"
steps:
- name: Download windows-x64 backend file
uses: actions/download-artifact@v4
with:
name: ${{ inputs.backend-artifact-name-prefix }}-windows-x64
path: ${{ runner.temp }}\backend
- name: Download linux-amd64 packaged files
uses: actions/download-artifact@v4
with:
name: ${{ inputs.package-artifact-name-prefix }}-linux-amd64
path: ${{ runner.temp }}\package
- name: Extract frontend files from linux-amd64 package
shell: pwsh
run: |
New-Item -ItemType Directory -Path package
tar -xzf (Get-ChildItem ${{ runner.temp }}\package\${{ inputs.package-file-name-prefix }}-linux-amd64.tar.gz) -C package
- name: Package windows-x64 package
shell: pwsh
run: |
New-Item -ItemType Directory -Path "ezbookkeeping"
New-Item -ItemType Directory -Path "ezbookkeeping\data"
New-Item -ItemType Directory -Path "ezbookkeeping\storage"
New-Item -ItemType Directory -Path "ezbookkeeping\log"
Copy-Item ${{ runner.temp }}\backend\ezbookkeeping.exe -Destination ezbookkeeping\
Copy-Item -Recurse -Force package\public -Destination ezbookkeeping\public
Copy-Item -Recurse -Force conf -Destination ezbookkeeping\conf
Copy-Item -Recurse -Force templates -Destination ezbookkeeping\templates
Copy-Item .\LICENSE -Destination ezbookkeeping\
Push-Location ezbookkeeping
7z a -r -tzip -mx9 ..\${{ inputs.package-file-name-prefix }}-windows-x64.zip *
Pop-Location
Remove-Item -Recurse -Force ezbookkeeping
- name: Upload windows artifact
uses: actions/upload-artifact@v4
with:
name: ${{ inputs.package-artifact-name-prefix }}-windows-x64
path: ${{ inputs.package-file-name-prefix }}-windows-x64.zip
if-no-files-found: error
@@ -1,58 +0,0 @@
name: Push linux docker multi-arch image to registry
inputs:
docker-image-name:
required: true
type: string
docker-username:
required: true
type: string
docker-password:
required: true
type: string
docker-bake-meta-file-path:
required: true
type: string
docker-bake-meta-artifact-name:
required: true
type: string
docker-bake-digests-file-path:
required: true
type: string
docker-bake-digests-artifact-name-prefix:
required: true
type: string
docker-image-tags:
required: true
type: string
runs:
using: "composite"
steps:
- name: Download docker bake meta artifact
uses: actions/download-artifact@v4
with:
name: ${{ inputs.docker-bake-meta-artifact-name }}
path: ${{ runner.temp }}
- name: Download digests artifact
uses: actions/download-artifact@v4
with:
pattern: ${{ inputs.docker-bake-digests-artifact-name-prefix }}-*
merge-multiple: true
path: ${{ inputs.docker-bake-digests-file-path }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ inputs.docker-username }}
password: ${{ inputs.docker-password }}
- name: Create manifest and push
shell: bash
working-directory: ${{ inputs.docker-bake-digests-file-path }}
run: |
docker buildx imagetools create $(echo "${{ inputs.docker-image-tags }}" | xargs -I {} echo -n " -t {}") $(printf '${{ inputs.docker-image-name }}@sha256:%s ' *)
+20 -137
View File
@@ -6,144 +6,27 @@ on:
- main
jobs:
setup:
build-linux-docker:
runs-on: ubuntu-latest
outputs:
build-unix-time: ${{ steps.variable.outputs.build_unix_time }}
build-date: ${{ steps.variable.outputs.build_date }}
docker-tags: ${{ steps.meta.outputs.tags }}
docker-labels: ${{ steps.meta.outputs.labels }}
ezbookkeeping-docker-bake-meta-file-path: ${{ steps.variable.outputs.ezbookkeeping_docker_bake_meta_file_path }}
ezbookkeeping-docker-bake-meta-artifact-name: ${{ steps.variable.outputs.ezbookkeeping_docker_bake_meta_artifact_name }}
ezbookkeeping-docker-digests-file-path: ${{ steps.variable.outputs.ezbookkeeping_docker_digests_file_path }}
ezbookkeeping-docker-digests-artifact-name-prefix: ${{ steps.variable.outputs.ezbookkeeping_docker_digests_artifact_name_prefix }}
ezbookkeeping-package-file-name-prefix: ${{ steps.variable.outputs.ezbookkeeping_package_file_name_prefix }}
ezbookkeeping-package-artifact-name-prefix: ${{ steps.variable.outputs.ezbookkeeping_package_artifact_name_prefix }}
ezbookkeeping-backend-artifact-name-prefix: ${{ steps.variable.outputs.ezbookkeeping_backend_artifact_name_prefix }}
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
-
name: Checkout
uses: actions/checkout@v4
-
name: Login to DockerHub
uses: docker/login-action@v3
with:
images: |
${{ vars.DOCKER_IMAGE_NAME }}
tags: |
type=raw,value=dev-${{ github.run_id }}
- name: Set up variables
id: variable
run: |
echo "build_unix_time=$(date '+%s')" >> "$GITHUB_OUTPUT"
echo "build_date=$(date '+%Y%m%d')" >> "$GITHUB_OUTPUT"
echo "ezbookkeeping_docker_bake_meta_file_path=${{ runner.temp }}/bake-meta.json" >> "$GITHUB_OUTPUT"
echo "ezbookkeeping_docker_bake_meta_artifact_name=ezbookkeeping-build-dev-docker-bake-meta-${{ github.run_id }}" >> "$GITHUB_OUTPUT"
echo "ezbookkeeping_docker_digests_file_path=${{ runner.temp }}/digests" >> "$GITHUB_OUTPUT"
echo "ezbookkeeping_docker_digests_artifact_name_prefix=ezbookkeeping-build-dev-digests-${{ github.run_id }}" >> "$GITHUB_OUTPUT"
echo "ezbookkeeping_package_file_name_prefix=ezbookkeeping-dev-package-${{ github.run_id }}" >> "$GITHUB_OUTPUT"
echo "ezbookkeeping_package_artifact_name_prefix=ezbookkeeping-build-dev-package-${{ github.run_id }}" >> "$GITHUB_OUTPUT"
echo "ezbookkeeping_backend_artifact_name_prefix=ezbookkeeping-build-dev-backend-${{ github.run_id }}" >> "$GITHUB_OUTPUT"
- name: Rename docker bake meta file
run: |
mv "${{ steps.meta.outputs.bake-file }}" "${{ steps.variable.outputs.ezbookkeeping_docker_bake_meta_file_path }}"
- name: Upload docker bake meta artifact
uses: actions/upload-artifact@v4
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
-
name: Build
uses: docker/build-push-action@v6
with:
name: ${{ steps.variable.outputs.ezbookkeeping_docker_bake_meta_artifact_name }}
path: ${{ steps.variable.outputs.ezbookkeeping_docker_bake_meta_file_path }}
if-no-files-found: error
build-linux-docker-and-package-x86:
runs-on: ubuntu-24.04
needs:
- setup
steps:
- name: Checkout
uses: actions/checkout@v5
- uses: ./.github/actions/build-linux-docker-and-package
with:
build-unix-time: ${{ needs.setup.outputs.build-unix-time }}
build-date: ${{ needs.setup.outputs.build-date }}
check-3rd-api: ${{ vars.CHECK_3RD_API }}
skip-tests: ${{ vars.SKIP_TESTS }}
platform: linux/amd64
platform-name: linux-amd64
docker-push: false
docker-image-name: ${{ vars.DOCKER_IMAGE_NAME }}
docker-bake-meta-file-path: ${{ needs.setup.outputs.ezbookkeeping-docker-bake-meta-file-path }}
docker-bake-meta-artifact-name: ${{ needs.setup.outputs.ezbookkeeping-docker-bake-meta-artifact-name }}
docker-bake-digests-file-path: ${{ needs.setup.outputs.ezbookkeeping-docker-digests-file-path }}
docker-bake-digests-artifact-name-prefix: ${{ needs.setup.outputs.ezbookkeeping-docker-digests-artifact-name-prefix }}
package-file-name-prefix: ${{ needs.setup.outputs.ezbookkeeping-package-file-name-prefix }}
package-artifact-name-prefix: ${{ needs.setup.outputs.ezbookkeeping-package-artifact-name-prefix }}
build-linux-docker-and-package-arm:
runs-on: ubuntu-24.04-arm
needs:
- setup
strategy:
matrix:
include:
- platform: linux/arm64/v8
platform-name: linux-arm64
- platform: linux/arm/v7
platform-name: linux-armv7
- platform: linux/arm/v6
platform-name: linux-armv6
steps:
- name: Checkout
uses: actions/checkout@v5
- uses: ./.github/actions/build-linux-docker-and-package
with:
build-unix-time: ${{ needs.setup.outputs.build-unix-time }}
build-date: ${{ needs.setup.outputs.build-date }}
check-3rd-api: ${{ vars.CHECK_3RD_API }}
skip-tests: ${{ vars.SKIP_TESTS }}
platform: ${{ matrix.platform }}
platform-name: ${{ matrix.platform-name }}
docker-push: false
docker-image-name: ${{ vars.DOCKER_IMAGE_NAME }}
docker-bake-meta-file-path: ${{ needs.setup.outputs.ezbookkeeping-docker-bake-meta-file-path }}
docker-bake-meta-artifact-name: ${{ needs.setup.outputs.ezbookkeeping-docker-bake-meta-artifact-name }}
docker-bake-digests-file-path: ${{ needs.setup.outputs.ezbookkeeping-docker-digests-file-path }}
docker-bake-digests-artifact-name-prefix: ${{ needs.setup.outputs.ezbookkeeping-docker-digests-artifact-name-prefix }}
package-file-name-prefix: ${{ needs.setup.outputs.ezbookkeeping-package-file-name-prefix }}
package-artifact-name-prefix: ${{ needs.setup.outputs.ezbookkeeping-package-artifact-name-prefix }}
build-windows-backend:
needs:
- setup
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v5
- uses: ./.github/actions/build-windows-backend
with:
build-unix-time: ${{ needs.setup.outputs.build-unix-time }}
build-date: ${{ needs.setup.outputs.build-date }}
check-3rd-api: ${{ vars.CHECK_3RD_API }}
skip-tests: ${{ vars.SKIP_TESTS }}
backend-artifact-name-prefix: ${{ needs.setup.outputs.ezbookkeeping-backend-artifact-name-prefix }}
build-windows-package:
needs:
- setup
- build-windows-backend
- build-linux-docker-and-package-x86
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v5
- uses: ./.github/actions/build-windows-package
with:
package-file-name-prefix: ${{ needs.setup.outputs.ezbookkeeping-package-file-name-prefix }}
package-artifact-name-prefix: ${{ needs.setup.outputs.ezbookkeeping-package-artifact-name-prefix }}
backend-artifact-name-prefix: ${{ needs.setup.outputs.ezbookkeeping-backend-artifact-name-prefix }}
file: Dockerfile
context: .
platforms: linux/amd64
push: false
build-args: |
BUILD_PIPELINE=1
CHECK_3RD_API=${{ vars.CHECK_3RD_API }}
SKIP_TESTS=${{ vars.SKIP_TESTS }}
+162 -142
View File
@@ -6,194 +6,214 @@ on:
- "v*.*.*"
jobs:
setup:
build-linux-docker:
runs-on: ubuntu-latest
outputs:
build-unix-time: ${{ steps.variable.outputs.build_unix_time }}
build-date: ${{ steps.variable.outputs.build_date }}
docker-version: ${{ steps.meta.outputs.version }}
docker-tags: ${{ steps.meta.outputs.tags }}
docker-labels: ${{ steps.meta.outputs.labels }}
ezbookkeeping-docker-bake-meta-file-path: ${{ steps.variable.outputs.ezbookkeeping_docker_bake_meta_file_path }}
ezbookkeeping-docker-bake-meta-artifact-name: ${{ steps.variable.outputs.ezbookkeeping_docker_bake_meta_artifact_name }}
ezbookkeeping-docker-digests-file-path: ${{ steps.variable.outputs.ezbookkeeping_docker_digests_file_path }}
ezbookkeeping-docker-digests-artifact-name-prefix: ${{ steps.variable.outputs.ezbookkeeping_docker_digests_artifact_name_prefix }}
ezbookkeeping-package-file-name-prefix: ${{ steps.variable.outputs.ezbookkeeping_package_file_name_prefix }}
ezbookkeeping-package-artifact-name-prefix: ${{ steps.variable.outputs.ezbookkeeping_package_artifact_name_prefix }}
ezbookkeeping-backend-artifact-name-prefix: ${{ steps.variable.outputs.ezbookkeeping_backend_artifact_name_prefix }}
image-tag: ${{ steps.meta.outputs.version }}
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
${{ vars.DOCKER_IMAGE_NAME }}
${{ secrets.DOCKER_USERNAME }}/ezbookkeeping
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,value=latest
- name: Set up variables
id: variable
run: |
echo "build_unix_time=$(date '+%s')" >> "$GITHUB_OUTPUT"
echo "build_date=$(date '+%Y%m%d')" >> "$GITHUB_OUTPUT"
echo "ezbookkeeping_docker_bake_meta_file_path=${{ runner.temp }}/bake-meta.json" >> "$GITHUB_OUTPUT"
echo "ezbookkeeping_docker_bake_meta_artifact_name=ezbookkeeping-build-release-docker-bake-meta-${{ github.run_id }}" >> "$GITHUB_OUTPUT"
echo "ezbookkeeping_docker_digests_file_path=${{ runner.temp }}/digests" >> "$GITHUB_OUTPUT"
echo "ezbookkeeping_docker_digests_artifact_name_prefix=ezbookkeeping-build-release-digests-${{ github.run_id }}" >> "$GITHUB_OUTPUT"
echo "ezbookkeeping_package_file_name_prefix=ezbookkeeping-${{ github.ref_name }}" >> "$GITHUB_OUTPUT"
echo "ezbookkeeping_package_artifact_name_prefix=ezbookkeeping-build-release-package-${{ github.run_id }}" >> "$GITHUB_OUTPUT"
echo "ezbookkeeping_backend_artifact_name_prefix=ezbookkeeping-build-release-backend-${{ github.run_id }}" >> "$GITHUB_OUTPUT"
- name: Rename docker bake meta file
run: |
mv "${{ steps.meta.outputs.bake-file }}" "${{ steps.variable.outputs.ezbookkeeping_docker_bake_meta_file_path }}"
- name: Upload docker bake meta artifact
uses: actions/upload-artifact@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
name: ${{ steps.variable.outputs.ezbookkeeping_docker_bake_meta_artifact_name }}
path: ${{ steps.variable.outputs.ezbookkeeping_docker_bake_meta_file_path }}
if-no-files-found: error
image: tonistiigi/binfmt:qemu-v8.1.5
build-linux-docker-and-package-x86:
runs-on: ubuntu-24.04
needs:
- setup
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- uses: ./.github/actions/build-linux-docker-and-package
- name: Login to DockerHub
uses: docker/login-action@v3
with:
release-build: 1
build-unix-time: ${{ needs.setup.outputs.build-unix-time }}
build-date: ${{ needs.setup.outputs.build-date }}
check-3rd-api: ${{ vars.CHECK_3RD_API }}
skip-tests: ${{ vars.SKIP_TESTS }}
platform: linux/amd64
platform-name: linux-amd64
docker-push: true
docker-image-name: ${{ vars.DOCKER_IMAGE_NAME }}
docker-username: ${{ vars.DOCKER_USERNAME }}
docker-password: ${{ secrets.DOCKER_PASSWORD }}
docker-bake-meta-file-path: ${{ needs.setup.outputs.ezbookkeeping-docker-bake-meta-file-path }}
docker-bake-meta-artifact-name: ${{ needs.setup.outputs.ezbookkeeping-docker-bake-meta-artifact-name }}
docker-bake-digests-file-path: ${{ needs.setup.outputs.ezbookkeeping-docker-digests-file-path }}
docker-bake-digests-artifact-name-prefix: ${{ needs.setup.outputs.ezbookkeeping-docker-digests-artifact-name-prefix }}
package-file-name-prefix: ${{ needs.setup.outputs.ezbookkeeping-package-file-name-prefix }}
package-artifact-name-prefix: ${{ needs.setup.outputs.ezbookkeeping-package-artifact-name-prefix }}
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
build-linux-docker-and-package-arm:
runs-on: ubuntu-24.04-arm
needs:
- setup
- name: Build and push
uses: docker/build-push-action@v6
with:
file: Dockerfile
context: .
platforms: |
linux/amd64
linux/arm64/v8
linux/arm/v7
linux/arm/v6
push: true
build-args: |
RELEASE_BUILD=1
BUILD_PIPELINE=1
CHECK_3RD_API=${{ vars.CHECK_3RD_API }}
SKIP_TESTS=${{ vars.SKIP_TESTS }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
upload-linux-artifact:
needs: build-linux-docker
runs-on: ubuntu-latest
strategy:
matrix:
include:
- platform: linux/arm64/v8
platform-name: linux-arm64
- platform: linux/arm/v7
platform-name: linux-armv7
- platform: linux/arm/v6
platform-name: linux-armv6
- arch: linux/amd64
arch_alias: linux-amd64
- arch: linux/arm64/v8
arch_alias: linux-arm64
- arch: linux/arm/v7
arch_alias: linux-armv7
- arch: linux/arm/v6
arch_alias: linux-armv6
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v4
- uses: ./.github/actions/build-linux-docker-and-package
- name: Login to DockerHub
uses: docker/login-action@v3
with:
release-build: 1
build-unix-time: ${{ needs.setup.outputs.build-unix-time }}
build-date: ${{ needs.setup.outputs.build-date }}
check-3rd-api: ${{ vars.CHECK_3RD_API }}
skip-tests: ${{ vars.SKIP_TESTS }}
platform: ${{ matrix.platform }}
platform-name: ${{ matrix.platform-name }}
docker-push: true
docker-image-name: ${{ vars.DOCKER_IMAGE_NAME }}
docker-username: ${{ vars.DOCKER_USERNAME }}
docker-password: ${{ secrets.DOCKER_PASSWORD }}
docker-bake-meta-file-path: ${{ needs.setup.outputs.ezbookkeeping-docker-bake-meta-file-path }}
docker-bake-meta-artifact-name: ${{ needs.setup.outputs.ezbookkeeping-docker-bake-meta-artifact-name }}
docker-bake-digests-file-path: ${{ needs.setup.outputs.ezbookkeeping-docker-digests-file-path }}
docker-bake-digests-artifact-name-prefix: ${{ needs.setup.outputs.ezbookkeeping-docker-digests-artifact-name-prefix }}
package-file-name-prefix: ${{ needs.setup.outputs.ezbookkeeping-package-file-name-prefix }}
package-artifact-name-prefix: ${{ needs.setup.outputs.ezbookkeeping-package-artifact-name-prefix }}
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
push-linux-docker:
needs:
- setup
- build-linux-docker-and-package-x86
- build-linux-docker-and-package-arm
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Pull and save packaged files for ${{ matrix.arch }}
run: |
VERSION=${{ needs.build-linux-docker.outputs.image-tag }}
IMAGE=${{ secrets.DOCKER_USERNAME }}/ezbookkeeping:${VERSION}
docker pull --platform ${{ matrix.arch }} ${IMAGE}
cid=$(docker create "${IMAGE}")
docker cp ${cid}:/ezbookkeeping ./ezbookkeeping
docker rm ${cid}
cd ezbookkeeping
tar -czf ../ezbookkeeping-v${VERSION}-${{ matrix.arch_alias }}.tar.gz *
cd ..
rm -rf ezbookkeeping
- uses: ./.github/actions/push-linux-docker
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
docker-image-name: ${{ vars.DOCKER_IMAGE_NAME }}
docker-username: ${{ vars.DOCKER_USERNAME }}
docker-password: ${{ secrets.DOCKER_PASSWORD }}
docker-bake-meta-file-path: ${{ needs.setup.outputs.ezbookkeeping-docker-bake-meta-file-path }}
docker-bake-meta-artifact-name: ${{ needs.setup.outputs.ezbookkeeping-docker-bake-meta-artifact-name }}
docker-bake-digests-file-path: ${{ needs.setup.outputs.ezbookkeeping-docker-digests-file-path }}
docker-bake-digests-artifact-name-prefix: ${{ needs.setup.outputs.ezbookkeeping-docker-digests-artifact-name-prefix }}
docker-image-tags: ${{ needs.setup.outputs.docker-tags }}
name: ezbookkeeping-${{ github.ref_name }}-${{ matrix.arch_alias }}
path: ezbookkeeping-${{ github.ref_name }}-${{ matrix.arch_alias }}.tar.gz
if-no-files-found: error
build-windows-backend:
needs:
- setup
build-and-upload-windows-package:
needs: upload-linux-artifact
runs-on: windows-latest
env:
GO_VERSION: "1.25.1"
MINGW_VERSION: "14.2.0"
MINGW_REVISION: "v12-rev2"
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v4
- uses: ./.github/actions/build-windows-backend
- name: Download linux-amd64 packaged files
uses: actions/download-artifact@v4
with:
release-build: 1
build-unix-time: ${{ needs.setup.outputs.build-unix-time }}
build-date: ${{ needs.setup.outputs.build-date }}
check-3rd-api: ${{ vars.CHECK_3RD_API }}
skip-tests: ${{ vars.SKIP_TESTS }}
backend-artifact-name-prefix: ${{ needs.setup.outputs.ezbookkeeping-backend-artifact-name-prefix }}
name: ezbookkeeping-${{ github.ref_name }}-linux-amd64
path: artifacts
build-windows-package:
needs:
- setup
- build-windows-backend
- build-linux-docker-and-package-x86
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Extract frontend files from linux-amd64 package
run: |
New-Item -ItemType Directory -Path package
tar -xzf (Get-ChildItem artifacts\ezbookkeeping-${{ github.ref_name }}-linux-amd64.tar.gz) -C package
- uses: ./.github/actions/build-windows-package
- name: Set up Go
uses: actions/setup-go@v6
with:
package-file-name-prefix: ${{ needs.setup.outputs.ezbookkeeping-package-file-name-prefix }}
package-artifact-name-prefix: ${{ needs.setup.outputs.ezbookkeeping-package-artifact-name-prefix }}
backend-artifact-name-prefix: ${{ needs.setup.outputs.ezbookkeeping-backend-artifact-name-prefix }}
go-version: ${{ env.GO_VERSION }}
- name: Install MinGW
run: |
$mingwVersion = "${{ env.MINGW_VERSION }}"
$mingwRevision = "${{ env.MINGW_REVISION }}"
$url = "https://github.com/niXman/mingw-builds-binaries/releases/download/${mingwVersion}-rt_${mingwRevision}/x86_64-${mingwVersion}-release-posix-seh-ucrt-rt_${mingwRevision}.7z"
$archive = "C:\mingw.7z"
$mingwDir = "C:\mingw64"
Write-Host "Downloading MinGW from ${url}"
Invoke-WebRequest -Uri ${url} -OutFile ${archive}
Remove-Item -Recurse -Force ${mingwDir}
New-Item -ItemType Directory -Path ${mingwDir}
Write-Host "Extracting MinGW to ${mingwDir}"
7z x ${archive} -oC:\
"${mingwDir}\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
- name: Build backend for windows-x64
env:
RELEASE_BUILD: "1"
BUILD_PIPELINE: "1"
CHECK_3RD_API: ${{ vars.CHECK_3RD_API }}
SKIP_TESTS: ${{ vars.SKIP_TESTS }}
run: |
.\build.ps1 backend
- name: Package Windows build
run: |
New-Item -ItemType Directory -Path "ezbookkeeping"
New-Item -ItemType Directory -Path "ezbookkeeping\data"
New-Item -ItemType Directory -Path "ezbookkeeping\storage"
New-Item -ItemType Directory -Path "ezbookkeeping\log"
Copy-Item ezbookkeeping.exe -Destination ezbookkeeping\
Copy-Item -Recurse -Force package\public -Destination ezbookkeeping\public
Copy-Item -Recurse -Force conf -Destination ezbookkeeping\conf
Copy-Item -Recurse -Force templates -Destination ezbookkeeping\templates
Copy-Item .\LICENSE -Destination ezbookkeeping\
Push-Location ezbookkeeping
7z a -r -tzip -mx9 ..\ezbookkeeping-${{ github.ref_name }}-windows-x64.zip *
Pop-Location
Remove-Item -Recurse -Force ezbookkeeping
- name: Upload Windows artifact
uses: actions/upload-artifact@v4
with:
name: ezbookkeeping-${{ github.ref_name }}-windows-x64
path: ezbookkeeping-${{ github.ref_name }}-windows-x64.zip
if-no-files-found: error
publish-release:
runs-on: ubuntu-latest
needs:
- setup
- build-linux-docker-and-package-x86
- build-linux-docker-and-package-arm
- build-windows-package
- push-linux-docker
- upload-linux-artifact
- build-and-upload-windows-package
steps:
- name: Download all packaged files
- name: Download linux-amd64 packaged files
uses: actions/download-artifact@v4
with:
pattern: ${{ needs.setup.outputs.ezbookkeeping-package-artifact-name-prefix }}-*
merge-multiple: true
path: release-files
name: ezbookkeeping-${{ github.ref_name }}-linux-amd64
path: ./release-files
- name: Download linux-arm64 packaged files
uses: actions/download-artifact@v4
with:
name: ezbookkeeping-${{ github.ref_name }}-linux-arm64
path: ./release-files
- name: Download linux-armv6 packaged files
uses: actions/download-artifact@v4
with:
name: ezbookkeeping-${{ github.ref_name }}-linux-armv6
path: ./release-files
- name: Download linux-armv7 packaged files
uses: actions/download-artifact@v4
with:
name: ezbookkeeping-${{ github.ref_name }}-linux-armv7
path: ./release-files
- name: Download windows-x64 packaged files
uses: actions/download-artifact@v4
with:
name: ezbookkeeping-${{ github.ref_name }}-windows-x64
path: ./release-files
- name: Publish Release ${{ github.ref_name }}
uses: softprops/action-gh-release@v2
+132 -131
View File
@@ -6,172 +6,173 @@ on:
- main
jobs:
setup:
build-linux-docker:
runs-on: ubuntu-latest
outputs:
build-unix-time: ${{ steps.variable.outputs.build_unix_time }}
build-date: ${{ steps.variable.outputs.build_date }}
docker-version: ${{ steps.meta.outputs.version }}
docker-tags: ${{ steps.meta.outputs.tags }}
docker-labels: ${{ steps.meta.outputs.labels }}
ezbookkeeping-docker-bake-meta-file-path: ${{ steps.variable.outputs.ezbookkeeping_docker_bake_meta_file_path }}
ezbookkeeping-docker-bake-meta-artifact-name: ${{ steps.variable.outputs.ezbookkeeping_docker_bake_meta_artifact_name }}
ezbookkeeping-docker-digests-file-path: ${{ steps.variable.outputs.ezbookkeeping_docker_digests_file_path }}
ezbookkeeping-docker-digests-artifact-name-prefix: ${{ steps.variable.outputs.ezbookkeeping_docker_digests_artifact_name_prefix }}
ezbookkeeping-package-file-name-prefix: ${{ steps.variable.outputs.ezbookkeeping_package_file_name_prefix }}
ezbookkeeping-package-artifact-name-prefix: ${{ steps.variable.outputs.ezbookkeeping_package_artifact_name_prefix }}
ezbookkeeping-backend-artifact-name-prefix: ${{ steps.variable.outputs.ezbookkeeping_backend_artifact_name_prefix }}
image-tag: ${{ steps.meta.outputs.version }}
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
${{ vars.DOCKER_IMAGE_NAME }}
${{ secrets.DOCKER_USERNAME }}/ezbookkeeping
tags: |
type=raw,value=SNAPSHOT-{{date 'YYYYMMDD'}}-${{ github.run_id }}
type=raw,value=SNAPSHOT-{{date 'YYYYMMDD'}}.${{ github.run_id }}
type=raw,value=SNAPSHOT-{{date 'YYYYMMDD'}}
type=raw,value=latest-snapshot
- name: Set up variables
id: variable
run: |
echo "build_unix_time=$(date '+%s')" >> "$GITHUB_OUTPUT"
echo "build_date=$(date '+%Y%m%d')" >> "$GITHUB_OUTPUT"
echo "ezbookkeeping_docker_bake_meta_file_path=${{ runner.temp }}/bake-meta.json" >> "$GITHUB_OUTPUT"
echo "ezbookkeeping_docker_bake_meta_artifact_name=ezbookkeeping-build-dev-docker-bake-meta-${{ github.run_id }}" >> "$GITHUB_OUTPUT"
echo "ezbookkeeping_docker_digests_file_path=${{ runner.temp }}/digests" >> "$GITHUB_OUTPUT"
echo "ezbookkeeping_docker_digests_artifact_name_prefix=ezbookkeeping-build-dev-digests-${{ github.run_id }}" >> "$GITHUB_OUTPUT"
echo "ezbookkeeping_package_file_name_prefix=ezbookkeeping-dev-package-${{ github.run_id }}" >> "$GITHUB_OUTPUT"
echo "ezbookkeeping_package_artifact_name_prefix=ezbookkeeping-build-dev-package-${{ github.run_id }}" >> "$GITHUB_OUTPUT"
echo "ezbookkeeping_backend_artifact_name_prefix=ezbookkeeping-build-dev-backend-${{ github.run_id }}" >> "$GITHUB_OUTPUT"
- name: Rename docker bake meta file
run: |
mv "${{ steps.meta.outputs.bake-file }}" "${{ steps.variable.outputs.ezbookkeeping_docker_bake_meta_file_path }}"
- name: Upload docker bake meta artifact
uses: actions/upload-artifact@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
name: ${{ steps.variable.outputs.ezbookkeeping_docker_bake_meta_artifact_name }}
path: ${{ steps.variable.outputs.ezbookkeeping_docker_bake_meta_file_path }}
if-no-files-found: error
image: tonistiigi/binfmt:qemu-v8.1.5
build-linux-docker-and-package-x86:
runs-on: ubuntu-24.04
needs:
- setup
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- uses: ./.github/actions/build-linux-docker-and-package
- name: Login to DockerHub
uses: docker/login-action@v3
with:
build-unix-time: ${{ needs.setup.outputs.build-unix-time }}
build-date: ${{ needs.setup.outputs.build-date }}
check-3rd-api: ${{ vars.CHECK_3RD_API }}
skip-tests: ${{ vars.SKIP_TESTS }}
platform: linux/amd64
platform-name: linux-amd64
docker-push: true
docker-image-name: ${{ vars.DOCKER_IMAGE_NAME }}
docker-username: ${{ vars.DOCKER_USERNAME }}
docker-password: ${{ secrets.DOCKER_PASSWORD }}
docker-bake-meta-file-path: ${{ needs.setup.outputs.ezbookkeeping-docker-bake-meta-file-path }}
docker-bake-meta-artifact-name: ${{ needs.setup.outputs.ezbookkeeping-docker-bake-meta-artifact-name }}
docker-bake-digests-file-path: ${{ needs.setup.outputs.ezbookkeeping-docker-digests-file-path }}
docker-bake-digests-artifact-name-prefix: ${{ needs.setup.outputs.ezbookkeeping-docker-digests-artifact-name-prefix }}
package-file-name-prefix: ${{ needs.setup.outputs.ezbookkeeping-package-file-name-prefix }}
package-artifact-name-prefix: ${{ needs.setup.outputs.ezbookkeeping-package-artifact-name-prefix }}
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
build-linux-docker-and-package-arm:
runs-on: ubuntu-24.04-arm
needs:
- setup
- name: Build and push
uses: docker/build-push-action@v6
with:
file: Dockerfile
context: .
platforms: |
linux/amd64
linux/arm64/v8
linux/arm/v7
linux/arm/v6
push: true
build-args: |
BUILD_PIPELINE=1
CHECK_3RD_API=${{ vars.CHECK_3RD_API }}
SKIP_TESTS=${{ vars.SKIP_TESTS }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
upload-linux-artifact:
needs: build-linux-docker
runs-on: ubuntu-latest
strategy:
matrix:
include:
- platform: linux/arm64/v8
platform-name: linux-arm64
- platform: linux/arm/v7
platform-name: linux-armv7
- platform: linux/arm/v6
platform-name: linux-armv6
- arch: linux/amd64
arch_alias: linux-amd64
- arch: linux/arm64/v8
arch_alias: linux-arm64
- arch: linux/arm/v7
arch_alias: linux-armv7
- arch: linux/arm/v6
arch_alias: linux-armv6
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v4
- uses: ./.github/actions/build-linux-docker-and-package
- name: Login to DockerHub
uses: docker/login-action@v3
with:
build-unix-time: ${{ needs.setup.outputs.build-unix-time }}
build-date: ${{ needs.setup.outputs.build-date }}
check-3rd-api: ${{ vars.CHECK_3RD_API }}
skip-tests: ${{ vars.SKIP_TESTS }}
platform: ${{ matrix.platform }}
platform-name: ${{ matrix.platform-name }}
docker-push: true
docker-image-name: ${{ vars.DOCKER_IMAGE_NAME }}
docker-username: ${{ vars.DOCKER_USERNAME }}
docker-password: ${{ secrets.DOCKER_PASSWORD }}
docker-bake-meta-file-path: ${{ needs.setup.outputs.ezbookkeeping-docker-bake-meta-file-path }}
docker-bake-meta-artifact-name: ${{ needs.setup.outputs.ezbookkeeping-docker-bake-meta-artifact-name }}
docker-bake-digests-file-path: ${{ needs.setup.outputs.ezbookkeeping-docker-digests-file-path }}
docker-bake-digests-artifact-name-prefix: ${{ needs.setup.outputs.ezbookkeeping-docker-digests-artifact-name-prefix }}
package-file-name-prefix: ${{ needs.setup.outputs.ezbookkeeping-package-file-name-prefix }}
package-artifact-name-prefix: ${{ needs.setup.outputs.ezbookkeeping-package-artifact-name-prefix }}
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
push-linux-docker:
needs:
- setup
- build-linux-docker-and-package-x86
- build-linux-docker-and-package-arm
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Pull and save packaged files for ${{ matrix.arch }}
run: |
TAG=${{ needs.build-linux-docker.outputs.image-tag }}
IMAGE=${{ secrets.DOCKER_USERNAME }}/ezbookkeeping:${TAG}
docker pull --platform ${{ matrix.arch }} ${IMAGE}
cid=$(docker create "${IMAGE}")
docker cp ${cid}:/ezbookkeeping ./ezbookkeeping
docker rm ${cid}
cd ezbookkeeping
tar -czf ../ezbookkeeping-dev-${{ github.run_id }}-${{ matrix.arch_alias }}.tar.gz *
cd ..
rm -rf ezbookkeeping
- uses: ./.github/actions/push-linux-docker
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
docker-image-name: ${{ vars.DOCKER_IMAGE_NAME }}
docker-username: ${{ vars.DOCKER_USERNAME }}
docker-password: ${{ secrets.DOCKER_PASSWORD }}
docker-bake-meta-file-path: ${{ needs.setup.outputs.ezbookkeeping-docker-bake-meta-file-path }}
docker-bake-meta-artifact-name: ${{ needs.setup.outputs.ezbookkeeping-docker-bake-meta-artifact-name }}
docker-bake-digests-file-path: ${{ needs.setup.outputs.ezbookkeeping-docker-digests-file-path }}
docker-bake-digests-artifact-name-prefix: ${{ needs.setup.outputs.ezbookkeeping-docker-digests-artifact-name-prefix }}
docker-image-tags: ${{ needs.setup.outputs.docker-tags }}
name: ezbookkeeping-dev-${{ github.run_id }}-${{ matrix.arch_alias }}
path: ezbookkeeping-dev-${{ github.run_id }}-${{ matrix.arch_alias }}.tar.gz
if-no-files-found: error
build-windows-backend:
needs:
- setup
build-and-upload-windows-package:
needs: upload-linux-artifact
runs-on: windows-latest
env:
GO_VERSION: "1.25.1"
MINGW_VERSION: "14.2.0"
MINGW_REVISION: "v12-rev2"
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v4
- uses: ./.github/actions/build-windows-backend
- name: Download linux-amd64 packaged files
uses: actions/download-artifact@v4
with:
build-unix-time: ${{ needs.setup.outputs.build-unix-time }}
build-date: ${{ needs.setup.outputs.build-date }}
check-3rd-api: ${{ vars.CHECK_3RD_API }}
skip-tests: ${{ vars.SKIP_TESTS }}
backend-artifact-name-prefix: ${{ needs.setup.outputs.ezbookkeeping-backend-artifact-name-prefix }}
name: ezbookkeeping-dev-${{ github.run_id }}-linux-amd64
path: artifacts
build-windows-package:
needs:
- setup
- build-windows-backend
- build-linux-docker-and-package-x86
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Extract frontend files from linux-amd64 package
run: |
New-Item -ItemType Directory -Path package
tar -xzf (Get-ChildItem artifacts\ezbookkeeping-dev-${{ github.run_id }}-linux-amd64.tar.gz) -C package
- uses: ./.github/actions/build-windows-package
- name: Set up Go
uses: actions/setup-go@v6
with:
package-file-name-prefix: ${{ needs.setup.outputs.ezbookkeeping-package-file-name-prefix }}
package-artifact-name-prefix: ${{ needs.setup.outputs.ezbookkeeping-package-artifact-name-prefix }}
backend-artifact-name-prefix: ${{ needs.setup.outputs.ezbookkeeping-backend-artifact-name-prefix }}
go-version: ${{ env.GO_VERSION }}
- name: Install MinGW
run: |
$mingwVersion = "${{ env.MINGW_VERSION }}"
$mingwRevision = "${{ env.MINGW_REVISION }}"
$url = "https://github.com/niXman/mingw-builds-binaries/releases/download/${mingwVersion}-rt_${mingwRevision}/x86_64-${mingwVersion}-release-posix-seh-ucrt-rt_${mingwRevision}.7z"
$archive = "C:\mingw.7z"
$mingwDir = "C:\mingw64"
Write-Host "Downloading MinGW from ${url}"
Invoke-WebRequest -Uri ${url} -OutFile ${archive}
Remove-Item -Recurse -Force ${mingwDir}
New-Item -ItemType Directory -Path ${mingwDir}
Write-Host "Extracting MinGW to ${mingwDir}"
7z x ${archive} -oC:\
"${mingwDir}\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
- name: Build backend for windows-x64
env:
BUILD_PIPELINE: "1"
CHECK_3RD_API: ${{ vars.CHECK_3RD_API }}
SKIP_TESTS: ${{ vars.SKIP_TESTS }}
run: |
.\build.ps1 backend
- name: Package Windows build
run: |
New-Item -ItemType Directory -Path "ezbookkeeping"
New-Item -ItemType Directory -Path "ezbookkeeping\data"
New-Item -ItemType Directory -Path "ezbookkeeping\storage"
New-Item -ItemType Directory -Path "ezbookkeeping\log"
Copy-Item ezbookkeeping.exe -Destination ezbookkeeping\
Copy-Item -Recurse -Force package\public -Destination ezbookkeeping\public
Copy-Item -Recurse -Force conf -Destination ezbookkeeping\conf
Copy-Item -Recurse -Force templates -Destination ezbookkeeping\templates
Copy-Item .\LICENSE -Destination ezbookkeeping\
Push-Location ezbookkeeping
7z a -r -tzip -mx9 ..\ezbookkeeping-dev-${{ github.run_id }}-windows-x64.zip *
Pop-Location
Remove-Item -Recurse -Force ezbookkeeping
- name: Upload Windows artifact
uses: actions/upload-artifact@v4
with:
name: ezbookkeeping-dev-${{ github.run_id }}-windows-x64
path: ezbookkeeping-dev-${{ github.run_id }}-windows-x64.zip
if-no-files-found: error
+3 -11
View File
@@ -1,15 +1,11 @@
# Build backend binary file
FROM golang:1.25.5-alpine3.23 AS be-builder
FROM golang:1.25.1-alpine3.22 AS be-builder
ARG RELEASE_BUILD
ARG BUILD_PIPELINE
ARG BUILD_UNIXTIME
ARG BUILD_DATE
ARG CHECK_3RD_API
ARG SKIP_TESTS
ENV RELEASE_BUILD=$RELEASE_BUILD
ENV BUILD_PIPELINE=$BUILD_PIPELINE
ENV BUILD_UNIXTIME=$BUILD_UNIXTIME
ENV BUILD_DATE=$BUILD_DATE
ENV CHECK_3RD_API=$CHECK_3RD_API
ENV SKIP_TESTS=$SKIP_TESTS
WORKDIR /go/src/github.com/mayswind/ezbookkeeping
@@ -19,15 +15,11 @@ RUN apk add git gcc g++ libc-dev
RUN ./build.sh backend
# Build frontend files
FROM --platform=$BUILDPLATFORM node:24.12.0-alpine3.23 AS fe-builder
FROM --platform=$BUILDPLATFORM node:24.7.0-alpine3.22 AS fe-builder
ARG RELEASE_BUILD
ARG BUILD_PIPELINE
ARG BUILD_UNIXTIME
ARG BUILD_DATE
ENV RELEASE_BUILD=$RELEASE_BUILD
ENV BUILD_PIPELINE=$BUILD_PIPELINE
ENV BUILD_UNIXTIME=$BUILD_UNIXTIME
ENV BUILD_DATE=$BUILD_DATE
WORKDIR /go/src/github.com/mayswind/ezbookkeeping
COPY . .
RUN docker/frontend-build-pre-setup.sh
@@ -35,7 +27,7 @@ RUN apk add git
RUN ./build.sh frontend
# Package docker image
FROM alpine:3.23.2
FROM alpine:3.22.1
LABEL maintainer="MaysWind <i@mayswind.net>"
RUN addgroup -S -g 1000 ezbookkeeping && adduser -S -G ezbookkeeping -u 1000 ezbookkeeping
RUN apk --no-cache add tzdata
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2020-2026 MaysWind (i@mayswind.net)
Copyright (c) 2020-2025 MaysWind (i@mayswind.net)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+5 -10
View File
@@ -85,7 +85,7 @@ Download the latest release: [https://github.com/mayswind/ezbookkeeping/releases
By default, ezBookkeeping listens on port 8080. You can then visit `http://{YOUR_HOST_ADDRESS}:8080/` .
### Build from Source
Make sure you have [Golang](https://golang.org/), [GCC](https://gcc.gnu.org/), [Node.js](https://nodejs.org/) and [NPM](https://www.npmjs.com/) installed. Then download the source code, and follow these steps:
Make sure you have [Golang](https://golang.org/), [GCC](http://gcc.gnu.org/), [Node.js](https://nodejs.org/) and [NPM](https://www.npmjs.com/) installed. Then download the source code, and follow these steps:
**Linux / macOS**
@@ -129,19 +129,14 @@ Currently available translations:
| --- | --- | --- |
| de | Deutsch | [@chrgm](https://github.com/chrgm) |
| en | English | / |
| es | Español | [@Miguelonlonlon](https://github.com/Miguelonlonlon), [@abrugues](https://github.com/abrugues), [@AndresTeller](https://github.com/AndresTeller), [@diegofercri](https://github.com/diegofercri) |
| es | Español | [@Miguelonlonlon](https://github.com/Miguelonlonlon) |
| fr | Français | [@brieucdlf](https://github.com/brieucdlf) |
| it | Italiano | [@waron97](https://github.com/waron97) |
| ja | 日本語 | [@tkymmm](https://github.com/tkymmm) |
| kn | ಕನ್ನಡ | [@Darshanbm05](https://github.com/Darshanbm05) |
| ko | 한국어 | [@overworks](https://github.com/overworks) |
| nl | Nederlands | [@automagics](https://github.com/automagics) |
| nl | Nederlands | [@automagic](https://github.com/automagics) |
| pt-BR | Português (Brasil) | [@thecodergus](https://github.com/thecodergus) |
| ru | Русский | [@artegoser](https://github.com/artegoser) |
| sl | Slovenščina | [@thehijacker](https://github.com/thehijacker) |
| ta | தமிழ் | [@hhharsha36](https://github.com/hhharsha36) |
| th | ไทย | [@natthavat28](https://github.com/natthavat28) |
| tr | Türkçe | [@aydnykn](https://github.com/aydnykn) |
| uk | Українська | [@nktlitvinenko](https://github.com/nktlitvinenko) |
| vi | Tiếng Việt | [@f97](https://github.com/f97) |
| zh-Hans | 中文 (简体) | / |
@@ -150,8 +145,8 @@ Currently available translations:
Don't see your language? Help us add it.
## Documentation
1. [English](https://ezbookkeeping.mayswind.net)
1. [中文 (简体)](https://ezbookkeeping.mayswind.net/zh_Hans)
1. [English](http://ezbookkeeping.mayswind.net)
1. [中文 (简体)](http://ezbookkeeping.mayswind.net/zh_Hans)
## License
[MIT](https://github.com/mayswind/ezbookkeeping/blob/master/LICENSE)
+4 -10
View File
@@ -8,8 +8,8 @@ set "RELEASE=%RELEASE_BUILD%"
set "RELEASE_TYPE=unknown"
set "VERSION="
set "COMMIT_HASH="
set "BUILD_UNIXTIME=%BUILD_UNIXTIME%"
set "BUILD_DATE=%BUILD_DATE%"
set "BUILD_UNIXTIME="
set "BUILD_DATE="
set "PACKAGE_FILENAME="
for /f %%a in ('"prompt $E$S & echo on & for %%b in (1) do rem"') do set "ESC=%%a"
@@ -113,14 +113,8 @@ goto :pre_parse_args
set VERSION=%VERSION:,=%
set VERSION=%VERSION:"=%
for /f %%x in ('git rev-parse --short^=7 HEAD') do set "COMMIT_HASH=%%x"
if "%BUILD_UNIXTIME%"=="" (
call :set_unixtime BUILD_UNIXTIME
)
if "%BUILD_DATE%"=="" (
call :set_date BUILD_DATE
)
call :set_unixtime BUILD_UNIXTIME
call :set_date BUILD_DATE
:main
if "%TYPE%"=="backend" call :build_backend & goto :end
+4 -10
View File
@@ -11,8 +11,8 @@ $script:SkipTests = $env:SKIP_TESTS
$script:ReleaseType = "unknown"
$script:Version = ""
$script:CommitHash = ""
$script:BuildUnixTime = $env:BUILD_UNIXTIME
$script:BuildDate = $env:BUILD_DATE
$script:BuildUnixTime = ""
$script:BuildDate = ""
function Write-Red($msg) {
Write-Host $msg -ForegroundColor Red
@@ -79,14 +79,8 @@ function Check-Type-Dependencies {
function Set-Build-Parameters {
$script:Version = (Get-Content package.json | ConvertFrom-Json).version
$script:CommitHash = git rev-parse --short=7 HEAD
if (-not $BuildUnixTime) {
$script:BuildUnixTime = [int][double]::Parse((Get-Date -UFormat %s))
}
if (-not $BuildDate) {
$script:BuildDate = Get-Date -Format "yyyyMMdd"
}
$script:BuildUnixTime = [int][double]::Parse((Get-Date -UFormat %s))
$script:BuildDate = Get-Date -Format "yyyyMMdd"
}
function Build-Backend {
+4 -12
View File
@@ -8,8 +8,7 @@ RELEASE=${RELEASE_BUILD:-"0"}
RELEASE_TYPE="unknown"
VERSION=""
COMMIT_HASH=""
BUILD_UNIXTIME="${BUILD_UNIXTIME}"
BUILD_DATE="${BUILD_DATE}"
BUILD_UNIXTIME=""
PACKAGE_FILENAME=""
DOCKER_TAG=""
@@ -119,14 +118,7 @@ check_type_dependencies() {
set_build_parameters() {
VERSION="$(grep '"version": ' package.json | awk -F ':' '{print $2}' | tr -d ' ' | tr -d ',' | tr -d '"')"
COMMIT_HASH="$(git rev-parse --short=7 HEAD)"
if [ -z "$BUILD_UNIXTIME" ]; then
BUILD_UNIXTIME="$(date '+%s')"
fi
if [ -z "$BUILD_DATE" ]; then
BUILD_DATE="$(date '+%Y%m%d')"
fi
BUILD_UNIXTIME="$(date '+%s')"
}
build_backend() {
@@ -211,7 +203,7 @@ build_package() {
package_file_name="$VERSION";
if [ "$RELEASE" = "0" ]; then
package_file_name="$package_file_name-$BUILD_DATE"
package_file_name="$package_file_name-$(date '+%Y%m%d')"
fi
package_file_name="ezbookkeeping-$package_file_name-$(arch).tar.gz"
@@ -245,7 +237,7 @@ build_docker() {
docker_tag="$VERSION"
if [ "$RELEASE" = "0" ]; then
docker_tag="SNAPSHOT-$BUILD_DATE";
docker_tag="SNAPSHOT-$(date '+%Y%m%d')";
fi
docker_tag="ezbookkeeping:$docker_tag"
-24
View File
@@ -101,14 +101,6 @@ func updateAllDatabaseTablesStructure(c *core.CliContext) error {
log.BootInfof(c, "[database.updateAllDatabaseTablesStructure] transaction category table maintained successfully")
err = datastore.Container.UserDataStore.SyncStructs(new(models.TransactionTagGroup))
if err != nil {
return err
}
log.BootInfof(c, "[database.updateAllDatabaseTablesStructure] transaction tag group table maintained successfully")
err = datastore.Container.UserDataStore.SyncStructs(new(models.TransactionTag))
if err != nil {
@@ -157,21 +149,5 @@ func updateAllDatabaseTablesStructure(c *core.CliContext) error {
log.BootInfof(c, "[database.updateAllDatabaseTablesStructure] user application cloud settings table maintained successfully")
err = datastore.Container.UserDataStore.SyncStructs(new(models.UserExternalAuth))
if err != nil {
return err
}
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
}
+9 -35
View File
@@ -162,46 +162,20 @@ func getConfigWithoutSensitiveData(config *settings.Config) *settings.Config {
return config
}
if clonedConfig.DatabaseConfig.DatabasePassword != "" {
clonedConfig.DatabaseConfig.DatabasePassword = "****"
}
clonedConfig.DatabaseConfig.DatabasePassword = "****"
clonedConfig.SMTPConfig.SMTPPasswd = "****"
clonedConfig.MinIOConfig.SecretAccessKey = "****"
clonedConfig.SecretKey = "****"
clonedConfig.AmapApplicationSecret = "****"
if clonedConfig.SMTPConfig.SMTPPasswd != "" {
clonedConfig.SMTPConfig.SMTPPasswd = "****"
}
if clonedConfig.MinIOConfig.SecretAccessKey != "" {
clonedConfig.MinIOConfig.SecretAccessKey = "****"
}
if clonedConfig.SecretKey != "" {
clonedConfig.SecretKey = "****"
}
if clonedConfig.AmapApplicationSecret != "" {
clonedConfig.AmapApplicationSecret = "****"
}
if clonedConfig.WebDAVConfig != nil && clonedConfig.WebDAVConfig.Password != "" {
if clonedConfig.WebDAVConfig != nil {
clonedConfig.WebDAVConfig.Password = "****"
}
if clonedConfig.ReceiptImageRecognitionLLMConfig != nil {
if clonedConfig.ReceiptImageRecognitionLLMConfig.OpenAIAPIKey != "" {
clonedConfig.ReceiptImageRecognitionLLMConfig.OpenAIAPIKey = "****"
}
if clonedConfig.ReceiptImageRecognitionLLMConfig.OpenAICompatibleAPIKey != "" {
clonedConfig.ReceiptImageRecognitionLLMConfig.OpenAICompatibleAPIKey = "****"
}
if clonedConfig.ReceiptImageRecognitionLLMConfig.OpenRouterAPIKey != "" {
clonedConfig.ReceiptImageRecognitionLLMConfig.OpenRouterAPIKey = "****"
}
}
if clonedConfig.OAuth2ClientSecret != "" {
clonedConfig.OAuth2ClientSecret = "****"
clonedConfig.ReceiptImageRecognitionLLMConfig.OpenAIAPIKey = "****"
clonedConfig.ReceiptImageRecognitionLLMConfig.OpenAICompatibleAPIKey = "****"
clonedConfig.ReceiptImageRecognitionLLMConfig.OpenRouterAPIKey = "****"
}
return clonedConfig
+4 -16
View File
@@ -264,13 +264,7 @@ var UserData = &cli.Command{
Name: "type",
Aliases: []string{"t"},
Required: false,
Usage: "Specific token type, supports \"api\" and \"mcp\", default is \"api\"",
},
&cli.Int64Flag{
Name: "expiresInSeconds",
Aliases: []string{"e"},
Required: true,
Usage: "Token expiration time in seconds (0 - 4294967295, 0 means no expiration).",
Usage: "Specific token type, supports \"normal\" and \"mcp\", default is \"normal\"",
},
},
},
@@ -728,23 +722,17 @@ func createNewUserToken(c *core.CliContext) error {
username := c.String("username")
tokenType := c.String("type")
expiresInSeconds := c.Int64("expiresInSeconds")
if tokenType == "" {
tokenType = "api"
tokenType = "normal"
}
if tokenType != "api" && tokenType != "mcp" {
if tokenType != "normal" && tokenType != "mcp" {
log.CliErrorf(c, "[user_data.createNewUserToken] token type is invalid")
return nil
}
if expiresInSeconds < 0 || expiresInSeconds > 4294967295 {
log.CliErrorf(c, "[user_data.createNewUserToken] expiresInSeconds is out of range (0 - 4294967295)")
return nil
}
token, tokenString, err := clis.UserData.CreateNewUserToken(c, username, tokenType, expiresInSeconds)
token, tokenString, err := clis.UserData.CreateNewUserToken(c, username, tokenType)
if err != nil {
log.CliErrorf(c, "[user_data.createNewUserToken] error occurs when creating user token")
+13 -83
View File
@@ -15,7 +15,6 @@ import (
"github.com/urfave/cli/v3"
"github.com/mayswind/ezbookkeeping/pkg/api"
"github.com/mayswind/ezbookkeeping/pkg/auth/oauth2"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/cron"
"github.com/mayswind/ezbookkeeping/pkg/errs"
@@ -73,13 +72,6 @@ func startWebServer(c *core.CliContext) error {
return err
}
err = oauth2.InitializeOAuth2Provider(config)
if err != nil {
log.BootErrorf(c, "[webserver.startWebServer] initializes oauth 2.0 provider failed, because %s", err.Error())
return err
}
err = cron.InitializeCronJobSchedulerContainer(c, config, true)
if err != nil {
@@ -112,11 +104,9 @@ func startWebServer(c *core.CliContext) error {
_ = v.RegisterValidation("notBlank", validators.NotBlank)
_ = v.RegisterValidation("validUsername", validators.ValidUsername)
_ = v.RegisterValidation("validEmail", validators.ValidEmail)
_ = v.RegisterValidation("validNickname", validators.ValidNickname)
_ = v.RegisterValidation("validCurrency", validators.ValidCurrency)
_ = v.RegisterValidation("validHexRGBColor", validators.ValidHexRGBColor)
_ = v.RegisterValidation("validAmountFilter", validators.ValidAmountFilter)
_ = v.RegisterValidation("validTagFilter", validators.ValidTagFilter)
_ = v.RegisterValidation("validFiscalYearStart", validators.ValidateFiscalYearStart)
}
@@ -177,7 +167,7 @@ func startWebServer(c *core.CliContext) error {
if config.AvatarProvider == core.USER_AVATAR_PROVIDER_INTERNAL {
avatarRoute := router.Group("/avatar")
avatarRoute.Use(bindMiddleware(middlewares.JWTAuthorizationByQueryString(config)))
avatarRoute.Use(bindMiddleware(middlewares.JWTAuthorizationByQueryString))
{
avatarRoute.GET("/:fileName", bindImage(api.Users.UserGetAvatarHandler))
}
@@ -185,7 +175,7 @@ func startWebServer(c *core.CliContext) error {
if config.EnableTransactionPictures {
pictureRoute := router.Group("/pictures")
pictureRoute.Use(bindMiddleware(middlewares.JWTAuthorizationByQueryString(config)))
pictureRoute.Use(bindMiddleware(middlewares.JWTAuthorizationByQueryString))
{
pictureRoute.GET("/:fileName", bindImage(api.TransactionPictures.TransactionPictureGetHandler))
}
@@ -194,7 +184,7 @@ func startWebServer(c *core.CliContext) error {
router.GET("/healthz.json", bindApi(api.Healths.HealthStatusHandler))
proxyRoute := router.Group("/proxy")
proxyRoute.Use(bindMiddleware(middlewares.JWTAuthorizationByQueryString(config)))
proxyRoute.Use(bindMiddleware(middlewares.JWTAuthorizationByQueryString))
{
if config.EnableMapDataFetchProxy {
if config.MapProvider == settings.OpenStreetMapProvider ||
@@ -218,7 +208,7 @@ func startWebServer(c *core.CliContext) error {
if config.MapProvider == settings.AmapProvider && config.AmapSecurityVerificationMethod == settings.AmapSecurityVerificationInternalProxyMethod {
amapApiProxyRoute := router.Group("/_AMapService")
amapApiProxyRoute.Use(bindMiddleware(middlewares.JWTAuthorizationByCookie(config)))
amapApiProxyRoute.Use(bindMiddleware(middlewares.JWTAuthorizationByCookie))
{
amapApiProxyRoute.GET("/*action", bindProxy(api.AmapApis.AmapApiProxyHandler))
}
@@ -236,7 +226,7 @@ func startWebServer(c *core.CliContext) error {
mcpRoute.Use(bindMiddleware(middlewares.RequestId(config)))
mcpRoute.Use(bindMiddleware(middlewares.RequestLog))
mcpRoute.Use(bindMiddleware(middlewares.MCPServerIpLimit(config)))
mcpRoute.Use(bindMiddleware(middlewares.JWTMCPAuthorization(config)))
mcpRoute.Use(bindMiddleware(middlewares.JWTMCPAuthorization))
{
mcpRoute.POST("", bindJSONRPCApi(map[string]core.JSONRPCApiHandlerFunc{
"initialize": api.ModelContextProtocols.InitializeHandler,
@@ -252,43 +242,23 @@ func startWebServer(c *core.CliContext) error {
}
}
if config.EnableOAuth2Login {
oauth2Route := router.Group("/oauth2")
oauth2Route.Use(bindMiddleware(middlewares.RequestId(config)))
oauth2Route.Use(bindMiddleware(middlewares.RequestLog))
{
oauth2Route.GET("/login", bindRedirect(api.OAuth2Authentications.LoginHandler))
oauth2Route.GET("/callback", bindRedirect(api.OAuth2Authentications.CallbackHandler))
}
}
apiRoute := router.Group("/api")
apiRoute.Use(bindMiddleware(middlewares.RequestId(config)))
apiRoute.Use(bindMiddleware(middlewares.RequestLog))
{
if config.EnableInternalAuth {
apiRoute.POST("/authorize.json", bindApiWithTokenUpdate(api.Authorizations.AuthorizeHandler, config))
}
apiRoute.POST("/authorize.json", bindApiWithTokenUpdate(api.Authorizations.AuthorizeHandler, config))
if config.EnableInternalAuth && config.EnableTwoFactor {
if config.EnableTwoFactor {
twoFactorRoute := apiRoute.Group("/2fa")
twoFactorRoute.Use(bindMiddleware(middlewares.JWTTwoFactorAuthorization(config)))
twoFactorRoute.Use(bindMiddleware(middlewares.JWTTwoFactorAuthorization))
{
twoFactorRoute.POST("/authorize.json", bindApiWithTokenUpdate(api.Authorizations.TwoFactorAuthorizeHandler, config))
twoFactorRoute.POST("/recovery.json", bindApiWithTokenUpdate(api.Authorizations.TwoFactorAuthorizeByRecoveryCodeHandler, config))
}
}
if config.EnableOAuth2Login {
oauth2Route := apiRoute.Group("/oauth2")
oauth2Route.Use(bindMiddleware(middlewares.JWTOAuth2CallbackAuthorization(config)))
{
oauth2Route.POST("/authorize.json", bindApiWithTokenUpdate(api.Authorizations.OAuth2CallbackAuthorizeHandler, config))
}
}
if config.EnableInternalAuth && config.EnableUserRegister {
if config.EnableUserRegister {
apiRoute.POST("/register.json", bindApiWithTokenUpdate(api.Users.UserRegisterHandler, config))
}
@@ -296,17 +266,17 @@ func startWebServer(c *core.CliContext) error {
apiRoute.POST("/verify_email/resend.json", bindApi(api.Users.UserSendVerifyEmailByUnloginUserHandler))
emailVerifyRoute := apiRoute.Group("/verify_email")
emailVerifyRoute.Use(bindMiddleware(middlewares.JWTEmailVerifyAuthorization(config)))
emailVerifyRoute.Use(bindMiddleware(middlewares.JWTEmailVerifyAuthorization))
{
emailVerifyRoute.POST("/by_token.json", bindApi(api.Users.UserEmailVerifyHandler))
}
}
if config.EnableInternalAuth && config.EnableUserForgetPassword {
if config.EnableUserForgetPassword {
apiRoute.POST("/forget_password/request.json", bindApi(api.ForgetPasswords.UserForgetPasswordRequestHandler))
resetPasswordRoute := apiRoute.Group("/forget_password/reset")
resetPasswordRoute.Use(bindMiddleware(middlewares.JWTResetPasswordAuthorization(config)))
resetPasswordRoute.Use(bindMiddleware(middlewares.JWTResetPasswordAuthorization))
{
resetPasswordRoute.POST("/by_token.json", bindApi(api.ForgetPasswords.UserResetPasswordHandler))
}
@@ -315,11 +285,10 @@ func startWebServer(c *core.CliContext) error {
apiRoute.GET("/logout.json", bindApiWithTokenUpdate(api.Tokens.TokenRevokeCurrentHandler, config))
apiV1Route := apiRoute.Group("/v1")
apiV1Route.Use(bindMiddleware(middlewares.JWTAuthorization(config)))
apiV1Route.Use(bindMiddleware(middlewares.JWTAuthorization))
{
// Tokens
apiV1Route.GET("/tokens/list.json", bindApi(api.Tokens.TokenListHandler))
apiV1Route.POST("/tokens/generate/api.json", bindApi(api.Tokens.TokenGenerateAPIHandler))
apiV1Route.POST("/tokens/generate/mcp.json", bindApi(api.Tokens.TokenGenerateMCPHandler))
apiV1Route.POST("/tokens/revoke.json", bindApi(api.Tokens.TokenRevokeHandler))
apiV1Route.POST("/tokens/revoke_all.json", bindApi(api.Tokens.TokenRevokeAllHandler))
@@ -338,12 +307,6 @@ func startWebServer(c *core.CliContext) error {
apiV1Route.POST("/users/verify_email/resend.json", bindApi(api.Users.UserSendVerifyEmailByLoginedUserHandler))
}
// External Authentications
if config.EnableOAuth2Login {
apiV1Route.GET("/users/external_auth/list.json", bindApi(api.UserExternalAuths.ExternalAuthListHanlder))
apiV1Route.POST("/users/external_auth/unlink.json", bindApi(api.UserExternalAuths.UnlinkExternalAuthHandler))
}
// Application Cloud Settings
apiV1Route.GET("/users/settings/cloud/get.json", bindApi(api.UserApplicationCloudSettings.ApplicationSettingsGetHandler))
apiV1Route.POST("/users/settings/cloud/update.json", bindApi(api.UserApplicationCloudSettings.ApplicationSettingsUpdateHandler))
@@ -383,16 +346,13 @@ func startWebServer(c *core.CliContext) error {
apiV1Route.GET("/transactions/count.json", bindApi(api.Transactions.TransactionCountHandler))
apiV1Route.GET("/transactions/list.json", bindApi(api.Transactions.TransactionListHandler))
apiV1Route.GET("/transactions/list/by_month.json", bindApi(api.Transactions.TransactionMonthListHandler))
apiV1Route.GET("/transactions/list/all.json", bindApi(api.Transactions.TransactionListAllHandler))
apiV1Route.GET("/transactions/reconciliation_statements.json", bindApi(api.Transactions.TransactionReconciliationStatementHandler))
apiV1Route.GET("/transactions/statistics.json", bindApi(api.Transactions.TransactionStatisticsHandler))
apiV1Route.GET("/transactions/statistics/trends.json", bindApi(api.Transactions.TransactionStatisticsTrendsHandler))
apiV1Route.GET("/transactions/statistics/asset_trends.json", bindApi(api.Transactions.TransactionStatisticsAssetTrendsHandler))
apiV1Route.GET("/transactions/amounts.json", bindApi(api.Transactions.TransactionAmountsHandler))
apiV1Route.GET("/transactions/get.json", bindApi(api.Transactions.TransactionGetHandler))
apiV1Route.POST("/transactions/add.json", bindApi(api.Transactions.TransactionCreateHandler))
apiV1Route.POST("/transactions/modify.json", bindApi(api.Transactions.TransactionModifyHandler))
apiV1Route.POST("/transactions/move/all.json", bindApi(api.Transactions.TransactionMoveAllBetweenAccountsHandler))
apiV1Route.POST("/transactions/delete.json", bindApi(api.Transactions.TransactionDeleteHandler))
if config.EnableDataImport {
@@ -418,14 +378,6 @@ func startWebServer(c *core.CliContext) error {
apiV1Route.POST("/transaction/categories/move.json", bindApi(api.TransactionCategories.CategoryMoveHandler))
apiV1Route.POST("/transaction/categories/delete.json", bindApi(api.TransactionCategories.CategoryDeleteHandler))
// Transaction Tag Groups
apiV1Route.GET("/transaction/tags/groups/list.json", bindApi(api.TransactionTagGroups.TagGroupListHandler))
apiV1Route.GET("/transaction/tags/groups/get.json", bindApi(api.TransactionTagGroups.TagGroupGetHandler))
apiV1Route.POST("/transaction/tags/groups/add.json", bindApi(api.TransactionTagGroups.TagGroupCreateHandler))
apiV1Route.POST("/transaction/tags/groups/modify.json", bindApi(api.TransactionTagGroups.TagGroupModifyHandler))
apiV1Route.POST("/transaction/tags/groups/move.json", bindApi(api.TransactionTagGroups.TagGroupMoveHandler))
apiV1Route.POST("/transaction/tags/groups/delete.json", bindApi(api.TransactionTagGroups.TagGroupDeleteHandler))
// Transaction Tags
apiV1Route.GET("/transaction/tags/list.json", bindApi(api.TransactionTags.TagListHandler))
apiV1Route.GET("/transaction/tags/get.json", bindApi(api.TransactionTags.TagGetHandler))
@@ -445,15 +397,6 @@ func startWebServer(c *core.CliContext) error {
apiV1Route.POST("/transaction/templates/move.json", bindApi(api.TransactionTemplates.TemplateMoveHandler))
apiV1Route.POST("/transaction/templates/delete.json", bindApi(api.TransactionTemplates.TemplateDeleteHandler))
// Insights Explorers
apiV1Route.GET("/insights/explorers/list.json", bindApi(api.InsightsExplorers.InsightsExplorerListHandler))
apiV1Route.GET("/insights/explorers/get.json", bindApi(api.InsightsExplorers.InsightsExplorerGetHandler))
apiV1Route.POST("/insights/explorers/add.json", bindApi(api.InsightsExplorers.InsightsExplorerCreateHandler))
apiV1Route.POST("/insights/explorers/modify.json", bindApi(api.InsightsExplorers.InsightsExplorerModifyHandler))
apiV1Route.POST("/insights/explorers/hide.json", bindApi(api.InsightsExplorers.InsightsExplorerHideHandler))
apiV1Route.POST("/insights/explorers/move.json", bindApi(api.InsightsExplorers.InsightsExplorerMoveHandler))
apiV1Route.POST("/insights/explorers/delete.json", bindApi(api.InsightsExplorers.InsightsExplorerDeleteHandler))
// Large Language Models
if config.ReceiptImageRecognitionLLMConfig != nil && config.ReceiptImageRecognitionLLMConfig.LLMProvider != "" {
if config.TransactionFromAIImageRecognition {
@@ -500,19 +443,6 @@ func bindMiddleware(fn core.MiddlewareHandlerFunc) gin.HandlerFunc {
}
}
func bindRedirect(fn core.RedirectHandlerFunc) gin.HandlerFunc {
return func(ginCtx *gin.Context) {
c := core.WrapWebContext(ginCtx)
url, err := fn(c)
if err != nil {
utils.PrintJsonErrorResult(c, err)
} else {
c.Redirect(http.StatusFound, url)
}
}
}
func bindApi(fn core.ApiHandlerFunc) gin.HandlerFunc {
return func(ginCtx *gin.Context) {
c := core.WrapWebContext(ginCtx)
+7 -68
View File
@@ -1,4 +1,7 @@
[global]
# Application instance name
app_name = ezBookkeeping
# Either "production", "development"
mode = production
@@ -15,7 +18,7 @@ http_port = 8080
# The domain name used to access ezBookkeeping
domain = localhost
# The full url used to access ezBookkeeping in browser, supports placeholders: %(protocol)s, %(domain)s, %(http_port)s
# The full url used to access ezBookkeeping in browser
root_url = %(protocol)s://%(domain)s:%(http_port)s/
# https certification and its key file
@@ -260,9 +263,6 @@ email_verify_token_expired_time = 3600
# Password reset token expired seconds (60 - 4294967295), default is 3600 (60 minutes)
password_reset_token_expired_time = 3600
# Set to true to enable API token generation
enable_api_token = false
# 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
@@ -270,72 +270,15 @@ max_failures_per_ip_per_minute = 5
max_failures_per_user_per_minute = 5
[auth]
# Set to true to enable internal authentication
enable_internal_auth = true
# Set to true to enable OAuth 2.0 authentication
enable_oauth2_auth = false
# For "internal" authentication only, set to true to enable two-factor authorization
# Set to true to enable two-factor authorization
enable_two_factor = true
# For "internal" authentication only, set to true to allow users to reset password
# Set to true to allow users to reset password
enable_forget_password = true
# For "internal" authentication only, set to true to require email must be verified when use forget password
# Set to true to require email must be verified when use forget password
forget_password_require_email_verify = false
# For "oauth2" authentication only, OAuth 2.0 provider, supports "oidc", "nextcloud", "gitea" and "github" currently
oauth2_provider =
# For "oauth2" authentication only, OAuth 2.0 client ID
oauth2_client_id =
# For "oauth2" authentication only, OAuth 2.0 client secret
oauth2_client_secret =
# For "oauth2" authentication only, OAuth 2.0 provider user identifier claim name, supports "email" and "username", default is "email"
oauth2_user_identifier = email
# For "oauth2" authentication only, set to true to use PKCE
oauth2_use_pkce = false
# For "oauth2" authentication only, if the user returned by OAuth 2.0 is not registered, automatically create a new user (requires "enable_register" to be set to true)
oauth2_auto_register = true
# For "oauth2" authentication only, OAuth 2.0 state expired seconds (60 - 4294967295), default is 300 (5 minutes)
oauth2_state_expired_time = 300
# For "oauth2" authentication only, requesting OAuth 2.0 api timeout (0 - 4294967295 milliseconds)
# Set to 0 to disable timeout for requesting OAuth 2.0 api, default is 10000 (10 seconds)
oauth2_request_timeout = 10000
# For "oauth2" authentication only, proxy for ezbookkeeping server requesting OAuth 2.0 api, supports "system" (use system proxy), "none" (do not use proxy), or proxy URL which starts with "http://", "https://" or "socks5://", default is "system"
oauth2_proxy = system
# For "oauth2" authentication only, set to true to skip tls verification when request OAuth 2.0 api
oauth2_skip_tls_verify = false
# For "oauth2" authentication and "oidc" OAuth 2.0 provider only, OIDC provider issuer url. Make sure the ".well-known" directory is available under this path. For example, if it's set to "https://auth.example.com", the discovery URL should be "https://auth.example.com/.well-known/openid-configuration".
oidc_provider_base_url =
# For "oauth2" authentication and "oidc" OAuth 2.0 provider only, set to true to check whether the issuer url in the discovery response matches the above "oidc_provider_base_url"
oidc_provider_check_issuer_url = true
# For "oauth2" authentication and "oidc" OAuth 2.0 provider only, set to true to replace the text "Connect ID" in the "Log in with Connect ID" button with the below custom provider name
enable_oidc_display_name = false
# For "oauth2" authentication and "oidc" OAuth 2.0 provider only, the custom provider name to replace the text in the "Log in with Connect ID" button, it supports multi-language configuration
# Add an underscore and a language tag after the setting key to configure the display name in that language
# For example, oidc_custom_display_name_zh_hans means the display name in Chinese (Simplified)
oidc_custom_display_name =
# For "oauth2" authentication and "nextcloud" OAuth 2.0 provider only, Nextcloud base url, e.g. "https://cloud.example.org/"
nextcloud_base_url =
# For "oauth2" authentication and "gitea" OAuth 2.0 provider only, Gitea base url, e.g. "https://git.example.com/"
gitea_base_url =
[user]
# Set to true to allow users to register account by themselves
enable_register = true
@@ -379,10 +322,6 @@ max_user_avatar_size = 1048576
# 11: Clear All Data
# 12: Sync Application Settings
# 13: MCP (Model Context Protocol) Access
# 14: Create Transactions from AI Image Recognition
# 15: OAuth 2.0 Login
# 16: Unlink Third-party Login
# 17: Generate API Token
default_feature_restrictions =
[data]
-70
View File
@@ -1,70 +0,0 @@
{
"code": [
"jiangshengwu",
"vigdail",
"f97",
"Miguelonlonlon",
"seb26",
"nktlitvinenko",
"lvdou-bing",
"dshemin",
"lucdsouza",
"OuIChien"
],
"translators": {
"de": [
"chrgm"
],
"en": [],
"es": [
"Miguelonlonlon",
"abrugues",
"AndresTeller",
"diegofercri"
],
"fr": [
"brieucdlf"
],
"it": [
"waron97"
],
"ja": [
"tkymmm"
],
"kn": [
"Darshanbm05"
],
"ko": [
"overworks"
],
"nl": [
"automagics"
],
"pt-BR": [
"thecodergus"
],
"ru": [
"artegoser"
],
"sl": [
"thehijacker"
],
"ta": [
"hhharsha36"
],
"th": [
"natthavat28"
],
"tr": [
"aydnykn"
],
"uk": [
"nktlitvinenko"
],
"vi": [
"f97"
],
"zh-Hans": [],
"zh-Hant": []
}
}
-34
View File
@@ -1,34 +0,0 @@
variable "DEFAULT_TAG" {
default = "ezbookkeeping:local"
}
// Special target: https://github.com/docker/metadata-action#bake-definition
target "docker-metadata-action" {
tags = ["${DEFAULT_TAG}"]
}
// Default target if none specified
group "default" {
targets = ["image-local"]
}
target "image" {
inherits = ["docker-metadata-action"]
context = "./"
dockerfile = "Dockerfile"
}
target "image-local" {
inherits = ["image"]
output = ["type=docker"]
}
target "image-all" {
inherits = ["image"]
platforms = [
"linux/amd64",
"linux/arm64",
"linux/arm/v7",
"linux/arm/v6"
]
}
+1 -1
View File
@@ -1,5 +1,5 @@
[Unit]
Description=ezBookkeeping, a lightweight, self-hosted personal finance app with a user-friendly interface and powerful bookkeeping features.
Description=ezBookkeeping, a lightweight, self-hosted personal finance app with a sleek, user-friendly interface and powerful bookkeeping features.
After=syslog.target
After=network.target
After=mariadb.service mysqld.service postgresql.service
+23 -34
View File
@@ -4,34 +4,32 @@ go 1.25
require (
github.com/boombuler/barcode v1.1.0
github.com/coreos/go-oidc/v3 v3.17.0
github.com/extrame/xls v0.0.2-0.20200426124601-4a6cf263071b
github.com/gin-contrib/cache v1.4.1
github.com/gin-contrib/gzip v1.2.5
github.com/gin-gonic/gin v1.11.0
github.com/go-co-op/gocron/v2 v2.18.2
github.com/go-playground/validator/v10 v10.28.0
github.com/gin-contrib/gzip v1.2.3
github.com/gin-gonic/gin v1.10.1
github.com/go-co-op/gocron/v2 v2.16.5
github.com/go-playground/validator/v10 v10.27.0
github.com/go-sql-driver/mysql v1.9.3
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/invopop/jsonschema v0.13.0
github.com/lib/pq v1.10.9
github.com/mattn/go-sqlite3 v1.14.32
github.com/minio/minio-go/v7 v7.0.97
github.com/minio/minio-go/v7 v7.0.95
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/pquerna/otp v1.5.0
github.com/sirupsen/logrus v1.9.3
github.com/stretchr/testify v1.11.1
github.com/urfave/cli/v3 v3.6.1
github.com/urfave/cli/v3 v3.4.1
github.com/wk8/go-ordered-map/v2 v2.1.8
github.com/xuri/excelize/v2 v2.10.0
golang.org/x/crypto v0.46.0
golang.org/x/net v0.48.0
golang.org/x/oauth2 v0.34.0
golang.org/x/text v0.32.0
github.com/xuri/excelize/v2 v2.9.0
golang.org/x/crypto v0.41.0
golang.org/x/net v0.43.0
golang.org/x/text v0.28.0
gopkg.in/ini.v1 v1.67.0
gopkg.in/mail.v2 v2.3.1
xorm.io/builder v0.3.13
xorm.io/xorm v1.3.11
xorm.io/xorm v1.3.10
)
require (
@@ -39,40 +37,36 @@ require (
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.14.1 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/bytedance/sonic v1.13.3 // indirect
github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
github.com/chenzhuoyu/iasm v0.9.1 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/extrame/goyymmdd v0.0.0-20210114090516-7cc815f00d1a // 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.9 // indirect
github.com/gin-contrib/sse v1.1.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-playground/locales v0.14.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-yaml v1.18.0 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/gomodule/redigo v1.9.2 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jonboulle/clockwork v0.5.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/klauspost/crc32 v1.3.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.11 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/memcachier/mc/v3 v3.0.3 // indirect
github.com/minio/crc64nvme v1.1.0 // indirect
github.com/minio/crc64nvme v1.0.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/reflect2 v1.0.2 // indirect
@@ -80,8 +74,6 @@ require (
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/philhofer/fwd v1.2.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.55.0 // indirect
github.com/richardlehane/mscfb v1.0.4 // indirect
github.com/richardlehane/msoleps v1.0.4 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
@@ -90,20 +82,17 @@ require (
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/syndtr/goleveldb v1.0.0 // indirect
github.com/tealeg/xlsx v1.0.5 // indirect
github.com/tiendc/go-deepcopy v1.7.1 // indirect
github.com/tiendc/go-deepcopy v1.6.0 // indirect
github.com/tinylib/msgp v1.3.0 // 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.2.14 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
github.com/xuri/efp v0.0.1 // indirect
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // indirect
golang.org/x/arch v0.22.0 // indirect
github.com/xuri/nfp v0.0.1 // indirect
golang.org/x/arch v0.18.0 // indirect
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect
golang.org/x/mod v0.30.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/tools v0.39.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
golang.org/x/sys v0.35.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+47 -68
View File
@@ -10,14 +10,13 @@ github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf h1:TqhNAT4zKbT
github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c=
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
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/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.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7w=
github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0=
github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
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/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
@@ -25,11 +24,9 @@ github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583j
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA=
github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
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.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc=
github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -43,34 +40,30 @@ 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/go.mod h1:iACcgahst7BboCpIMSpnFs4SKyU9ZjsvZBfNbUxZOJI=
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.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
github.com/gin-contrib/cache v1.4.1 h1:HcLwLfw7p+FasNp5VAnFbbBj9SzB4bDtswvon7wYSg4=
github.com/gin-contrib/cache v1.4.1/go.mod h1:tykDV+FgItJHYEO0eCasuRYsZKPPyb4BYhAjuTlG6RM=
github.com/gin-contrib/gzip v1.2.5 h1:fIZs0S+l17pIu1P5XRJOo/YNqfIuPCrZZ3TWB7pjckI=
github.com/gin-contrib/gzip v1.2.5/go.mod h1:aomRgR7ftdZV3uWY0gW/m8rChfxau0n8YVvwlOHONzw=
github.com/gin-contrib/gzip v1.2.3 h1:dAhT722RuEG330ce2agAs75z7yB+NKvX/ZM1r8w0u2U=
github.com/gin-contrib/gzip v1.2.3/go.mod h1:ad72i4Bzmaypk8M762gNXa2wkxxjbz0icRNnuLJ9a/c=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
github.com/go-co-op/gocron/v2 v2.18.2 h1:+5VU41FUXPWSPKLXZQ/77SGzUiPCcakU0v7ENc2H20Q=
github.com/go-co-op/gocron/v2 v2.18.2/go.mod h1:Zii6he+Zfgy5W9B+JKk/KwejFOW0kZTFvHtwIpR4aBI=
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-co-op/gocron/v2 v2.16.5 h1:j228Jxk7bb9CF8LKR3gS+bK3rcjRUINjlVI+ZMp26Ss=
github.com/go-co-op/gocron/v2 v2.16.5/go.mod h1:zAfC/GFQ668qHxOVl/D68Jh5Ce7sDqX6TJnSQyRkRBc=
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
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/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
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/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-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -94,10 +87,8 @@ github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zt
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
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.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM=
github.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw=
github.com/klauspost/cpuid/v2 v2.2.11 h1:0OwqZRYI2rFrjS4kvkDnqJkKHdHaRnCm68/DY4OxRzU=
github.com/klauspost/cpuid/v2 v2.2.11/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
@@ -114,17 +105,18 @@ github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuE
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/memcachier/mc/v3 v3.0.3 h1:qii+lDiPKi36O4Xg+HVKwHu6Oq+Gt17b+uEiA0Drwv4=
github.com/memcachier/mc/v3 v3.0.3/go.mod h1:GzjocBahcXPxt2cmqzknrgqCOmMxiSzhVKPOe90Tpug=
github.com/minio/crc64nvme v1.1.0 h1:e/tAguZ+4cw32D+IO/8GSf5UVr9y+3eJcxZI2WOO/7Q=
github.com/minio/crc64nvme v1.1.0/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
github.com/minio/crc64nvme v1.0.2 h1:6uO1UxGAD+kwqWWp7mBFsi5gAse66C4NXO8cmcVculg=
github.com/minio/crc64nvme v1.0.2/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
github.com/minio/minio-go/v7 v7.0.97 h1:lqhREPyfgHTB/ciX8k2r8k0D93WaFqxbJX36UZq5occ=
github.com/minio/minio-go/v7 v7.0.97/go.mod h1:re5VXuo0pwEtoNLsNuSr0RrLfT/MBtohwdaSmPPSRSk=
github.com/minio/minio-go/v7 v7.0.95 h1:ywOUPg+PebTMTzn9VDsoFJy32ZuARN9zhB+K3IYEvYU=
github.com/minio/minio-go/v7 v7.0.95/go.mod h1:wOOX3uxS334vImCNRVyIDdXX9OsXDm89ToynKgqUKlo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@@ -139,10 +131,6 @@ 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/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
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.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk=
github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U=
github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM=
github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk=
github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
@@ -171,53 +159,44 @@ github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFd
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/go.mod h1:btRS8dz54TDnvKNosuAqxrM1QgN1udgk9O34bDCnORM=
github.com/tiendc/go-deepcopy v1.7.1 h1:LnubftI6nYaaMOcaz0LphzwraqN8jiWTwm416sitff4=
github.com/tiendc/go-deepcopy v1.7.1/go.mod h1:4bKjNC2r7boYOkD2IOuZpYjmlDdzjbpTRyCx+goBCJQ=
github.com/tiendc/go-deepcopy v1.6.0/go.mod h1:toXoeQoUqXOOS/X4sKuiAoSk6elIdqc0pN7MTgOOo2I=
github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww=
github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=
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/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/urfave/cli/v3 v3.6.1 h1:j8Qq8NyUawj/7rTYdBGrxcH7A/j7/G8Q5LhWEW4G3Mo=
github.com/urfave/cli/v3 v3.6.1/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
github.com/ugorji/go/codec v1.2.14 h1:yOQvXCBc3Ij46LRkRoh4Yd5qK6LVOgi0bYOXfb7ifjw=
github.com/ugorji/go/codec v1.2.14/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/urfave/cli/v3 v3.4.1 h1:1M9UOCy5bLmGnuu1yn3t3CB4rG79Rtoxuv1sPhnm6qM=
github.com/urfave/cli/v3 v3.4.1/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo=
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8=
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.0/go.mod h1:SC5TzhQkaOsTWpANfm+7bJCldzcnU/jrhqkTi/iBHBU=
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/excelize/v2 v2.9.0 h1:1tgOaEq92IOEumR1/JfYS/eR0KHOCsRv/rYXXh6YJQE=
github.com/xuri/excelize/v2 v2.9.0/go.mod h1:uqey4QBZ9gdMeWApPLdhm9x+9o2lq4iVmjiLfBS5hdE=
github.com/xuri/nfp v0.0.1 h1:MDamSGatIvp8uOmDP8FnmjuQpu90NzdJxo7242ANR9Q=
github.com/xuri/nfp v0.0.1/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc=
golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI=
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -236,5 +215,5 @@ nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYm
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
xorm.io/builder v0.3.13 h1:a3jmiVVL19psGeXx8GIurTp7p0IIgqeDmwhcR6BAOAo=
xorm.io/builder v0.3.13/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
xorm.io/xorm v1.3.11 h1:i4tlVUASogb0ZZFJHA7dZqoRU2pUpUsutnNdaOlFyMI=
xorm.io/xorm v1.3.11/go.mod h1:cs0ePc8O4a0jD78cNvD+0VFwhqotTvLQZv372QsDw7Q=
xorm.io/xorm v1.3.10 h1:yR83hTT4mKIPyA/lvWFTzS35xjLwkiYnwdw0Qupeh0o=
xorm.io/xorm v1.3.10/go.mod h1:Lo7hmsFF0F0GbDE7ubX5ZKa+eCf0eCuiJAUG3oI5cxQ=
+1652 -1626
View File
File diff suppressed because it is too large Load Diff
+29 -30
View File
@@ -1,6 +1,6 @@
{
"name": "ezbookkeeping",
"version": "1.3.1",
"version": "1.1.1",
"private": true,
"repository": {
"type": "git",
@@ -20,63 +20,62 @@
},
"dependencies": {
"@mdi/js": "^7.4.47",
"@vuepic/vue-datepicker": "^12.1.0",
"axios": "^1.13.2",
"@vuepic/vue-datepicker": "^11.0.2",
"axios": "^1.11.0",
"cbor-js": "^0.1.0",
"chardet": "^2.1.1",
"clipboard": "^2.0.11",
"crypto-js": "^4.2.0",
"dom7": "^4.0.6",
"echarts": "^6.0.0",
"framework7": "^9.0.2",
"echarts": "^5.5.1",
"framework7": "^8.3.4",
"framework7-icons": "^5.0.5",
"framework7-vue": "^9.0.2",
"framework7-vue": "^8.3.4",
"jalaali-js": "^1.2.8",
"leaflet": "^1.9.4",
"line-awesome": "^1.3.0",
"moment": "^2.30.1",
"moment-timezone": "^0.6.0",
"pinia": "^3.0.4",
"pinia": "^3.0.3",
"register-service-worker": "^1.7.2",
"skeleton-elements": "^4.0.1",
"swiper": "^12.0.3",
"swiper": "^10.2.0",
"ua-parser-js": "^1.0.39",
"vue": "^3.5.25",
"vue-echarts": "^8.0.1",
"vue-i18n": "^11.2.2",
"vue-router": "^4.6.4",
"vue": "^3.5.21",
"vue-echarts": "^7.0.3",
"vue-i18n": "^11.1.12",
"vue-router": "^4.5.1",
"vue3-perfect-scrollbar": "^2.0.0",
"vuedraggable": "^4.1.0",
"vuetify": "^3.11.3"
"vuetify": "^3.9.7"
},
"devDependencies": {
"@jest/globals": "^30.2.0",
"@tsconfig/node24": "^24.0.3",
"@jest/globals": "^30.1.2",
"@tsconfig/node24": "^24.0.1",
"@types/cbor-js": "^0.1.1",
"@types/crypto-js": "^4.2.2",
"@types/git-rev-sync": "^2.0.2",
"@types/jalaali-js": "^1.2.0",
"@types/jest": "^30.0.0",
"@types/node": "^24.1.0",
"@types/node": "^24.3.1",
"@types/ua-parser-js": "^0.7.39",
"@vitejs/plugin-vue": "^6.0.3",
"@vitejs/plugin-vue": "^6.0.1",
"@vue/eslint-config-typescript": "^14.6.0",
"@vue/tsconfig": "^0.8.1",
"cross-env": "^10.1.0",
"eslint": "^9.39.1",
"eslint-plugin-vue": "^10.6.2",
"cross-env": "^10.0.0",
"eslint": "^9.35.0",
"eslint-plugin-vue": "^10.4.0",
"git-rev-sync": "^3.0.2",
"jest": "^30.2.0",
"postcss-preset-env": "^10.5.0",
"sass": "^1.96.0",
"ts-jest": "^29.4.6",
"jest": "^30.1.3",
"postcss-preset-env": "^10.3.1",
"sass": "^1.92.1",
"ts-jest": "^29.4.1",
"ts-node": "^10.9.2",
"typescript": "^5.9.3",
"vite": "^7.2.7",
"vite-plugin-checker": "^0.12.0",
"vite-plugin-pwa": "^1.2.0",
"typescript": "^5.9.2",
"vite": "^7.1.4",
"vite-plugin-checker": "^0.10.3",
"vite-plugin-pwa": "^1.0.3",
"vite-plugin-vuetify": "^2.1.2",
"vue-tsc": "^3.1.8"
"vue-tsc": "^3.0.6"
},
"browserslist": [
"last 5 Chrome versions",
+16 -27
View File
@@ -150,10 +150,10 @@ func (a *AccountsApi) AccountCreateHandler(c *core.WebContext) (any, *errs.Error
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
clientTimezone, err := c.GetClientTimezone()
utcOffset, err := c.GetClientTimezoneOffset()
if err != nil {
log.Warnf(c, "[accounts.AccountCreateHandler] cannot get client timezone, because %s", err.Error())
log.Warnf(c, "[accounts.AccountCreateHandler] cannot get client timezone offset, because %s", err.Error())
return nil, errs.ErrClientTimezoneOffsetInvalid
}
@@ -278,7 +278,7 @@ func (a *AccountsApi) AccountCreateHandler(c *core.WebContext) (any, *errs.Error
}
}
err = a.accounts.CreateAccounts(c, mainAccount, accountCreateReq.BalanceTime, childrenAccounts, childrenAccountBalanceTimes, clientTimezone)
err = a.accounts.CreateAccounts(c, mainAccount, accountCreateReq.BalanceTime, childrenAccounts, childrenAccountBalanceTimes, utcOffset)
if err != nil {
log.Errorf(c, "[accounts.AccountCreateHandler] failed to create account \"id:%d\" for user \"uid:%d\", because %s", mainAccount.AccountId, uid, err.Error())
@@ -315,10 +315,10 @@ func (a *AccountsApi) AccountModifyHandler(c *core.WebContext) (any, *errs.Error
return nil, errs.ErrAccountIdInvalid
}
clientTimezone, err := c.GetClientTimezone()
utcOffset, err := c.GetClientTimezoneOffset()
if err != nil {
log.Warnf(c, "[accounts.AccountModifyHandler] cannot get client timezone, because %s", err.Error())
log.Warnf(c, "[accounts.AccountModifyHandler] cannot get client timezone offset, because %s", err.Error())
return nil, errs.ErrClientTimezoneOffsetInvalid
}
@@ -437,17 +437,6 @@ func (a *AccountsApi) AccountModifyHandler(c *core.WebContext) (any, *errs.Error
toUpdateAccount := a.getToUpdateAccount(uid, &accountModifyReq, mainAccount, false)
if toUpdateAccount != nil {
if toUpdateAccount.Category != mainAccount.Category {
maxOrderId, err := a.accounts.GetMaxDisplayOrder(c, uid, toUpdateAccount.Category)
if err != nil {
log.Errorf(c, "[accounts.AccountModifyHandler] failed to get max display order for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
toUpdateAccount.DisplayOrder = maxOrderId + 1
}
anythingUpdate = true
toUpdateAccounts = append(toUpdateAccounts, toUpdateAccount)
}
@@ -532,7 +521,7 @@ func (a *AccountsApi) AccountModifyHandler(c *core.WebContext) (any, *errs.Error
}
}
err = a.accounts.ModifyAccounts(c, mainAccount, toUpdateAccounts, toAddAccounts, toAddAccountBalanceTimes, toDeleteAccountIds, clientTimezone)
err = a.accounts.ModifyAccounts(c, mainAccount, toUpdateAccounts, toAddAccounts, toAddAccountBalanceTimes, toDeleteAccountIds, utcOffset)
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())
@@ -553,6 +542,7 @@ func (a *AccountsApi) AccountModifyHandler(c *core.WebContext) (any, *errs.Error
account.Type = oldAccount.Type
account.ParentAccountId = oldAccount.ParentAccountId
account.DisplayOrder = oldAccount.DisplayOrder
account.Currency = oldAccount.Currency
account.Balance = oldAccount.Balance
@@ -772,16 +762,15 @@ func (a *AccountsApi) getToUpdateAccount(uid int64, accountModifyReq *models.Acc
}
newAccount := &models.Account{
AccountId: oldAccount.AccountId,
Uid: uid,
Name: accountModifyReq.Name,
DisplayOrder: oldAccount.DisplayOrder,
Category: accountModifyReq.Category,
Icon: accountModifyReq.Icon,
Color: accountModifyReq.Color,
Comment: accountModifyReq.Comment,
Extend: newAccountExtend,
Hidden: accountModifyReq.Hidden,
AccountId: oldAccount.AccountId,
Uid: uid,
Name: accountModifyReq.Name,
Category: accountModifyReq.Category,
Icon: accountModifyReq.Icon,
Color: accountModifyReq.Color,
Comment: accountModifyReq.Comment,
Extend: newAccountExtend,
Hidden: accountModifyReq.Hidden,
}
if newAccount.Name != oldAccount.Name ||
+3 -201
View File
@@ -1,9 +1,6 @@
package api
import (
"encoding/json"
"errors"
"github.com/pquerna/otp/totp"
"github.com/mayswind/ezbookkeeping/pkg/avatars"
@@ -25,7 +22,6 @@ type AuthorizationsApi struct {
userAppCloudSettings *services.UserApplicationCloudSettingsService
tokens *services.TokenService
twoFactorAuthorizations *services.TwoFactorAuthorizationService
userExternalAuths *services.UserExternalAuthService
}
// Initialize a authorization api singleton instance
@@ -52,16 +48,11 @@ var (
userAppCloudSettings: services.UserApplicationCloudSettings,
tokens: services.Tokens,
twoFactorAuthorizations: services.TwoFactorAuthorizations,
userExternalAuths: services.UserExternalAuths,
}
)
// AuthorizeHandler verifies and authorizes current login request
func (a *AuthorizationsApi) AuthorizeHandler(c *core.WebContext) (any, *errs.Error) {
if !a.CurrentConfig().EnableInternalAuth {
return nil, errs.ErrCannotLoginByPassword
}
var credential models.UserLoginRequest
err := c.ShouldBindJSON(&credential)
@@ -150,7 +141,6 @@ func (a *AuthorizationsApi) AuthorizeHandler(c *core.WebContext) (any, *errs.Err
}
c.SetTokenClaims(claims)
c.SetTokenContext("")
userApplicationCloudSettings, err := a.userAppCloudSettings.GetUserApplicationCloudSettingsByUid(c, user.Uid)
var applicationCloudSettingSlice *models.ApplicationCloudSettingSlice = nil
@@ -161,7 +151,7 @@ func (a *AuthorizationsApi) AuthorizeHandler(c *core.WebContext) (any, *errs.Err
applicationCloudSettingSlice = &userApplicationCloudSettings.Settings
}
log.Infof(c, "[authorizations.AuthorizeHandler] user \"uid:%d\" has logged in, token type is %d, token will be expired at %d", user.Uid, claims.Type, claims.ExpiresAt)
log.Infof(c, "[authorizations.AuthorizeHandler] user \"uid:%d\" has logined, token type is %d, token will be expired at %d", user.Uid, claims.Type, claims.ExpiresAt)
authResp := a.getAuthResponse(c, token, twoFactorEnable, user, applicationCloudSettingSlice)
return authResp, nil
@@ -169,10 +159,6 @@ func (a *AuthorizationsApi) AuthorizeHandler(c *core.WebContext) (any, *errs.Err
// TwoFactorAuthorizeHandler verifies and authorizes current 2fa login by passcode
func (a *AuthorizationsApi) TwoFactorAuthorizeHandler(c *core.WebContext) (any, *errs.Error) {
if !a.CurrentConfig().EnableInternalAuth {
return nil, errs.ErrCannotLoginByPassword
}
var credential models.TwoFactorLoginRequest
err := c.ShouldBindJSON(&credential)
@@ -212,7 +198,7 @@ func (a *AuthorizationsApi) TwoFactorAuthorizeHandler(c *core.WebContext) (any,
user, err := a.users.GetUserById(c, uid)
if err != nil {
log.Errorf(c, "[authorizations.TwoFactorAuthorizeHandler] failed to get user \"uid:%d\" info, because %s", uid, err.Error())
log.Errorf(c, "[authorizations.TwoFactorAuthorizeHandler] failed to get user \"uid:%d\" info, because %s", user.Uid, err.Error())
return nil, errs.ErrUserNotFound
}
@@ -242,7 +228,6 @@ func (a *AuthorizationsApi) TwoFactorAuthorizeHandler(c *core.WebContext) (any,
c.SetTextualToken(token)
c.SetTokenClaims(claims)
c.SetTokenContext("")
userApplicationCloudSettings, err := a.userAppCloudSettings.GetUserApplicationCloudSettingsByUid(c, user.Uid)
var applicationCloudSettingSlice *models.ApplicationCloudSettingSlice = nil
@@ -261,10 +246,6 @@ func (a *AuthorizationsApi) TwoFactorAuthorizeHandler(c *core.WebContext) (any,
// TwoFactorAuthorizeByRecoveryCodeHandler verifies and authorizes current 2fa login by recovery code
func (a *AuthorizationsApi) TwoFactorAuthorizeByRecoveryCodeHandler(c *core.WebContext) (any, *errs.Error) {
if !a.CurrentConfig().EnableInternalAuth {
return nil, errs.ErrCannotLoginByPassword
}
var credential models.TwoFactorRecoveryCodeLoginRequest
err := c.ShouldBindJSON(&credential)
@@ -295,7 +276,7 @@ func (a *AuthorizationsApi) TwoFactorAuthorizeByRecoveryCodeHandler(c *core.WebC
user, err := a.users.GetUserById(c, uid)
if err != nil {
log.Errorf(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] failed to get user \"uid:%d\" info, because %s", uid, err.Error())
log.Errorf(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] failed to get user \"uid:%d\" info, because %s", user.Uid, err.Error())
return nil, errs.ErrUserNotFound
}
@@ -341,7 +322,6 @@ func (a *AuthorizationsApi) TwoFactorAuthorizeByRecoveryCodeHandler(c *core.WebC
c.SetTextualToken(token)
c.SetTokenClaims(claims)
c.SetTokenContext("")
userApplicationCloudSettings, err := a.userAppCloudSettings.GetUserApplicationCloudSettingsByUid(c, user.Uid)
var applicationCloudSettingSlice *models.ApplicationCloudSettingSlice = nil
@@ -358,184 +338,6 @@ func (a *AuthorizationsApi) TwoFactorAuthorizeByRecoveryCodeHandler(c *core.WebC
return authResp, nil
}
// OAuth2CallbackAuthorizeHandler verifies and authorizes current OAuth 2.0 callback login
func (a *AuthorizationsApi) OAuth2CallbackAuthorizeHandler(c *core.WebContext) (any, *errs.Error) {
if !a.CurrentConfig().EnableOAuth2Login {
return nil, errs.ErrOAuth2NotEnabled
}
var credential models.OAuth2CallbackLoginRequest
err := c.ShouldBindJSON(&credential)
if err != nil {
log.Warnf(c, "[authorizations.OAuth2CallbackAuthorizeHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
var tokenContext models.OAuth2CallbackTokenContext
err = json.Unmarshal([]byte(c.GetTokenContext()), &tokenContext)
if err != nil {
log.Warnf(c, "[authorizations.OAuth2CallbackAuthorizeHandler] parse token context failed, because %s", err.Error())
return nil, errs.ErrOperationFailed
}
if !tokenContext.ExternalAuthType.IsValid() {
log.Warnf(c, "[authorizations.OAuth2CallbackAuthorizeHandler] external auth type \"%s\" is invalid", tokenContext.ExternalAuthType)
return nil, errs.ErrInvalidOAuth2Provider
}
uid := c.GetCurrentUid()
err = a.CheckFailureCount(c, uid)
if err != nil {
log.Warnf(c, "[authorizations.OAuth2CallbackAuthorizeHandler] cannot auth for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrFailureCountLimitReached)
}
user, err := a.users.GetUserById(c, uid)
if err != nil {
log.Errorf(c, "[authorizations.OAuth2CallbackAuthorizeHandler] failed to get user \"uid:%d\" info, because %s", uid, err.Error())
return nil, errs.ErrUserNotFound
}
if user.Disabled {
log.Warnf(c, "[authorizations.OAuth2CallbackAuthorizeHandler] user \"uid:%d\" is disabled", user.Uid)
return nil, errs.ErrUserIsDisabled
}
if a.CurrentConfig().EnableUserForceVerifyEmail && !user.EmailVerified {
log.Warnf(c, "[authorizations.OAuth2CallbackAuthorizeHandler] user \"uid:%d\" has not verified email", user.Uid)
return nil, errs.ErrEmailIsNotVerified
}
oldTokenClaims := c.GetTokenClaims()
if oldTokenClaims.Type == core.USER_TOKEN_TYPE_OAUTH2_CALLBACK_REQUIRE_VERIFY {
if credential.Password == "" {
return nil, errs.ErrPasswordIsEmpty
}
if !a.users.IsPasswordEqualsUserPassword(credential.Password, user) {
failureCheckErr := a.CheckAndIncreaseFailureCount(c, uid)
if failureCheckErr != nil {
log.Warnf(c, "[authorizations.OAuth2CallbackAuthorizeHandler] cannot login for user \"uid:%d\", because %s", user.Uid, failureCheckErr.Error())
return nil, errs.Or(failureCheckErr, errs.ErrFailureCountLimitReached)
}
return nil, errs.ErrUserPasswordWrong
}
if a.CurrentConfig().EnableTwoFactor {
twoFactorSetting, err := a.twoFactorAuthorizations.GetUserTwoFactorSettingByUid(c, uid)
if err != nil && !errors.Is(err, errs.ErrTwoFactorIsNotEnabled) {
log.Errorf(c, "[authorizations.OAuth2CallbackAuthorizeHandler] failed to check two-factor setting for user \"uid:%d\", because %s", user.Uid, err.Error())
return nil, errs.Or(err, errs.ErrSystemError)
}
if twoFactorSetting != nil {
if credential.Passcode == "" {
return nil, errs.ErrPasscodeEmpty
}
if !totp.Validate(credential.Passcode, twoFactorSetting.Secret) {
log.Warnf(c, "[authorizations.OAuth2CallbackAuthorizeHandler] passcode is invalid for user \"uid:%d\"", uid)
err = a.CheckAndIncreaseFailureCount(c, uid)
if err != nil {
log.Warnf(c, "[authorizations.OAuth2CallbackAuthorizeHandler] cannot auth for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrFailureCountLimitReached)
}
return nil, errs.ErrPasscodeInvalid
}
}
}
userExternalAuth := &models.UserExternalAuth{
Uid: user.Uid,
ExternalAuthType: tokenContext.ExternalAuthType,
ExternalUsername: tokenContext.ExternalUsername,
ExternalEmail: tokenContext.ExternalEmail,
}
err = a.userExternalAuths.CreateUserExternalAuth(c, userExternalAuth)
if err != nil {
log.Errorf(c, "[authorizations.OAuth2CallbackAuthorizeHandler] failed to create user external auth for user \"uid:%d\", because %s", user.Uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
log.Infof(c, "[authorizations.OAuth2CallbackAuthorizeHandler] user external auth has been created for user \"uid:%d\"", user.Uid)
} else if oldTokenClaims.Type == core.USER_TOKEN_TYPE_OAUTH2_CALLBACK {
_, err = a.userExternalAuths.GetUserExternalAuthByUid(c, uid, tokenContext.ExternalAuthType)
if err != nil {
log.Errorf(c, "[authorizations.OAuth2CallbackAuthorizeHandler] failed to get user external auth for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrUserExternalAuthNotFound)
}
} else {
return nil, errs.ErrSystemError
}
err = a.tokens.DeleteTokenByClaims(c, oldTokenClaims)
if err != nil {
log.Warnf(c, "[authorizations.OAuth2CallbackAuthorizeHandler] failed to revoke temporary token \"utid:%s\" for user \"uid:%d\", because %s", oldTokenClaims.UserTokenId, user.Uid, err.Error())
}
var token string
var claims *core.UserTokenClaims
if credential.Token != "" {
_, claims, _, err = a.tokens.ParseToken(c, credential.Token)
if err != nil {
log.Errorf(c, "[authorizations.OAuth2CallbackAuthorizeHandler] failed to parse token, because %s", err.Error())
return nil, errs.ErrInvalidToken
}
if claims.Uid != user.Uid {
log.Warnf(c, "[authorizations.OAuth2CallbackAuthorizeHandler] oauth 2.0 user \"uid:%d\" does not match current user \"uid:%d\"", user.Uid, claims.Uid)
token = ""
claims = nil
} else {
token = credential.Token
}
}
if token == "" {
token, claims, err = a.tokens.CreateToken(c, user)
if err != nil {
log.Errorf(c, "[authorizations.OAuth2CallbackAuthorizeHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
return nil, errs.ErrTokenGenerating
}
}
c.SetTextualToken(token)
c.SetTokenClaims(claims)
c.SetTokenContext("")
userApplicationCloudSettings, err := a.userAppCloudSettings.GetUserApplicationCloudSettingsByUid(c, user.Uid)
var applicationCloudSettingSlice *models.ApplicationCloudSettingSlice = nil
if err != nil {
log.Warnf(c, "[authorizations.OAuth2CallbackAuthorizeHandler] failed to get latest user application cloud settings for user \"uid:%d\", because %s", user.Uid, err.Error())
} else if userApplicationCloudSettings != nil && len(userApplicationCloudSettings.Settings) > 0 {
applicationCloudSettingSlice = &userApplicationCloudSettings.Settings
}
log.Infof(c, "[authorizations.OAuth2CallbackAuthorizeHandler] user \"uid:%d\" has logged in, token will be expired at %d", user.Uid, claims.ExpiresAt)
authResp := a.getAuthResponse(c, token, false, user, applicationCloudSettingSlice)
return authResp, nil
}
func (a *AuthorizationsApi) getAuthResponse(c *core.WebContext, token string, need2FA bool, user *models.User, applicationCloudSettings *models.ApplicationCloudSettingSlice) *models.AuthResponse {
return &models.AuthResponse{
Token: token,
-11
View File
@@ -3,7 +3,6 @@ package api
import (
"fmt"
"sort"
"time"
"github.com/mayswind/ezbookkeeping/pkg/avatars"
"github.com/mayswind/ezbookkeeping/pkg/core"
@@ -114,11 +113,6 @@ func (a *ApiUsingDuplicateChecker) GetSubmissionRemark(checkerType duplicatechec
return a.container.GetSubmissionRemark(checkerType, uid, identification)
}
// SetSubmissionRemarkWithCustomExpiration saves the identification and remark by the current duplicate checker with custom expiration time
func (a *ApiUsingDuplicateChecker) SetSubmissionRemarkWithCustomExpiration(checkerType duplicatechecker.DuplicateCheckerType, uid int64, identification string, remark string, expiration time.Duration) {
a.container.SetSubmissionRemarkWithCustomExpiration(checkerType, uid, identification, remark, expiration)
}
// SetSubmissionRemarkIfEnable saves the identification and remark by the current duplicate checker if the duplicate submission check is enabled
func (a *ApiUsingDuplicateChecker) SetSubmissionRemarkIfEnable(checkerType duplicatechecker.DuplicateCheckerType, uid int64, identification string, remark string) {
if a.CurrentConfig().EnableDuplicateSubmissionsCheck {
@@ -126,11 +120,6 @@ func (a *ApiUsingDuplicateChecker) SetSubmissionRemarkIfEnable(checkerType dupli
}
}
// RemoveSubmissionRemark removes the identification and remark by the current duplicate checker
func (a *ApiUsingDuplicateChecker) RemoveSubmissionRemark(checkerType duplicatechecker.DuplicateCheckerType, uid int64, identification string) {
a.container.RemoveSubmissionRemark(checkerType, uid, identification)
}
// RemoveSubmissionRemarkIfEnable removes the identification and remark by the current duplicate checker if the duplicate submission check is enabled
func (a *ApiUsingDuplicateChecker) RemoveSubmissionRemarkIfEnable(checkerType duplicatechecker.DuplicateCheckerType, uid int64, identification string) {
if a.CurrentConfig().EnableDuplicateSubmissionsCheck {
+24 -49
View File
@@ -2,7 +2,6 @@ package api
import (
"fmt"
"math"
"strings"
"time"
@@ -28,11 +27,9 @@ type DataManagementsApi struct {
transactions *services.TransactionService
categories *services.TransactionCategoryService
tags *services.TransactionTagService
tagGroups *services.TransactionTagGroupService
pictures *services.TransactionPictureService
templates *services.TransactionTemplateService
userCustomExchangeRates *services.UserCustomExchangeRatesService
insightsExploreres *services.InsightsExplorerService
}
// Initialize a data management api singleton instance
@@ -47,11 +44,9 @@ var (
transactions: services.Transactions,
categories: services.TransactionCategories,
tags: services.TransactionTags,
tagGroups: services.TransactionTagGroups,
pictures: services.TransactionPictures,
templates: services.TransactionTemplates,
userCustomExchangeRates: services.UserCustomExchangeRates,
insightsExploreres: services.InsightsExplorers,
}
)
@@ -103,13 +98,6 @@ func (a *DataManagementsApi) DataStatisticsHandler(c *core.WebContext) (any, *er
return nil, errs.ErrOperationFailed
}
totalInsightsExplorerCount, err := a.insightsExploreres.GetTotalInsightsExplorersCountByUid(c, uid)
if err != nil {
log.Errorf(c, "[data_managements.DataStatisticsHandler] failed to get total insights explorer count for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.ErrOperationFailed
}
totalTransactionTemplateCount, err := a.templates.GetTotalNormalTemplateCountByUid(c, uid)
if err != nil {
@@ -130,7 +118,6 @@ func (a *DataManagementsApi) DataStatisticsHandler(c *core.WebContext) (any, *er
TotalTransactionTagCount: totalTransactionTagCount,
TotalTransactionCount: totalTransactionCount,
TotalTransactionPictureCount: totalTransactionPictureCount,
TotalInsightsExplorerCount: totalInsightsExplorerCount,
TotalTransactionTemplateCount: totalTransactionTemplateCount,
TotalScheduledTransactionCount: totalScheduledTransactionCount,
}
@@ -195,13 +182,6 @@ func (a *DataManagementsApi) ClearAllDataHandler(c *core.WebContext) (any, *errs
return nil, errs.Or(err, errs.ErrOperationFailed)
}
err = a.tagGroups.DeleteAllTagGroups(c, uid)
if err != nil {
log.Errorf(c, "[data_managements.ClearAllDataHandler] failed to delete all transaction tag groups, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
err = a.userCustomExchangeRates.DeleteAllCustomExchangeRates(c, uid)
if err != nil {
@@ -209,13 +189,6 @@ func (a *DataManagementsApi) ClearAllDataHandler(c *core.WebContext) (any, *errs
return nil, errs.Or(err, errs.ErrOperationFailed)
}
err = a.insightsExploreres.DeleteAllInsightsExplorers(c, uid)
if err != nil {
log.Errorf(c, "[data_managements.ClearAllDataHandler] failed to delete all insights explorers, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
log.Infof(c, "[data_managements.ClearAllDataHandler] user \"uid:%d\" has cleared all data", uid)
return true, nil
}
@@ -324,15 +297,17 @@ func (a *DataManagementsApi) getExportedFileContent(c *core.WebContext, fileType
err := c.ShouldBindQuery(&exportTransactionDataReq)
if err != nil {
log.Warnf(c, "[data_managements.getExportedFileContent] parse request failed, because %s", err.Error())
log.Warnf(c, "[data_managements.ExportDataHandler] parse request failed, because %s", err.Error())
return nil, "", errs.NewIncompleteOrIncorrectSubmissionError(err)
}
clientTimezone, err := c.GetClientTimezone()
timezone := time.Local
utcOffset, err := c.GetClientTimezoneOffset()
if err != nil {
log.Warnf(c, "[data_managements.getExportedFileContent] cannot get client timezone, because %s", err.Error())
clientTimezone = time.Local
log.Warnf(c, "[data_managements.ExportDataHandler] cannot get client timezone offset, because %s", err.Error())
} else {
timezone = time.FixedZone("Client Timezone", int(utcOffset)*60)
}
uid := c.GetCurrentUid()
@@ -340,7 +315,7 @@ func (a *DataManagementsApi) getExportedFileContent(c *core.WebContext, fileType
if err != nil {
if !errs.IsCustomError(err) {
log.Warnf(c, "[data_managements.getExportedFileContent] failed to get user for user \"uid:%d\", because %s", uid, err.Error())
log.Warnf(c, "[data_managements.ExportDataHandler] failed to get user for user \"uid:%d\", because %s", uid, err.Error())
}
return nil, "", errs.ErrUserNotFound
@@ -353,28 +328,28 @@ func (a *DataManagementsApi) getExportedFileContent(c *core.WebContext, fileType
accounts, err := a.accounts.GetAllAccountsByUid(c, uid)
if err != nil {
log.Errorf(c, "[data_managements.getExportedFileContent] failed to get all accounts for user \"uid:%d\", because %s", uid, err.Error())
log.Errorf(c, "[data_managements.ExportDataHandler] failed to get all accounts for user \"uid:%d\", because %s", uid, err.Error())
return nil, "", errs.ErrOperationFailed
}
categories, err := a.categories.GetAllCategoriesByUid(c, uid, 0, -1)
if err != nil {
log.Errorf(c, "[data_managements.getExportedFileContent] failed to get categories for user \"uid:%d\", because %s", uid, err.Error())
log.Errorf(c, "[data_managements.ExportDataHandler] failed to get categories for user \"uid:%d\", because %s", uid, err.Error())
return nil, "", errs.ErrOperationFailed
}
tags, err := a.tags.GetAllTagsByUid(c, uid)
if err != nil {
log.Errorf(c, "[data_managements.getExportedFileContent] failed to get tags for user \"uid:%d\", because %s", uid, err.Error())
log.Errorf(c, "[data_managements.ExportDataHandler] failed to get tags for user \"uid:%d\", because %s", uid, err.Error())
return nil, "", errs.ErrOperationFailed
}
tagIndexes, err := a.tags.GetAllTagIdsMapOfAllTransactions(c, uid)
if err != nil {
log.Errorf(c, "[data_managements.getExportedFileContent] failed to get tag index for user \"uid:%d\", because %s", uid, err.Error())
log.Errorf(c, "[data_managements.ExportDataHandler] failed to get tag index for user \"uid:%d\", because %s", uid, err.Error())
return nil, "", errs.ErrOperationFailed
}
@@ -385,30 +360,30 @@ func (a *DataManagementsApi) getExportedFileContent(c *core.WebContext, fileType
allAccountIds, err := a.accounts.GetAccountOrSubAccountIds(c, exportTransactionDataReq.AccountIds, uid)
if err != nil {
log.Warnf(c, "[data_managements.getExportedFileContent] get account error, because %s", err.Error())
log.Warnf(c, "[data_managements.ExportDataHandler] get account error, because %s", err.Error())
return nil, "", errs.Or(err, errs.ErrOperationFailed)
}
allCategoryIds, err := a.categories.GetCategoryOrSubCategoryIds(c, exportTransactionDataReq.CategoryIds, uid)
if err != nil {
log.Warnf(c, "[data_managements.getExportedFileContent] get transaction category error, because %s", err.Error())
log.Warnf(c, "[data_managements.ExportDataHandler] get transaction category error, because %s", err.Error())
return nil, "", errs.Or(err, errs.ErrOperationFailed)
}
noTags := exportTransactionDataReq.TagFilter == models.TransactionNoTagFilterValue
var tagFilters []*models.TransactionTagFilter
var allTagIds []int64
noTags := exportTransactionDataReq.TagIds == "none"
if !noTags {
tagFilters, err = models.ParseTransactionTagFilter(exportTransactionDataReq.TagFilter)
allTagIds, err = a.tags.GetTagIds(exportTransactionDataReq.TagIds)
if err != nil {
log.Warnf(c, "[data_managements.getExportedFileContent] parse transaction tag filters error, because %s", err.Error())
log.Warnf(c, "[data_managements.ExportDataHandler] get transaction tag ids error, because %s", err.Error())
return nil, "", errs.Or(err, errs.ErrOperationFailed)
}
}
maxTransactionTime := int64(math.MaxInt64)
maxTransactionTime := utils.GetMaxTransactionTimeFromUnixTime(time.Now().Unix())
minTransactionTime := int64(0)
if exportTransactionDataReq.MaxTime > 0 {
@@ -419,10 +394,10 @@ func (a *DataManagementsApi) getExportedFileContent(c *core.WebContext, fileType
minTransactionTime = utils.GetMinTransactionTimeFromUnixTime(exportTransactionDataReq.MinTime)
}
allTransactions, err := a.transactions.GetAllSpecifiedTransactions(c, uid, maxTransactionTime, minTransactionTime, exportTransactionDataReq.Type, allCategoryIds, allAccountIds, tagFilters, noTags, exportTransactionDataReq.AmountFilter, exportTransactionDataReq.Keyword, pageCountForDataExport, true)
allTransactions, err := a.transactions.GetAllSpecifiedTransactions(c, uid, maxTransactionTime, minTransactionTime, exportTransactionDataReq.Type, allCategoryIds, allAccountIds, allTagIds, noTags, exportTransactionDataReq.TagFilterType, exportTransactionDataReq.AmountFilter, exportTransactionDataReq.Keyword, pageCountForDataExport, true)
if err != nil {
log.Errorf(c, "[data_managements.getExportedFileContent] failed to all transactions user \"uid:%d\", because %s", uid, err.Error())
log.Errorf(c, "[data_managements.ExportDataHandler] failed to all transactions user \"uid:%d\", because %s", uid, err.Error())
return nil, "", errs.ErrOperationFailed
}
@@ -435,17 +410,17 @@ func (a *DataManagementsApi) getExportedFileContent(c *core.WebContext, fileType
result, err := dataExporter.ToExportedContent(c, uid, allTransactions, accountMap, categoryMap, tagMap, tagIndexes)
if err != nil {
log.Errorf(c, "[data_managements.getExportedFileContent] failed to get exported data for \"uid:%d\", because %s", uid, err.Error())
log.Errorf(c, "[data_managements.ExportDataHandler] failed to get csv format exported data for \"uid:%d\", because %s", uid, err.Error())
return nil, "", errs.Or(err, errs.ErrOperationFailed)
}
fileName := a.getFileName(user, clientTimezone, fileType)
fileName := a.getFileName(user, timezone, fileType)
return result, fileName, nil
}
func (a *DataManagementsApi) getFileName(user *models.User, clientTimezone *time.Location, fileExtension string) string {
currentTime := utils.FormatUnixTimeToLongDateTimeWithoutSecond(time.Now().Unix(), clientTimezone)
func (a *DataManagementsApi) getFileName(user *models.User, timezone *time.Location, fileExtension string) string {
currentTime := utils.FormatUnixTimeToLongDateTimeWithoutSecond(time.Now().Unix(), timezone)
currentTime = strings.Replace(currentTime, "-", "_", -1)
currentTime = strings.Replace(currentTime, " ", "_", -1)
currentTime = strings.Replace(currentTime, ":", "_", -1)
-274
View File
@@ -1,274 +0,0 @@
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
}
+1 -23
View File
@@ -4,7 +4,6 @@ import (
"time"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/duplicatechecker"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/models"
@@ -15,7 +14,6 @@ import (
// ForgetPasswordsApi represents user forget password api
type ForgetPasswordsApi struct {
ApiUsingConfig
ApiUsingDuplicateChecker
users *services.UserService
tokens *services.TokenService
forgetPasswords *services.ForgetPasswordService
@@ -27,12 +25,6 @@ var (
ApiUsingConfig: ApiUsingConfig{
container: settings.Container,
},
ApiUsingDuplicateChecker: ApiUsingDuplicateChecker{
ApiUsingConfig: ApiUsingConfig{
container: settings.Container,
},
container: duplicatechecker.Container,
},
users: services.Users,
tokens: services.Tokens,
forgetPasswords: services.ForgetPasswords,
@@ -49,13 +41,6 @@ func (a *ForgetPasswordsApi) UserForgetPasswordRequestHandler(c *core.WebContext
return nil, errs.ErrEmailIsEmptyOrInvalid
}
err = a.CheckFailureCount(c, 0)
if err != nil {
log.Warnf(c, "[forget_passwords.UserForgetPasswordRequestHandler] cannot send forget password mail to \"%s\", because %s", request.Email, err.Error())
return nil, errs.Or(err, errs.ErrFailureCountLimitReached)
}
user, err := a.users.GetUserByEmail(c, request.Email)
if err != nil {
@@ -63,13 +48,6 @@ func (a *ForgetPasswordsApi) UserForgetPasswordRequestHandler(c *core.WebContext
log.Errorf(c, "[forget_passwords.UserForgetPasswordRequestHandler] failed to get user, because %s", err.Error())
}
failureCheckErr := a.CheckAndIncreaseFailureCount(c, 0)
if failureCheckErr != nil {
log.Warnf(c, "[forget_passwords.UserForgetPasswordRequestHandler] cannot send forget password mail to \"%s\", because %s", request.Email, failureCheckErr.Error())
return nil, errs.Or(failureCheckErr, errs.ErrFailureCountLimitReached)
}
return nil, errs.ErrUserNotFound
}
@@ -146,7 +124,7 @@ func (a *ForgetPasswordsApi) UserResetPasswordHandler(c *core.WebContext) (any,
if user.Email != request.Email {
log.Warnf(c, "[forget_passwords.UserResetPasswordHandler] request email not equals the user email")
return nil, errs.ErrEmailIsInvalid
return nil, errs.ErrEmptyIsInvalid
}
if a.users.IsPasswordEqualsUserPassword(request.Password, user) {
+7 -9
View File
@@ -47,13 +47,14 @@ func (a *LargeLanguageModelsApi) RecognizeReceiptImageHandler(c *core.WebContext
return nil, errs.ErrLargeLanguageModelProviderNotEnabled
}
clientTimezone, err := c.GetClientTimezone()
utcOffset, err := c.GetClientTimezoneOffset()
if err != nil {
log.Warnf(c, "[large_language_models.RecognizeReceiptImageHandler] cannot get client timezone, because %s", err.Error())
log.Warnf(c, "[large_language_models.RecognizeReceiptImageHandler] cannot get client timezone offset, because %s", err.Error())
return nil, errs.ErrClientTimezoneOffsetInvalid
}
timezone := time.FixedZone("Client Timezone", int(utcOffset)*60)
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(c, uid)
@@ -191,12 +192,11 @@ func (a *LargeLanguageModelsApi) RecognizeReceiptImageHandler(c *core.WebContext
systemPrompt, err := templates.GetTemplate(templates.SYSTEM_PROMPT_RECEIPT_IMAGE_RECOGNITION)
if err != nil {
log.Errorf(c, "[large_language_models.RecognizeReceiptImageHandler] failed to get system prompt template for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
systemPromptParams := map[string]any{
"CurrentDateTime": utils.FormatUnixTimeToLongDateTime(time.Now().Unix(), clientTimezone),
"CurrentDateTime": utils.FormatUnixTimeToLongDateTime(time.Now().Unix(), timezone),
"AllExpenseCategoryNames": strings.Join(expenseCategoryNames, "\n"),
"AllIncomeCategoryNames": strings.Join(incomeCategoryNames, "\n"),
"AllTransferCategoryNames": strings.Join(transferCategoryNames, "\n"),
@@ -208,7 +208,6 @@ func (a *LargeLanguageModelsApi) RecognizeReceiptImageHandler(c *core.WebContext
err = systemPrompt.Execute(&bodyBuffer, systemPromptParams)
if err != nil {
log.Errorf(c, "[large_language_models.RecognizeReceiptImageHandler] failed to get final system prompt from template for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
@@ -223,7 +222,6 @@ func (a *LargeLanguageModelsApi) RecognizeReceiptImageHandler(c *core.WebContext
llmResponse, err := llm.Container.GetJsonResponseByReceiptImageRecognitionModel(c, c.GetCurrentUid(), a.CurrentConfig(), llmRequest)
if err != nil {
log.Errorf(c, "[large_language_models.RecognizeReceiptImageHandler] failed to get llm response user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
@@ -238,10 +236,10 @@ func (a *LargeLanguageModelsApi) RecognizeReceiptImageHandler(c *core.WebContext
return nil, errs.Or(err, errs.ErrOperationFailed)
}
return a.parseRecognizedReceiptImageResponse(c, uid, clientTimezone, result, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
return a.parseRecognizedReceiptImageResponse(c, uid, utcOffset, result, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
}
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) {
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) {
recognizedReceiptImageResponse := &models.RecognizedReceiptImageResponse{
Type: models.TRANSACTION_TYPE_EXPENSE,
}
@@ -290,7 +288,7 @@ func (a *LargeLanguageModelsApi) parseRecognizedReceiptImageResponse(c *core.Web
if len(recognizedResult.Time) > 0 {
longDateTime := a.getLongDateTime(recognizedResult.Time)
timestamp, err := utils.ParseFromLongDateTimeInTimeZone(longDateTime, clientTimezone)
timestamp, err := utils.ParseFromLongDateTime(longDateTime, utcOffset)
if err != nil {
log.Warnf(c, "[large_language_models.parseRecognizedReceiptImageResponse] recoginzed time \"%s\" is invalid", recognizedResult.Time)
+4 -20
View File
@@ -5,12 +5,11 @@ import (
"net/http/httputil"
"net/url"
"strings"
"sync"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/httpclient"
"github.com/mayswind/ezbookkeeping/pkg/settings"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
const openStreetMapTileImageUrlFormat = "https://tile.openstreetmap.org/{z}/{x}/{y}.png" // https://tile.openstreetmap.org/{z}/{x}/{y}.png
@@ -26,8 +25,6 @@ const tianDiTuMapAnnotationUrlFormat = "https://t0.tianditu.gov.cn/cva_w/wmts?SE
// MapImageProxy represents map image proxy
type MapImageProxy struct {
ApiUsingConfig
mutex sync.Mutex
transport *http.Transport
}
// Initialize a map image proxy singleton instance
@@ -39,18 +36,6 @@ var (
}
)
func (p *MapImageProxy) initializeHttpTransport() {
p.mutex.Lock()
defer p.mutex.Unlock()
if p.transport != nil {
return
}
p.transport = http.DefaultTransport.(*http.Transport).Clone()
httpclient.SetProxyUrl(p.transport, p.CurrentConfig().MapProxy)
}
// MapTileImageProxyHandler returns map tile image
func (p *MapImageProxy) MapTileImageProxyHandler(c *core.WebContext) (*httputil.ReverseProxy, *errs.Error) {
return p.mapImageProxyHandler(c, func(c *core.WebContext, mapProvider string) (string, *errs.Error) {
@@ -124,9 +109,8 @@ func (p *MapImageProxy) mapImageProxyHandler(c *core.WebContext, fn func(c *core
return nil, err
}
if p.transport == nil {
p.initializeHttpTransport()
}
transport := http.DefaultTransport.(*http.Transport).Clone()
utils.SetProxyUrl(transport, p.CurrentConfig().MapProxy)
director := func(req *http.Request) {
imageRawUrl := targetUrl
@@ -142,7 +126,7 @@ func (p *MapImageProxy) mapImageProxyHandler(c *core.WebContext, fn func(c *core
}
return &httputil.ReverseProxy{
Transport: p.transport,
Transport: transport,
Director: director,
}, nil
}
+2 -2
View File
@@ -13,7 +13,7 @@ import (
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
const mcpServerName = core.ApplicationName + "-mcp"
const mcpServerName = "ezBookkeeping-mcp"
// ModelContextProtocolAPI represents model context protocol api
type ModelContextProtocolAPI struct {
@@ -102,7 +102,7 @@ func (a *ModelContextProtocolAPI) InitializeHandler(c *core.WebContext, jsonRPCR
},
ServerInfo: &mcp.MCPImplementation{
Name: mcpServerName,
Title: core.ApplicationName,
Title: a.CurrentConfig().AppName,
Version: settings.Version,
},
}
-423
View File
@@ -1,423 +0,0 @@
package api
import (
"encoding/json"
"errors"
"fmt"
"net/url"
"strings"
"github.com/mayswind/ezbookkeeping/pkg/auth/oauth2"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/duplicatechecker"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/locales"
"github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/services"
"github.com/mayswind/ezbookkeeping/pkg/settings"
"github.com/mayswind/ezbookkeeping/pkg/utils"
"github.com/mayswind/ezbookkeeping/pkg/validators"
)
const oauth2CallbackPageUrlSuccessFormat = "%sdesktop#/oauth2_callback?platform=%s&provider=%s&token=%s"
const oauth2CallbackPageUrlNeedVerifyFormat = "%sdesktop#/oauth2_callback?platform=%s&provider=%s&userName=%s&token=%s"
const oauth2CallbackPageUrlFailedFormat = "%sdesktop#/oauth2_callback?errorCode=%d&errorMessage=%s"
const oauth2CallbackPageUrlErrorMessageFormat = "%sdesktop#/oauth2_callback?errorMessage=%s"
// OAuth2AuthenticationApi represents OAuth 2.0 authorization api
type OAuth2AuthenticationApi struct {
ApiUsingConfig
ApiUsingDuplicateChecker
users *services.UserService
tokens *services.TokenService
userExternalAuths *services.UserExternalAuthService
}
// Initialize a OAuth 2.0 authentication api singleton instance
var (
OAuth2Authentications = &OAuth2AuthenticationApi{
ApiUsingConfig: ApiUsingConfig{
container: settings.Container,
},
ApiUsingDuplicateChecker: ApiUsingDuplicateChecker{
ApiUsingConfig: ApiUsingConfig{
container: settings.Container,
},
container: duplicatechecker.Container,
},
users: services.Users,
tokens: services.Tokens,
userExternalAuths: services.UserExternalAuths,
}
)
// LoginHandler handles user login request via OAuth 2.0
func (a *OAuth2AuthenticationApi) LoginHandler(c *core.WebContext) (string, *errs.Error) {
var oauth2LoginReq models.OAuth2LoginRequest
err := c.ShouldBindQuery(&oauth2LoginReq)
if err != nil {
log.Warnf(c, "[oauth2_authentications.LoginHandler] parse request failed, because %s", err.Error())
return a.redirectToFailedCallbackPage(c, errs.NewIncompleteOrIncorrectSubmissionError(err))
}
if oauth2LoginReq.Platform != "mobile" && oauth2LoginReq.Platform != "desktop" {
return a.redirectToFailedCallbackPage(c, errs.ErrInvalidOAuth2LoginRequest)
}
found, remark := a.GetSubmissionRemark(duplicatechecker.DUPLICATE_CHECKER_TYPE_OAUTH2_REDIRECT, 0, oauth2LoginReq.ClientSessionId)
if found {
log.Errorf(c, "[oauth2_authentications.LoginHandler] another oauth 2.0 state \"%s\" has been processing for client session id \"%s\"", remark, oauth2LoginReq.ClientSessionId)
return a.redirectToFailedCallbackPage(c, errs.ErrRepeatedRequest)
}
uid := int64(0)
if oauth2LoginReq.Token != "" {
_, claims, _, err := a.tokens.ParseToken(c, oauth2LoginReq.Token)
if err != nil {
log.Errorf(c, "[oauth2_authentications.LoginHandler] failed to parse token, because %s", err.Error())
return a.redirectToFailedCallbackPage(c, errs.ErrInvalidToken)
}
uid = claims.Uid
user, err := a.users.GetUserById(c, uid)
if err != nil && !errors.Is(err, errs.ErrUserNotFound) {
log.Errorf(c, "[oauth2_authentications.LoginHandler] failed to get user by id %d, because %s", uid, err.Error())
return a.redirectToFailedCallbackPage(c, errs.Or(err, errs.ErrOperationFailed))
}
if user.FeatureRestriction.Contains(core.USER_FEATURE_RESTRICTION_TYPE_OAUTH2_LOGIN) {
return a.redirectToFailedCallbackPage(c, errs.ErrNotPermittedToPerformThisAction)
}
}
verifier, err := utils.GetRandomNumberOrLowercaseLetter(64)
if err != nil {
log.Errorf(c, "[oauth2_authentications.LoginHandler] failed to generate random string for oauth 2.0 state, because %s", err.Error())
return a.redirectToFailedCallbackPage(c, errs.ErrSystemError)
}
remark = fmt.Sprintf("%s|%s|%d|%s", oauth2LoginReq.Platform, oauth2LoginReq.ClientSessionId, uid, verifier)
state := fmt.Sprintf("%s|%s|%s", oauth2LoginReq.Platform, oauth2LoginReq.ClientSessionId, utils.MD5EncodeToString([]byte(remark)))
redirectUrl, err := oauth2.GetOAuth2AuthUrl(c, state, verifier)
if err != nil {
log.Errorf(c, "[oauth2_authentications.LoginHandler] failed to get oauth 2.0 auth url, because %s", err.Error())
return a.redirectToFailedCallbackPage(c, errs.Or(err, errs.ErrSystemError))
}
a.SetSubmissionRemarkWithCustomExpiration(duplicatechecker.DUPLICATE_CHECKER_TYPE_OAUTH2_REDIRECT, 0, oauth2LoginReq.ClientSessionId, remark, a.CurrentConfig().OAuth2StateExpiredTimeDuration)
return redirectUrl, nil
}
// CallbackHandler handles OAuth 2.0 callback request
func (a *OAuth2AuthenticationApi) CallbackHandler(c *core.WebContext) (string, *errs.Error) {
var oauth2CallbackReq models.OAuth2CallbackRequest
err := c.ShouldBindQuery(&oauth2CallbackReq)
if err != nil {
log.Warnf(c, "[oauth2_authentications.CallbackHandler] parse request failed, because %s", err.Error())
return a.redirectToFailedCallbackPage(c, errs.NewIncompleteOrIncorrectSubmissionError(err))
}
if oauth2CallbackReq.State == "" {
return a.redirectToFailedCallbackPage(c, errs.ErrMissingOAuth2State)
}
if oauth2CallbackReq.Code == "" {
if oauth2CallbackReq.ErrorDescription != "" {
log.Errorf(c, "[oauth2_authentications.CallbackHandler] oauth 2.0 provider returned error: %s, description: %s", oauth2CallbackReq.Error, oauth2CallbackReq.ErrorDescription)
return a.redirectToErrorMessageCallbackPage(c, oauth2CallbackReq.ErrorDescription)
}
return a.redirectToFailedCallbackPage(c, errs.ErrMissingOAuth2Code)
}
platform := ""
clientSessionId := ""
stateParts := strings.Split(oauth2CallbackReq.State, "|")
if len(stateParts) == 3 {
platform = stateParts[0]
clientSessionId = stateParts[1]
} else {
return a.redirectToFailedCallbackPage(c, errs.ErrInvalidOAuth2State)
}
if platform != "mobile" && platform != "desktop" {
return a.redirectToFailedCallbackPage(c, errs.ErrInvalidOAuth2LoginRequest)
}
found, remark := a.GetSubmissionRemark(duplicatechecker.DUPLICATE_CHECKER_TYPE_OAUTH2_REDIRECT, 0, clientSessionId)
if !found {
log.Errorf(c, "[oauth2_authentications.CallbackHandler] cannot find oauth 2.0 state in duplicate checker for client session id \"%s\"", clientSessionId)
return a.redirectToFailedCallbackPage(c, errs.ErrInvalidOAuth2Callback)
}
remarkParts := strings.Split(remark, "|")
if len(remarkParts) != 4 || remarkParts[0] != platform || remarkParts[1] != clientSessionId {
log.Errorf(c, "[oauth2_authentications.CallbackHandler] invalid oauth 2.0 state \"%s\" in duplicate checker for client session id \"%s\"", remark, clientSessionId)
return a.redirectToFailedCallbackPage(c, errs.ErrInvalidOAuth2State)
}
uid, err := utils.StringToInt64(remarkParts[2])
if err != nil {
log.Errorf(c, "[oauth2_authentications.CallbackHandler] invalid uid \"%s\" in oauth 2.0 state \"%s\"", remarkParts[2], remark)
return a.redirectToFailedCallbackPage(c, errs.ErrInvalidOAuth2State)
}
verifier := remarkParts[3]
expectedRemark := fmt.Sprintf("%s|%s|%d|%s", platform, clientSessionId, uid, verifier)
expectedState := fmt.Sprintf("%s|%s|%s", platform, clientSessionId, utils.MD5EncodeToString([]byte(expectedRemark)))
if oauth2CallbackReq.State != expectedState {
log.Errorf(c, "[oauth2_authentications.CallbackHandler] mismatched random string in oauth 2.0 state, expected \"%s\", got \"%s\"", expectedState, oauth2CallbackReq.State)
return a.redirectToFailedCallbackPage(c, errs.ErrInvalidOAuth2State)
}
a.RemoveSubmissionRemark(duplicatechecker.DUPLICATE_CHECKER_TYPE_OAUTH2_REDIRECT, 0, clientSessionId)
oauth2Token, err := oauth2.GetOAuth2Token(c, oauth2CallbackReq.Code, verifier)
if err != nil {
log.Errorf(c, "[oauth2_authentications.CallbackHandler] failed to retrieve oauth 2.0 token, because %s", err.Error())
return a.redirectToFailedCallbackPage(c, errs.Or(err, errs.ErrCannotRetrieveOAuth2Token))
}
oauth2UserInfo, err := oauth2.GetOAuth2UserInfo(c, oauth2Token)
if err != nil {
log.Errorf(c, "[oauth2_authentications.CallbackHandler] failed to retrieve oauth 2.0 user info, because %s", err.Error())
return a.redirectToFailedCallbackPage(c, errs.Or(err, errs.ErrInvalidOAuth2Token))
}
if oauth2UserInfo == nil {
log.Errorf(c, "[oauth2_authentications.CallbackHandler] failed to retrieve oauth 2.0 user info, because user info is nil")
return a.redirectToFailedCallbackPage(c, errs.ErrCannotRetrieveUserInfo)
}
log.Infof(c, "[oauth2_authentications.CallbackHandler] oauth 2.0 user info, userName: %s, email: %s", oauth2UserInfo.UserName, oauth2UserInfo.Email)
if oauth2UserInfo.UserName == "" && oauth2UserInfo.Email == "" {
return a.redirectToFailedCallbackPage(c, errs.ErrOAuth2UserNameAndEmailEmpty)
}
if a.CurrentConfig().OAuth2UserIdentifier == settings.OAuth2UserIdentifierEmail && oauth2UserInfo.Email == "" {
log.Errorf(c, "[oauth2_authentications.CallbackHandler] invalid oauth 2.0 user info, email is empty")
return a.redirectToFailedCallbackPage(c, errs.ErrOAuth2EmailEmpty)
}
if a.CurrentConfig().OAuth2UserIdentifier == settings.OAuth2UserIdentifierUsername && oauth2UserInfo.UserName == "" {
log.Errorf(c, "[oauth2_authentications.CallbackHandler] invalid oauth 2.0 user info, userName is empty")
return a.redirectToFailedCallbackPage(c, errs.ErrOAuth2UserNameEmpty)
}
userExternalAuthType := oauth2.GetExternalUserAuthType()
var userExternalAuth *models.UserExternalAuth
if a.CurrentConfig().OAuth2UserIdentifier == settings.OAuth2UserIdentifierEmail {
userExternalAuth, err = a.userExternalAuths.GetUserExternalAuthByExternalEmail(c, oauth2UserInfo.Email, userExternalAuthType)
} else if a.CurrentConfig().OAuth2UserIdentifier == settings.OAuth2UserIdentifierUsername {
userExternalAuth, err = a.userExternalAuths.GetUserExternalAuthByExternalUserName(c, oauth2UserInfo.UserName, userExternalAuthType)
} else {
return a.redirectToFailedCallbackPage(c, errs.ErrNotSupported)
}
if err != nil && !errors.Is(err, errs.ErrUserExternalAuthNotFound) {
log.Errorf(c, "[oauth2_authentications.CallbackHandler] failed to get user external auth, because %s", err.Error())
return a.redirectToFailedCallbackPage(c, errs.Or(err, errs.ErrOperationFailed))
}
if uid != 0 && userExternalAuth != nil && userExternalAuth.Uid != uid {
log.Errorf(c, "[oauth2_authentications.CallbackHandler] oauth 2.0 external auth has been bound to another user \"uid:%d\", current user \"uid:%d\"", userExternalAuth.Uid, uid)
return a.redirectToFailedCallbackPage(c, errs.ErrOAuth2UserAlreadyBoundToAnotherUser)
}
var user *models.User
if err == nil { // user already bound to external auth, redirect to success page
user, err = a.users.GetUserById(c, userExternalAuth.Uid)
if err != nil {
log.Errorf(c, "[oauth2_authentications.CallbackHandler] failed to get user by id %d, because %s", userExternalAuth.Uid, err.Error())
return a.redirectToFailedCallbackPage(c, errs.Or(err, errs.ErrOperationFailed))
}
} else { // errors.Is(err, errs.ErrUserExternalAuthNotFound) // user not bound to external auth, try to bind or register new user
if uid != 0 {
user, err = a.users.GetUserById(c, uid)
if err != nil && !errors.Is(err, errs.ErrUserNotFound) {
log.Errorf(c, "[oauth2_authentications.CallbackHandler] failed to get user by id %d, because %s", uid, err.Error())
return a.redirectToFailedCallbackPage(c, errs.Or(err, errs.ErrOperationFailed))
}
} else {
if a.CurrentConfig().OAuth2UserIdentifier == settings.OAuth2UserIdentifierEmail {
user, err = a.users.GetUserByEmail(c, oauth2UserInfo.Email)
} else if a.CurrentConfig().OAuth2UserIdentifier == settings.OAuth2UserIdentifierUsername {
user, err = a.users.GetUserByUsername(c, oauth2UserInfo.UserName)
} else {
err = errs.ErrNotSupported
}
if err != nil && !errors.Is(err, errs.ErrUserNotFound) {
log.Errorf(c, "[oauth2_authentications.CallbackHandler] failed to get user, because %s", err.Error())
return a.redirectToFailedCallbackPage(c, errs.Or(err, errs.ErrOperationFailed))
}
}
if user == nil && a.CurrentConfig().EnableUserRegister && a.CurrentConfig().OAuth2AutoRegister {
if oauth2UserInfo.UserName == "" {
return a.redirectToFailedCallbackPage(c, errs.ErrOAuth2UserNameEmptyCannotRegister)
}
if oauth2UserInfo.Email == "" {
return a.redirectToFailedCallbackPage(c, errs.ErrOAuth2EmailEmptyCannotRegister)
}
userName := strings.TrimSpace(oauth2UserInfo.UserName)
email := strings.TrimSpace(oauth2UserInfo.Email)
nickName := strings.TrimSpace(oauth2UserInfo.NickName)
languageCode := ""
currencyCode := "USD"
if nickName == "" {
nickName = userName
}
if !utils.IsValidUsername(userName) {
return a.redirectToFailedCallbackPage(c, errs.ErrUserNameIsInvalid)
}
if !utils.IsValidEmail(email) {
return a.redirectToFailedCallbackPage(c, errs.ErrEmailIsInvalid)
}
if !utils.IsValidNickName(nickName) {
return a.redirectToFailedCallbackPage(c, errs.ErrNickNameIsInvalid)
}
if _, exists := locales.AllLanguages[oauth2UserInfo.LanguageCode]; exists {
languageCode = oauth2UserInfo.LanguageCode
}
if _, exists := validators.AllCurrencyNames[oauth2UserInfo.CurrencyCode]; exists {
currencyCode = oauth2UserInfo.CurrencyCode
}
user = &models.User{
Username: userName,
Email: email,
Nickname: nickName,
Language: languageCode,
DefaultCurrency: currencyCode,
FirstDayOfWeek: oauth2UserInfo.FirstDayOfWeek,
FiscalYearStart: core.FISCAL_YEAR_START_DEFAULT,
TransactionEditScope: models.TRANSACTION_EDIT_SCOPE_ALL,
FeatureRestriction: a.CurrentConfig().DefaultFeatureRestrictions,
}
if user.FeatureRestriction.Contains(core.USER_FEATURE_RESTRICTION_TYPE_OAUTH2_LOGIN) {
return a.redirectToFailedCallbackPage(c, errs.ErrNotPermittedToPerformThisAction)
}
err = a.users.CreateUser(c, user, true)
if err != nil {
log.Errorf(c, "[oauth2_authentications.CallbackHandler] failed to create user \"%s\", because %s", user.Username, err.Error())
return a.redirectToFailedCallbackPage(c, errs.Or(err, errs.ErrOperationFailed))
}
log.Infof(c, "[oauth2_authentications.CallbackHandler] user \"%s\" has registered successfully, uid is %d", user.Username, user.Uid)
userExternalAuth = &models.UserExternalAuth{
Uid: user.Uid,
ExternalAuthType: userExternalAuthType,
ExternalUsername: oauth2UserInfo.UserName,
ExternalEmail: oauth2UserInfo.Email,
}
err = a.userExternalAuths.CreateUserExternalAuth(c, userExternalAuth)
if err != nil {
log.Errorf(c, "[oauth2_authentications.CallbackHandler] failed to create user external auth for user \"uid:%d\", because %s", user.Uid, err.Error())
return a.redirectToFailedCallbackPage(c, errs.Or(err, errs.ErrOperationFailed))
}
log.Infof(c, "[oauth2_authentications.CallbackHandler] user external auth has been created for user \"uid:%d\"", user.Uid)
} else if user == nil {
return a.redirectToFailedCallbackPage(c, errs.ErrOAuth2AutoRegistrationNotEnabled)
}
}
if user.FeatureRestriction.Contains(core.USER_FEATURE_RESTRICTION_TYPE_OAUTH2_LOGIN) {
return a.redirectToFailedCallbackPage(c, errs.ErrNotPermittedToPerformThisAction)
}
if userExternalAuth == nil {
tokenContext, err := json.Marshal(&models.OAuth2CallbackTokenContext{
ExternalAuthType: userExternalAuthType,
ExternalUsername: oauth2UserInfo.UserName,
ExternalEmail: oauth2UserInfo.Email,
})
if err != nil {
log.Errorf(c, "[oauth2_authentications.CallbackHandler] failed to marshal oauth 2.0 callback verify token context, because %s", err.Error())
return a.redirectToFailedCallbackPage(c, errs.ErrOperationFailed)
}
token, _, err := a.tokens.CreateOAuth2CallbackRequireVerifyToken(c, user, string(tokenContext))
if err != nil {
log.Errorf(c, "[oauth2_authentications.CallbackHandler] failed to create oauth 2.0 callback verify token, because %s", err.Error())
return a.redirectToFailedCallbackPage(c, errs.ErrTokenGenerating)
}
return a.redirectToVerifyCallbackPage(c, platform, userExternalAuthType, user.Username, token)
} else {
tokenContext, err := json.Marshal(&models.OAuth2CallbackTokenContext{
ExternalAuthType: userExternalAuthType,
})
if err != nil {
log.Errorf(c, "[oauth2_authentications.CallbackHandler] failed to marshal oauth 2.0 callback token context, because %s", err.Error())
return a.redirectToFailedCallbackPage(c, errs.ErrOperationFailed)
}
token, _, err := a.tokens.CreateOAuth2CallbackToken(c, user, string(tokenContext))
if err != nil {
log.Errorf(c, "[oauth2_authentications.CallbackHandler] failed to create oauth 2.0 callback token, because %s", err.Error())
return a.redirectToFailedCallbackPage(c, errs.ErrTokenGenerating)
}
return a.redirectToSuccessCallbackPage(c, platform, userExternalAuthType, token)
}
}
func (a *OAuth2AuthenticationApi) redirectToSuccessCallbackPage(c *core.WebContext, platform string, externalAuthType core.UserExternalAuthType, token string) (string, *errs.Error) {
return fmt.Sprintf(oauth2CallbackPageUrlSuccessFormat, a.CurrentConfig().RootUrl, platform, externalAuthType, url.QueryEscape(token)), nil
}
func (a *OAuth2AuthenticationApi) redirectToVerifyCallbackPage(c *core.WebContext, platform string, externalAuthType core.UserExternalAuthType, userName string, token string) (string, *errs.Error) {
return fmt.Sprintf(oauth2CallbackPageUrlNeedVerifyFormat, a.CurrentConfig().RootUrl, platform, externalAuthType, userName, url.QueryEscape(token)), nil
}
func (a *OAuth2AuthenticationApi) redirectToFailedCallbackPage(c *core.WebContext, err *errs.Error) (string, *errs.Error) {
return fmt.Sprintf(oauth2CallbackPageUrlFailedFormat, a.CurrentConfig().RootUrl, err.Code(), url.QueryEscape(utils.GetDisplayErrorMessage(err))), nil
}
func (a *OAuth2AuthenticationApi) redirectToErrorMessageCallbackPage(c *core.WebContext, message string) (string, *errs.Error) {
return fmt.Sprintf(oauth2CallbackPageUrlErrorMessageFormat, a.CurrentConfig().RootUrl, url.QueryEscape(message)), nil
}
+3 -12
View File
@@ -35,23 +35,14 @@ func (a *ServerSettingsApi) ServerSettingsJavascriptHandler(c *core.WebContext)
builder := &strings.Builder{}
builder.WriteString(ezbookkeepingServerSettingsJavascriptFileHeader)
a.appendBooleanSetting(builder, "a", config.EnableInternalAuth)
a.appendBooleanSetting(builder, "o", config.EnableOAuth2Login)
a.appendBooleanSetting(builder, "r", config.EnableInternalAuth && config.EnableUserRegister)
a.appendBooleanSetting(builder, "f", config.EnableInternalAuth && config.EnableUserForgetPassword)
a.appendBooleanSetting(builder, "t", config.EnableAPIToken)
a.appendBooleanSetting(builder, "r", config.EnableUserRegister)
a.appendBooleanSetting(builder, "f", config.EnableUserForgetPassword)
a.appendBooleanSetting(builder, "v", config.EnableUserVerifyEmail)
a.appendBooleanSetting(builder, "p", config.EnableTransactionPictures)
a.appendBooleanSetting(builder, "s", config.EnableScheduledTransaction)
a.appendBooleanSetting(builder, "e", config.EnableDataExport)
a.appendBooleanSetting(builder, "i", config.EnableDataImport)
a.appendStringSetting(builder, "op", config.OAuth2Provider)
if config.OAuth2Provider == settings.OAuth2ProviderOIDC && config.OAuth2OIDCCustomDisplayNameConfig.Enabled {
a.appendMultiLanguageTipSetting(builder, "ocn", config.OAuth2OIDCCustomDisplayNameConfig)
}
if config.EnableMCPServer {
a.appendBooleanSetting(builder, "mcp", config.EnableMCPServer)
}
@@ -147,7 +138,7 @@ func (a *ServerSettingsApi) appendStringSetting(builder *strings.Builder, key st
builder.WriteString(";\n")
}
func (a *ServerSettingsApi) appendMultiLanguageTipSetting(builder *strings.Builder, key string, value settings.MultiLanguageContentConfig) {
func (a *ServerSettingsApi) appendMultiLanguageTipSetting(builder *strings.Builder, key string, value settings.TipConfig) {
builder.WriteString(ezbookkeepingServerSettingsGlobalVariableFullName)
builder.WriteString("[")
a.appendEncodedString(builder, key)
+3 -53
View File
@@ -69,9 +69,7 @@ func (a *TokensApi) TokenListHandler(c *core.WebContext) (any, *errs.Error) {
tokenResp.IsCurrent = true
}
if token.TokenType == core.USER_TOKEN_TYPE_API && token.UserAgent != services.TokenUserAgentCreatedViaCli {
tokenResp.UserAgent = services.TokenUserAgentForAPI
} else if token.TokenType == core.USER_TOKEN_TYPE_MCP && token.UserAgent != services.TokenUserAgentCreatedViaCli {
if token.TokenType == core.USER_TOKEN_TYPE_MCP && token.UserAgent != services.TokenUserAgentCreatedViaCli {
tokenResp.UserAgent = services.TokenUserAgentForMCP
}
@@ -83,53 +81,6 @@ func (a *TokensApi) TokenListHandler(c *core.WebContext) (any, *errs.Error) {
return tokenResps, nil
}
// TokenGenerateAPIHandler generates a new API token for current user
func (a *TokensApi) TokenGenerateAPIHandler(c *core.WebContext) (any, *errs.Error) {
if !a.CurrentConfig().EnableAPIToken {
return nil, errs.ErrAPITokenNotEnabled
}
var generateAPITokenReq models.TokenGenerateAPIRequest
err := c.ShouldBindJSON(&generateAPITokenReq)
if err != nil {
log.Warnf(c, "[tokens.TokenGenerateAPIHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(c, uid)
if err != nil {
log.Warnf(c, "[tokens.TokenGenerateAPIHandler] failed to get user \"uid:%d\" info, because %s", uid, err.Error())
return nil, errs.ErrUserNotFound
}
if user.FeatureRestriction.Contains(core.USER_FEATURE_RESTRICTION_TYPE_GENERATE_API_TOKEN) {
return false, errs.ErrNotPermittedToPerformThisAction
}
if !a.users.IsPasswordEqualsUserPassword(generateAPITokenReq.Password, user) {
return nil, errs.ErrUserPasswordWrong
}
token, claims, err := a.tokens.CreateAPIToken(c, user, generateAPITokenReq.ExpiredInSeconds)
if err != nil {
log.Errorf(c, "[tokens.TokenGenerateAPIHandler] failed to create api token for user \"uid:%d\", because %s", user.Uid, err.Error())
return nil, errs.Or(err, errs.ErrTokenGenerating)
}
log.Infof(c, "[tokens.TokenGenerateAPIHandler] user \"uid:%d\" has generated api token, new token will be expired at %d", user.Uid, claims.ExpiresAt)
generateAPITokenResp := &models.TokenGenerateAPIResponse{
Token: token,
APIBaseUrl: a.CurrentConfig().RootUrl + "api",
}
return generateAPITokenResp, nil
}
// TokenGenerateMCPHandler generates a new MCP token for current user
func (a *TokensApi) TokenGenerateMCPHandler(c *core.WebContext) (any, *errs.Error) {
if !a.CurrentConfig().EnableMCPServer {
@@ -160,7 +111,7 @@ func (a *TokensApi) TokenGenerateMCPHandler(c *core.WebContext) (any, *errs.Erro
return nil, errs.ErrUserPasswordWrong
}
token, claims, err := a.tokens.CreateMCPToken(c, user, generateMCPTokenReq.ExpiredInSeconds)
token, claims, err := a.tokens.CreateMCPToken(c, user)
if err != nil {
log.Errorf(c, "[tokens.TokenGenerateMCPHandler] failed to create mcp token for user \"uid:%d\", because %s", user.Uid, err.Error())
@@ -185,7 +136,7 @@ func (a *TokensApi) TokenRevokeCurrentHandler(c *core.WebContext) (any, *errs.Er
return false, errs.ErrTokenIsEmpty
}
_, claims, _, err := a.tokens.ParseToken(c, tokenString)
_, claims, err := a.tokens.ParseToken(c, tokenString)
if err != nil {
return nil, errs.Or(err, errs.NewIncompleteOrIncorrectSubmissionError(err))
@@ -393,7 +344,6 @@ func (a *TokensApi) TokenRefreshHandler(c *core.WebContext) (any, *errs.Error) {
c.SetTextualToken(token)
c.SetTokenClaims(claims)
c.SetTokenContext("")
userApplicationCloudSettings, err := a.userAppCloudSettings.GetUserApplicationCloudSettingsByUid(c, user.Uid)
var applicationCloudSettingSlice *models.ApplicationCloudSettingSlice = nil
+1 -10
View File
@@ -214,7 +214,6 @@ func (a *TransactionCategoriesApi) CategoryModifyHandler(c *core.WebContext) (an
Uid: uid,
ParentCategoryId: categoryModifyReq.ParentId,
Name: categoryModifyReq.Name,
DisplayOrder: category.DisplayOrder,
Icon: categoryModifyReq.Icon,
Color: categoryModifyReq.Color,
Comment: categoryModifyReq.Comment,
@@ -260,15 +259,6 @@ func (a *TransactionCategoriesApi) CategoryModifyHandler(c *core.WebContext) (an
if toPrimaryCategory.ParentCategoryId != models.LevelOneTransactionCategoryParentId {
return nil, errs.Or(err, errs.ErrNotAllowUseSecondaryTransactionAsPrimaryCategory)
}
maxOrderId, err := a.categories.GetMaxSubCategoryDisplayOrder(c, uid, category.Type, newCategory.ParentCategoryId)
if err != nil {
log.Errorf(c, "[transaction_categories.CategoryModifyHandler] failed to get max display order for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
newCategory.DisplayOrder = maxOrderId + 1
}
err = a.categories.ModifyCategory(c, newCategory)
@@ -281,6 +271,7 @@ func (a *TransactionCategoriesApi) CategoryModifyHandler(c *core.WebContext) (an
log.Infof(c, "[transaction_categories.CategoryModifyHandler] user \"uid:%d\" has updated category \"id:%d\" successfully", uid, categoryModifyReq.Id)
newCategory.Type = category.Type
newCategory.DisplayOrder = category.DisplayOrder
categoryResp := newCategory.ToTransactionCategoryInfoResponse()
return categoryResp, nil
-210
View File
@@ -1,210 +0,0 @@
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,
}
}
+7 -33
View File
@@ -78,7 +78,7 @@ func (a *TransactionTagsApi) TagCreateHandler(c *core.WebContext) (any, *errs.Er
uid := c.GetCurrentUid()
maxOrderId, err := a.tags.GetMaxDisplayOrder(c, uid, tagCreateReq.GroupId)
maxOrderId, err := a.tags.GetMaxDisplayOrder(c, uid)
if err != nil {
log.Errorf(c, "[transaction_tags.TagCreateHandler] failed to get max display order for user \"uid:%d\", because %s", uid, err.Error())
@@ -111,16 +111,9 @@ func (a *TransactionTagsApi) TagCreateBatchHandler(c *core.WebContext) (any, *er
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
for i := 0; i < len(tagCreateBatchReq.Tags); i++ {
if tagCreateBatchReq.Tags[i].GroupId != tagCreateBatchReq.GroupId {
log.Warnf(c, "[transaction_tags.TagCreateBatchHandler] the group id \"%d\" of tag#%d is inconsistent with the batch group id \"%d\"", tagCreateBatchReq.Tags[i].GroupId, i, tagCreateBatchReq.GroupId)
return nil, errs.ErrTransactionTagGroupIdInvalid
}
}
uid := c.GetCurrentUid()
maxOrderId, err := a.tags.GetMaxDisplayOrder(c, uid, tagCreateBatchReq.GroupId)
maxOrderId, err := a.tags.GetMaxDisplayOrder(c, uid)
if err != nil {
log.Errorf(c, "[transaction_tags.TagCreateBatchHandler] failed to get max display order for user \"uid:%d\", because %s", uid, err.Error())
@@ -168,31 +161,16 @@ func (a *TransactionTagsApi) TagModifyHandler(c *core.WebContext) (any, *errs.Er
}
newTag := &models.TransactionTag{
TagId: tag.TagId,
Uid: uid,
Name: tagModifyReq.Name,
TagGroupId: tagModifyReq.GroupId,
DisplayOrder: tag.DisplayOrder,
TagId: tag.TagId,
Uid: uid,
Name: tagModifyReq.Name,
}
tagNameChanged := newTag.Name != tag.Name
if !tagNameChanged && newTag.TagGroupId == tag.TagGroupId {
if newTag.Name == tag.Name {
return nil, errs.ErrNothingWillBeUpdated
}
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)
err = a.tags.ModifyTag(c, newTag)
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())
@@ -202,8 +180,6 @@ func (a *TransactionTagsApi) TagModifyHandler(c *core.WebContext) (any, *errs.Er
log.Infof(c, "[transaction_tags.TagModifyHandler] user \"uid:%d\" has updated tag \"id:%d\" successfully", uid, tagModifyReq.Id)
tag.Name = newTag.Name
tag.TagGroupId = newTag.TagGroupId
tag.DisplayOrder = newTag.DisplayOrder
tagResp := tag.ToTransactionTagInfoResponse()
return tagResp, nil
@@ -292,7 +268,6 @@ func (a *TransactionTagsApi) createNewTagModel(uid int64, tagCreateReq *models.T
return &models.TransactionTag{
Uid: uid,
Name: tagCreateReq.Name,
TagGroupId: tagCreateReq.GroupId,
DisplayOrder: order,
}
}
@@ -303,7 +278,6 @@ func (a *TransactionTagsApi) createNewTagModels(uid int64, tagCreateBatchReq *mo
for i := 0; i < len(tagCreateBatchReq.Tags); i++ {
tagCreateReq := tagCreateBatchReq.Tags[i]
tag := a.createNewTagModel(uid, tagCreateReq, order+int32(i))
tag.TagGroupId = tagCreateBatchReq.GroupId
tags[i] = tag
}
+58 -304
View File
@@ -4,10 +4,8 @@ import (
"encoding/json"
"fmt"
"io"
"math"
"sort"
"strings"
"time"
orderedmap "github.com/wk8/go-ordered-map/v2"
@@ -85,19 +83,19 @@ func (a *TransactionsApi) TransactionCountHandler(c *core.WebContext) (any, *err
return nil, errs.Or(err, errs.ErrOperationFailed)
}
noTags := transactionCountReq.TagFilter == models.TransactionNoTagFilterValue
var tagFilters []*models.TransactionTagFilter
var allTagIds []int64
noTags := transactionCountReq.TagIds == "none"
if !noTags {
tagFilters, err = models.ParseTransactionTagFilter(transactionCountReq.TagFilter)
allTagIds, err = a.transactionTags.GetTagIds(transactionCountReq.TagIds)
if err != nil {
log.Warnf(c, "[transactions.TransactionCountHandler] parse transaction filters error, because %s", err.Error())
log.Warnf(c, "[transactions.TransactionCountHandler] get transaction tag ids error, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
}
totalCount, err := a.transactions.GetTransactionCount(c, uid, transactionCountReq.MaxTime, transactionCountReq.MinTime, transactionCountReq.Type, allCategoryIds, allAccountIds, tagFilters, noTags, transactionCountReq.AmountFilter, transactionCountReq.Keyword)
totalCount, err := a.transactions.GetTransactionCount(c, uid, transactionCountReq.MaxTime, transactionCountReq.MinTime, transactionCountReq.Type, allCategoryIds, allAccountIds, allTagIds, noTags, transactionCountReq.TagFilterType, transactionCountReq.AmountFilter, transactionCountReq.Keyword)
if err != nil {
log.Errorf(c, "[transactions.TransactionCountHandler] failed to get transaction count for user \"uid:%d\", because %s", uid, err.Error())
@@ -121,10 +119,10 @@ func (a *TransactionsApi) TransactionListHandler(c *core.WebContext) (any, *errs
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
clientTimezone, err := c.GetClientTimezone()
utcOffset, err := c.GetClientTimezoneOffset()
if err != nil {
log.Warnf(c, "[transactions.TransactionListHandler] cannot get client timezone, because %s", err.Error())
log.Warnf(c, "[transactions.TransactionListHandler] cannot get client timezone offset, because %s", err.Error())
return nil, errs.ErrClientTimezoneOffsetInvalid
}
@@ -153,14 +151,14 @@ func (a *TransactionsApi) TransactionListHandler(c *core.WebContext) (any, *errs
return nil, errs.Or(err, errs.ErrOperationFailed)
}
noTags := transactionListReq.TagFilter == models.TransactionNoTagFilterValue
var tagFilters []*models.TransactionTagFilter
var allTagIds []int64
noTags := transactionListReq.TagIds == "none"
if !noTags {
tagFilters, err = models.ParseTransactionTagFilter(transactionListReq.TagFilter)
allTagIds, err = a.transactionTags.GetTagIds(transactionListReq.TagIds)
if err != nil {
log.Warnf(c, "[transactions.TransactionListHandler] parse transaction tag filters error, because %s", err.Error())
log.Warnf(c, "[transactions.TransactionListHandler] get transaction tag ids error, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
}
@@ -168,7 +166,7 @@ func (a *TransactionsApi) TransactionListHandler(c *core.WebContext) (any, *errs
var totalCount int64
if transactionListReq.WithCount {
totalCount, err = a.transactions.GetTransactionCount(c, uid, transactionListReq.MaxTime, transactionListReq.MinTime, transactionListReq.Type, allCategoryIds, allAccountIds, tagFilters, noTags, transactionListReq.AmountFilter, transactionListReq.Keyword)
totalCount, err = a.transactions.GetTransactionCount(c, uid, transactionListReq.MaxTime, transactionListReq.MinTime, transactionListReq.Type, allCategoryIds, allAccountIds, allTagIds, noTags, transactionListReq.TagFilterType, transactionListReq.AmountFilter, transactionListReq.Keyword)
if err != nil {
log.Errorf(c, "[transactions.TransactionListHandler] failed to get transaction count for user \"uid:%d\", because %s", uid, err.Error())
@@ -176,7 +174,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, tagFilters, noTags, 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, allTagIds, noTags, transactionListReq.TagFilterType, transactionListReq.AmountFilter, transactionListReq.Keyword, transactionListReq.Page, transactionListReq.Count, true, true)
if err != nil {
log.Errorf(c, "[transactions.TransactionListHandler] failed to get transactions earlier than \"%d\" for user \"uid:%d\", because %s", transactionListReq.MaxTime, uid, err.Error())
@@ -192,7 +190,7 @@ func (a *TransactionsApi) TransactionListHandler(c *core.WebContext) (any, *errs
transactions = transactions[:transactionListReq.Count]
}
transactionResult, err := a.getTransactionResponseListResult(c, user, transactions, clientTimezone, transactionListReq.WithPictures, transactionListReq.TrimAccount, transactionListReq.TrimCategory, transactionListReq.TrimTag)
transactionResult, err := a.getTransactionResponseListResult(c, user, transactions, utcOffset, transactionListReq.WithPictures, transactionListReq.TrimAccount, transactionListReq.TrimCategory, transactionListReq.TrimTag)
if err != nil {
log.Errorf(c, "[transactions.TransactionListHandler] failed to assemble transaction result for user \"uid:%d\", because %s", uid, err.Error())
@@ -224,10 +222,10 @@ func (a *TransactionsApi) TransactionMonthListHandler(c *core.WebContext) (any,
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
clientTimezone, err := c.GetClientTimezone()
utcOffset, err := c.GetClientTimezoneOffset()
if err != nil {
log.Warnf(c, "[transactions.TransactionMonthListHandler] cannot get client timezone, because %s", err.Error())
log.Warnf(c, "[transactions.TransactionMonthListHandler] cannot get client timezone offset, because %s", err.Error())
return nil, errs.ErrClientTimezoneOffsetInvalid
}
@@ -256,26 +254,26 @@ func (a *TransactionsApi) TransactionMonthListHandler(c *core.WebContext) (any,
return nil, errs.Or(err, errs.ErrOperationFailed)
}
noTags := transactionListReq.TagFilter == models.TransactionNoTagFilterValue
var tagFilters []*models.TransactionTagFilter
var allTagIds []int64
noTags := transactionListReq.TagIds == "none"
if !noTags {
tagFilters, err = models.ParseTransactionTagFilter(transactionListReq.TagFilter)
allTagIds, err = a.transactionTags.GetTagIds(transactionListReq.TagIds)
if err != nil {
log.Warnf(c, "[transactions.TransactionMonthListHandler] parse transaction tag filters error, because %s", err.Error())
log.Warnf(c, "[transactions.TransactionMonthListHandler] get transaction tag ids error, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
}
transactions, err := a.transactions.GetTransactionsInMonthByPage(c, uid, transactionListReq.Year, transactionListReq.Month, transactionListReq.Type, allCategoryIds, allAccountIds, tagFilters, noTags, transactionListReq.AmountFilter, transactionListReq.Keyword)
transactions, err := a.transactions.GetTransactionsInMonthByPage(c, uid, transactionListReq.Year, transactionListReq.Month, transactionListReq.Type, allCategoryIds, allAccountIds, allTagIds, noTags, transactionListReq.TagFilterType, transactionListReq.AmountFilter, transactionListReq.Keyword)
if err != nil {
log.Errorf(c, "[transactions.TransactionMonthListHandler] failed to get transactions in month \"%d-%d\" for user \"uid:%d\", because %s", transactionListReq.Year, transactionListReq.Month, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
transactionResult, err := a.getTransactionResponseListResult(c, user, transactions, clientTimezone, transactionListReq.WithPictures, transactionListReq.TrimAccount, transactionListReq.TrimCategory, transactionListReq.TrimTag)
transactionResult, err := a.getTransactionResponseListResult(c, user, transactions, utcOffset, transactionListReq.WithPictures, transactionListReq.TrimAccount, transactionListReq.TrimCategory, transactionListReq.TrimTag)
if err != nil {
log.Errorf(c, "[transactions.TransactionMonthListHandler] failed to assemble transaction result for user \"uid:%d\", because %s", uid, err.Error())
@@ -290,88 +288,6 @@ func (a *TransactionsApi) TransactionMonthListHandler(c *core.WebContext) (any,
return transactionResps, nil
}
// TransactionListAllHandler returns all transaction list of current user
func (a *TransactionsApi) TransactionListAllHandler(c *core.WebContext) (any, *errs.Error) {
var transactionAllListReq models.TransactionAllListRequest
err := c.ShouldBindQuery(&transactionAllListReq)
if err != nil {
log.Warnf(c, "[transactions.TransactionListAllHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
clientTimezone, err := c.GetClientTimezone()
if err != nil {
log.Warnf(c, "[transactions.TransactionListAllHandler] cannot get client timezone, because %s", err.Error())
return nil, errs.ErrClientTimezoneOffsetInvalid
}
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(c, uid)
if err != nil {
if !errs.IsCustomError(err) {
log.Errorf(c, "[transactions.TransactionListAllHandler] failed to get user, because %s", err.Error())
}
return nil, errs.ErrUserNotFound
}
allAccountIds, err := a.accounts.GetAccountOrSubAccountIds(c, transactionAllListReq.AccountIds, uid)
if err != nil {
log.Warnf(c, "[transactions.TransactionListAllHandler] get account error, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
allCategoryIds, err := a.transactionCategories.GetCategoryOrSubCategoryIds(c, transactionAllListReq.CategoryIds, uid)
if err != nil {
log.Warnf(c, "[transactions.TransactionListAllHandler] get transaction category error, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
noTags := transactionAllListReq.TagFilter == models.TransactionNoTagFilterValue
var tagFilters []*models.TransactionTagFilter
if !noTags {
tagFilters, err = models.ParseTransactionTagFilter(transactionAllListReq.TagFilter)
if err != nil {
log.Warnf(c, "[transactions.TransactionListAllHandler] parse transaction tag filters error, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
}
maxTransactionTime := int64(math.MaxInt64)
minTransactionTime := int64(0)
if transactionAllListReq.EndTime > 0 {
maxTransactionTime = utils.GetMaxTransactionTimeFromUnixTime(transactionAllListReq.EndTime)
}
if transactionAllListReq.StartTime > 0 {
minTransactionTime = utils.GetMinTransactionTimeFromUnixTime(transactionAllListReq.StartTime)
}
allTransactions, err := a.transactions.GetAllSpecifiedTransactions(c, uid, maxTransactionTime, minTransactionTime, transactionAllListReq.Type, allCategoryIds, allAccountIds, tagFilters, noTags, transactionAllListReq.AmountFilter, transactionAllListReq.Keyword, pageCountForDataExport, true)
if err != nil {
log.Errorf(c, "[transactions.TransactionListAllHandler] failed to get all transactions for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
transactionResult, err := a.getTransactionResponseListResult(c, user, allTransactions, clientTimezone, transactionAllListReq.WithPictures, transactionAllListReq.TrimAccount, transactionAllListReq.TrimCategory, transactionAllListReq.TrimTag)
if err != nil {
log.Errorf(c, "[transactions.TransactionListAllHandler] failed to assemble transaction result for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
return transactionResult, nil
}
// TransactionReconciliationStatementHandler returns transaction reconciliation statement list of current user
func (a *TransactionsApi) TransactionReconciliationStatementHandler(c *core.WebContext) (any, *errs.Error) {
var reconciliationStatementRequest models.TransactionReconciliationStatementRequest
@@ -382,10 +298,10 @@ func (a *TransactionsApi) TransactionReconciliationStatementHandler(c *core.WebC
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
clientTimezone, err := c.GetClientTimezone()
utcOffset, err := c.GetClientTimezoneOffset()
if err != nil {
log.Warnf(c, "[transactions.TransactionReconciliationStatementHandler] cannot get client timezone, because %s", err.Error())
log.Warnf(c, "[transactions.TransactionReconciliationStatementHandler] cannot get client timezone offset, because %s", err.Error())
return nil, errs.ErrClientTimezoneOffsetInvalid
}
@@ -424,7 +340,7 @@ func (a *TransactionsApi) TransactionReconciliationStatementHandler(c *core.WebC
minTransactionTime = utils.GetMinTransactionTimeFromUnixTime(reconciliationStatementRequest.StartTime)
}
transactionsWithAccountBalance, totalInflows, totalOutflows, openingBalance, closingBalance, err := a.transactions.GetAllTransactionsInOneAccountWithAccountBalanceByMaxTime(c, uid, pageCountForAccountStatement, maxTransactionTime, minTransactionTime, reconciliationStatementRequest.AccountId, account.Category)
transactionsWithAccountBalance, totalInflows, totalOutflows, openingBalance, closingBalance, err := a.transactions.GetAllTransactionsWithAccountBalanceByMaxTime(c, uid, pageCountForAccountStatement, maxTransactionTime, minTransactionTime, reconciliationStatementRequest.AccountId, account.Category)
if err != nil {
log.Errorf(c, "[transactions.TransactionReconciliationStatementHandler] failed to get transactions from \"%d\" to \"%d\" for user \"uid:%d\", because %s", reconciliationStatementRequest.StartTime, reconciliationStatementRequest.EndTime, uid, err.Error())
@@ -441,7 +357,7 @@ func (a *TransactionsApi) TransactionReconciliationStatementHandler(c *core.WebC
transactionAccountBalanceMap[transactionWithBalance.RelatedId] = transactionWithBalance
}
transactionResult, err := a.getTransactionResponseListResult(c, user, transactions, clientTimezone, false, true, true, true)
transactionResult, err := a.getTransactionResponseListResult(c, user, transactions, utcOffset, false, true, true, true)
if err != nil {
log.Errorf(c, "[transactions.TransactionReconciliationStatementHandler] failed to assemble transaction result for user \"uid:%d\", because %s", uid, err.Error())
@@ -490,27 +406,27 @@ func (a *TransactionsApi) TransactionStatisticsHandler(c *core.WebContext) (any,
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
clientTimezone, err := c.GetClientTimezone()
utcOffset, err := c.GetClientTimezoneOffset()
if err != nil {
log.Warnf(c, "[transactions.TransactionStatisticsHandler] cannot get client timezone, because %s", err.Error())
log.Warnf(c, "[transactions.TransactionStatisticsHandler] cannot get client timezone offset, because %s", err.Error())
return nil, errs.ErrClientTimezoneOffsetInvalid
}
noTags := statisticReq.TagFilter == models.TransactionNoTagFilterValue
var tagFilters []*models.TransactionTagFilter
var allTagIds []int64
noTags := statisticReq.TagIds == "none"
if !noTags {
tagFilters, err = models.ParseTransactionTagFilter(statisticReq.TagFilter)
allTagIds, err = a.transactionTags.GetTagIds(statisticReq.TagIds)
if err != nil {
log.Warnf(c, "[transactions.TransactionStatisticsHandler] parse transaction tag filters error, because %s", err.Error())
log.Warnf(c, "[transactions.TransactionStatisticsHandler] get transaction tag ids error, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
}
uid := c.GetCurrentUid()
totalAmounts, err := a.transactions.GetAccountsAndCategoriesTotalInflowAndOutflow(c, uid, statisticReq.StartTime, statisticReq.EndTime, tagFilters, noTags, statisticReq.Keyword, clientTimezone, statisticReq.UseTransactionTimezone)
totalAmounts, err := a.transactions.GetAccountsAndCategoriesTotalIncomeAndExpense(c, uid, statisticReq.StartTime, statisticReq.EndTime, allTagIds, noTags, statisticReq.TagFilterType, statisticReq.Keyword, utcOffset, statisticReq.UseTransactionTimezone)
if err != nil {
log.Errorf(c, "[transactions.TransactionStatisticsHandler] failed to get accounts and categories total income and expense for user \"uid:%d\", because %s", uid, err.Error())
@@ -531,11 +447,6 @@ func (a *TransactionsApi) TransactionStatisticsHandler(c *core.WebContext) (any,
AccountId: totalAmountItem.AccountId,
TotalAmount: totalAmountItem.Amount,
}
if totalAmountItem.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT || totalAmountItem.Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN {
statisticResp.Items[i].RelatedAccountId = totalAmountItem.RelatedAccountId
statisticResp.Items[i].RelatedAccountType, _ = totalAmountItem.Type.ToTransactionRelatedAccountType()
}
}
return statisticResp, nil
@@ -551,10 +462,10 @@ func (a *TransactionsApi) TransactionStatisticsTrendsHandler(c *core.WebContext)
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
clientTimezone, err := c.GetClientTimezone()
utcOffset, err := c.GetClientTimezoneOffset()
if err != nil {
log.Warnf(c, "[transactions.TransactionStatisticsTrendsHandler] cannot get client timezone, because %s", err.Error())
log.Warnf(c, "[transactions.TransactionStatisticsTrendsHandler] cannot get client timezone offset, because %s", err.Error())
return nil, errs.ErrClientTimezoneOffsetInvalid
}
@@ -565,20 +476,20 @@ func (a *TransactionsApi) TransactionStatisticsTrendsHandler(c *core.WebContext)
return nil, errs.Or(err, errs.ErrOperationFailed)
}
noTags := statisticTrendsReq.TagFilter == models.TransactionNoTagFilterValue
var tagFilters []*models.TransactionTagFilter
var allTagIds []int64
noTags := statisticTrendsReq.TagIds == "none"
if !noTags {
tagFilters, err = models.ParseTransactionTagFilter(statisticTrendsReq.TagFilter)
allTagIds, err = a.transactionTags.GetTagIds(statisticTrendsReq.TagIds)
if err != nil {
log.Warnf(c, "[transactions.TransactionStatisticsTrendsHandler] parse transaction tag filters error, because %s", err.Error())
log.Warnf(c, "[transactions.TransactionStatisticsTrendsHandler] get transaction tag ids error, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
}
uid := c.GetCurrentUid()
allMonthlyTotalAmounts, err := a.transactions.GetAccountsAndCategoriesMonthlyInflowAndOutflow(c, uid, startYear, startMonth, endYear, endMonth, tagFilters, noTags, statisticTrendsReq.Keyword, clientTimezone, statisticTrendsReq.UseTransactionTimezone)
allMonthlyTotalAmounts, err := a.transactions.GetAccountsAndCategoriesMonthlyIncomeAndExpense(c, uid, startYear, startMonth, endYear, endMonth, allTagIds, noTags, statisticTrendsReq.TagFilterType, statisticTrendsReq.Keyword, utcOffset, statisticTrendsReq.UseTransactionTimezone)
if err != nil {
log.Errorf(c, "[transactions.TransactionStatisticsTrendsHandler] failed to get accounts and categories total income and expense for user \"uid:%d\", because %s", uid, err.Error())
@@ -601,11 +512,6 @@ func (a *TransactionsApi) TransactionStatisticsTrendsHandler(c *core.WebContext)
AccountId: totalAmountItem.AccountId,
TotalAmount: totalAmountItem.Amount,
}
if totalAmountItem.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT || totalAmountItem.Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN {
monthlyStatisticResp.Items[i].RelatedAccountId = totalAmountItem.RelatedAccountId
monthlyStatisticResp.Items[i].RelatedAccountType, _ = totalAmountItem.Type.ToTransactionRelatedAccountType()
}
}
statisticTrendsResp = append(statisticTrendsResp, monthlyStatisticResp)
@@ -616,71 +522,6 @@ func (a *TransactionsApi) TransactionStatisticsTrendsHandler(c *core.WebContext)
return statisticTrendsResp, nil
}
// TransactionStatisticsAssetTrendsHandler returns transaction statistics asset trends of current user
func (a *TransactionsApi) TransactionStatisticsAssetTrendsHandler(c *core.WebContext) (any, *errs.Error) {
var statisticAssetTrendsReq models.TransactionStatisticAssetTrendsRequest
err := c.ShouldBindQuery(&statisticAssetTrendsReq)
if err != nil {
log.Warnf(c, "[transactions.TransactionStatisticsAssetTrendsHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
clientTimezone, err := c.GetClientTimezone()
if err != nil {
log.Warnf(c, "[transactions.TransactionStatisticsAssetTrendsHandler] cannot get client timezone, because %s", err.Error())
return nil, errs.ErrClientTimezoneOffsetInvalid
}
uid := c.GetCurrentUid()
maxTransactionTime := int64(0)
if statisticAssetTrendsReq.EndTime > 0 {
maxTransactionTime = utils.GetMaxTransactionTimeFromUnixTime(statisticAssetTrendsReq.EndTime)
}
minTransactionTime := int64(0)
if statisticAssetTrendsReq.StartTime > 0 {
minTransactionTime = utils.GetMinTransactionTimeFromUnixTime(statisticAssetTrendsReq.StartTime)
}
accountDailyBalances, err := a.transactions.GetAllAccountsDailyOpeningAndClosingBalance(c, uid, maxTransactionTime, minTransactionTime, clientTimezone)
if err != nil {
log.Errorf(c, "[transactions.TransactionStatisticsAssetTrendsHandler] failed to get transactions from \"%d\" to \"%d\" for user \"uid:%d\", because %s", statisticAssetTrendsReq.StartTime, statisticAssetTrendsReq.EndTime, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
statisticAssetTrendsResp := make(models.TransactionStatisticAssetTrendsResponseItemSlice, 0)
for yearMonthDay, dailyAccountBalances := range accountDailyBalances {
dailyStatisticResp := &models.TransactionStatisticAssetTrendsResponseItem{
Year: yearMonthDay / 10000,
Month: (yearMonthDay % 10000) / 100,
Day: yearMonthDay % 100,
Items: make([]*models.TransactionStatisticAssetTrendsResponseDataItem, len(dailyAccountBalances)),
}
for i := 0; i < len(dailyAccountBalances); i++ {
accountBalance := dailyAccountBalances[i]
dailyStatisticResp.Items[i] = &models.TransactionStatisticAssetTrendsResponseDataItem{
AccountId: accountBalance.AccountId,
AccountOpeningBalance: accountBalance.AccountOpeningBalance,
AccountClosingBalance: accountBalance.AccountClosingBalance,
}
}
statisticAssetTrendsResp = append(statisticAssetTrendsResp, dailyStatisticResp)
}
sort.Sort(statisticAssetTrendsResp)
return statisticAssetTrendsResp, nil
}
// TransactionAmountsHandler returns transaction amounts of current user
func (a *TransactionsApi) TransactionAmountsHandler(c *core.WebContext) (any, *errs.Error) {
var transactionAmountsReq models.TransactionAmountsRequest
@@ -727,10 +568,10 @@ func (a *TransactionsApi) TransactionAmountsHandler(c *core.WebContext) (any, *e
}
}
clientTimezone, err := c.GetClientTimezone()
utcOffset, err := c.GetClientTimezoneOffset()
if err != nil {
log.Warnf(c, "[transactions.TransactionAmountsHandler] cannot get client timezone, because %s", err.Error())
log.Warnf(c, "[transactions.TransactionAmountsHandler] cannot get client timezone offset, because %s", err.Error())
return nil, errs.ErrClientTimezoneOffsetInvalid
}
@@ -749,7 +590,7 @@ func (a *TransactionsApi) TransactionAmountsHandler(c *core.WebContext) (any, *e
for i := 0; i < len(requestItems); i++ {
requestItem := requestItems[i]
incomeAmounts, expenseAmounts, err := a.transactions.GetAccountsTotalIncomeAndExpense(c, uid, requestItem.StartTime, requestItem.EndTime, excludeAccountIds, excludeCategoryIds, clientTimezone, transactionAmountsReq.UseTransactionTimezone)
incomeAmounts, expenseAmounts, err := a.transactions.GetAccountsTotalIncomeAndExpense(c, uid, requestItem.StartTime, requestItem.EndTime, excludeAccountIds, excludeCategoryIds, utcOffset, transactionAmountsReq.UseTransactionTimezone)
if err != nil {
log.Errorf(c, "[transactions.TransactionAmountsHandler] failed to get transaction amounts item for user \"uid:%d\", because %s", uid, err.Error())
@@ -830,10 +671,10 @@ func (a *TransactionsApi) TransactionGetHandler(c *core.WebContext) (any, *errs.
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
clientTimezone, err := c.GetClientTimezone()
utcOffset, err := c.GetClientTimezoneOffset()
if err != nil {
log.Warnf(c, "[transactions.TransactionGetHandler] cannot get client timezone, because %s", err.Error())
log.Warnf(c, "[transactions.TransactionGetHandler] cannot get client timezone offset, because %s", err.Error())
return nil, errs.ErrClientTimezoneOffsetInvalid
}
@@ -919,7 +760,7 @@ func (a *TransactionsApi) TransactionGetHandler(c *core.WebContext) (any, *errs.
}
}
transactionEditable := transaction.IsEditable(user, clientTimezone, accountMap[transaction.AccountId], accountMap[transaction.RelatedAccountId])
transactionEditable := transaction.IsEditable(user, utcOffset, accountMap[transaction.AccountId], accountMap[transaction.RelatedAccountId])
transactionTagIds := allTransactionTagIds[transaction.TransactionId]
transactionResp := transaction.ToTransactionInfoResponse(transactionTagIds, transactionEditable)
@@ -960,13 +801,6 @@ func (a *TransactionsApi) TransactionCreateHandler(c *core.WebContext) (any, *er
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
clientTimezone, err := c.GetClientTimezone()
if err != nil {
log.Warnf(c, "[transactions.TransactionCreateHandler] cannot get client timezone, because %s", err.Error())
return nil, errs.ErrClientTimezoneOffsetInvalid
}
tagIds, err := utils.StringArrayToInt64Array(transactionCreateReq.TagIds)
if err != nil {
@@ -1024,7 +858,7 @@ func (a *TransactionsApi) TransactionCreateHandler(c *core.WebContext) (any, *er
}
transaction := a.createNewTransactionModel(uid, &transactionCreateReq, c.ClientIP())
transactionEditable := user.CanEditTransactionByTransactionTime(transaction.TransactionTime, clientTimezone)
transactionEditable := user.CanEditTransactionByTransactionTime(transaction.TransactionTime, transactionCreateReq.UtcOffset)
if !transactionEditable {
return nil, errs.ErrCannotCreateTransactionWithThisTransactionTime
@@ -1097,13 +931,6 @@ func (a *TransactionsApi) TransactionModifyHandler(c *core.WebContext) (any, *er
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
clientTimezone, err := c.GetClientTimezone()
if err != nil {
log.Warnf(c, "[transactions.TransactionModifyHandler] cannot get client timezone, because %s", err.Error())
return nil, errs.ErrClientTimezoneOffsetInvalid
}
tagIds, err := utils.StringArrayToInt64Array(transactionModifyReq.TagIds)
if err != nil {
@@ -1217,8 +1044,8 @@ func (a *TransactionsApi) TransactionModifyHandler(c *core.WebContext) (any, *er
return nil, errs.ErrNothingWillBeUpdated
}
transactionEditable := user.CanEditTransactionByTransactionTime(transaction.TransactionTime, clientTimezone)
newTransactionEditable := user.CanEditTransactionByTransactionTime(newTransaction.TransactionTime, clientTimezone)
transactionEditable := user.CanEditTransactionByTransactionTime(transaction.TransactionTime, transaction.TimezoneUtcOffset)
newTransactionEditable := user.CanEditTransactionByTransactionTime(newTransaction.TransactionTime, transactionModifyReq.UtcOffset)
if !transactionEditable || !newTransactionEditable {
return nil, errs.ErrCannotModifyTransactionWithThisTransactionTime
@@ -1287,63 +1114,6 @@ func (a *TransactionsApi) TransactionModifyHandler(c *core.WebContext) (any, *er
return newTransactionResp, nil
}
// TransactionMoveAllBetweenAccountsHandler moves all transactions from one account to another account for current user
func (a *TransactionsApi) TransactionMoveAllBetweenAccountsHandler(c *core.WebContext) (any, *errs.Error) {
var transactionMoveReq models.TransactionMoveBetweenAccountsRequest
err := c.ShouldBindJSON(&transactionMoveReq)
if err != nil {
log.Warnf(c, "[transactions.TransactionMoveAllBetweenAccountsHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
if transactionMoveReq.FromAccountId == transactionMoveReq.ToAccountId {
return nil, errs.ErrCannotMoveTransactionToSameAccount
}
uid := c.GetCurrentUid()
accountMap, err := a.accounts.GetAccountsByAccountIds(c, uid, []int64{transactionMoveReq.FromAccountId, transactionMoveReq.ToAccountId})
if err != nil {
log.Errorf(c, "[transactions.TransactionMoveAllBetweenAccountsHandler] failed to get accounts for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
fromAccount, exists := accountMap[transactionMoveReq.FromAccountId]
if !exists {
return nil, errs.ErrSourceAccountNotFound
}
toAccount, exists := accountMap[transactionMoveReq.ToAccountId]
if !exists {
return nil, errs.ErrDestinationAccountNotFound
}
if fromAccount.Hidden || toAccount.Hidden {
return nil, errs.ErrCannotMoveTransactionFromOrToHiddenAccount
}
if fromAccount.Type == models.ACCOUNT_TYPE_MULTI_SUB_ACCOUNTS || toAccount.Type == models.ACCOUNT_TYPE_MULTI_SUB_ACCOUNTS {
return nil, errs.ErrCannotMoveTransactionFromOrToParentAccount
}
if fromAccount.Currency != toAccount.Currency {
return nil, errs.ErrCannotMoveTransactionBetweenAccountsWithDifferentCurrencies
}
err = a.transactions.MoveAllTransactionsBetweenAccounts(c, uid, transactionMoveReq.FromAccountId, transactionMoveReq.ToAccountId)
if err != nil {
log.Errorf(c, "[transactions.TransactionMoveAllBetweenAccountsHandler] failed to move all transactions from account \"id:%d\" to account \"id:%d\" for user \"uid:%d\", because %s", transactionMoveReq.FromAccountId, transactionMoveReq.ToAccountId, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
log.Infof(c, "[transactions.TransactionMoveAllBetweenAccountsHandler] user \"uid:%d\" has moved all transactions from account \"id:%d\" to account \"id:%d\" successfully", uid, transactionMoveReq.FromAccountId, transactionMoveReq.ToAccountId)
return true, nil
}
// TransactionDeleteHandler deletes an existed transaction by request parameters for current user
func (a *TransactionsApi) TransactionDeleteHandler(c *core.WebContext) (any, *errs.Error) {
var transactionDeleteReq models.TransactionDeleteRequest
@@ -1354,10 +1124,10 @@ func (a *TransactionsApi) TransactionDeleteHandler(c *core.WebContext) (any, *er
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
clientTimezone, err := c.GetClientTimezone()
utcOffset, err := c.GetClientTimezoneOffset()
if err != nil {
log.Warnf(c, "[transactions.TransactionDeleteHandler] cannot get client timezone, because %s", err.Error())
log.Warnf(c, "[transactions.TransactionDeleteHandler] cannot get client timezone offset, because %s", err.Error())
return nil, errs.ErrClientTimezoneOffsetInvalid
}
@@ -1384,7 +1154,7 @@ func (a *TransactionsApi) TransactionDeleteHandler(c *core.WebContext) (any, *er
return nil, errs.ErrTransactionTypeInvalid
}
transactionEditable := user.CanEditTransactionByTransactionTime(transaction.TransactionTime, clientTimezone)
transactionEditable := user.CanEditTransactionByTransactionTime(transaction.TransactionTime, utcOffset)
if !transactionEditable {
return nil, errs.ErrCannotDeleteTransactionWithThisTransactionTime
@@ -1488,10 +1258,10 @@ func (a *TransactionsApi) TransactionParseImportFileHandler(c *core.WebContext)
return nil, errs.ErrParameterInvalid
}
clientTimezone, err := c.GetClientTimezone()
utcOffset, err := c.GetClientTimezoneOffset()
if err != nil {
log.Warnf(c, "[transactions.TransactionParseImportFileHandler] cannot get client timezone, because %s", err.Error())
log.Warnf(c, "[transactions.TransactionParseImportFileHandler] cannot get client timezone offset, because %s", err.Error())
return nil, errs.ErrClientTimezoneOffsetInvalid
}
@@ -1503,15 +1273,6 @@ func (a *TransactionsApi) TransactionParseImportFileHandler(c *core.WebContext)
fileType := fileTypes[0]
textualOptions := form.Value["options"]
textualOption := ""
if len(textualOptions) > 0 {
textualOption = textualOptions[0]
}
additionalOptions := converter.ParseImporterOptions(textualOption)
var dataImporter converter.TransactionDataImporter
if converters.IsCustomDelimiterSeparatedValuesFileType(fileType) {
@@ -1688,7 +1449,7 @@ func (a *TransactionsApi) TransactionParseImportFileHandler(c *core.WebContext)
tagMap := a.transactionTags.GetVisibleTagNameMapByList(tags)
parsedTransactions, _, _, _, _, _, err := dataImporter.ParseImportedData(c, user, fileData, clientTimezone, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
parsedTransactions, _, _, _, _, _, err := dataImporter.ParseImportedData(c, user, fileData, utcOffset, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
if err != nil {
log.Errorf(c, "[transactions.TransactionParseImportFileHandler] failed to parse imported data for user \"uid:%d\", because %s", user.Uid, err.Error())
@@ -1719,13 +1480,6 @@ func (a *TransactionsApi) TransactionImportHandler(c *core.WebContext) (any, *er
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
clientTimezone, err := c.GetClientTimezone()
if err != nil {
log.Warnf(c, "[transactions.TransactionImportHandler] cannot get client timezone, because %s", err.Error())
return nil, errs.ErrClientTimezoneOffsetInvalid
}
uid := c.GetCurrentUid()
if a.CurrentConfig().EnableDuplicateSubmissionsCheck && transactionImportReq.ClientSessionId != "" {
@@ -1811,7 +1565,7 @@ func (a *TransactionsApi) TransactionImportHandler(c *core.WebContext) (any, *er
for i := 0; i < len(transactionImportReq.Transactions); i++ {
transactionCreateReq := transactionImportReq.Transactions[i]
transaction := a.createNewTransactionModel(uid, transactionCreateReq, c.ClientIP())
transactionEditable := user.CanEditTransactionByTransactionTime(transaction.TransactionTime, clientTimezone)
transactionEditable := user.CanEditTransactionByTransactionTime(transaction.TransactionTime, transactionCreateReq.UtcOffset)
if !transactionEditable {
return nil, errs.ErrCannotCreateTransactionWithThisTransactionTime
@@ -1928,7 +1682,7 @@ func (a *TransactionsApi) getTransactionTagInfoResponses(tagIds []int64, allTran
return allTags
}
func (a *TransactionsApi) getTransactionResponseListResult(c *core.WebContext, user *models.User, transactions []*models.Transaction, clientTimezone *time.Location, withPictures bool, trimAccount bool, trimCategory bool, trimTag bool) (models.TransactionInfoResponseSlice, error) {
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) {
uid := user.Uid
transactionIds := make([]int64, len(transactions))
accountIds := make([]int64, 0, len(transactions)*2)
@@ -2007,7 +1761,7 @@ func (a *TransactionsApi) getTransactionResponseListResult(c *core.WebContext, u
transaction = a.transactions.GetRelatedTransferTransaction(transaction)
}
transactionEditable := transaction.IsEditable(user, clientTimezone, allAccounts[transaction.AccountId], allAccounts[transaction.RelatedAccountId])
transactionEditable := transaction.IsEditable(user, utcOffset, allAccounts[transaction.AccountId], allAccounts[transaction.RelatedAccountId])
transactionTagIds := allTransactionTagIds[transaction.TransactionId]
result[i] = transaction.ToTransactionInfoResponse(transactionTagIds, transactionEditable)
+1 -2
View File
@@ -85,7 +85,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorEnableRequestHandler(c *core.WebCo
return nil, errs.ErrNotPermittedToPerformThisAction
}
key, err := a.twoFactorAuthorizations.GenerateTwoFactorSecret(c, user, c.GetClientLocale())
key, err := a.twoFactorAuthorizations.GenerateTwoFactorSecret(c, user)
if err != nil {
log.Errorf(c, "[twofactor_authorizations.TwoFactorEnableRequestHandler] failed to generate two-factor secret, because %s", err.Error())
@@ -205,7 +205,6 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorEnableConfirmHandler(c *core.WebCo
c.SetTextualToken(token)
c.SetTokenClaims(claims)
c.SetTokenContext("")
log.Infof(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] user \"uid:%d\" token refreshed, new token will be expired at %d", user.Uid, claims.ExpiresAt)
-108
View File
@@ -1,108 +0,0 @@
package api
import (
"sort"
"github.com/mayswind/ezbookkeeping/pkg/auth/oauth2"
"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"
)
// UserExternalAuthsApi represents user external auth api
type UserExternalAuthsApi struct {
users *services.UserService
userExternalAuths *services.UserExternalAuthService
}
// Initialize a user external auth api singleton instance
var (
UserExternalAuths = &UserExternalAuthsApi{
users: services.Users,
userExternalAuths: services.UserExternalAuths,
}
)
// ExternalAuthListHanlder returns external authentications list of current user
func (a *UserExternalAuthsApi) ExternalAuthListHanlder(c *core.WebContext) (any, *errs.Error) {
uid := c.GetCurrentUid()
userExternalAuths, err := a.userExternalAuths.GetUserAllExternalAuthsByUid(c, uid)
if err != nil {
log.Errorf(c, "[user_external_auths.ExternalAuthListHanlder] failed to get all external authentications for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
userExternalAuthResps := make(models.UserExternalAuthInfoResponsesSlice, 0, len(userExternalAuths)+1)
currentExternalAuthType := oauth2.GetExternalUserAuthType()
hasCurrentExternalAuth := false
for i := 0; i < len(userExternalAuths); i++ {
userExternalAuth := userExternalAuths[i]
if userExternalAuth.ExternalAuthType == currentExternalAuthType {
hasCurrentExternalAuth = true
}
userExternalAuthResps = append(userExternalAuthResps, userExternalAuth.ToUserExternalAuthInfoResponse())
}
if !hasCurrentExternalAuth {
userExternalAuthResps = append(userExternalAuthResps, &models.UserExternalAuthInfoResponse{
ExternalAuthCategory: currentExternalAuthType.GetCategory(),
ExternalAuthType: currentExternalAuthType,
Linked: false,
})
}
sort.Sort(userExternalAuthResps)
return userExternalAuthResps, nil
}
// UnlinkExternalAuthHandler unlinks external authentication for current user
func (a *UserExternalAuthsApi) UnlinkExternalAuthHandler(c *core.WebContext) (any, *errs.Error) {
var externalAuthLinkReq models.UserExternalAuthUnlinkRequest
err := c.ShouldBindJSON(&externalAuthLinkReq)
if err != nil {
log.Warnf(c, "[user_external_auths.UnlinkExternalAuthHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(c, uid)
if err != nil {
if !errs.IsCustomError(err) {
log.Warnf(c, "[user_external_auths.UnlinkExternalAuthHandler] failed to get user for user \"uid:%d\", because %s", uid, err.Error())
}
return nil, errs.ErrUserNotFound
}
if !a.users.IsPasswordEqualsUserPassword(externalAuthLinkReq.Password, user) {
return nil, errs.ErrUserPasswordWrong
}
if user.FeatureRestriction.Contains(core.USER_FEATURE_RESTRICTION_TYPE_UNLINK_THIRD_PARTY_LOGIN) {
return nil, errs.ErrNotPermittedToPerformThisAction
}
externalAuthType := core.UserExternalAuthType(externalAuthLinkReq.ExternalAuthType)
if !externalAuthType.IsValid() {
return nil, errs.ErrUserExternalAuthNotFound
}
err = a.userExternalAuths.DeleteUserExternalAuth(c, uid, externalAuthType)
if err != nil {
log.Errorf(c, "[user_external_auths.UnlinkExternalAuthHandler] failed to unlink external authentication \"%s\" for user \"uid:%d\", because %s", externalAuthLinkReq.ExternalAuthType, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
return true, nil
}
+3 -6
View File
@@ -83,7 +83,7 @@ func (a *UsersApi) UserRegisterHandler(c *core.WebContext) (any, *errs.Error) {
FeatureRestriction: a.CurrentConfig().DefaultFeatureRestrictions,
}
err = a.users.CreateUser(c, user, false)
err = a.users.CreateUser(c, user)
if err != nil {
log.Errorf(c, "[users.UserRegisterHandler] failed to create user \"%s\", because %s", user.Username, err.Error())
@@ -142,9 +142,8 @@ func (a *UsersApi) UserRegisterHandler(c *core.WebContext) (any, *errs.Error) {
authResp.Token = token
c.SetTextualToken(token)
c.SetTokenClaims(claims)
c.SetTokenContext("")
log.Infof(c, "[users.UserRegisterHandler] user \"uid:%d\" has logged in, token will be expired at %d", user.Uid, claims.ExpiresAt)
log.Infof(c, "[users.UserRegisterHandler] user \"uid:%d\" has logined, token will be expired at %d", user.Uid, claims.ExpiresAt)
return authResp, nil
}
@@ -206,7 +205,6 @@ func (a *UsersApi) UserEmailVerifyHandler(c *core.WebContext) (any, *errs.Error)
c.SetTextualToken(token)
c.SetTokenClaims(claims)
c.SetTokenContext("")
log.Infof(c, "[users.UserEmailVerifyHandler] user \"uid:%d\" token created, new token will be expired at %d", user.Uid, claims.ExpiresAt)
}
@@ -277,7 +275,7 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.WebContext) (any, *errs.Erro
return nil, errs.ErrNotPermittedToPerformThisAction
}
if user.Password != "" && !a.users.IsPasswordEqualsUserPassword(userUpdateReq.OldPassword, user) {
if !a.users.IsPasswordEqualsUserPassword(userUpdateReq.OldPassword, user) {
return nil, errs.ErrUserPasswordWrong
}
@@ -590,7 +588,6 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.WebContext) (any, *errs.Erro
resp.NewToken = token
c.SetTextualToken(token)
c.SetTokenClaims(claims)
c.SetTokenContext("")
log.Infof(c, "[users.UserUpdateProfileHandler] user \"uid:%d\" token refreshed, new token will be expired at %d", user.Uid, claims.ExpiresAt)
-13
View File
@@ -1,13 +0,0 @@
package data
import "github.com/mayswind/ezbookkeeping/pkg/core"
// OAuth2UserInfo represents the user info retrieved from OAuth 2.0 provider
type OAuth2UserInfo struct {
UserName string
Email string
NickName string
LanguageCode string
CurrencyCode string
FirstDayOfWeek core.WeekDay
}
-122
View File
@@ -1,122 +0,0 @@
package oauth2
import (
"net/http"
"golang.org/x/oauth2"
"github.com/mayswind/ezbookkeeping/pkg/auth/oauth2/data"
"github.com/mayswind/ezbookkeeping/pkg/auth/oauth2/provider"
"github.com/mayswind/ezbookkeeping/pkg/auth/oauth2/provider/gitea"
"github.com/mayswind/ezbookkeeping/pkg/auth/oauth2/provider/github"
"github.com/mayswind/ezbookkeeping/pkg/auth/oauth2/provider/nextcloud"
"github.com/mayswind/ezbookkeeping/pkg/auth/oauth2/provider/oidc"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/httpclient"
"github.com/mayswind/ezbookkeeping/pkg/settings"
)
// OAuth2Container contains the current OAuth 2.0 authentication provider
type OAuth2Container struct {
current provider.OAuth2Provider
usePKCE bool
oauth2HttpClient *http.Client
externalUserAuthType core.UserExternalAuthType
}
// Initialize a OAuth 2.0 container singleton instance
var (
Container = &OAuth2Container{}
)
// InitializeOAuth2Provider initializes the current OAuth 2.0 provider according to the config
func InitializeOAuth2Provider(config *settings.Config) error {
if !config.EnableOAuth2Login {
return nil
}
if config.OAuth2ClientID == "" || config.OAuth2ClientSecret == "" || config.OAuth2UserIdentifier == "" || config.OAuth2Provider == "" {
return errs.ErrInvalidOAuth2Config
}
var err error
var oauth2Provider provider.OAuth2Provider
var externalUserAuthType core.UserExternalAuthType
redirectUrl := config.RootUrl + "oauth2/callback"
if config.OAuth2Provider == settings.OAuth2ProviderOIDC {
oauth2Provider, err = oidc.NewOIDCProvider(config, redirectUrl)
externalUserAuthType = core.USER_EXTERNAL_AUTH_TYPE_OAUTH2_OIDC
} else if config.OAuth2Provider == settings.OAuth2ProviderNextcloud {
oauth2Provider, err = nextcloud.NewNextcloudOAuth2Provider(config, redirectUrl)
externalUserAuthType = core.USER_EXTERNAL_AUTH_TYPE_OAUTH2_NEXTCLOUD
} else if config.OAuth2Provider == settings.OAuth2ProviderGitea {
oauth2Provider, err = gitea.NewGiteaOAuth2Provider(config, redirectUrl)
externalUserAuthType = core.USER_EXTERNAL_AUTH_TYPE_OAUTH2_GITEA
} else if config.OAuth2Provider == settings.OAuth2ProviderGithub {
oauth2Provider, err = github.NewGithubOAuth2Provider(config, redirectUrl)
externalUserAuthType = core.USER_EXTERNAL_AUTH_TYPE_OAUTH2_GITHUB
} else {
return errs.ErrInvalidOAuth2Provider
}
if err != nil {
return err
}
Container.current = oauth2Provider
Container.usePKCE = config.OAuth2UsePKCE
Container.oauth2HttpClient = httpclient.NewHttpClient(config.OAuth2RequestTimeout, config.OAuth2Proxy, config.OAuth2SkipTLSVerify, settings.GetUserAgent(), config.EnableDebugLog)
Container.externalUserAuthType = externalUserAuthType
return nil
}
// GetOAuth2AuthUrl returns the OAuth 2.0 authentication url
func GetOAuth2AuthUrl(c core.Context, state string, verifier string) (string, error) {
if Container.current == nil {
return "", errs.ErrOAuth2NotEnabled
}
var opts []oauth2.AuthCodeOption
if Container.usePKCE {
opts = append(opts, oauth2.S256ChallengeOption(verifier))
}
return Container.current.GetOAuth2AuthUrl(wrapOAuth2Context(c, Container.oauth2HttpClient), state, opts...)
}
// GetOAuth2Token exchanges the authorization code for an OAuth 2.0 token
func GetOAuth2Token(c core.Context, code string, verifier string) (*oauth2.Token, error) {
if Container.current == nil || Container.oauth2HttpClient == nil {
return nil, errs.ErrOAuth2NotEnabled
}
var opts []oauth2.AuthCodeOption
if Container.usePKCE {
opts = append(opts, oauth2.VerifierOption(verifier))
}
return Container.current.GetOAuth2Token(wrapOAuth2Context(c, Container.oauth2HttpClient), code, opts...)
}
// GetOAuth2UserInfo retrieves the OAuth 2.0 user info using the provided OAuth 2.0 token
func GetOAuth2UserInfo(c core.Context, token *oauth2.Token) (*data.OAuth2UserInfo, error) {
if Container.current == nil || Container.oauth2HttpClient == nil {
return nil, errs.ErrOAuth2NotEnabled
}
if token == nil {
return nil, errs.ErrInvalidOAuth2Token
}
return Container.current.GetUserInfo(wrapOAuth2Context(c, Container.oauth2HttpClient), token)
}
// GetExternalUserAuthType returns the external user auth type of the current OAuth 2.0 provider
func GetExternalUserAuthType() core.UserExternalAuthType {
return Container.externalUserAuthType
}
-31
View File
@@ -1,31 +0,0 @@
package oauth2
import (
"net/http"
"golang.org/x/oauth2"
"github.com/mayswind/ezbookkeeping/pkg/core"
)
// OAuth2Context represents the context for OAuth 2.0 operations
type OAuth2Context struct {
core.Context
httpClient *http.Client
}
// Value returns the value associated with key
func (c *OAuth2Context) Value(key any) any {
if key == oauth2.HTTPClient {
return c.httpClient
}
return c.Context.Value(key)
}
func wrapOAuth2Context(ctx core.Context, httpClient *http.Client) core.Context {
return &OAuth2Context{
Context: ctx,
httpClient: httpClient,
}
}
@@ -1,108 +0,0 @@
package common
import (
"io"
"net/http"
"golang.org/x/oauth2"
"github.com/mayswind/ezbookkeeping/pkg/auth/oauth2/data"
"github.com/mayswind/ezbookkeeping/pkg/auth/oauth2/provider"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/httpclient"
"github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/settings"
)
// CommonOAuth2Provider represents common OAuth 2.0 provider
type CommonOAuth2Provider struct {
provider.OAuth2Provider
oauth2Config *oauth2.Config
dataSource CommonOAuth2DataSource
}
// CommonOAuth2DataSource defines the structure of OAuth 2.0 data source
type CommonOAuth2DataSource interface {
// GetAuthUrl returns the authentication url of the data source
GetAuthUrl() string
// GetTokenUrl returns the token url of the data source
GetTokenUrl() string
// GetUserInfoRequest returns the user info request of the data source
GetUserInfoRequest() (*http.Request, error)
// GetScopes returns the scopes required by the data source
GetScopes() []string
// ParseUserInfo returns the user info by parsing the response body
ParseUserInfo(c core.Context, body []byte) (*data.OAuth2UserInfo, error)
}
// GetOAuth2AuthUrl returns the authentication url of the common OAuth 2.0 provider
func (p *CommonOAuth2Provider) GetOAuth2AuthUrl(c core.Context, state string, opts ...oauth2.AuthCodeOption) (string, error) {
return p.oauth2Config.AuthCodeURL(state, opts...), nil
}
// GetOAuth2Token returns the OAuth 2.0 token of the common OAuth 2.0 provider
func (p *CommonOAuth2Provider) GetOAuth2Token(c core.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) {
return p.oauth2Config.Exchange(c, code, opts...)
}
// GetUserInfo returns the user info by the common OAuth 2.0 provider
func (p *CommonOAuth2Provider) GetUserInfo(c core.Context, oauth2Token *oauth2.Token) (*data.OAuth2UserInfo, error) {
req, err := p.dataSource.GetUserInfoRequest()
if err != nil {
log.Errorf(c, "[common_oauth2_provider.GetUserInfo] failed to get user info request, because %s", err.Error())
return nil, errs.ErrFailedToRequestRemoteApi
}
oauth2Client := oauth2.NewClient(c, oauth2.StaticTokenSource(oauth2Token))
req = req.WithContext(httpclient.CustomHttpResponseLog(c, func(data []byte) {
log.Debugf(c, "[common_oauth2_provider.GetUserInfo] response is %s", data)
}))
resp, err := oauth2Client.Do(req)
if err != nil {
log.Errorf(c, "[common_oauth2_provider.GetUserInfo] failed to get user info response, because %s", err.Error())
return nil, errs.ErrFailedToRequestRemoteApi
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if resp.StatusCode != 200 {
log.Errorf(c, "[common_oauth2_provider.GetUserInfo] failed to get user info response, because response code is %d", resp.StatusCode)
return nil, errs.ErrFailedToRequestRemoteApi
}
return p.dataSource.ParseUserInfo(c, body)
}
// GetDataSource returns the data source of the common OAuth 2.0 provider
func (p *CommonOAuth2Provider) GetDataSource() CommonOAuth2DataSource {
return p.dataSource
}
// NewCommonOAuth2Provider returns a new common OAuth 2.0 provider
func NewCommonOAuth2Provider(config *settings.Config, redirectUrl string, dataSource CommonOAuth2DataSource) *CommonOAuth2Provider {
oauth2Config := &oauth2.Config{
ClientID: config.OAuth2ClientID,
ClientSecret: config.OAuth2ClientSecret,
Endpoint: oauth2.Endpoint{
AuthURL: dataSource.GetAuthUrl(),
TokenURL: dataSource.GetTokenUrl(),
},
RedirectURL: redirectUrl,
Scopes: dataSource.GetScopes(),
}
return &CommonOAuth2Provider{
oauth2Config: oauth2Config,
dataSource: dataSource,
}
}
@@ -1,95 +0,0 @@
package gitea
import (
"encoding/json"
"net/http"
"github.com/mayswind/ezbookkeeping/pkg/auth/oauth2/data"
"github.com/mayswind/ezbookkeeping/pkg/auth/oauth2/provider"
"github.com/mayswind/ezbookkeeping/pkg/auth/oauth2/provider/common"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/settings"
)
type giteaUserInfoResponse struct {
Login string `json:"login"`
FullName string `json:"full_name"`
Email string `json:"email"`
}
// GiteaOAuth2DataSource represents Gitea OAuth 2.0 data source
type GiteaOAuth2DataSource struct {
common.CommonOAuth2DataSource
baseUrl string
}
// GetAuthUrl returns the authentication url of the Gitea data source
func (s *GiteaOAuth2DataSource) GetAuthUrl() string {
// Reference: https://docs.gitea.com/development/oauth2-provider
return s.baseUrl + "login/oauth/authorize"
}
// GetTokenUrl returns the token url of the Gitea data source
func (s *GiteaOAuth2DataSource) GetTokenUrl() string {
// Reference: https://docs.gitea.com/development/oauth2-provider
return s.baseUrl + "login/oauth/access_token"
}
// GetUserInfoRequest returns the user info request of the Gitea data source
func (s *GiteaOAuth2DataSource) GetUserInfoRequest() (*http.Request, error) {
// Reference: https://gitea.com/api/swagger#/user/userGetCurrent
req, err := http.NewRequest("GET", s.baseUrl+"api/v1/user", nil)
if err != nil {
return nil, err
}
req.Header.Set("Accept", "application/json")
return req, nil
}
// GetScopes returns the scopes required by the Gitea provider
func (s *GiteaOAuth2DataSource) GetScopes() []string {
return []string{"read:user"}
}
// ParseUserInfo returns the user info by parsing the response body
func (s *GiteaOAuth2DataSource) ParseUserInfo(c core.Context, body []byte) (*data.OAuth2UserInfo, error) {
userInfoResp := &giteaUserInfoResponse{}
err := json.Unmarshal(body, &userInfoResp)
if err != nil {
log.Warnf(c, "[gitea_oauth2_datasource.ParseUserInfo] failed to parse user profile response body, because %s", err.Error())
return nil, errs.ErrCannotRetrieveUserInfo
}
if userInfoResp.Login == "" {
log.Warnf(c, "[gitea_oauth2_datasource.ParseUserInfo] invalid user profile response body")
return nil, errs.ErrCannotRetrieveUserInfo
}
return &data.OAuth2UserInfo{
UserName: userInfoResp.Login,
Email: userInfoResp.Email,
NickName: userInfoResp.FullName,
}, nil
}
// NewGiteaOAuth2Provider creates a new Gitea OAuth 2.0 provider instance
func NewGiteaOAuth2Provider(config *settings.Config, redirectUrl string) (provider.OAuth2Provider, error) {
if len(config.OAuth2GiteaBaseUrl) < 1 {
return nil, errs.ErrInvalidOAuth2Config
}
baseUrl := config.OAuth2GiteaBaseUrl
if baseUrl[len(baseUrl)-1] != '/' {
baseUrl += "/"
}
return common.NewCommonOAuth2Provider(config, redirectUrl, &GiteaOAuth2DataSource{
baseUrl: baseUrl,
}), nil
}
@@ -1,71 +0,0 @@
package gitea
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/mayswind/ezbookkeeping/pkg/auth/oauth2/provider/common"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/settings"
)
func TestNewGiteaOAuth2Provider(t *testing.T) {
provider, err := NewGiteaOAuth2Provider(&settings.Config{
OAuth2GiteaBaseUrl: "https://example.com/",
}, "")
assert.Nil(t, err)
assert.Equal(t, "https://example.com/login/oauth/authorize", provider.(*common.CommonOAuth2Provider).GetDataSource().GetAuthUrl())
assert.Equal(t, "https://example.com/login/oauth/access_token", provider.(*common.CommonOAuth2Provider).GetDataSource().GetTokenUrl())
provider, err = NewGiteaOAuth2Provider(&settings.Config{
OAuth2GiteaBaseUrl: "https://example.com",
}, "")
assert.Nil(t, err)
assert.Equal(t, "https://example.com/login/oauth/authorize", provider.(*common.CommonOAuth2Provider).GetDataSource().GetAuthUrl())
assert.Equal(t, "https://example.com/login/oauth/access_token", provider.(*common.CommonOAuth2Provider).GetDataSource().GetTokenUrl())
provider, err = NewGiteaOAuth2Provider(&settings.Config{}, "")
assert.Equal(t, errs.ErrInvalidOAuth2Config, err)
}
func TestGiteaOAuth2Datasource_GetUserInfoRequest(t *testing.T) {
datasource := &GiteaOAuth2DataSource{baseUrl: "https://example.com/"}
req, err := datasource.GetUserInfoRequest()
assert.Nil(t, err)
assert.Equal(t, "GET", req.Method)
assert.Equal(t, "https://example.com/api/v1/user", req.URL.String())
assert.Equal(t, "application/json", req.Header.Get("Accept"))
}
func TestGiteaOAuth2Datasource_ParseUserInfo_Success(t *testing.T) {
datasource := &GiteaOAuth2DataSource{}
responseContent := `{
"login": "user1",
"full_name": "User",
"email": "user1@example.com"
}`
info, err := datasource.ParseUserInfo(core.NewNullContext(), []byte(responseContent))
assert.Nil(t, err)
assert.Equal(t, "user1", info.UserName)
assert.Equal(t, "user1@example.com", info.Email)
assert.Equal(t, "User", info.NickName)
}
func TestGiteaOAuth2Datasource_ParseUserInfo_InvalidJson(t *testing.T) {
datasource := &GiteaOAuth2DataSource{}
_, err := datasource.ParseUserInfo(core.NewNullContext(), []byte("invalid"))
assert.Equal(t, errs.ErrCannotRetrieveUserInfo, err)
}
func TestGiteaOAuth2Datasource_ParseUserInfo_EmptyLogin(t *testing.T) {
datasource := &GiteaOAuth2DataSource{}
responseContent := `{"login": ""}`
_, err := datasource.ParseUserInfo(core.NewNullContext(), []byte(responseContent))
assert.Equal(t, errs.ErrCannotRetrieveUserInfo, err)
}
@@ -1,193 +0,0 @@
package github
import (
"encoding/json"
"io"
"net/http"
"golang.org/x/oauth2"
"github.com/mayswind/ezbookkeeping/pkg/auth/oauth2/data"
"github.com/mayswind/ezbookkeeping/pkg/auth/oauth2/provider"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/httpclient"
"github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/settings"
)
const githubOAuth2AuthUrl = "https://github.com/login/oauth/authorize" // Reference: https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps
const githubOAuth2TokenUrl = "https://github.com/login/oauth/access_token" // Reference: https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps
const githubUserProfileApiUrl = "https://api.github.com/user" // Reference: https://docs.github.com/en/rest/users/users
const githubUserEmailApiUrl = "https://api.github.com/user/emails" // Reference: https://docs.github.com/en/rest/users/emails
var githubOAuth2Scopes = []string{"user:email"}
type githubUserProfileResponse struct {
Login string `json:"login"`
Name string `json:"name"`
Email string `json:"email"`
}
type githubUserEmailsResponse struct {
Email string `json:"email"`
Primary bool `json:"primary"`
Verified bool `json:"verified"`
}
// GithubOAuth2Provider represents Github OAuth 2.0 provider
type GithubOAuth2Provider struct {
provider.OAuth2Provider
oauth2Config *oauth2.Config
}
// GetOAuth2AuthUrl returns the authentication url of the GitHub OAuth 2.0 provider
func (p *GithubOAuth2Provider) GetOAuth2AuthUrl(c core.Context, state string, opts ...oauth2.AuthCodeOption) (string, error) {
return p.oauth2Config.AuthCodeURL(state, opts...), nil
}
// GetOAuth2Token returns the OAuth 2.0 token of the GitHub OAuth 2.0 provider
func (p *GithubOAuth2Provider) GetOAuth2Token(c core.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) {
return p.oauth2Config.Exchange(c, code, opts...)
}
// GetUserInfo returns the user info by the Github OAuth 2.0 provider
func (p *GithubOAuth2Provider) GetUserInfo(c core.Context, oauth2Token *oauth2.Token) (*data.OAuth2UserInfo, error) {
// first get user name and nick name from user profile
req, err := p.buildAPIRequest(githubUserProfileApiUrl)
if err != nil {
log.Errorf(c, "[github_oauth2_provider.GetUserInfo] failed to get user info request, because %s", err.Error())
return nil, errs.ErrFailedToRequestRemoteApi
}
oauth2Client := oauth2.NewClient(c, oauth2.StaticTokenSource(oauth2Token))
req = req.WithContext(httpclient.CustomHttpResponseLog(c, func(data []byte) {
log.Debugf(c, "[github_oauth2_provider.GetUserInfo] user profile response is %s", data)
}))
resp, err := oauth2Client.Do(req)
if err != nil {
log.Errorf(c, "[github_oauth2_provider.GetUserInfo] failed to get user info response, because %s", err.Error())
return nil, errs.ErrFailedToRequestRemoteApi
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if resp.StatusCode != 200 {
log.Errorf(c, "[github_oauth2_provider.GetUserInfo] failed to get user info response, because response code is %d", resp.StatusCode)
return nil, errs.ErrFailedToRequestRemoteApi
}
userProfileResp, err := p.parseUserProfile(c, body)
if err != nil {
return nil, err
}
// then get user primary email
req, err = p.buildAPIRequest(githubUserEmailApiUrl)
if err != nil {
log.Errorf(c, "[github_oauth2_provider.GetUserInfo] failed to get user emails request, because %s", err.Error())
return nil, errs.ErrFailedToRequestRemoteApi
}
req = req.WithContext(httpclient.CustomHttpResponseLog(c, func(data []byte) {
log.Debugf(c, "[github_oauth2_provider.GetUserInfo] user emails response is %s", data)
}))
resp, err = oauth2Client.Do(req)
if err != nil {
log.Errorf(c, "[github_oauth2_provider.GetUserInfo] failed to get user emails response, because %s", err.Error())
return nil, errs.ErrFailedToRequestRemoteApi
}
defer resp.Body.Close()
body, err = io.ReadAll(resp.Body)
if resp.StatusCode != 200 {
log.Errorf(c, "[github_oauth2_provider.GetUserInfo] failed to get user emails response, because response code is %d", resp.StatusCode)
return nil, errs.ErrFailedToRequestRemoteApi
}
email, err := p.parsePrimaryEmail(c, body)
if err != nil {
return nil, err
}
return &data.OAuth2UserInfo{
UserName: userProfileResp.Login,
Email: email,
NickName: userProfileResp.Name,
}, nil
}
func (p *GithubOAuth2Provider) parseUserProfile(c core.Context, body []byte) (*githubUserProfileResponse, error) {
userProfileResp := &githubUserProfileResponse{}
err := json.Unmarshal(body, &userProfileResp)
if err != nil {
log.Warnf(c, "[github_oauth2_provider.parseUserProfile] failed to parse user profile response body, because %s", err.Error())
return nil, errs.ErrCannotRetrieveUserInfo
}
if userProfileResp.Login == "" {
log.Warnf(c, "[github_oauth2_provider.parseUserProfile] invalid user profile response body")
return nil, errs.ErrCannotRetrieveUserInfo
}
return userProfileResp, nil
}
func (p *GithubOAuth2Provider) parsePrimaryEmail(c core.Context, body []byte) (string, error) {
emailsResp := make([]githubUserEmailsResponse, 0)
err := json.Unmarshal(body, &emailsResp)
if err != nil {
log.Warnf(c, "[github_oauth2_provider.parsePrimaryEmail] failed to parse user emails response body, because %s", err.Error())
return "", errs.ErrCannotRetrieveUserInfo
}
for _, emailEntry := range emailsResp {
if emailEntry.Primary && emailEntry.Verified {
return emailEntry.Email, nil
}
}
return "", nil
}
func (p *GithubOAuth2Provider) buildAPIRequest(url string) (*http.Request, error) {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
req.Header.Set("Accept", "application/vnd.github+json")
return req, nil
}
// NewGithubOAuth2Provider creates a new Github OAuth 2.0 provider instance
func NewGithubOAuth2Provider(config *settings.Config, redirectUrl string) (provider.OAuth2Provider, error) {
oauth2Config := &oauth2.Config{
ClientID: config.OAuth2ClientID,
ClientSecret: config.OAuth2ClientSecret,
Endpoint: oauth2.Endpoint{
AuthURL: githubOAuth2AuthUrl,
TokenURL: githubOAuth2TokenUrl,
},
RedirectURL: redirectUrl,
Scopes: githubOAuth2Scopes,
}
return &GithubOAuth2Provider{
oauth2Config: oauth2Config,
}, nil
}
@@ -1,95 +0,0 @@
package github
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
)
func TestGithubOAuth2Datasource_ParseUserProfile_Success(t *testing.T) {
datasource := &GithubOAuth2Provider{}
responseContent := `{
"login": "octocat",
"id": 1,
"node_id": "MDQ6VXNlcjE=",
"avatar_url": "https://github.com/images/error/octocat_happy.gif",
"gravatar_id": "",
"url": "https://api.github.com/users/octocat",
"html_url": "https://github.com/octocat",
"followers_url": "https://api.github.com/users/octocat/followers",
"following_url": "https://api.github.com/users/octocat/following{/other_user}",
"gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
"starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
"organizations_url": "https://api.github.com/users/octocat/orgs",
"repos_url": "https://api.github.com/users/octocat/repos",
"events_url": "https://api.github.com/users/octocat/events{/privacy}",
"received_events_url": "https://api.github.com/users/octocat/received_events",
"type": "User",
"site_admin": false,
"name": "monalisa octocat",
"company": "GitHub",
"blog": "https://github.com/blog",
"location": "San Francisco",
"email": "octocat@github.com",
"hireable": false,
"bio": "There once was...",
"twitter_username": "monatheoctocat",
"public_repos": 2,
"public_gists": 1,
"followers": 20,
"following": 0,
"created_at": "2008-01-14T04:33:35Z",
"updated_at": "2008-01-14T04:33:35Z",
"private_gists": 81,
"total_private_repos": 100,
"owned_private_repos": 100,
"disk_usage": 10000,
"collaborators": 8,
"two_factor_authentication": true,
"plan": {
"name": "Medium",
"space": 400,
"private_repos": 20,
"collaborators": 0
}
}`
info, err := datasource.parseUserProfile(core.NewNullContext(), []byte(responseContent))
assert.Nil(t, err)
assert.Equal(t, "octocat", info.Login)
assert.Equal(t, "monalisa octocat", info.Name)
}
func TestGithubOAuth2Datasource_ParseUserProfile_EmptyLogin(t *testing.T) {
datasource := &GithubOAuth2Provider{}
responseContent := `{"login": ""}`
_, err := datasource.parseUserProfile(core.NewNullContext(), []byte(responseContent))
assert.Equal(t, errs.ErrCannotRetrieveUserInfo, err)
}
func TestGithubOAuth2Datasource_ParsePrimaryEmail(t *testing.T) {
datasource := &GithubOAuth2Provider{}
responseContent := `[
{
"email": "foo@bar.com",
"primary": false,
"verified": true,
"visibility": null
},
{
"email": "octocat@github.com",
"primary": true,
"verified": true,
"visibility": "public"
}
]`
email, err := datasource.parsePrimaryEmail(core.NewNullContext(), []byte(responseContent))
assert.Nil(t, err)
assert.Equal(t, "octocat@github.com", email)
}
@@ -1,114 +0,0 @@
package nextcloud
import (
"encoding/json"
"net/http"
"github.com/mayswind/ezbookkeeping/pkg/auth/oauth2/data"
"github.com/mayswind/ezbookkeeping/pkg/auth/oauth2/provider"
"github.com/mayswind/ezbookkeeping/pkg/auth/oauth2/provider/common"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/settings"
)
type nextcloudUserInfoResponse struct {
OCS *struct {
Meta *struct {
Status string `json:"status"`
StatusCode int `json:"statuscode"`
} `json:"meta"`
Data *struct {
ID string `json:"id"`
Email string `json:"email"`
DisplayName string `json:"display-name"`
} `json:"data"`
} `json:"ocs"`
}
// NextcloudOAuth2DataSource represents Nextcloud OAuth 2.0 data source
type NextcloudOAuth2DataSource struct {
common.CommonOAuth2DataSource
baseUrl string
}
// GetAuthUrl returns the authentication url of the Nextcloud data source
func (s *NextcloudOAuth2DataSource) GetAuthUrl() string {
// Reference: https://docs.nextcloud.com/server/stable/developer_manual/_static/openapi.html#/operations/oauth2-login_redirector-authorize
return s.baseUrl + "apps/oauth2/authorize"
}
// GetTokenUrl returns the token url of the Nextcloud data source
func (s *NextcloudOAuth2DataSource) GetTokenUrl() string {
// Reference: https://docs.nextcloud.com/server/stable/developer_manual/_static/openapi.html#/operations/oauth2-oauth_api-get-token
return s.baseUrl + "apps/oauth2/api/v1/token"
}
// GetUserInfoRequest returns the user info request of the Nextcloud data source
func (s *NextcloudOAuth2DataSource) GetUserInfoRequest() (*http.Request, error) {
// Reference: https://docs.nextcloud.com/server/stable/developer_manual/_static/openapi.html#/operations/provisioning_api-users-get-current-user
req, err := http.NewRequest("GET", s.baseUrl+"ocs/v2.php/cloud/user", nil)
if err != nil {
return nil, err
}
req.Header.Set("Accept", "application/json")
req.Header.Set("OCS-APIRequest", "true")
return req, nil
}
// GetScopes returns the scopes required by the Nextcloud provider
func (s *NextcloudOAuth2DataSource) GetScopes() []string {
return []string{}
}
// ParseUserInfo returns the user info by parsing the response body
func (s *NextcloudOAuth2DataSource) ParseUserInfo(c core.Context, body []byte) (*data.OAuth2UserInfo, error) {
userInfoResp := &nextcloudUserInfoResponse{}
err := json.Unmarshal(body, &userInfoResp)
if err != nil {
log.Warnf(c, "[nextcloud_oauth2_datasource.ParseUserInfo] failed to parse user info response body, because %s", err.Error())
return nil, errs.ErrCannotRetrieveUserInfo
}
if userInfoResp.OCS == nil || userInfoResp.OCS.Meta == nil || userInfoResp.OCS.Data == nil {
log.Warnf(c, "[nextcloud_oauth2_datasource.ParseUserInfo] invalid user info response body")
return nil, errs.ErrCannotRetrieveUserInfo
}
if userInfoResp.OCS.Meta.StatusCode != 200 {
log.Warnf(c, "[nextcloud_oauth2_datasource.ParseUserInfo] user info response status code is %d", userInfoResp.OCS.Meta.StatusCode)
return nil, errs.ErrCannotRetrieveUserInfo
}
if userInfoResp.OCS.Data.ID == "" {
log.Warnf(c, "[nextcloud_oauth2_datasource.ParseUserInfo] user info id is empty")
return nil, errs.ErrCannotRetrieveUserInfo
}
return &data.OAuth2UserInfo{
UserName: userInfoResp.OCS.Data.ID,
Email: userInfoResp.OCS.Data.Email,
NickName: userInfoResp.OCS.Data.DisplayName,
}, nil
}
// NewNextcloudOAuth2Provider creates a new Nextcloud OAuth 2.0 provider instance
func NewNextcloudOAuth2Provider(config *settings.Config, redirectUrl string) (provider.OAuth2Provider, error) {
if len(config.OAuth2NextcloudBaseUrl) < 1 {
return nil, errs.ErrInvalidOAuth2Config
}
baseUrl := config.OAuth2NextcloudBaseUrl
if baseUrl[len(baseUrl)-1] != '/' {
baseUrl += "/"
}
return common.NewCommonOAuth2Provider(config, redirectUrl, &NextcloudOAuth2DataSource{
baseUrl: baseUrl,
}), nil
}
@@ -1,116 +0,0 @@
package nextcloud
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/mayswind/ezbookkeeping/pkg/auth/oauth2/provider/common"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/settings"
)
func TestNewNextcloudOAuth2Provider(t *testing.T) {
provider, err := NewNextcloudOAuth2Provider(&settings.Config{
OAuth2NextcloudBaseUrl: "https://example.com/",
}, "")
assert.Nil(t, err)
assert.Equal(t, "https://example.com/apps/oauth2/authorize", provider.(*common.CommonOAuth2Provider).GetDataSource().GetAuthUrl())
assert.Equal(t, "https://example.com/apps/oauth2/api/v1/token", provider.(*common.CommonOAuth2Provider).GetDataSource().GetTokenUrl())
provider, err = NewNextcloudOAuth2Provider(&settings.Config{
OAuth2NextcloudBaseUrl: "https://example.com/index.php",
}, "")
assert.Nil(t, err)
assert.Equal(t, "https://example.com/index.php/apps/oauth2/authorize", provider.(*common.CommonOAuth2Provider).GetDataSource().GetAuthUrl())
assert.Equal(t, "https://example.com/index.php/apps/oauth2/api/v1/token", provider.(*common.CommonOAuth2Provider).GetDataSource().GetTokenUrl())
provider, err = NewNextcloudOAuth2Provider(&settings.Config{}, "")
assert.Equal(t, errs.ErrInvalidOAuth2Config, err)
}
func TestNextcloudOAuth2Datasource_GetUserInfoRequest(t *testing.T) {
datasource := &NextcloudOAuth2DataSource{baseUrl: "https://example.com/"}
req, err := datasource.GetUserInfoRequest()
assert.Nil(t, err)
assert.Equal(t, "GET", req.Method)
assert.Equal(t, "https://example.com/ocs/v2.php/cloud/user", req.URL.String())
assert.Equal(t, "application/json", req.Header.Get("Accept"))
assert.Equal(t, "true", req.Header.Get("OCS-APIRequest"))
}
func TestNextcloudOAuth2Datasource_ParseUserInfo_Success(t *testing.T) {
datasource := &NextcloudOAuth2DataSource{}
responseContent := `{
"ocs": {
"meta": {
"status": "ok",
"statuscode": 200
},
"data": {
"id": "user1",
"email": "user1@example.com",
"display-name": "User"
}
}
}`
info, err := datasource.ParseUserInfo(core.NewNullContext(), []byte(responseContent))
assert.Nil(t, err)
assert.Equal(t, "user1", info.UserName)
assert.Equal(t, "user1@example.com", info.Email)
assert.Equal(t, "User", info.NickName)
}
func TestNextcloudOAuth2Datasource_ParseUserInfo_InvalidJson(t *testing.T) {
datasource := &NextcloudOAuth2DataSource{}
_, err := datasource.ParseUserInfo(core.NewNullContext(), []byte("invalid"))
assert.Equal(t, errs.ErrCannotRetrieveUserInfo, err)
}
func TestNextcloudOAuth2Datasource_ParseUserInfo_MissingFields(t *testing.T) {
datasource := &NextcloudOAuth2DataSource{}
responseContent := `{"ocs": {}}`
_, err := datasource.ParseUserInfo(core.NewNullContext(), []byte(responseContent))
assert.Equal(t, errs.ErrCannotRetrieveUserInfo, err)
}
func TestNextcloudOAuth2Datasource_ParseUserInfo_Non200StatusCode(t *testing.T) {
datasource := &NextcloudOAuth2DataSource{}
responseContent := `{
"ocs": {
"meta": {
"status": "error",
"statuscode": 400
},
"data": {}
}
}`
_, err := datasource.ParseUserInfo(core.NewNullContext(), []byte(responseContent))
assert.Equal(t, errs.ErrCannotRetrieveUserInfo, err)
}
func TestNextcloudOAuth2Datasource_ParseUserInfo_EmptyID(t *testing.T) {
datasource := &NextcloudOAuth2DataSource{}
responseContent := `{
"ocs": {
"meta": {
"status": "ok",
"statuscode": 200
},
"data": {
"id": "",
"email": "user1@example.com",
"display-name": "User One"
}
}
}`
_, err := datasource.ParseUserInfo(core.NewNullContext(), []byte(responseContent))
assert.Equal(t, errs.ErrCannotRetrieveUserInfo, err)
}
@@ -1,20 +0,0 @@
package provider
import (
"golang.org/x/oauth2"
"github.com/mayswind/ezbookkeeping/pkg/auth/oauth2/data"
"github.com/mayswind/ezbookkeeping/pkg/core"
)
// OAuth2Provider defines the structure of OAuth 2.0 provider
type OAuth2Provider interface {
// GetOAuth2AuthUrl returns the authentication url of the provider
GetOAuth2AuthUrl(c core.Context, state string, opts ...oauth2.AuthCodeOption) (string, error)
// GetOAuth2Token returns the OAuth 2.0 token of the provider
GetOAuth2Token(c core.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error)
// GetUserInfo returns the user info
GetUserInfo(c core.Context, oauth2Token *oauth2.Token) (*data.OAuth2UserInfo, error)
}
@@ -1,188 +0,0 @@
package oidc
import (
"context"
"golang.org/x/oauth2"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/mayswind/ezbookkeeping/pkg/auth/oauth2/data"
"github.com/mayswind/ezbookkeeping/pkg/auth/oauth2/provider"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/httpclient"
"github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/settings"
)
// OIDCClaims represents OIDC claims
type OIDCClaims struct {
PreferredUserName string `json:"preferred_username"`
UserName string `json:"username"`
Name string `json:"name"`
Email string `json:"email"`
}
// OIDCProvider represents OIDC provider
type OIDCProvider struct {
provider.OAuth2Provider
oidcIssuerURL string
oidcCheckIssuerURL bool
redirectUrl string
oauth2ClientID string
oauth2ClientSecret string
oauth2Config *oauth2.Config
oidcProvider *oidc.Provider
oidcVerifier *oidc.IDTokenVerifier
}
// GetOAuth2AuthUrl returns the authentication url of the OIDC provider
func (p *OIDCProvider) GetOAuth2AuthUrl(c core.Context, state string, opts ...oauth2.AuthCodeOption) (string, error) {
oauth2Config, err := p.getOAuth2Config(c)
if err != nil {
return "", err
}
return oauth2Config.AuthCodeURL(state, opts...), nil
}
// GetOAuth2Token returns the OAuth 2.0 token of the OIDC provider
func (p *OIDCProvider) GetOAuth2Token(c core.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) {
oauth2Config, err := p.getOAuth2Config(c)
if err != nil {
return nil, err
}
return oauth2Config.Exchange(c, code, opts...)
}
// GetUserInfo returns the user info by the OIDC provider
func (p *OIDCProvider) GetUserInfo(c core.Context, oauth2Token *oauth2.Token) (*data.OAuth2UserInfo, error) {
_, err := p.getOAuth2Config(c)
if err != nil {
return nil, err
}
rawIDToken, ok := oauth2Token.Extra("id_token").(string)
if !ok {
log.Errorf(c, "[oidc_provider.GetUserInfo] missing \"id_token\" field in oauth 2.0 token")
return nil, errs.ErrInvalidOAuth2Token
}
idToken, err := p.oidcVerifier.Verify(c, rawIDToken)
if err != nil {
log.Errorf(c, "[oidc_provider.GetUserInfo] failed to verify \"id_token\" field in oauth 2.0 token, because %s", err.Error())
return nil, errs.ErrInvalidOAuth2Token
}
var claims OIDCClaims
err = idToken.Claims(&claims)
if err != nil {
log.Errorf(c, "[oidc_provider.GetUserInfo] failed to parse claims in oauth 2.0 token, because %s", err.Error())
return nil, errs.ErrInvalidOAuth2Token
}
userName := claims.PreferredUserName
email := claims.Email
nickName := claims.Name
if userName == "" || email == "" || nickName == "" {
userInfo, err := p.oidcProvider.UserInfo(httpclient.CustomHttpResponseLog(c, func(data []byte) {
log.Debugf(c, "[oidc_provider.GetUserInfo] response is %s", data)
}), oauth2.StaticTokenSource(oauth2Token))
if err != nil {
log.Errorf(c, "[oidc_provider.GetUserInfo] failed to get user info, because %s", err.Error())
return nil, errs.ErrCannotRetrieveUserInfo
}
err = userInfo.Claims(&claims)
if err != nil {
log.Errorf(c, "[oidc_provider.GetUserInfo] failed to parse user info, because %s", err.Error())
return nil, errs.ErrCannotRetrieveUserInfo
}
if userName == "" {
userName = claims.PreferredUserName
}
if userName == "" {
userName = claims.UserName
}
if email == "" {
email = claims.Email
}
if nickName == "" {
nickName = claims.Name
}
}
return &data.OAuth2UserInfo{
UserName: userName,
Email: email,
NickName: nickName,
}, nil
}
func (p *OIDCProvider) getOAuth2Config(c core.Context) (*oauth2.Config, error) {
if p.oauth2Config != nil {
return p.oauth2Config, nil
}
var ctx context.Context = c
if !p.oidcCheckIssuerURL {
ctx = oidc.InsecureIssuerURLContext(c, p.oidcIssuerURL)
}
oidcProvider, err := oidc.NewProvider(ctx, p.oidcIssuerURL)
if err != nil {
log.Errorf(c, "[oidc_provider.getOAuth2Config] failed to create oidc provider, because %s", err.Error())
return nil, err
}
oidcVerifier := oidcProvider.Verifier(&oidc.Config{
ClientID: p.oauth2ClientID,
SkipIssuerCheck: !p.oidcCheckIssuerURL,
})
oauth2Config := &oauth2.Config{
ClientID: p.oauth2ClientID,
ClientSecret: p.oauth2ClientSecret,
Endpoint: oidcProvider.Endpoint(),
RedirectURL: p.redirectUrl,
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
}
p.oauth2Config = oauth2Config
p.oidcProvider = oidcProvider
p.oidcVerifier = oidcVerifier
return oauth2Config, nil
}
// NewOIDCProvider returns a new OIDC provider
func NewOIDCProvider(config *settings.Config, redirectUrl string) (*OIDCProvider, error) {
if len(config.OAuth2OIDCProviderIssuerURL) < 1 {
return nil, errs.ErrInvalidOAuth2Config
}
return &OIDCProvider{
oidcIssuerURL: config.OAuth2OIDCProviderIssuerURL,
oidcCheckIssuerURL: config.OAuth2OIDCProviderCheckIssuerURL,
redirectUrl: redirectUrl,
oauth2ClientID: config.OAuth2ClientID,
oauth2ClientSecret: config.OAuth2ClientSecret,
oauth2Config: nil,
}, nil
}
+8 -17
View File
@@ -5,7 +5,6 @@ import (
"time"
"github.com/mayswind/ezbookkeeping/pkg/converters"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/log"
@@ -92,7 +91,7 @@ func (l *UserDataCli) AddNewUser(c *core.CliContext, username string, email stri
FeatureRestriction: l.CurrentConfig().DefaultFeatureRestrictions,
}
err := l.users.CreateUser(c, user, false)
err := l.users.CreateUser(c, user)
if err != nil {
log.CliErrorf(c, "[user_data.AddNewUser] failed to create user \"%s\", because %s", user.Username, err.Error())
@@ -406,7 +405,7 @@ func (l *UserDataCli) ListUserTokens(c *core.CliContext, username string) ([]*mo
}
// CreateNewUserToken returns a new token for the specified user
func (l *UserDataCli) CreateNewUserToken(c *core.CliContext, username string, tokenType string, expiresInSeconds int64) (*models.TokenRecord, string, error) {
func (l *UserDataCli) CreateNewUserToken(c *core.CliContext, username string, tokenType string) (*models.TokenRecord, string, error) {
if username == "" {
log.CliErrorf(c, "[user_data.CreateNewUserToken] user name is empty")
return nil, "", errs.ErrUsernameIsEmpty
@@ -422,17 +421,7 @@ func (l *UserDataCli) CreateNewUserToken(c *core.CliContext, username string, to
var token string
var tokenRecord *models.TokenRecord
if tokenType == "api" {
if !l.CurrentConfig().EnableAPIToken {
return nil, "", errs.ErrAPITokenNotEnabled
}
if user.FeatureRestriction.Contains(core.USER_FEATURE_RESTRICTION_TYPE_GENERATE_API_TOKEN) {
return nil, "", errs.ErrNotPermittedToPerformThisAction
}
token, tokenRecord, err = l.tokens.CreateAPITokenViaCli(c, user, expiresInSeconds)
} else if tokenType == "mcp" {
if tokenType == "mcp" {
if !l.CurrentConfig().EnableMCPServer {
return nil, "", errs.ErrMCPServerNotEnabled
}
@@ -441,7 +430,9 @@ func (l *UserDataCli) CreateNewUserToken(c *core.CliContext, username string, to
return nil, "", errs.ErrNotPermittedToPerformThisAction
}
token, tokenRecord, err = l.tokens.CreateMCPTokenViaCli(c, user, expiresInSeconds)
token, tokenRecord, err = l.tokens.CreateMCPTokenViaCli(c, user)
} else if tokenType == "normal" {
token, tokenRecord, err = l.tokens.CreateTokenViaCli(c, user)
} else {
return nil, "", errs.ErrParameterInvalid
}
@@ -456,7 +447,7 @@ func (l *UserDataCli) CreateNewUserToken(c *core.CliContext, username string, to
// RevokeUserToken revokes the specified token of the user
func (l *UserDataCli) RevokeUserToken(c *core.CliContext, token string) error {
_, claims, _, err := l.tokens.ParseToken(c, token)
_, claims, err := l.tokens.ParseToken(c, token)
if err != nil {
log.CliErrorf(c, "[user_data.RevokeUserToken] failed to parse token, because %s", err.Error())
@@ -819,7 +810,7 @@ func (l *UserDataCli) ImportTransaction(c *core.CliContext, username string, fil
return err
}
parsedTransactions, newAccounts, newSubExpenseCategories, newSubIncomeCategories, newSubTransferCategories, newTags, err := dataImporter.ParseImportedData(c, user, data, time.Local, converter.DefaultImporterOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
parsedTransactions, newAccounts, newSubExpenseCategories, newSubIncomeCategories, newSubTransferCategories, newTags, err := dataImporter.ParseImportedData(c, user, data, utils.GetTimezoneOffsetMinutes(time.Local), accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
if err != nil {
log.CliErrorf(c, "[user_data.ImportTransaction] failed to parse imported data for \"%s\", because %s", username, err.Error())
@@ -10,7 +10,7 @@ var (
AlipayAppTransactionDataCsvFileImporter = &alipayAppTransactionDataCsvFileImporter{
alipayTransactionDataCsvFileImporter{
fileHeaderLine: "------------------------------------------------------------------------------------",
dataHeaderStartContent: []string{"支付宝(中国)网络技术有限公司 电子客户回单", "支付宝支付科技有限公司 电子客户回单"},
dataHeaderStartContent: "支付宝(中国)网络技术有限公司 电子客户回单",
originalColumnNames: alipayTransactionColumnNames{
timeColumnName: "交易时间",
categoryColumnName: "交易分类",
@@ -2,7 +2,6 @@ package alipay
import (
"bytes"
"time"
"golang.org/x/text/encoding/simplifiedchinese"
"golang.org/x/text/transform"
@@ -48,13 +47,13 @@ type alipayTransactionColumnNames struct {
// alipayTransactionDataCsvFileImporter defines the structure of alipay csv importer for transaction data
type alipayTransactionDataCsvFileImporter struct {
fileHeaderLine string
dataHeaderStartContent []string
dataHeaderStartContent string
dataBottomEndLineRune rune
originalColumnNames alipayTransactionColumnNames
}
// ParseImportedData returns the imported data by parsing the alipay transaction csv data
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) {
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) {
enc := simplifiedchinese.GB18030
reader := transform.NewReader(bytes.NewReader(data), enc.NewDecoder())
@@ -84,5 +83,5 @@ func (c *alipayTransactionDataCsvFileImporter) ParseImportedData(ctx core.Contex
transactionDataTable := datatable.CreateNewTransactionDataTableFromCommonDataTable(commonDataTable, alipayTransactionSupportedColumns, transactionRowParser)
dataTableImporter := converter.CreateNewSimpleImporterWithTypeNameMapping(alipayTransactionTypeNameMapping)
return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezone, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezoneOffset, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
}
@@ -8,7 +8,6 @@ import (
"golang.org/x/text/encoding/simplifiedchinese"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/models"
@@ -16,7 +15,7 @@ import (
)
func TestAlipayCsvFileImporterParseImportedData_MinimumValidData(t *testing.T) {
importer := AlipayWebTransactionDataCsvFileImporter
converter := AlipayWebTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -36,7 +35,7 @@ func TestAlipayCsvFileImporterParseImportedData_MinimumValidData(t *testing.T) {
"------------------------------------------------------------------------------------\n")
assert.Nil(t, err)
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := importer.ParseImportedData(context, user, []byte(data), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := converter.ParseImportedData(context, user, []byte(data), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 4, len(allNewTransactions))
@@ -95,7 +94,7 @@ func TestAlipayCsvFileImporterParseImportedData_MinimumValidData(t *testing.T) {
}
func TestAlipayCsvFileImporterParseImportedData_ParseRefundTransaction(t *testing.T) {
importer := AlipayWebTransactionDataCsvFileImporter
converter := AlipayWebTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -113,7 +112,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseRefundTransaction(t *testin
"------------------------------------------------------------------------------------\n")
assert.Nil(t, err)
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(data1), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(data1), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, int64(1234567890), allNewTransactions[0].Uid)
@@ -133,7 +132,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseRefundTransaction(t *testin
"------------------------------------------------------------------------------------\n")
assert.Nil(t, err)
allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data2), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data2), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, int64(1234567890), allNewTransactions[0].Uid)
@@ -145,7 +144,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseRefundTransaction(t *testin
}
func TestAlipayCsvFileImporterParseImportedData_ParseInvestmentRefundTransaction(t *testing.T) {
importer := AlipayAppTransactionDataCsvFileImporter
converter := AlipayAppTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -164,7 +163,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseInvestmentRefundTransaction
"2024-09-01 01:00:00,Test Account2,xxx-买入,不计收支,0.01,Test Account,退款成功,\n" +
"2024-09-01 02:00:00,Test Account2,xxx-买入退款,不计收支,0.01,Test Account,退款成功,\n")
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(data1), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(data1), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 2, len(allNewTransactions))
@@ -185,7 +184,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseInvestmentRefundTransaction
}
func TestAlipayCsvFileImporterParseImportedData_ParseInvalidTime(t *testing.T) {
importer := AlipayWebTransactionDataCsvFileImporter
converter := AlipayWebTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -202,7 +201,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseInvalidTime(t *testing.T) {
"------------------------------------------------------------------------------------\n")
assert.Nil(t, err)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data1), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data1), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
data2, err := simplifiedchinese.GB18030.NewEncoder().String("支付宝交易记录明细查询\n" +
@@ -214,12 +213,12 @@ func TestAlipayCsvFileImporterParseImportedData_ParseInvalidTime(t *testing.T) {
"------------------------------------------------------------------------------------\n")
assert.Nil(t, err)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data2), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data2), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
}
func TestAlipayCsvFileImporterParseImportedData_ParseInvalidType(t *testing.T) {
importer := AlipayWebTransactionDataCsvFileImporter
converter := AlipayWebTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -236,12 +235,12 @@ func TestAlipayCsvFileImporterParseImportedData_ParseInvalidType(t *testing.T) {
"------------------------------------------------------------------------------------\n")
assert.Nil(t, err)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrNotFoundTransactionDataInFile.Message)
}
func TestAlipayCsvFileImporterParseImportedData_ParseAccountName(t *testing.T) {
importer := AlipayWebTransactionDataCsvFileImporter
converter := AlipayWebTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -259,7 +258,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseAccountName(t *testing.T) {
"------------------------------------------------------------------------------------\n")
assert.Nil(t, err)
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(data1), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(data1), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
@@ -275,7 +274,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseAccountName(t *testing.T) {
"------------------------------------------------------------------------------------\n")
assert.Nil(t, err)
allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data2), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data2), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
@@ -291,7 +290,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseAccountName(t *testing.T) {
"------------------------------------------------------------------------------------\n")
assert.Nil(t, err)
allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data3), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data3), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
@@ -308,7 +307,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseAccountName(t *testing.T) {
"------------------------------------------------------------------------------------\n")
assert.Nil(t, err)
allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data4), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data4), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
@@ -325,7 +324,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseAccountName(t *testing.T) {
"------------------------------------------------------------------------------------\n")
assert.Nil(t, err)
allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data5), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data5), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
@@ -342,7 +341,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseAccountName(t *testing.T) {
"------------------------------------------------------------------------------------\n")
assert.Nil(t, err)
allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data6), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data6), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
@@ -359,7 +358,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseAccountName(t *testing.T) {
"------------------------------------------------------------------------------------\n")
assert.Nil(t, err)
allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data7), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data7), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
@@ -368,7 +367,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseAccountName(t *testing.T) {
}
func TestAlipayCsvFileImporterParseImportedData_ParseCategory(t *testing.T) {
importer := AlipayAppTransactionDataCsvFileImporter
converter := AlipayAppTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -389,7 +388,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseCategory(t *testing.T) {
"2024-09-01 23:59:59,Test Category3,充值-普通充值,不计收支,0.05,交易成功,\n")
assert.Nil(t, err)
allNewTransactions, _, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, _, err := importer.ParseImportedData(context, user, []byte(data1), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
allNewTransactions, _, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, _, err := converter.ParseImportedData(context, user, []byte(data1), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 3, len(allNewTransactions))
@@ -408,7 +407,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseCategory(t *testing.T) {
}
func TestAlipayCsvFileImporterParseImportedData_ParseRelatedAccount(t *testing.T) {
importer := AlipayAppTransactionDataCsvFileImporter
converter := AlipayAppTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -435,7 +434,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseRelatedAccount(t *testing.T
"2024-09-01 08:00:00,Test Account4,信用卡还款,不计收支,0.01,Test Account,还款成功,repayment,\n")
assert.Nil(t, err)
allNewTransactions, allNewAccounts, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(data1), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
allNewTransactions, allNewAccounts, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(data1), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 9, len(allNewTransactions))
@@ -530,7 +529,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseRelatedAccount(t *testing.T
}
func TestAlipayCsvFileImporterParseImportedData_ParseDescription(t *testing.T) {
importer := AlipayWebTransactionDataCsvFileImporter
converter := AlipayWebTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -547,7 +546,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseDescription(t *testing.T) {
"------------------------------------------------------------------------------------\n")
assert.Nil(t, err)
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(data1), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(data1), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
@@ -562,7 +561,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseDescription(t *testing.T) {
"------------------------------------------------------------------------------------\n")
assert.Nil(t, err)
allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data2), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data2), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
@@ -570,7 +569,7 @@ func TestAlipayCsvFileImporterParseImportedData_ParseDescription(t *testing.T) {
}
func TestAlipayCsvFileImporterParseImportedData_SkipClosedIncomeOrTransferTransaction(t *testing.T) {
importer := AlipayWebTransactionDataCsvFileImporter
converter := AlipayWebTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -587,12 +586,12 @@ func TestAlipayCsvFileImporterParseImportedData_SkipClosedIncomeOrTransferTransa
"2024-09-01 23:59:59 ,充值-普通充值 ,0.05 ,不计收支 ,交易关闭 ,\n" +
"------------------------------------------------------------------------------------\n")
assert.Nil(t, err)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrNotFoundTransactionDataInFile.Message)
}
func TestAlipayCsvFileImporterParseImportedData_SkipUnknownProductTransferTransaction(t *testing.T) {
importer := AlipayWebTransactionDataCsvFileImporter
converter := AlipayWebTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -608,12 +607,12 @@ func TestAlipayCsvFileImporterParseImportedData_SkipUnknownProductTransferTransa
"2024-09-01 23:59:59 ,xxxx ,0.05 ,不计收支 ,交易成功 ,\n" +
"------------------------------------------------------------------------------------\n")
assert.Nil(t, err)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrNotFoundTransactionDataInFile.Message)
}
func TestAlipayCsvFileImporterParseImportedData_SkipUnknownStatusTransaction(t *testing.T) {
importer := AlipayWebTransactionDataCsvFileImporter
converter := AlipayWebTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -629,12 +628,12 @@ func TestAlipayCsvFileImporterParseImportedData_SkipUnknownStatusTransaction(t *
"2024-09-01 01:23:45 ,xxxx ,0.12 ,收入 ,xxxx ,\n" +
"------------------------------------------------------------------------------------\n")
assert.Nil(t, err)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrNotFoundTransactionDataInFile.Message)
}
func TestAlipayCsvFileImporterParseImportedData_MissingFileHeader(t *testing.T) {
importer := AlipayWebTransactionDataCsvFileImporter
converter := AlipayWebTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -648,15 +647,15 @@ func TestAlipayCsvFileImporterParseImportedData_MissingFileHeader(t *testing.T)
"------------------------------------------------------------------------------------\n")
assert.Nil(t, err)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrInvalidFileHeader.Message)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(""), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrInvalidFileHeader.Message)
}
func TestAlipayCsvFileImporterParseImportedData_MissingRequiredColumn(t *testing.T) {
importer := AlipayWebTransactionDataCsvFileImporter
converter := AlipayWebTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -672,7 +671,7 @@ func TestAlipayCsvFileImporterParseImportedData_MissingRequiredColumn(t *testing
"金额(元),收/支 ,交易状态 ,\n" +
"0.12 ,收入 ,交易成功 ,\n" +
"------------------------------------------------------------------------------------\n")
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data1), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data1), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
// Missing Amount Column
@@ -683,7 +682,7 @@ func TestAlipayCsvFileImporterParseImportedData_MissingRequiredColumn(t *testing
"交易创建时间 ,收/支 ,交易状态 ,\n" +
"2024-09-01 12:34:56 ,收入 ,交易成功 ,\n" +
"------------------------------------------------------------------------------------\n")
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data2), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data2), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
// Missing Status Column
@@ -694,7 +693,7 @@ func TestAlipayCsvFileImporterParseImportedData_MissingRequiredColumn(t *testing
"交易创建时间 ,金额(元),收/支 ,\n" +
"2024-09-01 12:34:56 ,0.12 ,收入 ,\n" +
"------------------------------------------------------------------------------------\n")
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data3), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data3), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
// Missing Type Column
@@ -705,12 +704,12 @@ func TestAlipayCsvFileImporterParseImportedData_MissingRequiredColumn(t *testing
"交易创建时间 ,金额(元),交易状态 ,\n" +
"2024-09-01 12:34:56 ,0.12 ,交易成功 ,\n" +
"------------------------------------------------------------------------------------\n")
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data4), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data4), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
}
func TestAlipayCsvFileImporterParseImportedData_NoTransactionData(t *testing.T) {
importer := AlipayWebTransactionDataCsvFileImporter
converter := AlipayWebTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -724,6 +723,6 @@ func TestAlipayCsvFileImporterParseImportedData_NoTransactionData(t *testing.T)
"---------------------------------交易记录明细列表------------------------------------\n" +
"交易创建时间 ,金额(元),收/支 ,交易状态 ,\n" +
"------------------------------------------------------------------------------------\n")
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(data1), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data1), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrNotFoundTransactionDataInFile.Message)
}
@@ -11,7 +11,7 @@ import (
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
func createNewAlipayTransactionBasicDataTable(ctx core.Context, originalDataTable datatable.BasicDataTable, fileHeaderLine string, dataHeaderStartContent []string, dataBottomEndLineRune rune) (datatable.BasicDataTable, error) {
func createNewAlipayTransactionBasicDataTable(ctx core.Context, originalDataTable datatable.BasicDataTable, fileHeaderLine string, dataHeaderStartContent string, dataBottomEndLineRune rune) (datatable.BasicDataTable, error) {
iterator := originalDataTable.DataRowIterator()
allOriginalLines := make([][]string, 0)
hasFileHeader := false
@@ -35,7 +35,7 @@ func createNewAlipayTransactionBasicDataTable(ctx core.Context, originalDataTabl
if !foundContentBeforeDataHeaderLine {
if row.ColumnCount() <= 0 {
continue
} else if utils.ContainsAnyString(row.GetData(0), dataHeaderStartContent) {
} else if strings.Index(row.GetData(0), dataHeaderStartContent) >= 0 {
foundContentBeforeDataHeaderLine = true
continue
} else {
@@ -13,7 +13,6 @@ import (
const alipayTransactionDataStatusSuccessName = "交易成功"
const alipayTransactionDataStatusPaymentSuccessName = "支付成功"
const alipayTransactionDataStatusPendingGoodsReceiptConfirmationName = "等待确认收货"
const alipayTransactionDataStatusRepaymentSuccessName = "还款成功"
const alipayTransactionDataStatusClosedName = "交易关闭"
const alipayTransactionDataStatusRefundSuccessName = "退款成功"
@@ -47,7 +46,6 @@ func (p *alipayTransactionDataRowParser) Parse(ctx core.Context, user *models.Us
if dataRow.GetData(p.columns.statusColumnName) != alipayTransactionDataStatusSuccessName &&
dataRow.GetData(p.columns.statusColumnName) != alipayTransactionDataStatusPaymentSuccessName &&
dataRow.GetData(p.columns.statusColumnName) != alipayTransactionDataStatusPendingGoodsReceiptConfirmationName &&
dataRow.GetData(p.columns.statusColumnName) != alipayTransactionDataStatusRepaymentSuccessName &&
dataRow.GetData(p.columns.statusColumnName) != alipayTransactionDataStatusClosedName &&
dataRow.GetData(p.columns.statusColumnName) != alipayTransactionDataStatusRefundSuccessName &&
@@ -10,7 +10,7 @@ var (
AlipayWebTransactionDataCsvFileImporter = &alipayWebTransactionDataCsvFileImporter{
alipayTransactionDataCsvFileImporter{
fileHeaderLine: "支付宝交易记录明细查询",
dataHeaderStartContent: []string{"交易记录明细列表"},
dataHeaderStartContent: "交易记录明细列表",
dataBottomEndLineRune: '-',
originalColumnNames: alipayTransactionColumnNames{
timeColumnName: "交易创建时间",
@@ -1,8 +1,6 @@
package beancount
import (
"time"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/models"
@@ -26,7 +24,7 @@ var (
)
// ParseImportedData returns the imported data by parsing the Beancount transaction data
func (c *beancountTransactionDataImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, 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) {
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) {
beancountDataReader, err := createNewBeancountDataReader(ctx, data)
if err != nil {
@@ -47,5 +45,5 @@ func (c *beancountTransactionDataImporter) ParseImportedData(ctx core.Context, u
dataTableImporter := converter.CreateNewImporterWithTypeNameMapping(beancountTransactionTypeNameMapping, "", "", BEANCOUNT_TRANSACTION_TAG_SEPARATOR)
return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezone, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezoneOffset, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
}
@@ -2,11 +2,9 @@ package beancount
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/models"
@@ -14,7 +12,7 @@ import (
)
func TestBeancountTransactionDataFileParseImportedData_MinimumValidData(t *testing.T) {
importer := BeancountTransactionDataImporter
converter := BeancountTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -22,7 +20,7 @@ func TestBeancountTransactionDataFileParseImportedData_MinimumValidData(t *testi
DefaultCurrency: "CNY",
}
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := importer.ParseImportedData(context, user, []byte(
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := converter.ParseImportedData(context, user, []byte(
"2024-09-01 *\n"+
" Equity:Opening-Balances -123.45 CNY\n"+
" Assets:TestAccount 123.45 CNY\n"+
@@ -34,7 +32,7 @@ func TestBeancountTransactionDataFileParseImportedData_MinimumValidData(t *testi
" Expenses:TestCategory2 1.00 CNY\n"+
"2024-09-04 *\n"+
" Assets:TestAccount -0.05 CNY\n"+
" Assets:TestAccount2 0.05 CNY\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
" Assets:TestAccount2 0.05 CNY\n"), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
@@ -93,7 +91,7 @@ func TestBeancountTransactionDataFileParseImportedData_MinimumValidData(t *testi
}
func TestBeancountTransactionDataFileParseImportedData_MinimumValidData2(t *testing.T) {
importer := BeancountTransactionDataImporter
converter := BeancountTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -101,7 +99,7 @@ func TestBeancountTransactionDataFileParseImportedData_MinimumValidData2(t *test
DefaultCurrency: "CNY",
}
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := importer.ParseImportedData(context, user, []byte(
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := converter.ParseImportedData(context, user, []byte(
"2024-09-01 *\n"+
" Assets:TestAccount 123.45 CNY\n"+
" Equity:Opening-Balances -123.45 CNY\n"+
@@ -113,7 +111,7 @@ func TestBeancountTransactionDataFileParseImportedData_MinimumValidData2(t *test
" Assets:TestAccount -1.00 CNY\n"+
"2024-09-04 *\n"+
" Assets:TestAccount2 0.05 CNY\n"+
" Assets:TestAccount -0.05 CNY\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
" Assets:TestAccount -0.05 CNY\n"), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
@@ -172,7 +170,7 @@ func TestBeancountTransactionDataFileParseImportedData_MinimumValidData2(t *test
}
func TestBeancountTransactionDataFileParseImportedData_ParseInvalidTime(t *testing.T) {
importer := BeancountTransactionDataImporter
converter := BeancountTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -180,15 +178,15 @@ func TestBeancountTransactionDataFileParseImportedData_ParseInvalidTime(t *testi
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
"2024/09/01 *\n"+
" Equity:Opening-Balances -123.45 CNY\n"+
" Assets:TestAccount 123.45 CNY\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
" Assets:TestAccount 123.45 CNY\n"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrNotFoundTransactionDataInFile.Message)
}
func TestBeancountTransactionDataFileParseImportedData_ParseValidCurrency(t *testing.T) {
importer := BeancountTransactionDataImporter
converter := BeancountTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -196,10 +194,10 @@ func TestBeancountTransactionDataFileParseImportedData_ParseValidCurrency(t *tes
DefaultCurrency: "CNY",
}
allNewTransactions, allNewAccounts, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
allNewTransactions, allNewAccounts, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
"2024-09-01 * \"Payee Name\" \"Hello\nWorld\"\n"+
" Assets:TestAccount -0.12 USD\n"+
" Assets:TestAccount2 0.84 CNY\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
" Assets:TestAccount2 0.84 CNY\n"), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
@@ -224,7 +222,7 @@ func TestBeancountTransactionDataFileParseImportedData_ParseValidCurrency(t *tes
}
func TestBeancountTransactionDataFileParseImportedData_ParseInvalidAmount(t *testing.T) {
importer := BeancountTransactionDataImporter
converter := BeancountTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -232,21 +230,21 @@ func TestBeancountTransactionDataFileParseImportedData_ParseInvalidAmount(t *tes
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
"2024-09-01 *\n"+
" Equity:Opening-Balances -abc CNY\n"+
" Assets:TestAccount abc CNY\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
" Assets:TestAccount abc CNY\n"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
"2024-09-01 *\n"+
" Equity:Opening-Balances -1/0 CNY\n"+
" Assets:TestAccount 1/0 CNY\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
" Assets:TestAccount 1/0 CNY\n"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
}
func TestBeancountTransactionDataFileParseImportedData_ParseDescription(t *testing.T) {
importer := BeancountTransactionDataImporter
converter := BeancountTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -254,13 +252,13 @@ func TestBeancountTransactionDataFileParseImportedData_ParseDescription(t *testi
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
"2024-09-01 * \"foo bar\t#test\n\"\n"+
" Equity:Opening-Balances -123.45 CNY\n"+
" Assets:TestAccount 123.45 CNY\n"+
"2024-09-02 * \"Payee Name\" \"Hello\nWorld\"\n"+
" Income:TestCategory -0.12 CNY\n"+
" Assets:TestAccount 0.12 CNY\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
" Assets:TestAccount 0.12 CNY\n"), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
@@ -271,7 +269,7 @@ func TestBeancountTransactionDataFileParseImportedData_ParseDescription(t *testi
}
func TestBeancountTransactionDataFileParseImportedData_InvalidTransaction(t *testing.T) {
importer := BeancountTransactionDataImporter
converter := BeancountTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -279,33 +277,33 @@ func TestBeancountTransactionDataFileParseImportedData_InvalidTransaction(t *tes
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
"2024-09-02 * \"Payee Name\" \"Hello\nWorld\"\n"+
" Assets:TestAccount 0.11 CNY\n"+
" Assets:TestAccount2 0.11 CNY\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
" Assets:TestAccount2 0.11 CNY\n"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrInvalidBeancountFile.Message)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
"2024-09-02 * \"Payee Name\" \"Hello\nWorld\"\n"+
" Expenses:TestCategory -0.11 CNY\n"+
" Expenses:TestCategory2 0.11 CNY\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
" Expenses:TestCategory2 0.11 CNY\n"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrThereAreNotSupportedTransactionType.Message)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
"2024-09-02 * \"Payee Name\" \"Hello\nWorld\"\n"+
" Income:TestCategory -0.11 CNY\n"+
" Income:TestCategory2 0.11 CNY\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
" Income:TestCategory2 0.11 CNY\n"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrThereAreNotSupportedTransactionType.Message)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
"2024-09-02 * \"Payee Name\" \"Hello\nWorld\"\n"+
" Equity:TestCategory -0.11 CNY\n"+
" Equity:TestCategory2 0.11 CNY\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
" Equity:TestCategory2 0.11 CNY\n"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrThereAreNotSupportedTransactionType.Message)
}
func TestBeancountTransactionDataFileParseImportedData_NotSupportedToParseSplitTransaction(t *testing.T) {
importer := BeancountTransactionDataImporter
converter := BeancountTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -313,16 +311,16 @@ func TestBeancountTransactionDataFileParseImportedData_NotSupportedToParseSplitT
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
"2024-09-02 * \"Payee Name\" \"Hello\nWorld\"\n"+
" Assets:TestAccount -0.23 CNY\n"+
" Assets:TestAccount2 0.11 CNY\n"+
" Assets:TestAccount3 0.12 CNY\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
" Assets:TestAccount3 0.12 CNY\n"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrNotSupportedSplitTransactions.Message)
}
func TestBeancountTransactionDataFileParseImportedData_MissingTransactionRequiredData(t *testing.T) {
importer := BeancountTransactionDataImporter
converter := BeancountTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -331,30 +329,30 @@ func TestBeancountTransactionDataFileParseImportedData_MissingTransactionRequire
}
// Missing Transaction Time
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
"* \"narration\"\n"+
" Equity:Opening-Balances -123.45 CNY\n"+
" Assets:TestAccount 123.45 CNY\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
" Assets:TestAccount 123.45 CNY\n"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrNotFoundTransactionDataInFile.Message)
// Missing Account Name
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
"2024-09-01 * \"narration\"\n"+
" Equity:Opening-Balances -123.45 CNY\n"+
" 123.45 CNY\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
" 123.45 CNY\n"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrInvalidBeancountFile.Message)
// Missing Amount
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
"2024-09-01 * \"narration\"\n"+
" Equity:Opening-Balances\n"+
" Assets:TestAccount\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
" Assets:TestAccount\n"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrInvalidBeancountFile.Message)
// Missing Commodity
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
"2024-09-01 * \"narration\"\n"+
" Equity:Opening-Balances -123.45\n"+
" Assets:TestAccount 123.45\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
" Assets:TestAccount 123.45\n"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrInvalidBeancountFile.Message)
}
@@ -231,7 +231,7 @@ func (t *camtStatementTransactionDataRowIterator) parseTransaction(ctx core.Cont
}
data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME] = utils.FormatUnixTimeToLongDateTime(dateTime.Unix(), dateTime.Location())
data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE] = utils.FormatTimezoneOffset(dateTime.Unix(), dateTime.Location())
data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE] = utils.FormatTimezoneOffset(dateTime.Location())
} else if entry.BookingDate != nil && entry.BookingDate.Date != "" {
data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME] = fmt.Sprintf("%s 00:00:00", entry.BookingDate.Date)
data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE] = datatable.TRANSACTION_DATA_TABLE_TIMEZONE_NOT_AVAILABLE
@@ -1,8 +1,6 @@
package camt
import (
"time"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/models"
@@ -25,7 +23,7 @@ var (
)
// ParseImportedData returns the imported data by parsing the camt.053 file transaction data
func (c *camt053TransactionDataImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, 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) {
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) {
camt053DataReader, err := createNewCamt053FileReader(data)
if err != nil {
@@ -46,5 +44,5 @@ func (c *camt053TransactionDataImporter) ParseImportedData(ctx core.Context, use
dataTableImporter := converter.CreateNewSimpleImporterWithTypeNameMapping(camtTransactionTypeNameMapping)
return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezone, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezoneOffset, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
}
@@ -2,11 +2,9 @@ package camt
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/models"
@@ -14,7 +12,7 @@ import (
)
func TestCamt053TransactionDataFileParseImportedData_MinimumValidData(t *testing.T) {
importer := Camt053TransactionDataImporter
converter := Camt053TransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -22,7 +20,7 @@ func TestCamt053TransactionDataFileParseImportedData_MinimumValidData(t *testing
DefaultCurrency: "CNY",
}
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := importer.ParseImportedData(context, user, []byte(
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := converter.ParseImportedData(context, user, []byte(
`<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt>
@@ -66,7 +64,7 @@ func TestCamt053TransactionDataFileParseImportedData_MinimumValidData(t *testing
</Ntry>
</Stmt>
</BkToCstmrStmt>
</Document>`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
</Document>`), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
@@ -117,7 +115,7 @@ func TestCamt053TransactionDataFileParseImportedData_MinimumValidData(t *testing
}
func TestCamt053TransactionDataFileParseImportedData_ParseValidTransactionTime(t *testing.T) {
importer := Camt053TransactionDataImporter
converter := Camt053TransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -125,7 +123,7 @@ func TestCamt053TransactionDataFileParseImportedData_ParseValidTransactionTime(t
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
`<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt>
@@ -159,7 +157,7 @@ func TestCamt053TransactionDataFileParseImportedData_ParseValidTransactionTime(t
</Ntry>
</Stmt>
</BkToCstmrStmt>
</Document>`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
</Document>`), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 3, len(allNewTransactions))
@@ -170,7 +168,7 @@ func TestCamt053TransactionDataFileParseImportedData_ParseValidTransactionTime(t
}
func TestCamt053TransactionDataFileParseImportedData_ParseInvalidTransactionTime(t *testing.T) {
importer := Camt053TransactionDataImporter
converter := Camt053TransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -178,7 +176,7 @@ func TestCamt053TransactionDataFileParseImportedData_ParseInvalidTransactionTime
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
`<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt>
@@ -198,10 +196,10 @@ func TestCamt053TransactionDataFileParseImportedData_ParseInvalidTransactionTime
</Ntry>
</Stmt>
</BkToCstmrStmt>
</Document>`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
</Document>`), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
`<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt>
@@ -221,10 +219,10 @@ func TestCamt053TransactionDataFileParseImportedData_ParseInvalidTransactionTime
</Ntry>
</Stmt>
</BkToCstmrStmt>
</Document>`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
</Document>`), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
`<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt>
@@ -244,10 +242,10 @@ func TestCamt053TransactionDataFileParseImportedData_ParseInvalidTransactionTime
</Ntry>
</Stmt>
</BkToCstmrStmt>
</Document>`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
</Document>`), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
`<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt>
@@ -267,12 +265,12 @@ func TestCamt053TransactionDataFileParseImportedData_ParseInvalidTransactionTime
</Ntry>
</Stmt>
</BkToCstmrStmt>
</Document>`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
</Document>`), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
}
func TestCamt053TransactionDataFileParseImportedData_ParseTransactionValidAmountAndCurrency(t *testing.T) {
importer := Camt053TransactionDataImporter
converter := Camt053TransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -280,7 +278,7 @@ func TestCamt053TransactionDataFileParseImportedData_ParseTransactionValidAmount
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
`<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt>
@@ -316,7 +314,7 @@ func TestCamt053TransactionDataFileParseImportedData_ParseTransactionValidAmount
</Ntry>
</Stmt>
</BkToCstmrStmt>
</Document>`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
</Document>`), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 2, len(allNewTransactions))
@@ -325,7 +323,7 @@ func TestCamt053TransactionDataFileParseImportedData_ParseTransactionValidAmount
assert.Equal(t, "USD", allNewTransactions[1].OriginalSourceAccountCurrency)
assert.Equal(t, int64(10023), allNewTransactions[1].Amount)
allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
`<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt>
@@ -367,7 +365,7 @@ func TestCamt053TransactionDataFileParseImportedData_ParseTransactionValidAmount
</Ntry>
</Stmt>
</BkToCstmrStmt>
</Document>`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
</Document>`), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 2, len(allNewTransactions))
@@ -376,7 +374,7 @@ func TestCamt053TransactionDataFileParseImportedData_ParseTransactionValidAmount
assert.Equal(t, "USD", allNewTransactions[1].OriginalSourceAccountCurrency)
assert.Equal(t, int64(9999), allNewTransactions[1].Amount)
allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
`<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt>
@@ -405,14 +403,14 @@ func TestCamt053TransactionDataFileParseImportedData_ParseTransactionValidAmount
</Ntry>
</Stmt>
</BkToCstmrStmt>
</Document>`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
</Document>`), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, "USD", allNewTransactions[0].OriginalSourceAccountCurrency)
assert.Equal(t, int64(12345), allNewTransactions[0].Amount)
allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
`<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt>
@@ -432,7 +430,7 @@ func TestCamt053TransactionDataFileParseImportedData_ParseTransactionValidAmount
</Ntry>
</Stmt>
</BkToCstmrStmt>
</Document>`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
</Document>`), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
@@ -441,7 +439,7 @@ func TestCamt053TransactionDataFileParseImportedData_ParseTransactionValidAmount
}
func TestCamt053TransactionDataFileParseImportedData_ParseTransactionInvalidAmountAndCurrency(t *testing.T) {
importer := Camt053TransactionDataImporter
converter := Camt053TransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -449,7 +447,7 @@ func TestCamt053TransactionDataFileParseImportedData_ParseTransactionInvalidAmou
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
`<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt>
@@ -469,10 +467,10 @@ func TestCamt053TransactionDataFileParseImportedData_ParseTransactionInvalidAmou
</Ntry>
</Stmt>
</BkToCstmrStmt>
</Document>`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
</Document>`), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
`<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt>
@@ -500,10 +498,10 @@ func TestCamt053TransactionDataFileParseImportedData_ParseTransactionInvalidAmou
</Ntry>
</Stmt>
</BkToCstmrStmt>
</Document>`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
</Document>`), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
`<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt>
@@ -531,12 +529,12 @@ func TestCamt053TransactionDataFileParseImportedData_ParseTransactionInvalidAmou
</Ntry>
</Stmt>
</BkToCstmrStmt>
</Document>`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
</Document>`), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
}
func TestCamt053TransactionDataFileParseImportedData_ParseDescription(t *testing.T) {
importer := Camt053TransactionDataImporter
converter := Camt053TransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -544,7 +542,7 @@ func TestCamt053TransactionDataFileParseImportedData_ParseDescription(t *testing
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
`<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt>
@@ -574,13 +572,13 @@ func TestCamt053TransactionDataFileParseImportedData_ParseDescription(t *testing
</Ntry>
</Stmt>
</BkToCstmrStmt>
</Document>`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
</Document>`), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, "Test Transaction", allNewTransactions[0].Comment)
allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
`<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt>
@@ -609,13 +607,13 @@ func TestCamt053TransactionDataFileParseImportedData_ParseDescription(t *testing
</Ntry>
</Stmt>
</BkToCstmrStmt>
</Document>`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
</Document>`), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, "Test Line 1\nTest Line 2", allNewTransactions[0].Comment)
allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
`<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt>
@@ -636,7 +634,7 @@ func TestCamt053TransactionDataFileParseImportedData_ParseDescription(t *testing
</Ntry>
</Stmt>
</BkToCstmrStmt>
</Document>`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
</Document>`), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
@@ -644,7 +642,7 @@ func TestCamt053TransactionDataFileParseImportedData_ParseDescription(t *testing
}
func TestCamt053TransactionDataFileParseImportedData_MissingAccountNode(t *testing.T) {
importer := Camt053TransactionDataImporter
converter := Camt053TransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -652,7 +650,7 @@ func TestCamt053TransactionDataFileParseImportedData_MissingAccountNode(t *testi
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
`<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt>
@@ -666,12 +664,12 @@ func TestCamt053TransactionDataFileParseImportedData_MissingAccountNode(t *testi
</Ntry>
</Stmt>
</BkToCstmrStmt>
</Document>`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
</Document>`), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrMissingAccountData.Message)
}
func TestCamt053TransactionDataFileParseImportedData_MissingTransactionRequiredNode(t *testing.T) {
importer := Camt053TransactionDataImporter
converter := Camt053TransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -679,7 +677,7 @@ func TestCamt053TransactionDataFileParseImportedData_MissingTransactionRequiredN
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
`<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt>
@@ -696,10 +694,10 @@ func TestCamt053TransactionDataFileParseImportedData_MissingTransactionRequiredN
</Ntry>
</Stmt>
</BkToCstmrStmt>
</Document>`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
</Document>`), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrMissingTransactionTime.Message)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
`<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt>
@@ -718,10 +716,10 @@ func TestCamt053TransactionDataFileParseImportedData_MissingTransactionRequiredN
</Ntry>
</Stmt>
</BkToCstmrStmt>
</Document>`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
</Document>`), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrTransactionTypeInvalid.Message)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
`<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt>
@@ -740,10 +738,10 @@ func TestCamt053TransactionDataFileParseImportedData_MissingTransactionRequiredN
</Ntry>
</Stmt>
</BkToCstmrStmt>
</Document>`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
</Document>`), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
`<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt>
@@ -762,6 +760,6 @@ func TestCamt053TransactionDataFileParseImportedData_MissingTransactionRequiredN
</Ntry>
</Stmt>
</BkToCstmrStmt>
</Document>`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
</Document>`), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message)
}
@@ -28,11 +28,10 @@ func (c *DataTableTransactionDataExporter) BuildExportedContent(ctx core.Context
}
dataRowMap := make(map[datatable.TransactionDataTableColumn]string, 15)
transactionUnixTime := utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime)
transactionTimeZone := time.FixedZone("Transaction Timezone", int(transaction.TimezoneUtcOffset)*60)
dataRowMap[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME] = utils.FormatUnixTimeToLongDateTime(transactionUnixTime, transactionTimeZone)
dataRowMap[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE] = utils.FormatTimezoneOffset(transactionUnixTime, transactionTimeZone)
dataRowMap[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME] = utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime), transactionTimeZone)
dataRowMap[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE] = utils.FormatTimezoneOffset(transactionTimeZone)
dataRowMap[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = dataTableBuilder.ReplaceDelimiters(c.getDisplayTransactionTypeName(transaction.Type))
dataRowMap[datatable.TRANSACTION_DATA_TABLE_CATEGORY] = c.getExportedTransactionCategoryName(dataTableBuilder, transaction.CategoryId, categoryMap)
dataRowMap[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY] = c.getExportedTransactionSubCategoryName(dataTableBuilder, transaction.CategoryId, categoryMap)
@@ -3,7 +3,6 @@ package converter
import (
"sort"
"strings"
"time"
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
"github.com/mayswind/ezbookkeeping/pkg/core"
@@ -30,7 +29,7 @@ type DataTableTransactionDataImporter struct {
}
// ParseImportedData returns the imported transaction data
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) {
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) {
if dataTable.TransactionRowCount() < 1 {
log.Errorf(ctx, "[data_table_transaction_data_importer.ParseImportedData] cannot parse import data for user \"uid:%d\", because data table row count is less 1", user.Uid)
return nil, nil, nil, nil, nil, nil, errs.ErrNotFoundTransactionDataInFile
@@ -95,7 +94,7 @@ func (c *DataTableTransactionDataImporter) ParseImportedData(ctx core.Context, u
continue
}
timezone := defaultTimezone
timezoneOffset := defaultTimezoneOffset
if dataTable.HasColumn(datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE) &&
dataRow.GetData(datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE) != datatable.TRANSACTION_DATA_TABLE_TIMEZONE_NOT_AVAILABLE {
@@ -106,10 +105,10 @@ func (c *DataTableTransactionDataImporter) ParseImportedData(ctx core.Context, u
return nil, nil, nil, nil, nil, nil, errs.ErrTransactionTimeZoneInvalid
}
timezone = transactionTimezone
timezoneOffset = utils.GetTimezoneOffsetMinutes(transactionTimezone)
}
transactionTime, err := utils.ParseFromLongDateTimeInTimeZone(dataRow.GetData(datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME), timezone)
transactionTime, err := utils.ParseFromLongDateTime(dataRow.GetData(datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME), timezoneOffset)
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())
@@ -304,7 +303,6 @@ func (c *DataTableTransactionDataImporter) ParseImportedData(ctx core.Context, u
var tagIds []string
var tagNames []string
tagNamesMap := make(map[string]bool)
if dataTable.HasColumn(datatable.TRANSACTION_DATA_TABLE_TAGS) {
var tagNameItems []string
@@ -322,39 +320,19 @@ func (c *DataTableTransactionDataImporter) ParseImportedData(ctx core.Context, u
continue
}
allNewTags, tagIds, tagNames = c.addTag(user, tagName, tagNamesMap, tagMap, allNewTags, tagIds, tagNames)
}
}
tag, exists := tagMap[tagName]
if dataTable.HasColumn(datatable.TRANSACTION_DATA_TABLE_PAYEE) && additionalOptions.IsPayeeAsTag() {
payee := dataRow.GetData(datatable.TRANSACTION_DATA_TABLE_PAYEE)
if !exists {
tag = c.createNewTransactionTagModel(user.Uid, tagName)
allNewTags = append(allNewTags, tag)
tagMap[tagName] = tag
}
if payee != "" {
allNewTags, tagIds, tagNames = c.addTag(user, payee, tagNamesMap, tagMap, allNewTags, tagIds, tagNames)
}
}
if tag != nil {
tagIds = append(tagIds, utils.Int64ToString(tag.TagId))
}
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)
tagNames = append(tagNames, tagName)
}
}
@@ -364,17 +342,13 @@ func (c *DataTableTransactionDataImporter) ParseImportedData(ctx core.Context, u
description = dataRow.GetData(datatable.TRANSACTION_DATA_TABLE_DESCRIPTION)
}
if description == "" && additionalOptions.IsPayeeAsDescription() && dataTable.HasColumn(datatable.TRANSACTION_DATA_TABLE_PAYEE) {
description = dataRow.GetData(datatable.TRANSACTION_DATA_TABLE_PAYEE)
}
transaction := &models.ImportTransaction{
Transaction: &models.Transaction{
Uid: user.Uid,
Type: transactionDbType,
CategoryId: categoryId,
TransactionTime: utils.GetMinTransactionTimeFromUnixTime(transactionTime.Unix()),
TimezoneUtcOffset: utils.GetTimezoneOffsetMinutes(transactionTime.Unix(), timezone),
TimezoneUtcOffset: timezoneOffset,
AccountId: account.AccountId,
Amount: amount,
HideAmount: false,
@@ -485,27 +459,6 @@ func (c *DataTableTransactionDataImporter) getTransactionCategory(categories map
return subCategory, exists
}
func (c *DataTableTransactionDataImporter) addTag(user *models.User, tagName string, tagNamesMap map[string]bool, tagMap map[string]*models.TransactionTag, allNewTags []*models.TransactionTag, tagIds []string, tagNames []string) ([]*models.TransactionTag, []string, []string) {
if tagName != "" && !tagNamesMap[tagName] {
tag, exists := tagMap[tagName]
if !exists {
tag = c.createNewTransactionTagModel(user.Uid, tagName)
allNewTags = append(allNewTags, tag)
tagMap[tagName] = tag
}
if tag != nil {
tagIds = append(tagIds, utils.Int64ToString(tag.TagId))
}
tagNames = append(tagNames, tagName)
tagNamesMap[tagName] = true
}
return allNewTags, tagIds, tagNames
}
func (c *DataTableTransactionDataImporter) createNewAccountModel(uid int64, accountName string, currency string) *models.Account {
return &models.Account{
Uid: uid,
@@ -1,8 +1,6 @@
package converter
import (
"time"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/models"
)
@@ -16,7 +14,7 @@ type TransactionDataExporter interface {
// TransactionDataImporter defines the structure of transaction data importer
type TransactionDataImporter interface {
// ParseImportedData returns the imported data
ParseImportedData(ctx core.Context, user *models.User, data []byte, 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)
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)
}
// TransactionDataConverter defines the structure of transaction data converter
@@ -1,118 +0,0 @@
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
}
@@ -1,110 +0,0 @@
package converter
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestParseImporterOptions(t *testing.T) {
actualValue := ParseImporterOptions("payeeAsTag,memberAsTag")
expectedValue := TransactionDataImporterOptions{
payeeAsTag: true,
memberAsTag: true,
projectAsTag: false,
merchantAsTag: false,
}
assert.Equal(t, expectedValue, actualValue)
assert.Equal(t, true, actualValue.IsPayeeAsTag())
assert.Equal(t, true, actualValue.IsMemberAsTag())
assert.Equal(t, false, actualValue.IsProjectAsTag())
assert.Equal(t, false, actualValue.IsMerchantAsTag())
actualValue = ParseImporterOptions("")
expectedValue = TransactionDataImporterOptions{
payeeAsTag: false,
memberAsTag: false,
projectAsTag: false,
merchantAsTag: false,
}
assert.Equal(t, expectedValue, actualValue)
assert.Equal(t, false, actualValue.IsPayeeAsTag())
assert.Equal(t, false, actualValue.IsMemberAsTag())
assert.Equal(t, false, actualValue.IsProjectAsTag())
assert.Equal(t, false, actualValue.IsMerchantAsTag())
}
func TestParseImporterOptions_WithAllOptions(t *testing.T) {
actualValue := ParseImporterOptions("payeeAsTag,payeeAsDescription,memberAsTag,projectAsTag,merchantAsTag")
expectedValue := TransactionDataImporterOptions{
payeeAsTag: true,
payeeAsDescription: true,
memberAsTag: true,
projectAsTag: true,
merchantAsTag: true,
}
assert.Equal(t, expectedValue, actualValue)
assert.Equal(t, true, actualValue.IsPayeeAsTag())
assert.Equal(t, true, actualValue.IsPayeeAsDescription())
assert.Equal(t, true, actualValue.IsMemberAsTag())
assert.Equal(t, true, actualValue.IsProjectAsTag())
assert.Equal(t, true, actualValue.IsMerchantAsTag())
}
func TestParseImporterOptions_WithInvalidOptions(t *testing.T) {
actualValue := ParseImporterOptions("invalidOption,payeeAsTag,memberAsTag")
expectedValue := TransactionDataImporterOptions{
payeeAsTag: true,
memberAsTag: true,
projectAsTag: false,
merchantAsTag: false,
}
assert.Equal(t, expectedValue, actualValue)
assert.Equal(t, true, actualValue.IsPayeeAsTag())
assert.Equal(t, true, actualValue.IsMemberAsTag())
assert.Equal(t, false, actualValue.IsProjectAsTag())
assert.Equal(t, false, actualValue.IsMerchantAsTag())
actualValue = ParseImporterOptions("invalidOption")
expectedValue = TransactionDataImporterOptions{
payeeAsTag: false,
memberAsTag: false,
projectAsTag: false,
merchantAsTag: false,
}
assert.Equal(t, expectedValue, actualValue)
assert.Equal(t, false, actualValue.IsPayeeAsTag())
assert.Equal(t, false, actualValue.IsMemberAsTag())
assert.Equal(t, false, actualValue.IsProjectAsTag())
assert.Equal(t, false, actualValue.IsMerchantAsTag())
}
func TestParseImporterOptions_Clone(t *testing.T) {
original := TransactionDataImporterOptions{
payeeAsTag: true,
payeeAsDescription: false,
memberAsTag: false,
projectAsTag: true,
merchantAsTag: false,
}
cloned := original.Clone()
assert.Equal(t, original, cloned)
// Modify cloned options and verify original options are not affected
cloned.payeeAsTag = false
cloned.payeeAsDescription = true
cloned.memberAsTag = true
assert.Equal(t, true, original.payeeAsTag)
assert.Equal(t, false, original.payeeAsDescription)
assert.Equal(t, false, original.memberAsTag)
assert.Equal(t, true, original.projectAsTag)
assert.Equal(t, false, original.merchantAsTag)
assert.Equal(t, false, cloned.payeeAsTag)
assert.Equal(t, true, cloned.payeeAsDescription)
assert.Equal(t, true, cloned.memberAsTag)
assert.Equal(t, true, cloned.projectAsTag)
assert.Equal(t, false, cloned.merchantAsTag)
}
@@ -72,10 +72,6 @@ const (
TRANSACTION_DATA_TABLE_GEOGRAPHIC_LOCATION TransactionDataTableColumn = 12
TRANSACTION_DATA_TABLE_TAGS TransactionDataTableColumn = 13
TRANSACTION_DATA_TABLE_DESCRIPTION TransactionDataTableColumn = 14
TRANSACTION_DATA_TABLE_PAYEE TransactionDataTableColumn = 101
TRANSACTION_DATA_TABLE_MEMBER TransactionDataTableColumn = 102
TRANSACTION_DATA_TABLE_PROJECT TransactionDataTableColumn = 103
TRANSACTION_DATA_TABLE_MERCHANT TransactionDataTableColumn = 104
)
// TRANSACTION_DATA_TABLE_TIMEZONE_NOT_AVAILABLE represents the constant for timezone not available
@@ -35,7 +35,7 @@ var (
)
// ParseImportedData returns the imported data by parsing the transaction json data
func (c *defaultTransactionDataJsonImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, 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) {
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) {
var importRequest models.ImportTransactionRequest
if err := json.Unmarshal(data, &importRequest); err != nil {
@@ -55,7 +55,7 @@ func (c *defaultTransactionDataJsonImporter) ParseImportedData(ctx core.Context,
ezbookkeepingTagSeparator,
)
return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezone, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezoneOffset, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
}
func (c *defaultTransactionDataJsonImporter) createNewDefaultTransactionDataTable(importRequest models.ImportTransactionRequest) (datatable.TransactionDataTable, error) {
@@ -75,11 +75,10 @@ func (c *defaultTransactionDataJsonImporter) createNewDefaultTransactionDataTabl
}
timezone := time.FixedZone("Transaction Timezone", utcOffset*60)
timezoneOffset := utils.FormatTimezoneOffset(time.Now().Unix(), timezone)
row := make(map[datatable.TransactionDataTableColumn]string, len(allJsonDataSupportedColumns))
row[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME] = transaction.Time
row[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE] = timezoneOffset
row[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE] = utils.FormatTimezoneOffset(timezone)
row[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = transaction.Type
row[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY] = transaction.CategoryName
row[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = transaction.SourceAccountName
@@ -1,8 +1,6 @@
package _default
import (
"time"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
"github.com/mayswind/ezbookkeeping/pkg/core"
@@ -86,7 +84,7 @@ func (c *defaultTransactionDataPlainTextConverter) ToExportedContent(ctx core.Co
}
// ParseImportedData returns the imported data by parsing the transaction plain text data
func (c *defaultTransactionDataPlainTextConverter) ParseImportedData(ctx core.Context, user *models.User, data []byte, 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) {
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) {
dataTable, err := createNewDefaultPlainTextDataTable(
string(data),
c.columnSeparator,
@@ -106,5 +104,5 @@ func (c *defaultTransactionDataPlainTextConverter) ParseImportedData(ctx core.Co
ezbookkeepingTagSeparator,
)
return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezone, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezoneOffset, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
}
@@ -2,11 +2,9 @@ package _default
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/models"
@@ -14,7 +12,7 @@ import (
)
func TestDefaultTransactionDataCSVFileConverterToExportedContent(t *testing.T) {
exporter := DefaultTransactionDataCSVFileConverter
converter := DefaultTransactionDataCSVFileConverter
context := core.NewNullContext()
transactions := make([]*models.Transaction, 3)
@@ -121,14 +119,14 @@ func TestDefaultTransactionDataCSVFileConverterToExportedContent(t *testing.T) {
"2024-09-01 12:34:56,+08:00,Income,Test Category,Test Sub Category,Test Account,CNY,123.45,,,,123.450000 45.670000,Test Tag;Test Tag2,Hello World\n" +
"2024-09-01 12:34:56,+00:00,Expense,Test Category2,Test Sub Category2,Test Account,CNY,-0.10,,,,,Test Tag,Foo#Bar\n" +
"2024-09-01 12:34:56,-05:00,Transfer,Test Category3,Test Sub Category3,Test Account,CNY,123.45,Test Account2,USD,17.35,,Test Tag2,T\te s t test\n"
actualContent, err := exporter.ToExportedContent(context, 123, transactions, accountMap, categoryMap, tagMap, allTagIndexes)
actualContent, err := converter.ToExportedContent(context, 123, transactions, accountMap, categoryMap, tagMap, allTagIndexes)
assert.Nil(t, err)
assert.Equal(t, expectedContent, string(actualContent))
}
func TestDefaultTransactionDataCSVFileConverterParseImportedData_MinimumValidData(t *testing.T) {
importer := DefaultTransactionDataCSVFileConverter
converter := DefaultTransactionDataCSVFileConverter
context := core.NewNullContext()
user := &models.User{
@@ -136,11 +134,11 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_MinimumValidDat
DefaultCurrency: "CNY",
}
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := importer.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := converter.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
"2024-09-01 00:00:00,Balance Modification,,Test Account,123.45,,\n"+
"2024-09-01 01:23:45,Income,Test Category,Test Account,0.12,,\n"+
"2024-09-01 12:34:56,Expense,Test Category2,Test Account,1.00,,\n"+
"2024-09-01 23:59:59,Transfer,Test Category3,Test Account,0.05,Test Account2,0.05"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"2024-09-01 23:59:59,Transfer,Test Category3,Test Account,0.05,Test Account2,0.05"), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
@@ -199,7 +197,7 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_MinimumValidDat
}
func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseInvalidTime(t *testing.T) {
importer := DefaultTransactionDataCSVFileConverter
converter := DefaultTransactionDataCSVFileConverter
context := core.NewNullContext()
user := &models.User{
@@ -207,17 +205,17 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseInvalidTim
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
"2024-09-01T12:34:56,Expense,Test Category,Test Account,123.45,,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
"2024-09-01T12:34:56,Expense,Test Category,Test Account,123.45,,"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
"09/01/2024 12:34:56,Expense,Test Category,Test Account,123.45,,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
"09/01/2024 12:34:56,Expense,Test Category,Test Account,123.45,,"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
}
func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseInvalidType(t *testing.T) {
importer := DefaultTransactionDataCSVFileConverter
converter := DefaultTransactionDataCSVFileConverter
context := core.NewNullContext()
user := &models.User{
@@ -225,13 +223,13 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseInvalidTyp
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
"2024-09-01 12:34:56,Type,Test Category,Test Account,123.45,,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
"2024-09-01 12:34:56,Type,Test Category,Test Account,123.45,,"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrTransactionTypeInvalid.Message)
}
func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseValidTimezone(t *testing.T) {
importer := DefaultTransactionDataCSVFileConverter
converter := DefaultTransactionDataCSVFileConverter
context := core.NewNullContext()
user := &models.User{
@@ -239,27 +237,27 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseValidTimez
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("Time,Timezone,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
"2024-09-01 12:34:56,-10:00,Expense,Test Category,Test Account,123.45,,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("Time,Timezone,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
"2024-09-01 12:34:56,-10:00,Expense,Test Category,Test Account,123.45,,"), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, int64(1725230096), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("Time,Timezone,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
"2024-09-01 12:34:56,+00:00,Expense,Test Category,Test Account,123.45,,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("Time,Timezone,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
"2024-09-01 12:34:56,+00:00,Expense,Test Category,Test Account,123.45,,"), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, int64(1725194096), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("Time,Timezone,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
"2024-09-01 12:34:56,+12:45,Expense,Test Category,Test Account,123.45,,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("Time,Timezone,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
"2024-09-01 12:34:56,+12:45,Expense,Test Category,Test Account,123.45,,"), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, int64(1725148196), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
}
func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseInvalidTimezone(t *testing.T) {
importer := DefaultTransactionDataCSVFileConverter
converter := DefaultTransactionDataCSVFileConverter
context := core.NewNullContext()
user := &models.User{
@@ -267,13 +265,13 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseInvalidTim
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("Time,Timezone,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
"2024-09-01 12:34:56,Asia/Shanghai,Expense,Test Category,Test Account,123.45,,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("Time,Timezone,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
"2024-09-01 12:34:56,Asia/Shanghai,Expense,Test Category,Test Account,123.45,,"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrTransactionTimeZoneInvalid.Message)
}
func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseValidAccountCurrency(t *testing.T) {
importer := DefaultTransactionDataCSVFileConverter
converter := DefaultTransactionDataCSVFileConverter
context := core.NewNullContext()
user := &models.User{
@@ -281,9 +279,9 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseValidAccou
DefaultCurrency: "CNY",
}
allNewTransactions, allNewAccounts, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount\n"+
allNewTransactions, allNewAccounts, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount\n"+
"2024-09-01 01:23:45,Balance Modification,,Test Account,USD,123.45,,,\n"+
"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)
"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)
assert.Nil(t, err)
@@ -300,7 +298,7 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseValidAccou
}
func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseInvalidAccountCurrency(t *testing.T) {
importer := DefaultTransactionDataCSVFileConverter
converter := DefaultTransactionDataCSVFileConverter
context := core.NewNullContext()
user := &models.User{
@@ -308,19 +306,19 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseInvalidAcc
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount\n"+
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount\n"+
"2024-09-01 01:23:45,Balance Modification,,Test Account,USD,123.45,,,\n"+
"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)
"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)
assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount\n"+
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount\n"+
"2024-09-01 01:23:45,Balance Modification,,Test Account,USD,123.45,,,\n"+
"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)
"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)
assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message)
}
func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseNotSupportedCurrency(t *testing.T) {
importer := DefaultTransactionDataCSVFileConverter
converter := DefaultTransactionDataCSVFileConverter
context := core.NewNullContext()
user := &models.User{
@@ -328,17 +326,17 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseNotSupport
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount\n"+
"2024-09-01 01:23:45,Balance Modification,,Test Account,XXX,123.45,,,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount\n"+
"2024-09-01 01:23:45,Balance Modification,,Test Account,XXX,123.45,,,"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount\n"+
"2024-09-01 01:23:45,Transfer,Test Category,Test Account,USD,123.45,Test Account2,XXX,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount\n"+
"2024-09-01 01:23:45,Transfer,Test Category,Test Account,USD,123.45,Test Account2,XXX,123.45"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message)
}
func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseInvalidAmount(t *testing.T) {
importer := DefaultTransactionDataCSVFileConverter
converter := DefaultTransactionDataCSVFileConverter
context := core.NewNullContext()
user := &models.User{
@@ -346,17 +344,17 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseInvalidAmo
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
"2024-09-01 12:34:56,Expense,Test Category,Test Account,123 45,,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
"2024-09-01 12:34:56,Expense,Test Category,Test Account,123 45,,"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
"2024-09-01 12:34:56,Transfer,Test Category,Test Account,123.45,Test Account2,123 45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
"2024-09-01 12:34:56,Transfer,Test Category,Test Account,123.45,Test Account2,123 45"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
}
func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseNoAmount2(t *testing.T) {
importer := DefaultTransactionDataCSVFileConverter
converter := DefaultTransactionDataCSVFileConverter
context := core.NewNullContext()
user := &models.User{
@@ -364,15 +362,15 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseNoAmount2(
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2\n"+
"2024-09-01 12:34:56,Expense,Test Category,Test Account,123.45,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2\n"+
"2024-09-01 12:34:56,Expense,Test Category,Test Account,123.45,"), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, int64(12345), allNewTransactions[0].Amount)
assert.Equal(t, int64(0), allNewTransactions[0].RelatedAccountAmount)
allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2\n"+
"2024-09-01 12:34:56,Transfer,Test Category,Test Account,123.45,Test Account2"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2\n"+
"2024-09-01 12:34:56,Transfer,Test Category,Test Account,123.45,Test Account2"), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, int64(12345), allNewTransactions[0].Amount)
@@ -380,7 +378,7 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseNoAmount2(
}
func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseValidGeographicLocation(t *testing.T) {
importer := DefaultTransactionDataCSVFileConverter
converter := DefaultTransactionDataCSVFileConverter
context := core.NewNullContext()
user := &models.User{
@@ -388,8 +386,8 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseValidGeogr
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount,Geographic Location\n"+
"2024-09-01 12:34:56,Expense,Test Category,Test Account,123.45,,,123.45 45.56"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount,Geographic Location\n"+
"2024-09-01 12:34:56,Expense,Test Category,Test Account,123.45,,,123.45 45.56"), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
@@ -398,7 +396,7 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseValidGeogr
}
func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseInvalidGeographicLocation(t *testing.T) {
importer := DefaultTransactionDataCSVFileConverter
converter := DefaultTransactionDataCSVFileConverter
context := core.NewNullContext()
user := &models.User{
@@ -406,24 +404,24 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseInvalidGeo
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount,Geographic Location\n"+
"2024-09-01 12:34:56,Expense,Test Category,Test Account,123.45,,,1"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount,Geographic Location\n"+
"2024-09-01 12:34:56,Expense,Test Category,Test Account,123.45,,,1"), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, float64(0), allNewTransactions[0].GeoLongitude)
assert.Equal(t, float64(0), allNewTransactions[0].GeoLatitude)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount,Geographic Location\n"+
"2024-09-01 12:34:56,Expense,Test Category,Test Account,123.45,,,a b"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount,Geographic Location\n"+
"2024-09-01 12:34:56,Expense,Test Category,Test Account,123.45,,,a b"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrGeographicLocationInvalid.Message)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount,Geographic Location\n"+
"2024-09-01 12:34:56,Expense,Test Category,Test Account,123.45,,,1 "), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount,Geographic Location\n"+
"2024-09-01 12:34:56,Expense,Test Category,Test Account,123.45,,,1 "), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrGeographicLocationInvalid.Message)
}
func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseTag(t *testing.T) {
importer := DefaultTransactionDataCSVFileConverter
converter := DefaultTransactionDataCSVFileConverter
context := core.NewNullContext()
user := &models.User{
@@ -431,8 +429,8 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseTag(t *tes
DefaultCurrency: "CNY",
}
_, _, _, _, _, allNewTags, err := importer.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount,Tags\n"+
"2024-09-01 00:00:00,Balance Modification,,Test Account,123.45,,,foo;;bar.;#test;hello\tworld;;"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
_, _, _, _, _, allNewTags, err := converter.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount,Tags\n"+
"2024-09-01 00:00:00,Balance Modification,,Test Account,123.45,,,foo;;bar.;#test;hello\tworld;;"), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
@@ -452,7 +450,7 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseTag(t *tes
}
func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseDescription(t *testing.T) {
importer := DefaultTransactionDataCSVFileConverter
converter := DefaultTransactionDataCSVFileConverter
context := core.NewNullContext()
user := &models.User{
@@ -460,8 +458,8 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseDescriptio
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount,Description\n"+
"2024-09-01 12:34:56,Expense,Test Category,Test Account,123.45,,,foo bar\t#test"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount,Description\n"+
"2024-09-01 12:34:56,Expense,Test Category,Test Account,123.45,,,foo bar\t#test"), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
@@ -469,7 +467,7 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseDescriptio
}
func TestDefaultTransactionDataCSVFileConverterParseImportedData_MissingFileHeader(t *testing.T) {
importer := DefaultTransactionDataCSVFileConverter
converter := DefaultTransactionDataCSVFileConverter
context := core.NewNullContext()
user := &models.User{
@@ -477,12 +475,12 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_MissingFileHead
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(""), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrNotFoundTransactionDataInFile.Message)
}
func TestDefaultTransactionDataCSVFileConverterParseImportedData_MissingRequiredColumn(t *testing.T) {
importer := DefaultTransactionDataCSVFileConverter
converter := DefaultTransactionDataCSVFileConverter
context := core.NewNullContext()
user := &models.User{
@@ -491,32 +489,32 @@ func TestDefaultTransactionDataCSVFileConverterParseImportedData_MissingRequired
}
// Missing Time Column
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("Timezone,Type,Category,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount,Geographic Location,Tags,Description\n"+
"+08:00,Balance Modification,,Test Sub Category,Test Account,CNY,123.45,,,,,,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("Timezone,Type,Category,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount,Geographic Location,Tags,Description\n"+
"+08:00,Balance Modification,,Test Sub Category,Test Account,CNY,123.45,,,,,,"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
// Missing Type Column
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("Time,Category,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount,Geographic Location,Tags,Description\n"+
"2024-09-01 00:00:00,+08:00,Test Category,Test Sub Category,Test Account,CNY,123.45,,,,,,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("Time,Category,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount,Geographic Location,Tags,Description\n"+
"2024-09-01 00:00:00,+08:00,Test Category,Test Sub Category,Test Account,CNY,123.45,,,,,,"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
// Missing Sub Category Column
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("Time,Type,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount,Geographic Location,Tags,Description\n"+
"2024-09-01 00:00:00,+08:00,Balance Modification,Test Account,CNY,123.45,,,,,,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("Time,Type,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount,Geographic Location,Tags,Description\n"+
"2024-09-01 00:00:00,+08:00,Balance Modification,Test Account,CNY,123.45,,,,,,"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
// Missing Account Name Column
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("Time,Timezone,Type,Category,Sub Category,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount,Geographic Location,Tags,Description\n"+
"2024-09-01 00:00:00,+08:00,Balance Modification,,Test Sub Category,CNY,123.45,,,,,,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("Time,Timezone,Type,Category,Sub Category,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount,Geographic Location,Tags,Description\n"+
"2024-09-01 00:00:00,+08:00,Balance Modification,,Test Sub Category,CNY,123.45,,,,,,"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
// Missing Amount Column
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("Time,Timezone,Type,Category,Sub Category,Account,Account Currency,Account2,Account2 Currency,Account2 Amount,Geographic Location,Tags,Description\n"+
"2024-09-01 00:00:00,+08:00,Balance Modification,,Test Sub Category,Test Account,CNY,,,,,,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("Time,Timezone,Type,Category,Sub Category,Account,Account Currency,Account2,Account2 Currency,Account2 Amount,Geographic Location,Tags,Description\n"+
"2024-09-01 00:00:00,+08:00,Balance Modification,,Test Sub Category,Test Account,CNY,,,,,,"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
// Missing Account2 Name Column
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("Time,Timezone,Type,Category,Sub Category,Account,Account Currency,Amount,Account2 Currency,Account2 Amount,Geographic Location,Tags,Description\n"+
"2024-09-01 00:00:00,+08:00,Balance Modification,,Test Sub Category,Test Account,CNY,123.45,,,,,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("Time,Timezone,Type,Category,Sub Category,Account,Account Currency,Amount,Account2 Currency,Account2 Amount,Geographic Location,Tags,Description\n"+
"2024-09-01 00:00:00,+08:00,Balance Modification,,Test Sub Category,Test Account,CNY,123.45,,,,,"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
}
@@ -5,7 +5,6 @@ import (
"encoding/csv"
"io"
"strings"
"time"
"golang.org/x/text/encoding"
"golang.org/x/text/encoding/charmap"
@@ -29,16 +28,13 @@ import (
var supportedFileTypeSeparators = map[string]rune{
"custom_csv": ',',
"custom_tsv": '\t',
"custom_ssv": ';',
}
var supportedFileEncodings = map[string]encoding.Encoding{
"utf-8": unicode.UTF8, // UTF-8
"utf-8-bom": unicode.UTF8BOM, // UTF-8 with BOM
"utf-16le": unicode.UTF16(unicode.LittleEndian, unicode.UseBOM), // UTF-16 Little Endian
"utf-16be": unicode.UTF16(unicode.BigEndian, unicode.UseBOM), // UTF-16 Big Endian
"utf-16le-bom": unicode.UTF16(unicode.LittleEndian, unicode.ExpectBOM), // UTF-16 Little Endian with BOM
"utf-16be-bom": unicode.UTF16(unicode.BigEndian, unicode.ExpectBOM), // UTF-16 Big Endian with BOM
"utf-16le": unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM), // UTF-16 Little Endian
"utf-16be": unicode.UTF16(unicode.BigEndian, unicode.IgnoreBOM), // UTF-16 Big Endian
"cp437": charmap.CodePage437, // OEM United States (CP-437)
"cp863": charmap.CodePage863, // OEM Canadian French (CP-863)
"cp037": charmap.CodePage037, // IBM EBCDIC US/Canada (CP-037)
@@ -150,7 +146,7 @@ func (c *customTransactionDataDsvFileImporter) ParseDsvFileLines(ctx core.Contex
}
// ParseImportedData returns the imported data by parsing the custom transaction dsv data
func (c *customTransactionDataDsvFileImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, 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) {
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) {
allLines, err := c.ParseDsvFileLines(ctx, data)
if err != nil {
@@ -161,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)
dataTableImporter := converter.CreateNewImporterWithTypeNameMapping(customTransactionTypeNameMapping, c.geoLocationSeparator, c.geoLocationOrder, c.transactionTagSeparator)
return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezone, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezoneOffset, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
}
// IsDelimiterSeparatedValuesFileType returns whether the file type is the delimiter-separated values file type
@@ -2,11 +2,9 @@ package dsv
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
@@ -17,21 +15,19 @@ import (
func TestIsDelimiterSeparatedValuesFileType(t *testing.T) {
assert.True(t, IsDelimiterSeparatedValuesFileType("custom_csv"))
assert.True(t, IsDelimiterSeparatedValuesFileType("custom_tsv"))
assert.True(t, IsDelimiterSeparatedValuesFileType("custom_ssv"))
assert.False(t, IsDelimiterSeparatedValuesFileType("dsv"))
assert.False(t, IsDelimiterSeparatedValuesFileType("csv"))
assert.False(t, IsDelimiterSeparatedValuesFileType("tsv"))
assert.False(t, IsDelimiterSeparatedValuesFileType("ssv"))
}
func TestCustomTransactionDataDsvFileParser_ParseDsvFileLines(t *testing.T) {
importer, err := CreateNewCustomTransactionDataDsvFileParser("custom_csv", "utf-8")
converter, err := CreateNewCustomTransactionDataDsvFileParser("custom_csv", "utf-8")
assert.Nil(t, err)
context := core.NewNullContext()
allLines, err := importer.ParseDsvFileLines(context, []byte(
allLines, err := converter.ParseDsvFileLines(context, []byte(
"2024-09-01 00:00:00,B,123.45\n"+
"2024-09-01 01:23:45,I,0.12\n"))
assert.Nil(t, err)
@@ -48,10 +44,10 @@ func TestCustomTransactionDataDsvFileParser_ParseDsvFileLines(t *testing.T) {
assert.Equal(t, "I", allLines[1][1])
assert.Equal(t, "0.12", allLines[1][2])
importer, err = CreateNewCustomTransactionDataDsvFileParser("custom_tsv", "utf-8")
converter, err = CreateNewCustomTransactionDataDsvFileParser("custom_tsv", "utf-8")
assert.Nil(t, err)
allLines, err = importer.ParseDsvFileLines(context, []byte(
allLines, err = converter.ParseDsvFileLines(context, []byte(
"2024-09-01 12:34:56\tE\t1.00\n"+
"2024-09-01 23:59:59\tT\t0.05"))
assert.Nil(t, err)
@@ -67,26 +63,6 @@ func TestCustomTransactionDataDsvFileParser_ParseDsvFileLines(t *testing.T) {
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])
importer, err = CreateNewCustomTransactionDataDsvFileParser("custom_ssv", "utf-8")
assert.Nil(t, err)
allLines, err = importer.ParseDsvFileLines(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) {
@@ -101,7 +77,7 @@ func TestCustomTransactionDataDsvFileImporter_MinimumValidData(t *testing.T) {
"E": models.TRANSACTION_TYPE_EXPENSE,
"T": models.TRANSACTION_TYPE_TRANSFER,
}
importer, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", ".", "", "", "", "", "")
converter, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", ".", "", "", "", "", "")
assert.Nil(t, err)
context := core.NewNullContext()
@@ -111,11 +87,11 @@ func TestCustomTransactionDataDsvFileImporter_MinimumValidData(t *testing.T) {
DefaultCurrency: "CNY",
}
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := importer.ParseImportedData(context, user, []byte(
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := converter.ParseImportedData(context, user, []byte(
"2024-09-01 00:00:00,B,123.45\n"+
"2024-09-01 01:23:45,I,0.12\n"+
"2024-09-01 12:34:56,E,1.00\n"+
"2024-09-01 23:59:59,T,0.05"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"2024-09-01 23:59:59,T,0.05"), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
@@ -192,7 +168,7 @@ func TestCustomTransactionDataDsvFileImporter_WithAllSupportedColumns(t *testing
"Expense": models.TRANSACTION_TYPE_EXPENSE,
"Transfer": models.TRANSACTION_TYPE_TRANSFER,
}
importer, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, true, "YYYY-MM-DD HH:mm:ss", "", ".", "", " ", "lonlat", ";")
converter, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, true, "YYYY-MM-DD HH:mm:ss", "", ".", "", " ", "lonlat", ";")
assert.Nil(t, err)
context := core.NewNullContext()
@@ -202,12 +178,12 @@ func TestCustomTransactionDataDsvFileImporter_WithAllSupportedColumns(t *testing
DefaultCurrency: "CNY",
}
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := importer.ParseImportedData(context, user, []byte(
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := converter.ParseImportedData(context, user, []byte(
"\"Time\",\"Timezone\",\"Type\",\"Category\",\"Sub Category\",\"Account\",\"Account Currency\",\"Amount\",\"Account2\",\"Account2 Currency\",\"Account2 Amount\",\"Geographic Location\",\"Tags\",\"Description\"\n"+
"\"2024-09-01 00:00:00\",\"+08:00\",\"Balance Modification\",\"\",\"\",\"Test Account\",\"CNY\",\"123.45\",\"\",\"\",\"\",\"\",\"\",\"\"\n"+
"\"2024-09-01 01:23:45\",\"+08:00\",\"Income\",\"Test Category\",\"Test Sub Category\",\"Test Account\",\"CNY\",\"0.12\",\"\",\"\",\"\",\"123.450000 45.670000\",\"Test Tag;Test Tag2\",\"Hello World\"\n"+
"\"2024-09-01 12:34:56\",\"+00:00\",\"Expense\",\"Test Category2\",\"Test Sub Category2\",\"Test Account\",\"CNY\",\"1.00\",\"\",\"\",\"\",\"\",\"Test Tag\",\"Foo#Bar\"\n"+
"\"2024-09-01 23:59:59\",\"-05:00\",\"Transfer\",\"Test Category3\",\"Test Sub Category3\",\"Test Account\",\"CNY\",\"0.05\",\"Test Account2\",\"USD\",\"0.35\",\"\",\"Test Tag2\",\"foo\tbar\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"\"2024-09-01 23:59:59\",\"-05:00\",\"Transfer\",\"Test Category3\",\"Test Sub Category3\",\"Test Account\",\"CNY\",\"0.05\",\"Test Account2\",\"USD\",\"0.35\",\"\",\"Test Tag2\",\"foo\tbar\""), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
@@ -285,7 +261,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidTime(t *testing.T) {
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", "", ".", "", "", "", "")
converter, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ".", "", "", "", "")
assert.Nil(t, err)
context := core.NewNullContext()
@@ -295,12 +271,12 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidTime(t *testing.T) {
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"2024-09-01T12:34:56,E,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
"2024-09-01T12:34:56,E,123.45"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"09/01/2024 12:34:56,E,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
"09/01/2024 12:34:56,E,123.45"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
}
@@ -316,7 +292,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseTransactionWithoutType(t *tes
"E": models.TRANSACTION_TYPE_EXPENSE,
"T": models.TRANSACTION_TYPE_TRANSFER,
}
importer, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ".", "", "", "", "")
converter, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ".", "", "", "", "")
assert.Nil(t, err)
context := core.NewNullContext()
@@ -326,8 +302,8 @@ func TestCustomTransactionDataDsvFileImporter_ParseTransactionWithoutType(t *tes
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,A,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,A,123.45"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrNotFoundTransactionDataInFile.Message)
}
@@ -340,7 +316,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidType(t *testing.T) {
transactionTypeMapping := map[string]models.TransactionType{
"B": 0,
}
importer, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ".", "", "", "", "")
converter, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ".", "", "", "", "")
assert.Nil(t, err)
context := core.NewNullContext()
@@ -350,8 +326,8 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidType(t *testing.T) {
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,B,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,B,123.45"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrTransactionTypeInvalid.Message)
}
@@ -364,7 +340,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseTimeWithTimezone(t *testing.T
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:ssZ", "", ".", "", "", "", "")
converter, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ssZ", "", ".", "", "", "", "")
assert.Nil(t, err)
context := core.NewNullContext()
@@ -374,20 +350,20 @@ func TestCustomTransactionDataDsvFileImporter_ParseTimeWithTimezone(t *testing.T
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56-10:00,E,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56-10:00,E,123.45"), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, int64(1725230096), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56+00:00,E,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56+00:00,E,123.45"), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, int64(1725194096), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56+12:45,E,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56+12:45,E,123.45"), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, int64(1725148196), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
@@ -402,7 +378,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseTimeWithTimezone2(t *testing.
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:ssZZ", "", ".", "", "", "", "")
converter, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ssZZ", "", ".", "", "", "", "")
assert.Nil(t, err)
context := core.NewNullContext()
@@ -412,20 +388,20 @@ func TestCustomTransactionDataDsvFileImporter_ParseTimeWithTimezone2(t *testing.
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56-1000,E,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56-1000,E,123.45"), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, int64(1725230096), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56+0000,E,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56+0000,E,123.45"), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, int64(1725194096), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56+1245,E,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56+1245,E,123.45"), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, int64(1725148196), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
@@ -441,7 +417,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseValidTimezone(t *testing.T) {
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", "", ".", "", "", "", "")
converter, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ".", "", "", "", "")
assert.Nil(t, err)
context := core.NewNullContext()
@@ -451,20 +427,20 @@ func TestCustomTransactionDataDsvFileImporter_ParseValidTimezone(t *testing.T) {
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,-10:00,E,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,-10:00,E,123.45"), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, int64(1725230096), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,+00:00,E,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,+00:00,E,123.45"), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, int64(1725194096), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,+12:45,E,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,+12:45,E,123.45"), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, int64(1725148196), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
@@ -480,7 +456,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseValidTimezone2(t *testing.T)
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", "ZZ", ".", "", "", "", "")
converter, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "ZZ", ".", "", "", "", "")
assert.Nil(t, err)
context := core.NewNullContext()
@@ -490,20 +466,20 @@ func TestCustomTransactionDataDsvFileImporter_ParseValidTimezone2(t *testing.T)
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,-1000,E,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,-1000,E,123.45"), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, int64(1725230096), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,+0000,E,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,+0000,E,123.45"), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, int64(1725194096), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,+1245,E,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,+1245,E,123.45"), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, int64(1725148196), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
@@ -519,7 +495,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidTimezoneFormat(t *test
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", "z", ".", "", "", "", "")
converter, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "z", ".", "", "", "", "")
assert.Nil(t, err)
context := core.NewNullContext()
@@ -529,8 +505,8 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidTimezoneFormat(t *test
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,CST,E,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,CST,E,123.45"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrImportFileTransactionTimezoneFormatInvalid.Message)
}
@@ -544,7 +520,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidTimezone(t *testing.T)
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", "", ".", "", "", "", "")
converter, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ".", "", "", "", "")
assert.Nil(t, err)
context := core.NewNullContext()
@@ -554,12 +530,12 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidTimezone(t *testing.T)
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,Asia/Shanghai,E,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,Asia/Shanghai,E,123.45"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrTransactionTimeZoneInvalid.Message)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,-0700,E,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,-0700,E,123.45"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrTransactionTimeZoneInvalid.Message)
}
@@ -573,7 +549,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidTimezone2(t *testing.T
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", "ZZ", ".", "", "", "", "")
converter, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "ZZ", ".", "", "", "", "")
assert.Nil(t, err)
context := core.NewNullContext()
@@ -583,12 +559,12 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidTimezone2(t *testing.T
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,Asia/Shanghai,E,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,Asia/Shanghai,E,123.45"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrTransactionTimeZoneInvalid.Message)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,0700,E,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,0700,E,123.45"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrTransactionTimeZoneInvalid.Message)
}
@@ -601,7 +577,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseAmountWithCustomFormat(t *tes
transactionTypeMapping := map[string]models.TransactionType{
"E": models.TRANSACTION_TYPE_EXPENSE,
}
importer, err := CreateNewCustomTransactionDataDsvFileImporter("custom_tsv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ",", ".", "", "", "")
converter, err := CreateNewCustomTransactionDataDsvFileImporter("custom_tsv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ",", ".", "", "", "")
assert.Nil(t, err)
context := core.NewNullContext()
@@ -611,8 +587,8 @@ func TestCustomTransactionDataDsvFileImporter_ParseAmountWithCustomFormat(t *tes
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56\tE\t1.234,56"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56\tE\t1.234,56"), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, int64(123456), allNewTransactions[0].Amount)
@@ -627,7 +603,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidAmountWithCustomFormat
transactionTypeMapping := map[string]models.TransactionType{
"E": models.TRANSACTION_TYPE_EXPENSE,
}
importer, err := CreateNewCustomTransactionDataDsvFileImporter("custom_tsv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ".", ",", "", "", "")
converter, err := CreateNewCustomTransactionDataDsvFileImporter("custom_tsv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ".", ",", "", "", "")
assert.Nil(t, err)
context := core.NewNullContext()
@@ -637,8 +613,8 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidAmountWithCustomFormat
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56\tE\t1.234,56"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56\tE\t1.234,56"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
}
@@ -651,7 +627,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidAmountWithCustomFormat
transactionTypeMapping := map[string]models.TransactionType{
"E": models.TRANSACTION_TYPE_EXPENSE,
}
importer, err := CreateNewCustomTransactionDataDsvFileImporter("custom_tsv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ",", "", "", "", "")
converter, err := CreateNewCustomTransactionDataDsvFileImporter("custom_tsv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ",", "", "", "", "")
assert.Nil(t, err)
context := core.NewNullContext()
@@ -661,8 +637,8 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidAmountWithCustomFormat
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56\tE\t1.234,56"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56\tE\t1.234,56"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
}
@@ -679,7 +655,7 @@ func TestCustomTransactionDataDsvFileImporter_ParsePrimaryCategory(t *testing.T)
"E": models.TRANSACTION_TYPE_EXPENSE,
"T": models.TRANSACTION_TYPE_TRANSFER,
}
importer, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ".", "", "", "", "")
converter, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ".", "", "", "", "")
assert.Nil(t, err)
context := core.NewNullContext()
@@ -689,11 +665,11 @@ func TestCustomTransactionDataDsvFileImporter_ParsePrimaryCategory(t *testing.T)
DefaultCurrency: "CNY",
}
allNewTransactions, _, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, _, err := importer.ParseImportedData(context, user, []byte(
allNewTransactions, _, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, _, err := converter.ParseImportedData(context, user, []byte(
"2024-09-01 00:00:00,B,,123.45\n"+
"2024-09-01 01:23:45,I,Test Category,0.12\n"+
"2024-09-01 12:34:56,E,Test Category2,1.00\n"+
"2024-09-01 23:59:59,T,Test Category3,0.05"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"2024-09-01 23:59:59,T,Test Category3,0.05"), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
@@ -748,7 +724,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseValidAccountCurrency(t *testi
"B": models.TRANSACTION_TYPE_MODIFY_BALANCE,
"T": models.TRANSACTION_TYPE_TRANSFER,
}
importer, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ".", "", "", "", "")
converter, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ".", "", "", "", "")
assert.Nil(t, err)
context := core.NewNullContext()
@@ -758,9 +734,9 @@ func TestCustomTransactionDataDsvFileImporter_ParseValidAccountCurrency(t *testi
DefaultCurrency: "CNY",
}
allNewTransactions, allNewAccounts, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
allNewTransactions, allNewAccounts, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
"2024-09-01 01:23:45,B,Test Account,USD,123.45,,,\n"+
"2024-09-01 12:34:56,T,Test Account,USD,1.23,Test Account2,EUR,1.10"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"2024-09-01 12:34:56,T,Test Account,USD,1.23,Test Account2,EUR,1.10"), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
@@ -791,7 +767,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidAccountCurrency(t *tes
"B": models.TRANSACTION_TYPE_MODIFY_BALANCE,
"T": models.TRANSACTION_TYPE_TRANSFER,
}
importer, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ".", "", "", "", "")
converter, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ".", "", "", "", "")
assert.Nil(t, err)
context := core.NewNullContext()
@@ -801,14 +777,14 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidAccountCurrency(t *tes
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
"2024-09-01 01:23:45,B,Test Account,USD,123.45,,,\n"+
"2024-09-01 12:34:56,T,Test Account,CNY,1.23,Test Account2,EUR,1.10"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"2024-09-01 12:34:56,T,Test Account,CNY,1.23,Test Account2,EUR,1.10"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
"2024-09-01 01:23:45,B,Test Account,USD,123.45,,,\n"+
"2024-09-01 12:34:56,T,Test Account2,CNY,1.23,Test Account,EUR,1.10"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"2024-09-01 12:34:56,T,Test Account2,CNY,1.23,Test Account,EUR,1.10"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message)
}
@@ -827,7 +803,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseNotSupportedCurrency(t *testi
"B": models.TRANSACTION_TYPE_MODIFY_BALANCE,
"T": models.TRANSACTION_TYPE_TRANSFER,
}
importer, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ".", "", "", "", "")
converter, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ".", "", "", "", "")
assert.Nil(t, err)
context := core.NewNullContext()
@@ -837,12 +813,12 @@ func TestCustomTransactionDataDsvFileImporter_ParseNotSupportedCurrency(t *testi
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"2024-09-01 01:23:45,B,Test Account,XXX,123.45,,,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
"2024-09-01 01:23:45,B,Test Account,XXX,123.45,,,"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"2024-09-01 01:23:45,T,Test Account,USD,123.45,Test Account2,XXX,123.45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
"2024-09-01 01:23:45,T,Test Account,USD,123.45,Test Account2,XXX,123.45"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message)
}
@@ -859,7 +835,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseValidAmount(t *testing.T) {
"E": models.TRANSACTION_TYPE_EXPENSE,
"T": models.TRANSACTION_TYPE_TRANSFER,
}
importer, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ".", "", "", "", "")
converter, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ".", "", "", "", "")
assert.Nil(t, err)
context := core.NewNullContext()
@@ -869,11 +845,11 @@ func TestCustomTransactionDataDsvFileImporter_ParseValidAmount(t *testing.T) {
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
"2024-09-01 00:00:00,B,123.45000000,\n"+
"2024-09-01 01:23:45,I,0.12000000,\n"+
"2024-09-01 12:34:56,E,1.00000000,\n"+
"2024-09-01 23:59:59,T,0.05000000,0.35000000"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"2024-09-01 23:59:59,T,0.05000000,0.35000000"), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
@@ -897,62 +873,6 @@ func TestCustomTransactionDataDsvFileImporter_ParseValidAmount(t *testing.T) {
assert.Equal(t, int64(35), allNewTransactions[3].RelatedAccountAmount)
}
func TestCustomTransactionDataDsvFileImporter_ParseAmountWithSpaceDigitGroupingSymbol(t *testing.T) {
columnIndexMapping := map[datatable.TransactionDataTableColumn]int{
datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME: 0,
datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE: 1,
datatable.TRANSACTION_DATA_TABLE_AMOUNT: 2,
}
transactionTypeMapping := map[string]models.TransactionType{
"E": models.TRANSACTION_TYPE_EXPENSE,
}
importer, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ".", " ", "", "", "")
assert.Nil(t, err)
context := core.NewNullContext()
user := &models.User{
Uid: 1234567890,
DefaultCurrency: "CNY",
}
// normal space
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"2024-09-01 00:00:00,E,1 234,\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, int64(123400), allNewTransactions[0].Amount)
// no-break space (NBSP)
allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"2024-09-01 00:00:00,E,1 234,\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, int64(123400), allNewTransactions[0].Amount)
// narrow no-break space (NNBSP)
allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"2024-09-01 00:00:00,E,1234,\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, int64(123400), allNewTransactions[0].Amount)
// figure space
allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"2024-09-01 00:00:00,E,1234,\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, int64(123400), allNewTransactions[0].Amount)
}
func TestCustomTransactionDataDsvFileImporter_ParseInvalidAmount(t *testing.T) {
columnIndexMapping := map[datatable.TransactionDataTableColumn]int{
datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME: 0,
@@ -966,7 +886,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidAmount(t *testing.T) {
"E": models.TRANSACTION_TYPE_EXPENSE,
"T": models.TRANSACTION_TYPE_TRANSFER,
}
importer, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ".", "", "", "", "")
converter, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ".", "", "", "", "")
assert.Nil(t, err)
context := core.NewNullContext()
@@ -976,12 +896,12 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidAmount(t *testing.T) {
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,E,Test Account,123 45,,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,E,Test Account,123 45,,"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,T,Test Account,123.45,Test Account2,123 45"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,T,Test Account,123.45,Test Account2,123 45"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
}
@@ -997,7 +917,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseNoAmount2(t *testing.T) {
"E": models.TRANSACTION_TYPE_EXPENSE,
"T": models.TRANSACTION_TYPE_TRANSFER,
}
importer, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ".", "", "", "", "")
converter, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ".", "", "", "", "")
assert.Nil(t, err)
context := core.NewNullContext()
@@ -1007,15 +927,15 @@ func TestCustomTransactionDataDsvFileImporter_ParseNoAmount2(t *testing.T) {
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,E,Test Account,123.45,"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,E,Test Account,123.45,"), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, int64(12345), allNewTransactions[0].Amount)
assert.Equal(t, int64(0), allNewTransactions[0].RelatedAccountAmount)
allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,T,Test Account,123.45,Test Account2"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,T,Test Account,123.45,Test Account2"), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, int64(12345), allNewTransactions[0].Amount)
@@ -1032,7 +952,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseValidGeographicLocation(t *te
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", "", ".", "", ";", "lonlat", "")
converter, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ".", "", ";", "lonlat", "")
assert.Nil(t, err)
context := core.NewNullContext()
@@ -1042,8 +962,8 @@ func TestCustomTransactionDataDsvFileImporter_ParseValidGeographicLocation(t *te
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,E,123.45,123.45;45.56"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,E,123.45,123.45;45.56"), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
@@ -1061,7 +981,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidGeographicLocation(t *
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", "", ".", "", " ", "lonlat", "")
converter, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ".", "", " ", "lonlat", "")
assert.Nil(t, err)
context := core.NewNullContext()
@@ -1071,15 +991,15 @@ func TestCustomTransactionDataDsvFileImporter_ParseInvalidGeographicLocation(t *
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,E,123.45,,,1"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,E,123.45,,,1"), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, float64(0), allNewTransactions[0].GeoLongitude)
assert.Equal(t, float64(0), allNewTransactions[0].GeoLatitude)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,E,123.45,a b"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,E,123.45,a b"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrGeographicLocationInvalid.Message)
}
@@ -1093,7 +1013,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseTag(t *testing.T) {
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", "", ".", "", "", "", ";")
converter, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ".", "", "", "", ";")
assert.Nil(t, err)
context := core.NewNullContext()
@@ -1103,8 +1023,8 @@ func TestCustomTransactionDataDsvFileImporter_ParseTag(t *testing.T) {
DefaultCurrency: "CNY",
}
_, _, _, _, _, allNewTags, err := importer.ParseImportedData(context, user, []byte(
"2024-09-01 00:00:00,E,123.45,foo;;bar.;#test;hello\tworld;;"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
_, _, _, _, _, allNewTags, err := converter.ParseImportedData(context, user, []byte(
"2024-09-01 00:00:00,E,123.45,foo;;bar.;#test;hello\tworld;;"), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
@@ -1133,7 +1053,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseTagWithoutSeparator(t *testin
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", "", ".", "", "", "", "")
converter, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ".", "", "", "", "")
assert.Nil(t, err)
context := core.NewNullContext()
@@ -1143,8 +1063,8 @@ func TestCustomTransactionDataDsvFileImporter_ParseTagWithoutSeparator(t *testin
DefaultCurrency: "CNY",
}
_, _, _, _, _, allNewTags, err := importer.ParseImportedData(context, user, []byte(
"2024-09-01 00:00:00,E,123.45,foo;;bar.;#test;hello\tworld;;"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
_, _, _, _, _, allNewTags, err := converter.ParseImportedData(context, user, []byte(
"2024-09-01 00:00:00,E,123.45,foo;;bar.;#test;hello\tworld;;"), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
@@ -1164,7 +1084,7 @@ func TestCustomTransactionDataDsvFileImporter_ParseDescription(t *testing.T) {
transactionTypeMapping := map[string]models.TransactionType{
"T": models.TRANSACTION_TYPE_TRANSFER,
}
importer, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ".", "", "", "", "")
converter, err := CreateNewCustomTransactionDataDsvFileImporter("custom_csv", "utf-8", columnIndexMapping, transactionTypeMapping, false, "YYYY-MM-DD HH:mm:ss", "", ".", "", "", "", "")
assert.Nil(t, err)
context := core.NewNullContext()
@@ -1174,8 +1094,8 @@ func TestCustomTransactionDataDsvFileImporter_ParseDescription(t *testing.T) {
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,T,123.45,foo bar\t#test"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
"2024-09-01 12:34:56,T,123.45,foo bar\t#test"), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
@@ -107,7 +107,6 @@ func (t *customPlainTextDataRowIterator) Next(ctx core.Context, user *models.Use
func (t *customPlainTextDataRowIterator) parseTransaction(ctx core.Context, user *models.User, row datatable.BasicDataTableRow) (map[datatable.TransactionDataTableColumn]string, bool, error) {
rowData := make(map[datatable.TransactionDataTableColumn]string, len(t.transactionDataTable.columnIndexMapping))
var transactionTime *time.Time = nil
for column, columnIndex := range t.transactionDataTable.columnIndexMapping {
if columnIndex < 0 || columnIndex >= row.ColumnCount() {
@@ -145,11 +144,10 @@ func (t *customPlainTextDataRowIterator) parseTransaction(ctx core.Context, user
return nil, false, errs.ErrTransactionTimeInvalid
}
transactionTime = &dateTime
rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME] = utils.FormatUnixTimeToLongDateTime(dateTime.Unix(), dateTime.Location())
if t.transactionDataTable.timeFormatIncludeTimezone {
rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE] = utils.FormatTimezoneOffset(dateTime.Unix(), dateTime.Location())
rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE] = utils.FormatTimezoneOffset(dateTime.Location())
}
}
@@ -166,19 +164,6 @@ func (t *customPlainTextDataRowIterator) parseTransaction(ctx core.Context, user
timezone = timezone[:3] + ":" + timezone[3:]
rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE] = timezone
} else if t.transactionDataTable.timezoneFormat == "zzz" { // IANA Timezone Name
timezoneName := rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE]
timezone, err := time.LoadLocation(timezoneName)
if err != nil {
return nil, false, errs.ErrTransactionTimeZoneInvalid
}
if transactionTime == nil {
return nil, false, errs.ErrTransactionTimeInvalid
}
rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE] = utils.FormatTimezoneOffset(transactionTime.Unix(), timezone)
} else {
return nil, false, errs.ErrImportFileTransactionTimezoneFormatInvalid
}
@@ -230,12 +215,6 @@ func (t *customPlainTextDataRowIterator) parseTransaction(ctx core.Context, user
func (t *customPlainTextDataRowIterator) parseAmount(ctx core.Context, amountValue string) (string, error) {
if t.transactionDataTable.amountDigitGroupingSymbol != "" {
amountValue = strings.ReplaceAll(amountValue, t.transactionDataTable.amountDigitGroupingSymbol, "")
if t.transactionDataTable.amountDigitGroupingSymbol == " " {
amountValue = strings.ReplaceAll(amountValue, "\u00A0", "") // No-Break Space (NBSP)
amountValue = strings.ReplaceAll(amountValue, "\u202F", "") // Narrow No-Break Space (NNBSP)
amountValue = strings.ReplaceAll(amountValue, "\u2007", "") // Figure Space
}
}
if t.transactionDataTable.amountDecimalSeparator != "" && t.transactionDataTable.amountDecimalSeparator != "." {
@@ -3,7 +3,6 @@ package feidee
import (
"bytes"
"strings"
"time"
"golang.org/x/text/encoding/unicode"
"golang.org/x/text/transform"
@@ -28,9 +27,6 @@ const feideeMymoneyAppTransactionAccountCurrencyColumnName = "账户币种"
const feideeMymoneyAppTransactionAmountColumnName = "金额"
const feideeMymoneyAppTransactionDescriptionColumnName = "备注"
const feideeMymoneyAppTransactionRelatedIdColumnName = "关联Id"
const feideeMymoneyAppTransactionMemberColumnName = "成员"
const feideeMymoneyAppTransactionProjectColumnName = "项目"
const feideeMymoneyAppTransactionMerchantColumnName = "商家"
const feideeMymoneyAppTransactionTypeModifyBalanceText = "余额变更"
const feideeMymoneyAppTransactionTypeModifyOutstandingBalanceText = "负债变更"
@@ -48,9 +44,6 @@ var feideeMymoneyAppDataColumnNameMapping = map[datatable.TransactionDataTableCo
datatable.TRANSACTION_DATA_TABLE_ACCOUNT_CURRENCY: feideeMymoneyAppTransactionAccountCurrencyColumnName,
datatable.TRANSACTION_DATA_TABLE_AMOUNT: feideeMymoneyAppTransactionAmountColumnName,
datatable.TRANSACTION_DATA_TABLE_DESCRIPTION: feideeMymoneyAppTransactionDescriptionColumnName,
datatable.TRANSACTION_DATA_TABLE_MEMBER: feideeMymoneyAppTransactionMemberColumnName,
datatable.TRANSACTION_DATA_TABLE_PROJECT: feideeMymoneyAppTransactionProjectColumnName,
datatable.TRANSACTION_DATA_TABLE_MERCHANT: feideeMymoneyAppTransactionMerchantColumnName,
}
// feideeMymoneyAppTransactionDataCsvFileImporter defines the structure of feidee mymoney app csv importer for transaction data
@@ -62,7 +55,7 @@ var (
)
// ParseImportedData returns the imported data by parsing the feidee mymoney app transaction csv data
func (c *feideeMymoneyAppTransactionDataCsvFileImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, 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) {
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) {
fallback := unicode.UTF8.NewDecoder()
reader := transform.NewReader(bytes.NewReader(data), unicode.BOMOverride(fallback))
@@ -98,7 +91,7 @@ func (c *feideeMymoneyAppTransactionDataCsvFileImporter) ParseImportedData(ctx c
dataTableImporter := converter.CreateNewSimpleImporterWithTypeNameMapping(feideeMymoneyTransactionTypeNameMapping)
return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezone, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezoneOffset, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
}
func (c *feideeMymoneyAppTransactionDataCsvFileImporter) createNewFeideeMymoneyAppTransactionDataTable(ctx core.Context, commonDataTable datatable.CommonDataTable) (datatable.TransactionDataTable, error) {
@@ -130,18 +123,6 @@ func (c *feideeMymoneyAppTransactionDataCsvFileImporter) createNewFeideeMymoneyA
newColumns = append(newColumns, datatable.TRANSACTION_DATA_TABLE_DESCRIPTION)
}
if commonDataTable.HasColumn(feideeMymoneyAppTransactionMemberColumnName) {
newColumns = append(newColumns, datatable.TRANSACTION_DATA_TABLE_MEMBER)
}
if commonDataTable.HasColumn(feideeMymoneyAppTransactionProjectColumnName) {
newColumns = append(newColumns, datatable.TRANSACTION_DATA_TABLE_PROJECT)
}
if commonDataTable.HasColumn(feideeMymoneyAppTransactionMerchantColumnName) {
newColumns = append(newColumns, datatable.TRANSACTION_DATA_TABLE_MERCHANT)
}
transactionRowParser := createFeideeMymoneyTransactionDataRowParser()
transactionDataTable := datatable.CreateNewWritableTransactionDataTableWithRowParser(newColumns, transactionRowParser)
transferTransactionsMap := make(map[string]map[datatable.TransactionDataTableColumn]string, 0)
@@ -6,7 +6,6 @@ import (
"github.com/stretchr/testify/assert"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/models"
@@ -14,7 +13,7 @@ import (
)
func TestFeideeMymoneyCsvFileImporterParseImportedData_MinimumValidData(t *testing.T) {
importer := FeideeMymoneyAppTransactionDataCsvFileImporter
converter := FeideeMymoneyAppTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -22,7 +21,7 @@ func TestFeideeMymoneyCsvFileImporterParseImportedData_MinimumValidData(t *testi
DefaultCurrency: "CNY",
}
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := importer.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
"\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+
"\"余额变更\",\"2024-09-01 00:00:00\",\"\",\"Test Account\",\"123.45\",\"\",\"\"\n"+
"\"余额变更\",\"2024-09-01 01:00:00\",\"\",\"Test Account2\",\"-0.12\",\"\",\"\"\n"+
@@ -31,7 +30,7 @@ func TestFeideeMymoneyCsvFileImporterParseImportedData_MinimumValidData(t *testi
"\"转出\",\"2024-09-01 23:59:59\",\"Test Category3\",\"Test Account\",\"0.05\",\"\",\"00000000-0000-0000-0000-000000000001\"\n"+
"\"转入\",\"2024-09-01 23:59:59\",\"Test Category3\",\"Test Account2\",\"0.05\",\"\",\"00000000-0000-0000-0000-000000000001\"\n"+
"\"转入\",\"2024-09-02 23:59:59\",\"Test Category3\",\"Test Account\",\"0.5\",\"\",\"00000000-0000-0000-0000-000000000002\"\n"+
"\"转出\",\"2024-09-02 23:59:59\",\"Test Category3\",\"Test Account2\",\"0.5\",\"\",\"00000000-0000-0000-0000-000000000002\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"\"转出\",\"2024-09-02 23:59:59\",\"Test Category3\",\"Test Account2\",\"0.5\",\"\",\"00000000-0000-0000-0000-000000000002\""), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
@@ -111,7 +110,7 @@ func TestFeideeMymoneyCsvFileImporterParseImportedData_MinimumValidData(t *testi
}
func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseOutstandingBalanceModification(t *testing.T) {
importer := FeideeMymoneyAppTransactionDataCsvFileImporter
converter := FeideeMymoneyAppTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -119,10 +118,10 @@ func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseOutstandingBalanceMo
DefaultCurrency: "CNY",
}
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, _, _, err := importer.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, _, _, err := converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
"\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+
"\"负债变更\",\"2024-09-01 00:00:00\",\"\",\"Test Account\",\"123.45\",\"\",\"\"\n"+
"\"负债变更\",\"2024-09-01 01:00:00\",\"\",\"Test Account2\",\"-0.12\",\"\",\"\"\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"\"负债变更\",\"2024-09-01 01:00:00\",\"\",\"Test Account2\",\"-0.12\",\"\",\"\"\n"), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
@@ -161,7 +160,7 @@ func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseOutstandingBalanceMo
}
func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseInvalidTime(t *testing.T) {
importer := FeideeMymoneyAppTransactionDataCsvFileImporter
converter := FeideeMymoneyAppTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -169,19 +168,19 @@ func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseInvalidTime(t *testi
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
"\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+
"\"收入\",\"2024-09-01T12:34:56\",\"Test Category\",\"Test Account\",\"0.12\",\"\",\"\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"\"收入\",\"2024-09-01T12:34:56\",\"Test Category\",\"Test Account\",\"0.12\",\"\",\"\""), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
"\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+
"\"收入\",\"09/01/2024 12:34:56\",\"Test Category\",\"Test Account\",\"0.12\",\"\",\"\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"\"收入\",\"09/01/2024 12:34:56\",\"Test Category\",\"Test Account\",\"0.12\",\"\",\"\""), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
}
func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseInvalidType(t *testing.T) {
importer := FeideeMymoneyAppTransactionDataCsvFileImporter
converter := FeideeMymoneyAppTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -189,14 +188,14 @@ func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseInvalidType(t *testi
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
"\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+
"\"Type\",\"2024-09-01 12:34:56\",\"Test Category\",\"Test Account\",\"0.12\",\"\",\"\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"\"Type\",\"2024-09-01 12:34:56\",\"Test Category\",\"Test Account\",\"0.12\",\"\",\"\""), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrTransactionTypeInvalid.Message)
}
func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseValidAccountCurrency(t *testing.T) {
importer := FeideeMymoneyAppTransactionDataCsvFileImporter
converter := FeideeMymoneyAppTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -204,11 +203,11 @@ func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseValidAccountCurrency
DefaultCurrency: "CNY",
}
allNewTransactions, allNewAccounts, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
allNewTransactions, allNewAccounts, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
"\"交易类型\",\"日期\",\"子类别\",\"账户\",\"账户币种\",\"金额\",\"备注\",\"关联Id\"\n"+
"\"余额变更\",\"2024-09-01 01:23:45\",\"\",\"Test Account\",\"USD\",\"123.45\",\"\",\"\"\n"+
"\"转出\",\"2024-09-01 12:34:56\",\"Test Category3\",\"Test Account\",\"USD\",\"1.23\",\"\",\"00000000-0000-0000-0000-000000000001\"\n"+
"\"转入\",\"2024-09-01 12:34:56\",\"Test Category3\",\"Test Account2\",\"EUR\",\"1.10\",\"\",\"00000000-0000-0000-0000-000000000001\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"\"转入\",\"2024-09-01 12:34:56\",\"Test Category3\",\"Test Account2\",\"EUR\",\"1.10\",\"\",\"00000000-0000-0000-0000-000000000001\""), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
@@ -225,7 +224,7 @@ func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseValidAccountCurrency
}
func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseInvalidAccountCurrency(t *testing.T) {
importer := FeideeMymoneyAppTransactionDataCsvFileImporter
converter := FeideeMymoneyAppTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -233,23 +232,23 @@ func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseInvalidAccountCurren
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
"\"交易类型\",\"日期\",\"子类别\",\"账户\",\"账户币种\",\"金额\",\"备注\",\"关联Id\"\n"+
"\"余额变更\",\"2024-09-01 01:23:45\",\"\",\"Test Account\",\"USD\",\"123.45\",\"\",\"\"\n"+
"\"转出\",\"2024-09-01 12:34:56\",\"Test Category3\",\"Test Account\",\"CNY\",\"1.23\",\"\",\"00000000-0000-0000-0000-000000000001\"\n"+
"\"转入\",\"2024-09-01 12:34:56\",\"Test Category3\",\"Test Account2\",\"EUR\",\"1.10\",\"\",\"00000000-0000-0000-0000-000000000001\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"\"转入\",\"2024-09-01 12:34:56\",\"Test Category3\",\"Test Account2\",\"EUR\",\"1.10\",\"\",\"00000000-0000-0000-0000-000000000001\""), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
"\"交易类型\",\"日期\",\"子类别\",\"账户\",\"账户币种\",\"金额\",\"备注\",\"关联Id\"\n"+
"\"余额变更\",\"2024-09-01 01:23:45\",\"\",\"Test Account\",\"USD\",\"123.45\",\"\",\"\"\n"+
"\"转出\",\"2024-09-01 12:34:56\",\"Test Category3\",\"Test Account2\",\"CNY\",\"1.23\",\"\",\"00000000-0000-0000-0000-000000000001\"\n"+
"\"转入\",\"2024-09-01 12:34:56\",\"Test Category3\",\"Test Account\",\"EUR\",\"1.10\",\"\",\"00000000-0000-0000-0000-000000000001\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"\"转入\",\"2024-09-01 12:34:56\",\"Test Category3\",\"Test Account\",\"EUR\",\"1.10\",\"\",\"00000000-0000-0000-0000-000000000001\""), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message)
}
func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseNotSupportedCurrency(t *testing.T) {
importer := FeideeMymoneyAppTransactionDataCsvFileImporter
converter := FeideeMymoneyAppTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -257,26 +256,26 @@ func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseNotSupportedCurrency
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
"\"交易类型\",\"日期\",\"子类别\",\"账户\",\"账户币种\",\"金额\",\"备注\",\"关联Id\"\n"+
"\"余额变更\",\"2024-09-01 01:23:45\",\"\",\"Test Account\",\"XXX\",\"123.45\",\"\",\"\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"\"余额变更\",\"2024-09-01 01:23:45\",\"\",\"Test Account\",\"XXX\",\"123.45\",\"\",\"\""), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
"\"交易类型\",\"日期\",\"子类别\",\"账户\",\"账户币种\",\"金额\",\"备注\",\"关联Id\"\n"+
"\"转出\",\"2024-09-01 12:34:56\",\"Test Category\",\"Test Account\",\"USD\",\"123.45\",\"\",\"00000000-0000-0000-0000-000000000001\"\n"+
"\"转入\",\"2024-09-01 12:34:56\",\"Test Category\",\"Test Account2\",\"XXX\",\"123.45\",\"\",\"00000000-0000-0000-0000-000000000001\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"\"转入\",\"2024-09-01 12:34:56\",\"Test Category\",\"Test Account2\",\"XXX\",\"123.45\",\"\",\"00000000-0000-0000-0000-000000000001\""), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
"\"交易类型\",\"日期\",\"子类别\",\"账户\",\"账户币种\",\"金额\",\"备注\",\"关联Id\"\n"+
"\"转出\",\"2024-09-01 12:34:56\",\"Test Category\",\"Test Account\",\"XXX\",\"123.45\",\"\",\"00000000-0000-0000-0000-000000000001\"\n"+
"\"转入\",\"2024-09-01 12:34:56\",\"Test Category\",\"Test Account2\",\"USD\",\"123.45\",\"\",\"00000000-0000-0000-0000-000000000001\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"\"转入\",\"2024-09-01 12:34:56\",\"Test Category\",\"Test Account2\",\"USD\",\"123.45\",\"\",\"00000000-0000-0000-0000-000000000001\""), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message)
}
func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseInvalidAmount(t *testing.T) {
importer := FeideeMymoneyAppTransactionDataCsvFileImporter
converter := FeideeMymoneyAppTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -284,31 +283,31 @@ func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseInvalidAmount(t *tes
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
"\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+
"\"余额变更\",\"2024-09-01 01:23:45\",\"\",\"Test Account\",\"123 45\",\"\",\"\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"\"余额变更\",\"2024-09-01 01:23:45\",\"\",\"Test Account\",\"123 45\",\"\",\"\""), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
"\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+
"\"负债变更\",\"2024-09-01 01:23:45\",\"\",\"Test Account\",\"123 45\",\"\",\"\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"\"负债变更\",\"2024-09-01 01:23:45\",\"\",\"Test Account\",\"123 45\",\"\",\"\""), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
"\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+
"\"转出\",\"2024-09-01 12:34:56\",\"Test Category\",\"Test Account\",\"123 45\",\"\",\"00000000-0000-0000-0000-000000000001\"\n"+
"\"转入\",\"2024-09-01 12:34:56\",\"Test Category\",\"Test Account2\",\"123.45\",\"\",\"00000000-0000-0000-0000-000000000001\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"\"转入\",\"2024-09-01 12:34:56\",\"Test Category\",\"Test Account2\",\"123.45\",\"\",\"00000000-0000-0000-0000-000000000001\""), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
"\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+
"\"转出\",\"2024-09-01 12:34:56\",\"Test Category\",\"Test Account\",\"123.45\",\"\",\"00000000-0000-0000-0000-000000000001\"\n"+
"\"转入\",\"2024-09-01 12:34:56\",\"Test Category\",\"Test Account2\",\"123 45\",\"\",\"00000000-0000-0000-0000-000000000001\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"\"转入\",\"2024-09-01 12:34:56\",\"Test Category\",\"Test Account2\",\"123 45\",\"\",\"00000000-0000-0000-0000-000000000001\""), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
}
func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseDescription(t *testing.T) {
importer := FeideeMymoneyAppTransactionDataCsvFileImporter
converter := FeideeMymoneyAppTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -316,47 +315,18 @@ func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseDescription(t *testi
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
"\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+
"\"支出\",\"2024-09-01 12:34:56\",\"Test Category\",\"Test Account\",\"123.45\",\"Test\n"+
"A new line break\",\"\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"A new line break\",\"\""), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, "Test\nA new line break", allNewTransactions[0].Comment)
}
func TestFeideeMymoneyCsvFileImporterParseImportedData_WithAdditionalOptions(t *testing.T) {
importer := FeideeMymoneyAppTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
Uid: 1234567890,
DefaultCurrency: "CNY",
}
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
converter := FeideeMymoneyAppTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -364,41 +334,41 @@ func TestFeideeMymoneyCsvFileImporterParseImportedData_InvalidRelatedId(t *testi
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
"\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+
"\"转出\",\"2024-09-01 23:59:59\",\"Test Category3\",\"Test Account\",\"0.05\",\"\",\"\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"\"转出\",\"2024-09-01 23:59:59\",\"Test Category3\",\"Test Account\",\"0.05\",\"\",\"\""), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrRelatedIdCannotBeBlank.Message)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
"\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+
"\"转入\",\"2024-09-01 23:59:59\",\"Test Category3\",\"Test Account\",\"0.05\",\"\",\"\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"\"转入\",\"2024-09-01 23:59:59\",\"Test Category3\",\"Test Account\",\"0.05\",\"\",\"\""), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrRelatedIdCannotBeBlank.Message)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
"\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+
"\"转出\",\"2024-09-01 23:59:59\",\"Test Category3\",\"Test Account\",\"0.05\",\"\",\"00000000-0000-0000-0000-000000000001\"\n"+
"\"转入\",\"2024-09-02 23:59:59\",\"Test Category3\",\"Test Account\",\"0.5\",\"\",\"00000000-0000-0000-0000-000000000002\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"\"转入\",\"2024-09-02 23:59:59\",\"Test Category3\",\"Test Account\",\"0.5\",\"\",\"00000000-0000-0000-0000-000000000002\""), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrFoundRecordNotHasRelatedRecord.Message)
}
func TestFeideeMymoneyCsvFileImporterParseImportedData_MissingFileHeader(t *testing.T) {
importer := FeideeMymoneyAppTransactionDataCsvFileImporter
converter := FeideeMymoneyAppTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
Uid: 1,
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+
"\"余额变更\",\"2024-09-01 00:00:00\",\"\",\"Test Account\",\"123.45\",\"\",\"\"\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+
"\"余额变更\",\"2024-09-01 00:00:00\",\"\",\"Test Account\",\"123.45\",\"\",\"\"\n"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrInvalidFileHeader.Message)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(""), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrInvalidFileHeader.Message)
}
func TestFeideeMymoneyCsvFileImporterParseImportedData_MissingRequiredColumn(t *testing.T) {
importer := FeideeMymoneyAppTransactionDataCsvFileImporter
converter := FeideeMymoneyAppTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -407,38 +377,38 @@ func TestFeideeMymoneyCsvFileImporterParseImportedData_MissingRequiredColumn(t *
}
// Missing Time Column
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
"\"交易类型\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+
"\"余额变更\",\"\",\"Test Account\",\"123.45\",\"\",\"\"\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"\"余额变更\",\"\",\"Test Account\",\"123.45\",\"\",\"\"\n"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
// Missing Type Column
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
"\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+
"\"2024-09-01 00:00:00\",\"Test Category\",\"Test Account\",\"123.45\",\"\",\"\"\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"\"2024-09-01 00:00:00\",\"Test Category\",\"Test Account\",\"123.45\",\"\",\"\"\n"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
// Missing Sub Category Column
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
"\"交易类型\",\"日期\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+
"\"余额变更\",\"2024-09-01 00:00:00\",\"Test Account\",\"123.45\",\"\",\"\"\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"\"余额变更\",\"2024-09-01 00:00:00\",\"Test Account\",\"123.45\",\"\",\"\"\n"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
// Missing Account Name Column
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
"\"交易类型\",\"日期\",\"子类别\",\"金额\",\"备注\",\"关联Id\"\n"+
"\"余额变更\",\"2024-09-01 00:00:00\",\"\",\"123.45\",\"\",\"\"\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"\"余额变更\",\"2024-09-01 00:00:00\",\"\",\"123.45\",\"\",\"\"\n"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
// Missing Amount Column
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
"\"交易类型\",\"日期\",\"子类别\",\"账户\",\"备注\",\"关联Id\"\n"+
"\"余额变更\",\"2024-09-01 00:00:00\",\"\",\"Test Account\",\"\",\"\"\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"\"余额变更\",\"2024-09-01 00:00:00\",\"\",\"Test Account\",\"\",\"\"\n"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
// Missing Related ID Column
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
"\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\"\n"+
"\"余额变更\",\"2024-09-01 00:00:00\",\"\",\"Test Account\",\"123.45\",\"\"\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"\"余额变更\",\"2024-09-01 00:00:00\",\"\",\"Test Account\",\"123.45\",\"\"\n"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
}
@@ -1,8 +1,6 @@
package feidee
import (
"time"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
"github.com/mayswind/ezbookkeeping/pkg/converters/excel"
@@ -20,9 +18,6 @@ var feideeMymoneyElecloudDataColumnNameMapping = map[datatable.TransactionDataTa
datatable.TRANSACTION_DATA_TABLE_AMOUNT: "金额",
datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME: "账户2",
datatable.TRANSACTION_DATA_TABLE_DESCRIPTION: "备注",
datatable.TRANSACTION_DATA_TABLE_MEMBER: "成员",
datatable.TRANSACTION_DATA_TABLE_PROJECT: "项目",
datatable.TRANSACTION_DATA_TABLE_MERCHANT: "商家",
}
// feideeMymoneyElecloudTransactionDataXlsxFileImporter defines the structure of feidee mymoney (elecloud) xlsx importer for transaction data
@@ -36,7 +31,7 @@ var (
)
// ParseImportedData returns the imported data by parsing the feidee mymoney (elecloud) transaction xlsx data
func (c *feideeMymoneyElecloudTransactionDataXlsxFileImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, 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) {
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) {
dataTable, err := excel.CreateNewExcelOOXMLFileBasicDataTable(data, true)
if err != nil {
@@ -47,5 +42,5 @@ func (c *feideeMymoneyElecloudTransactionDataXlsxFileImporter) ParseImportedData
transactionDataTable := datatable.CreateNewTransactionDataTableFromBasicDataTableWithRowParser(dataTable, feideeMymoneyElecloudDataColumnNameMapping, transactionRowParser)
dataTableImporter := converter.CreateNewSimpleImporter(feideeMymoneyElecloudTransactionTypeNameMapping)
return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezone, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezoneOffset, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
}
@@ -7,14 +7,13 @@ import (
"github.com/stretchr/testify/assert"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
func TestFeideeMymoneyElecloudTransactionDataXlsxImporterParseImportedData_MinimumValidData(t *testing.T) {
importer := FeideeMymoneyElecloudTransactionDataXlsxFileImporter
converter := FeideeMymoneyElecloudTransactionDataXlsxFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -25,7 +24,7 @@ func TestFeideeMymoneyElecloudTransactionDataXlsxImporterParseImportedData_Minim
testdata, err := os.ReadFile("../../../testdata/feidee_mymoney_elecloud_test_file.xlsx")
assert.Nil(t, err)
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := importer.ParseImportedData(context, user, testdata, time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := converter.ParseImportedData(context, user, testdata, 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 7, len(allNewTransactions))
@@ -1,8 +1,6 @@
package feidee
import (
"time"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
"github.com/mayswind/ezbookkeeping/pkg/converters/excel"
@@ -19,9 +17,6 @@ var feideeMymoneyWebDataColumnNameMapping = map[datatable.TransactionDataTableCo
datatable.TRANSACTION_DATA_TABLE_AMOUNT: "金额",
datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME: "账户2",
datatable.TRANSACTION_DATA_TABLE_DESCRIPTION: "备注",
datatable.TRANSACTION_DATA_TABLE_MEMBER: "成员",
datatable.TRANSACTION_DATA_TABLE_PROJECT: "项目",
datatable.TRANSACTION_DATA_TABLE_MERCHANT: "商家",
}
// feideeMymoneyWebTransactionDataXlsFileImporter defines the structure of feidee mymoney (web) xls importer for transaction data
@@ -35,7 +30,7 @@ var (
)
// ParseImportedData returns the imported data by parsing the feidee mymoney (web) transaction xls data
func (c *feideeMymoneyWebTransactionDataXlsFileImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, 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) {
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) {
dataTable, err := excel.CreateNewExcelMSCFBFileBasicDataTable(data, true)
if err != nil {
@@ -46,5 +41,5 @@ func (c *feideeMymoneyWebTransactionDataXlsFileImporter) ParseImportedData(ctx c
transactionDataTable := datatable.CreateNewTransactionDataTableFromBasicDataTableWithRowParser(dataTable, feideeMymoneyWebDataColumnNameMapping, transactionRowParser)
dataTableImporter := converter.CreateNewSimpleImporterWithTypeNameMapping(feideeMymoneyTransactionTypeNameMapping)
return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezone, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezoneOffset, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
}
@@ -7,14 +7,13 @@ import (
"github.com/stretchr/testify/assert"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
func TestFeideeMymoneyTransactionDataXlsImporterParseImportedData_MinimumValidData(t *testing.T) {
importer := FeideeMymoneyWebTransactionDataXlsFileImporter
converter := FeideeMymoneyWebTransactionDataXlsFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -25,7 +24,7 @@ func TestFeideeMymoneyTransactionDataXlsImporterParseImportedData_MinimumValidDa
testdata, err := os.ReadFile("../../../testdata/feidee_mymoney_test_file.xls")
assert.Nil(t, err)
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := importer.ParseImportedData(context, user, testdata, time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := converter.ParseImportedData(context, user, testdata, 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 7, len(allNewTransactions))
@@ -2,7 +2,6 @@ package fireflyIII
import (
"bytes"
"time"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/converters/csv"
@@ -41,7 +40,7 @@ var (
)
// ParseImportedData returns the imported data by parsing the firefly III transaction csv data
func (c *fireflyIIITransactionDataCsvFileImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, 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) {
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) {
reader := bytes.NewReader(data)
dataTable, err := csv.CreateNewCsvBasicDataTable(ctx, reader, true)
@@ -53,5 +52,5 @@ func (c *fireflyIIITransactionDataCsvFileImporter) ParseImportedData(ctx core.Co
transactionDataTable := datatable.CreateNewTransactionDataTableFromBasicDataTableWithRowParser(dataTable, fireflyIIITransactionDataColumnNameMapping, transactionRowParser)
dataTableImporter := converter.CreateNewImporterWithTypeNameMapping(fireflyIIITransactionTypeNameMapping, "", "", ",")
return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezone, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezoneOffset, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
}
@@ -2,19 +2,17 @@ package fireflyIII
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
func TestFireFlyIIICsvFileimporterParseImportedData_MinimumValidData(t *testing.T) {
importer := FireflyIIITransactionDataCsvFileImporter
func TestFireFlyIIICsvFileConverterParseImportedData_MinimumValidData(t *testing.T) {
converter := FireflyIIITransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -22,11 +20,11 @@ func TestFireFlyIIICsvFileimporterParseImportedData_MinimumValidData(t *testing.
DefaultCurrency: "CNY",
}
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := importer.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name,category\n"+
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := converter.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name,category\n"+
"\"Opening balance\",123.45,2024-09-01T00:00:00+08:00,\"Initial balance for \"\"Test Account\"\"\",\"Test Account\",\n"+
"Deposit,0.12,2024-09-01T01:23:45+08:00,\"A revenue account\",\"Test Account\",\"Test Category\"\n"+
"Withdrawal,-1.00,2024-09-01T12:34:56+08:00,\"Test Account\",\"A expense account\",\"Test Category2\"\n"+
"Transfer,0.05,2024-09-01T23:59:59+08:00,\"Test Account\",\"Test Account2\",\"Test Category3\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"Transfer,0.05,2024-09-01T23:59:59+08:00,\"Test Account\",\"Test Account2\",\"Test Category3\""), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
@@ -84,8 +82,8 @@ func TestFireFlyIIICsvFileimporterParseImportedData_MinimumValidData(t *testing.
assert.Equal(t, "Test Category3", allNewSubTransferCategories[0].Name)
}
func TestFireFlyIIICsvFileimporterParseImportedData_ParseInvalidTime(t *testing.T) {
importer := FireflyIIITransactionDataCsvFileImporter
func TestFireFlyIIICsvFileConverterParseImportedData_ParseInvalidTime(t *testing.T) {
converter := FireflyIIITransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -93,17 +91,17 @@ func TestFireFlyIIICsvFileimporterParseImportedData_ParseInvalidTime(t *testing.
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name,category\n"+
"Withdrawal,-1.00,2024-09-01T12:34:56,\"Test Account\",\"A expense account\",\"Test Category\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name,category\n"+
"Withdrawal,-1.00,2024-09-01T12:34:56,\"Test Account\",\"A expense account\",\"Test Category\""), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name,category\n"+
"Withdrawal,-1.00,2024-09-01 12:34:56+08:00,\"Test Account\",\"A expense account\",\"Test Category\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name,category\n"+
"Withdrawal,-1.00,2024-09-01 12:34:56+08:00,\"Test Account\",\"A expense account\",\"Test Category\""), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
}
func TestFireFlyIIICsvFileimporterParseImportedData_ParseInvalidType(t *testing.T) {
importer := FireflyIIITransactionDataCsvFileImporter
func TestFireFlyIIICsvFileConverterParseImportedData_ParseInvalidType(t *testing.T) {
converter := FireflyIIITransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -111,13 +109,13 @@ func TestFireFlyIIICsvFileimporterParseImportedData_ParseInvalidType(t *testing.
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name,category\n"+
"Type,123.45,2024-09-01T12:34:56+08:00,\"Test Account\",\"A expense account\",\"Test Category\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name,category\n"+
"Type,123.45,2024-09-01T12:34:56+08:00,\"Test Account\",\"A expense account\",\"Test Category\""), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrTransactionTypeInvalid.Message)
}
func TestFireFlyIIICsvFileimporterParseImportedData_ParseAccountNameAsCategoryName(t *testing.T) {
importer := FireflyIIITransactionDataCsvFileImporter
func TestFireFlyIIICsvFileConverterParseImportedData_ParseAccountNameAsCategoryName(t *testing.T) {
converter := FireflyIIITransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -125,23 +123,23 @@ func TestFireFlyIIICsvFileimporterParseImportedData_ParseAccountNameAsCategoryNa
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name,category\n"+
"Withdrawal,-1.00,2024-09-01T12:34:56+08:00,\"Test Account\",\"A expense account\",\"\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name,category\n"+
"Withdrawal,-1.00,2024-09-01T12:34:56+08:00,\"Test Account\",\"A expense account\",\"\""), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, "A expense account", allNewTransactions[0].OriginalCategoryName)
allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name,category\n"+
"Deposit,10.00,2024-09-01T12:34:56+08:00,\"A revenue account\",\"Test Account\",\"\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name,category\n"+
"Deposit,10.00,2024-09-01T12:34:56+08:00,\"A revenue account\",\"Test Account\",\"\""), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, "A revenue account", allNewTransactions[0].OriginalCategoryName)
}
func TestFireFlyIIICsvFileimporterParseImportedData_ParseValidTimezone(t *testing.T) {
importer := FireflyIIITransactionDataCsvFileImporter
func TestFireFlyIIICsvFileConverterParseImportedData_ParseValidTimezone(t *testing.T) {
converter := FireflyIIITransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -149,27 +147,27 @@ func TestFireFlyIIICsvFileimporterParseImportedData_ParseValidTimezone(t *testin
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name,category\n"+
"Withdrawal,-1.00,2024-09-01T12:34:56-10:00,\"Test Account\",\"A expense account\",\"Test Category\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name,category\n"+
"Withdrawal,-1.00,2024-09-01T12:34:56-10:00,\"Test Account\",\"A expense account\",\"Test Category\""), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, int64(1725230096), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name,category\n"+
"Withdrawal,-1.00,2024-09-01T12:34:56+00:00,\"Test Account\",\"A expense account\",\"Test Category\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name,category\n"+
"Withdrawal,-1.00,2024-09-01T12:34:56+00:00,\"Test Account\",\"A expense account\",\"Test Category\""), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, int64(1725194096), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name,category\n"+
"Withdrawal,-1.00,2024-09-01T12:34:56+12:45,\"Test Account\",\"A expense account\",\"Test Category\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name,category\n"+
"Withdrawal,-1.00,2024-09-01T12:34:56+12:45,\"Test Account\",\"A expense account\",\"Test Category\""), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, int64(1725148196), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
}
func TestFireFlyIIICsvFileimporterParseImportedData_ParseValidAccountCurrency(t *testing.T) {
importer := FireflyIIITransactionDataCsvFileImporter
func TestFireFlyIIICsvFileConverterParseImportedData_ParseValidAccountCurrency(t *testing.T) {
converter := FireflyIIITransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -177,9 +175,9 @@ func TestFireFlyIIICsvFileimporterParseImportedData_ParseValidAccountCurrency(t
DefaultCurrency: "CNY",
}
allNewTransactions, allNewAccounts, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("type,amount,foreign_amount,date,currency_code,foreign_currency_code,source_name,destination_name,category\n"+
allNewTransactions, allNewAccounts, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("type,amount,foreign_amount,date,currency_code,foreign_currency_code,source_name,destination_name,category\n"+
"\"Opening balance\",123.45,,2024-09-01T00:00:00+08:00,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\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"Transfer,1.23,1.10,2024-09-01T23:59:59+08:00,USD,EUR,\"Test Account\",\"Test Account2\",\"Test Category2\""), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
@@ -195,8 +193,8 @@ func TestFireFlyIIICsvFileimporterParseImportedData_ParseValidAccountCurrency(t
assert.Equal(t, "EUR", allNewAccounts[1].Currency)
}
func TestFireFlyIIICsvFileimporterParseImportedData_ParseValidForeignAmountAndCurrency(t *testing.T) {
importer := FireflyIIITransactionDataCsvFileImporter
func TestFireFlyIIICsvFileConverterParseImportedData_ParseValidForeignAmountAndCurrency(t *testing.T) {
converter := FireflyIIITransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -204,8 +202,8 @@ func TestFireFlyIIICsvFileimporterParseImportedData_ParseValidForeignAmountAndCu
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("type,amount,foreign_amount,date,currency_code,foreign_currency_code,source_name,destination_name,category\n"+
"Transfer,10.00,15.00,2024-09-01T12:34:56+08:00,USD,EUR,\"Test Account\",\"Test Account2\",\"Test Category\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("type,amount,foreign_amount,date,currency_code,foreign_currency_code,source_name,destination_name,category\n"+
"Transfer,10.00,15.00,2024-09-01T12:34:56+08:00,USD,EUR,\"Test Account\",\"Test Account2\",\"Test Category\""), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
@@ -215,8 +213,8 @@ func TestFireFlyIIICsvFileimporterParseImportedData_ParseValidForeignAmountAndCu
assert.Equal(t, "USD", allNewTransactions[0].OriginalSourceAccountCurrency)
assert.Equal(t, "EUR", allNewTransactions[0].OriginalDestinationAccountCurrency)
allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("type,amount,date,currency_code,foreign_currency_code,source_name,destination_name,category\n"+
"Transfer,10.00,2024-09-01T12:34:56+08:00,USD,EUR,\"Test Account\",\"Test Account2\",\"Test Category\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("type,amount,date,currency_code,foreign_currency_code,source_name,destination_name,category\n"+
"Transfer,10.00,2024-09-01T12:34:56+08:00,USD,EUR,\"Test Account\",\"Test Account2\",\"Test Category\""), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
@@ -225,8 +223,8 @@ func TestFireFlyIIICsvFileimporterParseImportedData_ParseValidForeignAmountAndCu
assert.Equal(t, "USD", allNewTransactions[0].OriginalSourceAccountCurrency)
assert.Equal(t, "EUR", allNewTransactions[0].OriginalDestinationAccountCurrency)
allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("type,amount,date,currency_code,foreign_currency_code,source_name,destination_name,category\n"+
"Transfer,10.00,2024-09-01T12:34:56+08:00,USD,,\"Test Account\",\"Test Account2\",\"Test Category\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("type,amount,date,currency_code,foreign_currency_code,source_name,destination_name,category\n"+
"Transfer,10.00,2024-09-01T12:34:56+08:00,USD,,\"Test Account\",\"Test Account2\",\"Test Category\""), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
@@ -234,8 +232,8 @@ func TestFireFlyIIICsvFileimporterParseImportedData_ParseValidForeignAmountAndCu
assert.Equal(t, "USD", allNewTransactions[0].OriginalDestinationAccountCurrency)
}
func TestFireFlyIIICsvFileimporterParseImportedData_ParseInvalidAccountCurrency(t *testing.T) {
importer := FireflyIIITransactionDataCsvFileImporter
func TestFireFlyIIICsvFileConverterParseImportedData_ParseInvalidAccountCurrency(t *testing.T) {
converter := FireflyIIITransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -243,19 +241,19 @@ func TestFireFlyIIICsvFileimporterParseImportedData_ParseInvalidAccountCurrency(
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("type,amount,foreign_amount,date,currency_code,foreign_currency_code,source_name,destination_name,category\n"+
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("type,amount,foreign_amount,date,currency_code,foreign_currency_code,source_name,destination_name,category\n"+
"\"Opening balance\",123.45,,2024-09-01T00:00:00+08:00,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\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"Transfer,1.23,1.10,2024-09-01T23:59:59+08:00,CNY,EUR,\"Test Account\",\"Test Account2\",\"Test Category3\""), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("type,amount,foreign_amount,date,currency_code,foreign_currency_code,source_name,destination_name,category\n"+
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("type,amount,foreign_amount,date,currency_code,foreign_currency_code,source_name,destination_name,category\n"+
"\"Opening balance\",123.45,,2024-09-01T00:00:00+08:00,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\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"Transfer,1.23,1.10,2024-09-01T23:59:59+08:00,CNY,EUR,\"Test Account2\",\"Test Account\",\"Test Category3\""), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message)
}
func TestFireFlyIIICsvFileimporterParseImportedData_ParseNotSupportedCurrency(t *testing.T) {
importer := FireflyIIITransactionDataCsvFileImporter
func TestFireFlyIIICsvFileConverterParseImportedData_ParseNotSupportedCurrency(t *testing.T) {
converter := FireflyIIITransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -263,17 +261,17 @@ func TestFireFlyIIICsvFileimporterParseImportedData_ParseNotSupportedCurrency(t
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("type,amount,foreign_amount,date,currency_code,foreign_currency_code,source_name,destination_name,category\n"+
"\"Opening balance\",123.45,,2024-09-01T00:00:00+08:00,XXX,,\"Initial balance for \"\"Test Account\"\"\",\"Test Account\",\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("type,amount,foreign_amount,date,currency_code,foreign_currency_code,source_name,destination_name,category\n"+
"\"Opening balance\",123.45,,2024-09-01T00:00:00+08:00,XXX,,\"Initial balance for \"\"Test Account\"\"\",\"Test Account\",\n"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("type,amount,foreign_amount,date,currency_code,foreign_currency_code,source_name,destination_name,category\n"+
"Transfer,123.45,123.45,2024-09-01T23:59:59+08:00,USD,XXX,\"Test Account\",\"Test Account2\",\"Test Category2\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("type,amount,foreign_amount,date,currency_code,foreign_currency_code,source_name,destination_name,category\n"+
"Transfer,123.45,123.45,2024-09-01T23:59:59+08:00,USD,XXX,\"Test Account\",\"Test Account2\",\"Test Category2\""), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message)
}
func TestFireFlyIIICsvFileimporterParseImportedData_ParseInvalidAmount(t *testing.T) {
importer := FireflyIIITransactionDataCsvFileImporter
func TestFireFlyIIICsvFileConverterParseImportedData_ParseInvalidAmount(t *testing.T) {
converter := FireflyIIITransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -281,17 +279,17 @@ func TestFireFlyIIICsvFileimporterParseImportedData_ParseInvalidAmount(t *testin
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name,category\n"+
"Withdrawal,-123 45,2024-09-01T12:34:56+08:00,\"Test Account\",\"A expense account\",\"Test Category\"\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name,category\n"+
"Withdrawal,-123 45,2024-09-01T12:34:56+08:00,\"Test Account\",\"A expense account\",\"Test Category\"\n"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("type,amount,foreign_amount,date,source_name,destination_name,category\n"+
"Transfer,123.45,123 45,2024-09-01T23:59:59+08:00,\"Test Account\",\"Test Account2\",\"Test Category2\""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("type,amount,foreign_amount,date,source_name,destination_name,category\n"+
"Transfer,123.45,123 45,2024-09-01T23:59:59+08:00,\"Test Account\",\"Test Account2\",\"Test Category2\""), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
}
func TestFireFlyIIICsvFileimporterParseImportedData_ParseDescription(t *testing.T) {
importer := FireflyIIITransactionDataCsvFileImporter
func TestFireFlyIIICsvFileConverterParseImportedData_ParseDescription(t *testing.T) {
converter := FireflyIIITransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -299,16 +297,16 @@ func TestFireFlyIIICsvFileimporterParseImportedData_ParseDescription(t *testing.
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("type,amount,description,date,source_name,destination_name,category\n"+
"Withdrawal,-123.45,\"foo bar\t#test\",2024-09-01T12:34:56+08:00,\"Test Account\",\"A expense account\",\"Test Category\"\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("type,amount,description,date,source_name,destination_name,category\n"+
"Withdrawal,-123.45,\"foo bar\t#test\",2024-09-01T12:34:56+08:00,\"Test Account\",\"A expense account\",\"Test Category\"\n"), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, "foo bar\t#test", allNewTransactions[0].Comment)
}
func TestFireFlyIIICsvFileimporterParseImportedData_ParseTags(t *testing.T) {
importer := FireflyIIITransactionDataCsvFileImporter
func TestFireFlyIIICsvFileConverterParseImportedData_ParseTags(t *testing.T) {
converter := FireflyIIITransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -316,8 +314,8 @@ func TestFireFlyIIICsvFileimporterParseImportedData_ParseTags(t *testing.T) {
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, allNewTags, err := importer.ParseImportedData(context, user, []byte("type,amount,tags,date,source_name,destination_name,category\n"+
"Withdrawal,-123.45,\"tag1,tag2,tag3\",2024-09-01T12:34:56+08:00,\"Test Account\",\"A expense account\",\"Test Category\"\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, allNewTags, err := converter.ParseImportedData(context, user, []byte("type,amount,tags,date,source_name,destination_name,category\n"+
"Withdrawal,-123.45,\"tag1,tag2,tag3\",2024-09-01T12:34:56+08:00,\"Test Account\",\"A expense account\",\"Test Category\"\n"), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
@@ -330,8 +328,8 @@ func TestFireFlyIIICsvFileimporterParseImportedData_ParseTags(t *testing.T) {
assert.Equal(t, "tag3", allNewTags[2].Name)
}
func TestFireFlyIIICsvFileimporterParseImportedData_MissingFileHeader(t *testing.T) {
importer := FireflyIIITransactionDataCsvFileImporter
func TestFireFlyIIICsvFileConverterParseImportedData_MissingFileHeader(t *testing.T) {
converter := FireflyIIITransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -339,12 +337,12 @@ func TestFireFlyIIICsvFileimporterParseImportedData_MissingFileHeader(t *testing
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(""), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(""), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrNotFoundTransactionDataInFile.Message)
}
func TestFireFlyIIICsvFileimporterParseImportedData_MissingRequiredColumn(t *testing.T) {
importer := FireflyIIITransactionDataCsvFileImporter
func TestFireFlyIIICsvFileConverterParseImportedData_MissingRequiredColumn(t *testing.T) {
converter := FireflyIIITransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -353,32 +351,32 @@ func TestFireFlyIIICsvFileimporterParseImportedData_MissingRequiredColumn(t *tes
}
// Missing Time Column
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("type,amount,source_name,destination_name,category\n"+
"\"Opening balance\",123.45,\"Initial balance for \"\"Test Account\"\"\",\"Test Account\",\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("type,amount,source_name,destination_name,category\n"+
"\"Opening balance\",123.45,\"Initial balance for \"\"Test Account\"\"\",\"Test Account\",\n"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
// Missing Type Column
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("amount,date,source_name,destination_name,category\n"+
"123.45,2024-09-01T00:00:00+08:00,\"Initial balance for \"\"Test Account\"\"\",\"Test Account\",\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("amount,date,source_name,destination_name,category\n"+
"123.45,2024-09-01T00:00:00+08:00,\"Initial balance for \"\"Test Account\"\"\",\"Test Account\",\n"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
// Missing Sub Category Column
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name\n"+
"\"Opening balance\",123.45,2024-09-01T00:00:00+08:00,\"Initial balance for \"\"Test Account\"\"\",\"Test Account\"\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name\n"+
"\"Opening balance\",123.45,2024-09-01T00:00:00+08:00,\"Initial balance for \"\"Test Account\"\"\",\"Test Account\"\n"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
// Missing Account Name Column
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("type,amount,date,destination_name,category\n"+
"\"Opening balance\",123.45,2024-09-01T00:00:00+08:00,\"Test Account\",\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("type,amount,date,destination_name,category\n"+
"\"Opening balance\",123.45,2024-09-01T00:00:00+08:00,\"Test Account\",\n"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
// Missing Amount Column
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("type,date,source_name,destination_name,category\n"+
"\"Opening balance\",2024-09-01T00:00:00+08:00,\"Initial balance for \"\"Test Account\"\"\",\"Test Account\",\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("type,date,source_name,destination_name,category\n"+
"\"Opening balance\",2024-09-01T00:00:00+08:00,\"Initial balance for \"\"Test Account\"\"\",\"Test Account\",\n"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
// Missing Account2 Name Column
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte("type,amount,date,source_name,category\n"+
"\"Opening balance\",123.45,2024-09-01T00:00:00+08:00,\"Initial balance for \"\"Test Account\"\"\",\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("type,amount,date,source_name,category\n"+
"\"Opening balance\",123.45,2024-09-01T00:00:00+08:00,\"Initial balance for \"\"Test Account\"\"\",\n"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
}
@@ -44,7 +44,7 @@ func (p *fireflyIIITransactionDataRowParser) Parse(data map[datatable.Transactio
}
rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME] = utils.FormatUnixTimeToLongDateTime(dateTime.Unix(), dateTime.Location())
rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE] = utils.FormatTimezoneOffset(dateTime.Unix(), dateTime.Location())
rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE] = utils.FormatTimezoneOffset(dateTime.Location())
}
// trim trailing zero in decimal
@@ -1,8 +1,6 @@
package gnucash
import (
"time"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/models"
@@ -26,7 +24,7 @@ var (
)
// ParseImportedData returns the imported data by parsing the gnucash transaction data
func (c *gnucashTransactionDataImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, 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) {
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) {
gnucashDataReader, err := createNewGnuCashDatabaseReader(data)
if err != nil {
@@ -47,5 +45,5 @@ func (c *gnucashTransactionDataImporter) ParseImportedData(ctx core.Context, use
dataTableImporter := converter.CreateNewSimpleImporterWithTypeNameMapping(gnucashTransactionTypeNameMapping)
return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezone, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezoneOffset, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
}
@@ -4,11 +4,9 @@ import (
"bytes"
"compress/gzip"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/models"
@@ -187,7 +185,7 @@ const gnucashCommonValidDataCaseFooter = "</gnc:book>\n" +
"</gnc-v2>\n"
func TestGnuCashTransactionDatabaseFileParseImportedData_MinimumValidData(t *testing.T) {
importer := GnuCashTransactionDataImporter
converter := GnuCashTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -195,14 +193,14 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_MinimumValidData(t *tes
DefaultCurrency: "CNY",
}
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := importer.ParseImportedData(context, user, []byte(gnucashMinimumValidDataCase), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := converter.ParseImportedData(context, user, []byte(gnucashMinimumValidDataCase), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
checkParsedMinimumValidData(t, allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags)
}
func TestGnuCashTransactionDatabaseFileParseImportedData_GzippedMinimumValidData(t *testing.T) {
importer := GnuCashTransactionDataImporter
converter := GnuCashTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -219,14 +217,14 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_GzippedMinimumValidData
assert.Nil(t, err)
gzippedData := buffer.Bytes()
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := importer.ParseImportedData(context, user, gzippedData, time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := converter.ParseImportedData(context, user, gzippedData, 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
checkParsedMinimumValidData(t, allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags)
}
func TestGnuCashTransactionDatabaseFileParseImportedData_MinimumValidDataWithReversedSplitOrder(t *testing.T) {
importer := GnuCashTransactionDataImporter
converter := GnuCashTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -234,7 +232,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_MinimumValidDataWithRev
DefaultCurrency: "CNY",
}
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := importer.ParseImportedData(context, user, []byte("<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n"+
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := converter.ParseImportedData(context, user, []byte("<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n"+
"<gnc-v2\n"+
" xmlns:gnc=\"http://www.gnucash.org/XML/gnc\"\n"+
" xmlns:act=\"http://www.gnucash.org/XML/act\"\n"+
@@ -358,14 +356,14 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_MinimumValidDataWithRev
" </trn:splits>\n"+
"</gnc:transaction>\n"+
"</gnc:book>\n"+
"</gnc-v2>\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"</gnc-v2>\n"), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
checkParsedMinimumValidData(t, allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags)
}
func TestGnuCashTransactionDatabaseFileParseImportedData_ParseInvalidTime(t *testing.T) {
importer := GnuCashTransactionDataImporter
converter := GnuCashTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -373,7 +371,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseInvalidTime(t *tes
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
gnucashCommonValidDataCaseHeader+
"<gnc:transaction version=\"2.0.0\">\n"+
" <trn:date-posted>\n"+
@@ -390,10 +388,10 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseInvalidTime(t *tes
" </trn:split>\n"+
" </trn:splits>\n"+
"</gnc:transaction>\n"+
gnucashCommonValidDataCaseFooter), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
gnucashCommonValidDataCaseFooter), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
gnucashCommonValidDataCaseHeader+
"<gnc:transaction version=\"2.0.0\">\n"+
" <trn:date-posted>\n"+
@@ -410,12 +408,12 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseInvalidTime(t *tes
" </trn:split>\n"+
" </trn:splits>\n"+
"</gnc:transaction>\n"+
gnucashCommonValidDataCaseFooter), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
gnucashCommonValidDataCaseFooter), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
}
func TestGnuCashTransactionDatabaseFileParseImportedData_ParseValidTimezone(t *testing.T) {
importer := GnuCashTransactionDataImporter
converter := GnuCashTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -423,7 +421,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseValidTimezone(t *t
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
gnucashCommonValidDataCaseHeader+
"<gnc:transaction version=\"2.0.0\">\n"+
" <trn:date-posted>\n"+
@@ -440,12 +438,12 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseValidTimezone(t *t
" </trn:split>\n"+
" </trn:splits>\n"+
"</gnc:transaction>\n"+
gnucashCommonValidDataCaseFooter), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
gnucashCommonValidDataCaseFooter), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, int64(1725230096), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
gnucashCommonValidDataCaseHeader+
"<gnc:transaction version=\"2.0.0\">\n"+
" <trn:date-posted>\n"+
@@ -462,14 +460,14 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseValidTimezone(t *t
" </trn:split>\n"+
" </trn:splits>\n"+
"</gnc:transaction>\n"+
gnucashCommonValidDataCaseFooter), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
gnucashCommonValidDataCaseFooter), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, int64(1725148196), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
}
func TestGnuCashTransactionDatabaseFileParseImportedData_ParseValidAccountCurrency(t *testing.T) {
importer := GnuCashTransactionDataImporter
converter := GnuCashTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -477,7 +475,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseValidAccountCurren
DefaultCurrency: "CNY",
}
allNewTransactions, allNewAccounts, _, _, _, _, err := importer.ParseImportedData(context, user, []byte("<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n"+
allNewTransactions, allNewAccounts, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n"+
"<gnc-v2\n"+
" xmlns:gnc=\"http://www.gnucash.org/XML/gnc\"\n"+
" xmlns:act=\"http://www.gnucash.org/XML/act\"\n"+
@@ -559,7 +557,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseValidAccountCurren
" </trn:splits>\n"+
"</gnc:transaction>\n"+
"</gnc:book>\n"+
"</gnc-v2>\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"</gnc-v2>\n"), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
@@ -576,7 +574,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseValidAccountCurren
}
func TestGnuCashTransactionDatabaseFileParseImportedData_ParseValidAmount(t *testing.T) {
importer := GnuCashTransactionDataImporter
converter := GnuCashTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -584,7 +582,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseValidAmount(t *tes
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
gnucashCommonValidDataCaseHeader+
"<gnc:transaction version=\"2.0.0\">\n"+
" <trn:date-posted>\n"+
@@ -601,13 +599,13 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseValidAmount(t *tes
" </trn:split>\n"+
" </trn:splits>\n"+
"</gnc:transaction>\n"+
gnucashCommonValidDataCaseFooter), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
gnucashCommonValidDataCaseFooter), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, int64(1234500), allNewTransactions[0].Amount)
allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
gnucashCommonValidDataCaseHeader+
"<gnc:transaction version=\"2.0.0\">\n"+
" <trn:date-posted>\n"+
@@ -624,7 +622,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseValidAmount(t *tes
" </trn:split>\n"+
" </trn:splits>\n"+
"</gnc:transaction>\n"+
gnucashCommonValidDataCaseFooter), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
gnucashCommonValidDataCaseFooter), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
@@ -632,7 +630,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseValidAmount(t *tes
}
func TestGnuCashTransactionDatabaseFileParseImportedData_ParseInvalidAmount(t *testing.T) {
importer := GnuCashTransactionDataImporter
converter := GnuCashTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -640,7 +638,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseInvalidAmount(t *t
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
gnucashCommonValidDataCaseHeader+
"<gnc:transaction version=\"2.0.0\">\n"+
" <trn:date-posted>\n"+
@@ -657,10 +655,10 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseInvalidAmount(t *t
" </trn:split>\n"+
" </trn:splits>\n"+
"</gnc:transaction>\n"+
gnucashCommonValidDataCaseFooter), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
gnucashCommonValidDataCaseFooter), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
gnucashCommonValidDataCaseHeader+
"<gnc:transaction version=\"2.0.0\">\n"+
" <trn:date-posted>\n"+
@@ -677,10 +675,10 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseInvalidAmount(t *t
" </trn:split>\n"+
" </trn:splits>\n"+
"</gnc:transaction>\n"+
gnucashCommonValidDataCaseFooter), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
gnucashCommonValidDataCaseFooter), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
gnucashCommonValidDataCaseHeader+
"<gnc:transaction version=\"2.0.0\">\n"+
" <trn:date-posted>\n"+
@@ -697,12 +695,12 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseInvalidAmount(t *t
" </trn:split>\n"+
" </trn:splits>\n"+
"</gnc:transaction>\n"+
gnucashCommonValidDataCaseFooter), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
gnucashCommonValidDataCaseFooter), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
}
func TestGnuCashTransactionDatabaseFileParseImportedData_ParseDescription(t *testing.T) {
importer := GnuCashTransactionDataImporter
converter := GnuCashTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -710,7 +708,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseDescription(t *tes
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
gnucashCommonValidDataCaseHeader+
"<gnc:transaction version=\"2.0.0\">\n"+
" <trn:date-posted>\n"+
@@ -728,7 +726,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseDescription(t *tes
" </trn:split>\n"+
" </trn:splits>\n"+
"</gnc:transaction>\n"+
gnucashCommonValidDataCaseFooter), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
gnucashCommonValidDataCaseFooter), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
@@ -736,7 +734,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_ParseDescription(t *tes
}
func TestGnuCashTransactionDatabaseFileParseImportedData_SkipZeroAmountTransaction(t *testing.T) {
importer := GnuCashTransactionDataImporter
converter := GnuCashTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -744,7 +742,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_SkipZeroAmountTransacti
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
gnucashCommonValidDataCaseHeader+
"<gnc:transaction version=\"2.0.0\">\n"+
" <trn:date-posted>\n"+
@@ -757,12 +755,12 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_SkipZeroAmountTransacti
" </trn:split>\n"+
" </trn:splits>\n"+
"</gnc:transaction>\n"+
gnucashCommonValidDataCaseFooter), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
gnucashCommonValidDataCaseFooter), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrNotFoundTransactionDataInFile.Message)
}
func TestGnuCashTransactionDatabaseFileParseImportedData_NotSupportedToParseSplitTransaction(t *testing.T) {
importer := GnuCashTransactionDataImporter
converter := GnuCashTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -770,7 +768,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_NotSupportedToParseSpli
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
gnucashCommonValidDataCaseHeader+
"<gnc:account version=\"2.0.0\">\n"+
" <act:name>Test Category2</act:name>\n"+
@@ -807,12 +805,12 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_NotSupportedToParseSpli
" </trn:split>\n"+
" </trn:splits>\n"+
"</gnc:transaction>\n"+
gnucashCommonValidDataCaseFooter), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
gnucashCommonValidDataCaseFooter), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrNotSupportedSplitTransactions.Message)
}
func TestGnuCashTransactionDatabaseFileParseImportedData_MissingAccountRequiredNode(t *testing.T) {
importer := GnuCashTransactionDataImporter
converter := GnuCashTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -821,7 +819,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_MissingAccountRequiredN
}
// Missing Account Currency Node
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
"<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n"+
"<gnc-v2\n"+
" xmlns:gnc=\"http://www.gnucash.org/XML/gnc\"\n"+
@@ -874,12 +872,12 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_MissingAccountRequiredN
" </trn:split>\n"+
" </trn:splits>\n"+
"</gnc:transaction>\n"+
gnucashCommonValidDataCaseFooter), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
gnucashCommonValidDataCaseFooter), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message)
}
func TestGnuCashTransactionDatabaseFileParseImportedData_MissingTransactionRequiredNode(t *testing.T) {
importer := GnuCashTransactionDataImporter
converter := GnuCashTransactionDataImporter
context := core.NewNullContext()
user := &models.User{
@@ -888,7 +886,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_MissingTransactionRequi
}
// Missing Transaction Time Node
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
gnucashCommonValidDataCaseHeader+
"<gnc:transaction version=\"2.0.0\">\n"+
" <trn:splits>\n"+
@@ -902,22 +900,22 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_MissingTransactionRequi
" </trn:split>\n"+
" </trn:splits>\n"+
"</gnc:transaction>\n"+
gnucashCommonValidDataCaseFooter), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
gnucashCommonValidDataCaseFooter), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrMissingTransactionTime.Message)
// Missing Transaction Splits Node
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
gnucashCommonValidDataCaseHeader+
"<gnc:transaction version=\"2.0.0\">\n"+
" <trn:date-posted>\n"+
" <ts:date>2024-09-01 00:00:00 +0000</ts:date>\n"+
" </trn:date-posted>\n"+
"</gnc:transaction>\n"+
gnucashCommonValidDataCaseFooter), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
gnucashCommonValidDataCaseFooter), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrInvalidGnuCashFile.Message)
// Missing Transaction Split Quantity Node
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
gnucashCommonValidDataCaseHeader+
"<gnc:transaction version=\"2.0.0\">\n"+
" <trn:date-posted>\n"+
@@ -932,11 +930,11 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_MissingTransactionRequi
" </trn:split>\n"+
" </trn:splits>\n"+
"</gnc:transaction>\n"+
gnucashCommonValidDataCaseFooter), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
gnucashCommonValidDataCaseFooter), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
// Missing Transaction Split Account Node
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
gnucashCommonValidDataCaseHeader+
"<gnc:transaction version=\"2.0.0\">\n"+
" <trn:date-posted>\n"+
@@ -952,7 +950,7 @@ func TestGnuCashTransactionDatabaseFileParseImportedData_MissingTransactionRequi
" </trn:split>\n"+
" </trn:splits>\n"+
"</gnc:transaction>\n"+
gnucashCommonValidDataCaseFooter), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
gnucashCommonValidDataCaseFooter), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrMissingAccountData.Message)
}
@@ -124,7 +124,7 @@ func (t *gnucashTransactionDataRowIterator) parseTransaction(ctx core.Context, u
}
data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME] = utils.FormatUnixTimeToLongDateTime(dateTime.Unix(), dateTime.Location())
data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE] = utils.FormatTimezoneOffset(dateTime.Unix(), dateTime.Location())
data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE] = utils.FormatTimezoneOffset(dateTime.Location())
if len(gnucashTransaction.Splits) == 2 {
splitData1 := gnucashTransaction.Splits[0]
@@ -1,8 +1,6 @@
package iif
import (
"time"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/models"
@@ -25,7 +23,7 @@ var (
)
// ParseImportedData returns the imported data by parsing the intuit interchange format (iif) data
func (c *iifTransactionDataFileImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, 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) {
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) {
iifDataReader := createNewIifDataReader(data)
accountDatasets, transactionDatasets, err := iifDataReader.read(ctx)
@@ -41,5 +39,5 @@ func (c *iifTransactionDataFileImporter) ParseImportedData(ctx core.Context, use
dataTableImporter := converter.CreateNewSimpleImporterWithTypeNameMapping(iifTransactionTypeNameMapping)
return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezone, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezoneOffset, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
}
@@ -2,11 +2,9 @@ package iif
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/models"
@@ -14,7 +12,7 @@ import (
)
func TestIIFTransactionDataFileParseImportedData_MinimumValidData(t *testing.T) {
importer := IifTransactionDataFileImporter
converter := IifTransactionDataFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -22,7 +20,7 @@ func TestIIFTransactionDataFileParseImportedData_MinimumValidData(t *testing.T)
DefaultCurrency: "CNY",
}
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := importer.ParseImportedData(context, user, []byte(
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := converter.ParseImportedData(context, user, []byte(
"!ACCNT\tNAME\tACCNTTYPE\n"+
"ACCNT\tTest Account\tBANK\n"+
"ACCNT\tTest Account2\tBANK\n"+
@@ -51,7 +49,7 @@ func TestIIFTransactionDataFileParseImportedData_MinimumValidData(t *testing.T)
"ENDTRNS\t\t\t\t\n"+
"TRNS\tCREDIT CARD\t09/07/2024\tTest Category2\t34.56\n"+
"SPL\tCREDIT CARD\t09/07/2024\tTest Account2\t-34.56\n"+
"ENDTRNS\t\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"ENDTRNS\t\t\t\t\n"), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
@@ -134,7 +132,7 @@ func TestIIFTransactionDataFileParseImportedData_MinimumValidData(t *testing.T)
}
func TestIIFTransactionDataFileParseImportedData_MinimumValidDataWithoutAccountData(t *testing.T) {
importer := IifTransactionDataFileImporter
converter := IifTransactionDataFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -142,13 +140,13 @@ func TestIIFTransactionDataFileParseImportedData_MinimumValidDataWithoutAccountD
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!SPL\tDATE\tACCNT\tAMOUNT\n"+
"!ENDTRNS\t\t\t\n"+
"TRNS\t09/01/2024\tTest Account\t123.45\n"+
"SPL\t09/01/2024\tTest Category\t-123.45\n"+
"ENDTRNS\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"ENDTRNS\t\t\t\n"), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
@@ -164,7 +162,7 @@ func TestIIFTransactionDataFileParseImportedData_MinimumValidDataWithoutAccountD
}
func TestIIFTransactionDataFileParseImportedData_MultipleDataset(t *testing.T) {
importer := IifTransactionDataFileImporter
converter := IifTransactionDataFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -172,7 +170,7 @@ func TestIIFTransactionDataFileParseImportedData_MultipleDataset(t *testing.T) {
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
"!ACCNT\tNAME\tACCNTTYPE\n"+
"ACCNT\tTest Account3\tBANK\n"+
"ACCNT\tTest Account4\tBANK\n"+
@@ -204,7 +202,7 @@ func TestIIFTransactionDataFileParseImportedData_MultipleDataset(t *testing.T) {
"ENDTRNS\t\t\t\t\n"+
"!ACCNT\tTEST\tNAME\tACCNTTYPE\n"+
"ACCNT\t\tTest Category\tINC\n"+
"ACCNT\t\tTest Category2\tEXP\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"ACCNT\t\tTest Category2\tEXP\n"), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
@@ -249,7 +247,7 @@ func TestIIFTransactionDataFileParseImportedData_MultipleDataset(t *testing.T) {
}
func TestIIFTransactionDataFileParseImportedData_ParseCategoryAndSubCategory(t *testing.T) {
importer := IifTransactionDataFileImporter
converter := IifTransactionDataFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -257,7 +255,7 @@ func TestIIFTransactionDataFileParseImportedData_ParseCategoryAndSubCategory(t *
DefaultCurrency: "CNY",
}
allNewTransactions, _, allNewSubExpenseCategories, allNewSubIncomeCategories, _, _, err := importer.ParseImportedData(context, user, []byte(
allNewTransactions, _, allNewSubExpenseCategories, allNewSubIncomeCategories, _, _, err := converter.ParseImportedData(context, user, []byte(
"!ACCNT\tNAME\tACCNTTYPE\n"+
"ACCNT\tTest Parent Category:Test Category\tINC\n"+
"ACCNT\tTest Parent Category2:Test Category2\tEXP\n"+
@@ -269,7 +267,7 @@ func TestIIFTransactionDataFileParseImportedData_ParseCategoryAndSubCategory(t *
"ENDTRNS\t\t\t\n"+
"TRNS\t09/02/2024\tTest Account2\t-123.45\n"+
"SPL\t09/02/2024\tTest Parent Category2:Test Category2\t123.45\n"+
"ENDTRNS\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"ENDTRNS\t\t\t\n"), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
@@ -301,7 +299,7 @@ func TestIIFTransactionDataFileParseImportedData_ParseCategoryAndSubCategory(t *
}
func TestIIFTransactionDataFileParseImportedData_ParseYearMonthDayFormatTime(t *testing.T) {
importer := IifTransactionDataFileImporter
converter := IifTransactionDataFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -309,7 +307,7 @@ func TestIIFTransactionDataFileParseImportedData_ParseYearMonthDayFormatTime(t *
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!SPL\tDATE\tACCNT\tAMOUNT\n"+
"!ENDTRNS\t\t\t\n"+
@@ -324,7 +322,7 @@ func TestIIFTransactionDataFileParseImportedData_ParseYearMonthDayFormatTime(t *
"ENDTRNS\t\t\t\n"+
"TRNS\t2024/9/4\tTest Account\t123.45\n"+
"SPL\t2024/9/4\tTest Account2\t-123.45\n"+
"ENDTRNS\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"ENDTRNS\t\t\t\n"), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
@@ -336,7 +334,7 @@ func TestIIFTransactionDataFileParseImportedData_ParseYearMonthDayFormatTime(t *
}
func TestIIFTransactionDataFileParseImportedData_ParseShortMonthDayYearFormatTime(t *testing.T) {
importer := IifTransactionDataFileImporter
converter := IifTransactionDataFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -344,7 +342,7 @@ func TestIIFTransactionDataFileParseImportedData_ParseShortMonthDayYearFormatTim
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!SPL\tDATE\tACCNT\tAMOUNT\n"+
"!ENDTRNS\t\t\t\n"+
@@ -356,7 +354,7 @@ func TestIIFTransactionDataFileParseImportedData_ParseShortMonthDayYearFormatTim
"ENDTRNS\t\t\t\n"+
"TRNS\t9/3/2024\tTest Account\t123.45\n"+
"SPL\t9/3/2024\tTest Account2\t-123.45\n"+
"ENDTRNS\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"ENDTRNS\t\t\t\n"), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
@@ -367,7 +365,7 @@ func TestIIFTransactionDataFileParseImportedData_ParseShortMonthDayYearFormatTim
}
func TestIIFTransactionDataFileParseImportedData_ParseShortMonthDayTwoDigitsYearFormatTime(t *testing.T) {
importer := IifTransactionDataFileImporter
converter := IifTransactionDataFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -375,7 +373,7 @@ func TestIIFTransactionDataFileParseImportedData_ParseShortMonthDayTwoDigitsYear
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!SPL\tDATE\tACCNT\tAMOUNT\n"+
"!ENDTRNS\t\t\t\n"+
@@ -387,7 +385,7 @@ func TestIIFTransactionDataFileParseImportedData_ParseShortMonthDayTwoDigitsYear
"ENDTRNS\t\t\t\n"+
"TRNS\t24/9/3\tTest Account\t123.45\n"+
"SPL\t24/9/3\tTest Account2\t-123.45\n"+
"ENDTRNS\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"ENDTRNS\t\t\t\n"), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
@@ -398,7 +396,7 @@ func TestIIFTransactionDataFileParseImportedData_ParseShortMonthDayTwoDigitsYear
}
func TestIIFTransactionDataFileParseImportedData_ParseInvalidTime(t *testing.T) {
importer := IifTransactionDataFileImporter
converter := IifTransactionDataFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -406,36 +404,36 @@ func TestIIFTransactionDataFileParseImportedData_ParseInvalidTime(t *testing.T)
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!SPL\tDATE\tACCNT\tAMOUNT\n"+
"!ENDTRNS\t\t\t\n"+
"TRNS\t09-01-2024\tTest Account\t123.45\n"+
"SPL\t09-01-2024\tTest Account2\t-123.45\n"+
"ENDTRNS\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"ENDTRNS\t\t\t\n"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!SPL\tDATE\tACCNT\tAMOUNT\n"+
"!ENDTRNS\t\t\t\n"+
"TRNS\t2024-09-01\tTest Account\t123.45\n"+
"SPL\t2024-09-01\tTest Account2\t-123.45\n"+
"ENDTRNS\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"ENDTRNS\t\t\t\n"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!SPL\tDATE\tACCNT\tAMOUNT\n"+
"!ENDTRNS\t\t\t\n"+
"TRNS\t9/24\tTest Account\t123.45\n"+
"SPL\t9/24\tTest Account2\t-123.45\n"+
"ENDTRNS\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"ENDTRNS\t\t\t\n"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
}
func TestIIFTransactionDataFileParseImportedData_ParseAmountWithThousandsSeparator(t *testing.T) {
importer := IifTransactionDataFileImporter
converter := IifTransactionDataFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -443,13 +441,13 @@ func TestIIFTransactionDataFileParseImportedData_ParseAmountWithThousandsSeparat
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!SPL\tDATE\tACCNT\tAMOUNT\n"+
"!ENDTRNS\t\t\t\n"+
"TRNS\t9/01/2024\tTest Account\t123,456.78\n"+
"SPL\t9/01/2024\tTest Account2\t-123,456.78\n"+
"ENDTRNS\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"ENDTRNS\t\t\t\n"), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
@@ -458,7 +456,7 @@ func TestIIFTransactionDataFileParseImportedData_ParseAmountWithThousandsSeparat
}
func TestIIFTransactionDataFileParseImportedData_ParseInvalidAmount(t *testing.T) {
importer := IifTransactionDataFileImporter
converter := IifTransactionDataFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -466,27 +464,27 @@ func TestIIFTransactionDataFileParseImportedData_ParseInvalidAmount(t *testing.T
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!SPL\tDATE\tACCNT\tAMOUNT\n"+
"!ENDTRNS\t\t\t\n"+
"TRNS\t09/01/2024\tTest Account\t123 45\n"+
"SPL\t09/01/2024\tTest Account2\t-123.45\n"+
"ENDTRNS\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"ENDTRNS\t\t\t\n"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!SPL\tDATE\tACCNT\tAMOUNT\n"+
"!ENDTRNS\t\t\t\n"+
"TRNS\t09/01/2024\tTest Account\t123.45\n"+
"SPL\t09/01/2024\tTest Account2\t-123 45\n"+
"ENDTRNS\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"ENDTRNS\t\t\t\n"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
}
func TestIIFTransactionDataFileParseImportedData_ParseDescription(t *testing.T) {
importer := IifTransactionDataFileImporter
converter := IifTransactionDataFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -494,25 +492,25 @@ func TestIIFTransactionDataFileParseImportedData_ParseDescription(t *testing.T)
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tNAME\tAMOUNT\tMEMO\n"+
"!SPL\tDATE\tACCNT\tNAME\tAMOUNT\tMEMO\n"+
"!ENDTRNS\t\t\t\t\t\n"+
"TRNS\t09/01/2024\tTest Account\t\"Test\"\t123.45\t\"foo bar\t#test\"\n"+
"SPL\t09/01/2024\tTest Account2\t\t-123.45\t\n"+
"ENDTRNS\t\t\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"ENDTRNS\t\t\t\t\t\n"), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, "foo bar\t#test", allNewTransactions[0].Comment)
allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tNAME\tAMOUNT\tMEMO\n"+
"!SPL\tDATE\tACCNT\tNAME\tAMOUNT\tMEMO\n"+
"!ENDTRNS\t\t\t\t\t\n"+
"TRNS\t09/01/2024\tTest Account\tTest\t123.45\t\n"+
"SPL\t09/01/2024\tTest Account2\t\t-123.45\t\n"+
"ENDTRNS\t\t\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"ENDTRNS\t\t\t\t\t\n"), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
@@ -520,7 +518,7 @@ func TestIIFTransactionDataFileParseImportedData_ParseDescription(t *testing.T)
}
func TestIIFTransactionDataFileParseImportedData_ParseSplitTransaction(t *testing.T) {
importer := IifTransactionDataFileImporter
converter := IifTransactionDataFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -528,7 +526,7 @@ func TestIIFTransactionDataFileParseImportedData_ParseSplitTransaction(t *testin
DefaultCurrency: "CNY",
}
allNewTransactions, allNewAccounts, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
allNewTransactions, allNewAccounts, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
"!ACCNT\tNAME\tACCNTTYPE\n"+
"ACCNT\tTest Category\tINC\n"+
"ACCNT\tTest Category2\tEXP\n"+
@@ -554,7 +552,7 @@ func TestIIFTransactionDataFileParseImportedData_ParseSplitTransaction(t *testin
"TRNS\t09/05/2024\tTest Category2\t100.00\n"+
"SPL\t09/05/2024\tTest Account3\t-40.00\n"+
"SPL\t09/05/2024\tTest Account4\t-60.00\n"+
"ENDTRNS\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"ENDTRNS\t\t\t\n"), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
@@ -653,7 +651,7 @@ func TestIIFTransactionDataFileParseImportedData_ParseSplitTransaction(t *testin
}
func TestIIFTransactionDataFileParseImportedData_ParseSplitTransactionDescription(t *testing.T) {
importer := IifTransactionDataFileImporter
converter := IifTransactionDataFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -661,21 +659,21 @@ func TestIIFTransactionDataFileParseImportedData_ParseSplitTransactionDescriptio
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tNAME\tAMOUNT\tMEMO\n"+
"!SPL\tDATE\tACCNT\tNAME\tAMOUNT\tMEMO\n"+
"!ENDTRNS\t\t\t\t\t\n"+
"TRNS\t09/01/2024\tTest Account\t\"Test\"\t123.45\t\"foo bar\t#test\"\n"+
"SPL\t09/01/2024\tTest Account2\t\t-100.00\t\"foo\ttest#bar\"\n"+
"SPL\t09/01/2024\tTest Account3\t\t-23.45\t\n"+
"ENDTRNS\t\t\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"ENDTRNS\t\t\t\t\t\n"), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 2, len(allNewTransactions))
assert.Equal(t, "foo\ttest#bar", allNewTransactions[0].Comment)
assert.Equal(t, "foo bar\t#test", allNewTransactions[1].Comment)
allNewTransactions, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tNAME\tAMOUNT\tMEMO\n"+
"!SPL\tDATE\tACCNT\tNAME\tAMOUNT\tMEMO\n"+
"!ENDTRNS\t\t\t\t\t\n"+
@@ -683,7 +681,7 @@ func TestIIFTransactionDataFileParseImportedData_ParseSplitTransactionDescriptio
"SPL\t09/01/2024\tTest Account2\t\t-100.00\t\"test\"\n"+
"SPL\t09/01/2024\tTest Account3\tfoo\t-12.34\t\n"+
"SPL\t09/01/2024\tTest Account4\t\t-11.11\t\n"+
"ENDTRNS\t\t\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"ENDTRNS\t\t\t\t\t\n"), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 3, len(allNewTransactions))
@@ -693,7 +691,7 @@ func TestIIFTransactionDataFileParseImportedData_ParseSplitTransactionDescriptio
}
func TestIIFTransactionDataFileParseImportedData_NotSupportedSplitTransaction(t *testing.T) {
importer := IifTransactionDataFileImporter
converter := IifTransactionDataFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -702,52 +700,52 @@ func TestIIFTransactionDataFileParseImportedData_NotSupportedSplitTransaction(t
}
// Opening balance transaction
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
"!TRNS\tTRNSTYPE\tDATE\tACCNT\tAMOUNT\n"+
"!SPL\tTRNSTYPE\tDATE\tACCNT\tAMOUNT\n"+
"!ENDTRNS\t\t\t\t\n"+
"TRNS\tBEGINBALCHECK\t09/01/2024\tTest Account\t123.45\n"+
"SPL\tBEGINBALCHECK\t09/01/2024\tTest Account2\t-100.00\n"+
"SPL\tBEGINBALCHECK\t09/01/2024\tTest Account3\t-23.45\n"+
"ENDTRNS\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"ENDTRNS\t\t\t\n"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrNotSupportedSplitTransactions.Message)
// Transaction with invalid amount
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!SPL\tDATE\tACCNT\tAMOUNT\n"+
"!ENDTRNS\t\t\t\n"+
"TRNS\t09/01/2024\tTest Account\t123 45\n"+
"SPL\t09/01/2024\tTest Account2\t-100.00\n"+
"SPL\t09/01/2024\tTest Account3\t-23.45\n"+
"ENDTRNS\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"ENDTRNS\t\t\t\n"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
// Transaction split data with invalid amount
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!SPL\tDATE\tACCNT\tAMOUNT\n"+
"!ENDTRNS\t\t\t\n"+
"TRNS\t09/01/2024\tTest Account\t123.45\n"+
"SPL\t09/01/2024\tTest Account2\t-100 00\n"+
"SPL\t09/01/2024\tTest Account3\t-23.45\n"+
"ENDTRNS\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"ENDTRNS\t\t\t\n"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
// Transaction amount not equal to sum of split data amount
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!SPL\tDATE\tACCNT\tAMOUNT\n"+
"!ENDTRNS\t\t\t\n"+
"TRNS\t09/01/2024\tTest Account\t123.00\n"+
"SPL\t09/01/2024\tTest Account2\t-100.00\n"+
"SPL\t09/01/2024\tTest Account3\t-23.45\n"+
"ENDTRNS\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"ENDTRNS\t\t\t\n"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrNotSupportedSplitTransactions.Message)
}
func TestIIFTransactionDataFileParseImportedData_InvalidDataLines(t *testing.T) {
importer := IifTransactionDataFileImporter
converter := IifTransactionDataFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -756,75 +754,75 @@ func TestIIFTransactionDataFileParseImportedData_InvalidDataLines(t *testing.T)
}
//Missing Transaction Line
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!SPL\tDATE\tACCNT\tAMOUNT\n"+
"!ENDTRNS\t\t\t\n"+
"SPL\t09/01/2024\tTest Account2\t-123.45\n"+
"ENDTRNS\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"ENDTRNS\t\t\t\n"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrInvalidIIFFile.Message)
// Missing Transaction And Split Line
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!SPL\tDATE\tACCNT\tAMOUNT\n"+
"!ENDTRNS\t\t\t\n"+
"ENDTRNS\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"ENDTRNS\t\t\t\n"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrInvalidIIFFile.Message)
// Missing Split Line
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!SPL\tDATE\tACCNT\tAMOUNT\n"+
"!ENDTRNS\t\t\t\n"+
"TRNS\t09/01/2024\tTest Account\t123.45\n"+
"ENDTRNS\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"ENDTRNS\t\t\t\n"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrInvalidIIFFile.Message)
// Missing Transaction End Line
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!SPL\tDATE\tACCNT\tAMOUNT\n"+
"!ENDTRNS\t\t\t\n"+
"TRNS\t09/01/2024\tTest Account\t123.45\n"+
"SPL\t09/01/2024\tTest Account2\t-123.45\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"SPL\t09/01/2024\tTest Account2\t-123.45\n"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrInvalidIIFFile.Message)
// Missing Transaction End Line (following is another header)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!SPL\tDATE\tACCNT\tAMOUNT\n"+
"!ENDTRNS\t\t\t\n"+
"TRNS\t09/01/2024\tTest Account\t123.45\n"+
"SPL\t09/01/2024\tTest Account2\t-123.45\n"+
"!ACCNT\tNAME\tACCNTTYPE\n"+
"ACCNT\tTest Account\tBANK\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"ACCNT\tTest Account\tBANK\n"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrInvalidIIFFile.Message)
// Invalid Line
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!SPL\tDATE\tACCNT\tAMOUNT\n"+
"!ENDTRNS\t\t\t\n"+
"TRNS\t09/01/2024\tTest Account\t123.45\n"+
"SPL\t09/01/2024\tTest Account2\t-123.45\n"+
"TEST\t\t\t\n"+
"ENDTRNS\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"ENDTRNS\t\t\t\n"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrInvalidIIFFile.Message)
// Repeat Transaction Line
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!SPL\tDATE\tACCNT\tAMOUNT\n"+
"!ENDTRNS\t\t\t\n"+
"TRNS\t09/01/2024\tTest Account\t123.45\n"+
"TRNS\t09/01/2024\tTest Account\t123.45\n"+
"SPL\t09/01/2024\tTest Account2\t-123.45\n"+
"ENDTRNS\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"ENDTRNS\t\t\t\n"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrInvalidIIFFile.Message)
// Repeat Transaction End Line
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\t\n"+
"!SPL\tDATE\tACCNT\tAMOUNT\t\n"+
"!ENDTRNS\t\t\t\n"+
@@ -832,12 +830,12 @@ func TestIIFTransactionDataFileParseImportedData_InvalidDataLines(t *testing.T)
"TRNS\t09/01/2024\tTest Account\t123.45\n"+
"SPL\t09/01/2024\tTest Account2\t-123.45\n"+
"ENDTRNS\t\t\t\n"+
"ENDTRNS\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"ENDTRNS\t\t\t\n"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrInvalidIIFFile.Message)
}
func TestIIFTransactionDataFileParseImportedData_InvalidHeaderLines(t *testing.T) {
importer := IifTransactionDataFileImporter
converter := IifTransactionDataFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -846,49 +844,49 @@ func TestIIFTransactionDataFileParseImportedData_InvalidHeaderLines(t *testing.T
}
// Missing All Sample Lines
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
"TRNS\t09/01/2024\tTest Account\t123.45\n"+
"SPL\t09/01/2024\tTest Account2\t-123.45\n"+
"ENDTRNS\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"ENDTRNS\t\t\t\n"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrInvalidIIFFile.Message)
// Missing Transaction Sample Line
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
"!SPL\tDATE\tACCNT\tAMOUNT\n"+
"!ENDTRNS\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"!ENDTRNS\t\t\t\n"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrInvalidIIFFile.Message)
// Missing Split Sample Line
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!ENDTRNS\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"!ENDTRNS\t\t\t\n"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrInvalidIIFFile.Message)
// Missing Transaction End Sample Line
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!SPL\tDATE\tACCNT\tAMOUNT\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"!SPL\tDATE\tACCNT\tAMOUNT\n"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrInvalidIIFFile.Message)
// Missing Transaction End Sample Line (following is data line)
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!SPL\tDATE\tACCNT\tAMOUNT\n"+
"TRNS\t09/01/2024\tTest Account\t123.45\n"+
"SPL\t09/01/2024\tTest Account2\t-123.45\n"+
"ENDTRNS\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"ENDTRNS\t\t\t\n"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrInvalidIIFFile.Message)
// Invalid Sample Line
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
"!TEST\tDATE\tACCNT\tAMOUNT\n"+
"!ENDTRNS\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"!ENDTRNS\t\t\t\n"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrInvalidIIFFile.Message)
}
func TestIIFTransactionDataFileParseImportedData_MissingRequiredColumn(t *testing.T) {
importer := IifTransactionDataFileImporter
converter := IifTransactionDataFileImporter
context := core.NewNullContext()
user := &models.User{
@@ -897,32 +895,32 @@ func TestIIFTransactionDataFileParseImportedData_MissingRequiredColumn(t *testin
}
// Missing Date Column
_, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
"!TRNS\tACCNT\tAMOUNT\t\n"+
"!SPL\tACCNT\tAMOUNT\t\n"+
"!ENDTRNS\t\t\t\n"+
"TRNS\tTest Account\t123.45\n"+
"SPL\tTest Account2\t-123.45\n"+
"ENDTRNS\t\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"ENDTRNS\t\t\t\t\n"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
// Missing Account Column
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tAMOUNT\t\n"+
"!SPL\tDATE\tAMOUNT\t\n"+
"!ENDTRNS\t\t\t\n"+
"TRNS\t09/01/2024\t123.45\n"+
"SPL\t09/01/2024\t-123.45\n"+
"ENDTRNS\t\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"ENDTRNS\t\t\t\t\n"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
// Missing Amount Column
_, _, _, _, _, _, err = importer.ParseImportedData(context, user, []byte(
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
"!TRNS\tDATE\tACCNT\t\n"+
"!SPL\tDATE\tACCNT\t\n"+
"!ENDTRNS\t\t\t\n"+
"TRNS\t09/01/2024\tTest Account\n"+
"SPL\t09/01/2024\tTest Account2\n"+
"ENDTRNS\t\t\t\t\n"), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
"ENDTRNS\t\t\t\t\n"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
}
@@ -2,7 +2,6 @@ package jdcom
import (
"bytes"
"time"
"golang.org/x/text/encoding/unicode"
"golang.org/x/text/transform"
@@ -28,7 +27,7 @@ var (
)
// ParseImportedData returns the imported data by parsing the jd.com finance transaction csv data
func (c *jdComFinanceTransactionDataCsvFileImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, 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) {
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) {
fallback := unicode.UTF8.NewDecoder()
reader := transform.NewReader(bytes.NewReader(data), unicode.BOMOverride(fallback))
@@ -61,5 +60,5 @@ func (c *jdComFinanceTransactionDataCsvFileImporter) ParseImportedData(ctx core.
transactionDataTable := datatable.CreateNewTransactionDataTableFromCommonDataTable(commonDataTable, jdComFinanceTransactionSupportedColumns, transactionRowParser)
dataTableImporter := converter.CreateNewSimpleImporterWithTypeNameMapping(jdComFinanceTransactionTypeNameMapping)
return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezone, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezoneOffset, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
}

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