Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e9503d6ae2 | |||
| d8736eebf8 |
@@ -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 }}
|
||||
|
||||
@@ -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,5 +1,6 @@
|
||||
name: Bug Report
|
||||
description: Report a bug in ezBookkeeping
|
||||
labels: bug
|
||||
body:
|
||||
- type: checkboxes
|
||||
id: checkboxes
|
||||
|
||||
@@ -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.7"
|
||||
mingw-version:
|
||||
required: false
|
||||
default: "15.2.0"
|
||||
mingw-revison:
|
||||
required: false
|
||||
default: "v13-rev1"
|
||||
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 ' *)
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
-14
@@ -147,17 +147,3 @@ dist/
|
||||
|
||||
# Roo Code
|
||||
.roo/
|
||||
|
||||
# Binary and build files
|
||||
ezbookkeeping
|
||||
!**/ezbookkeeping/
|
||||
package/
|
||||
|
||||
# Environment variable files
|
||||
.env
|
||||
**/.env
|
||||
|
||||
# Other directories
|
||||
data/
|
||||
storage/
|
||||
log/
|
||||
|
||||
+3
-11
@@ -1,15 +1,11 @@
|
||||
# Build backend binary file
|
||||
FROM golang:1.25.7-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.14.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.3
|
||||
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,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
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
[](https://trendshift.io/repositories/12917)
|
||||
|
||||
## Introduction
|
||||
ezBookkeeping is a lightweight, self-hosted personal finance app with a user-friendly interface and powerful bookkeeping features. It helps you record daily transactions, import data from various sources, and quickly search and filter your bills. You can analyze historical data using built-in charts or perform custom queries with your own chart dimensions to better understand spending patterns and financial trends. ezBookkeeping is easy to deploy, and you can start it with just one single Docker command. Designed to be resource-efficient, it runs smoothly on devices such as Raspberry Pi, NAS, and MicroServers.
|
||||
ezBookkeeping is a lightweight, self-hosted personal finance app with a user-friendly interface and powerful bookkeeping features. It's easy to deploy, and you can start it with just one single Docker command. Designed to be resource-efficient and highly scalable, it can run smoothly on devices as small as a Raspberry Pi, or scale up to NAS, MicroServers, and even large cluster environments.
|
||||
|
||||
ezBookkeeping offers tailored interfaces for both mobile and desktop devices. With support for PWA (Progressive Web Apps), you can even [add it to your mobile home screen](https://raw.githubusercontent.com/wiki/mayswind/ezbookkeeping/img/mobile/add_to_home_screen.gif) and use it like a native app.
|
||||
|
||||
@@ -21,9 +21,9 @@ Live Demo: [https://ezbookkeeping-demo.mayswind.net](https://ezbookkeeping-demo.
|
||||
- **Open Source & Self-Hosted**
|
||||
- Built for privacy and control
|
||||
- **Lightweight & Fast**
|
||||
- Minimal resource usage, runs smoothly even on low-resource devices
|
||||
- Optimized for performance, runs smoothly even on low-resource environments
|
||||
- **Easy Installation**
|
||||
- Docker support
|
||||
- Docker-ready
|
||||
- Supports SQLite, MySQL, PostgreSQL
|
||||
- Cross-platform (Windows, macOS, Linux)
|
||||
- Works on x86, amd64, ARM architectures
|
||||
@@ -33,28 +33,24 @@ Live Demo: [https://ezbookkeeping-demo.mayswind.net](https://ezbookkeeping-demo.
|
||||
- Dark mode
|
||||
- **AI-Powered Features**
|
||||
- Receipt image recognition
|
||||
- MCP (Model Context Protocol) support for AI integration
|
||||
- API command-line script tools for AI integration
|
||||
- Supports MCP (Model Context Protocol) for AI integration
|
||||
- **Powerful Bookkeeping**
|
||||
- Two-level accounts and categories
|
||||
- Image attachments for transactions
|
||||
- Attach images to transactions
|
||||
- Location tracking with maps
|
||||
- Scheduled transactions
|
||||
- Advanced filtering, search, visualization and analysis
|
||||
- **Localization & Internationalization**
|
||||
- Recurring transactions
|
||||
- Advanced filtering, search, visualization, and analysis
|
||||
- **Localization & Globalization**
|
||||
- Multi-language and multi-currency support
|
||||
- Multiple exchange rate sources with automatic updates
|
||||
- Multi-timezone support
|
||||
- Custom formats for dates, numbers and currencies
|
||||
- Automatic exchange rates
|
||||
- Multi-timezone awareness
|
||||
- Custom formats for dates, numbers, and currencies
|
||||
- **Security**
|
||||
- Two-factor authentication (2FA)
|
||||
- OIDC external authentication
|
||||
- Login rate limiting
|
||||
- Application lock (PIN code / WebAuthn)
|
||||
- **Data Import & Export**
|
||||
- Supports CSV, OFX, QFX, QIF, IIF, Camt.052, Camt.053, MT940, GnuCash, Firefly III, Beancount and more
|
||||
|
||||
For a full list of features, visit the [Full Feature List](https://ezbookkeeping.mayswind.net/comparison/).
|
||||
- **Data Import/Export**
|
||||
- Supports CSV, OFX, QFX, QIF, IIF, Camt.053, MT940, GnuCash, Firefly III, Beancount, and more
|
||||
|
||||
## Screenshots
|
||||
### Desktop Version
|
||||
@@ -89,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**
|
||||
|
||||
@@ -116,16 +112,16 @@ You can also build a Docker image. Make sure you have [Docker](https://www.docke
|
||||
## Contributing
|
||||
We welcome contributions of all kinds.
|
||||
|
||||
If you find a bug, please [submit an issue](https://github.com/mayswind/ezbookkeeping/issues) on GitHub.
|
||||
Found a bug? [Submit an issue](https://github.com/mayswind/ezbookkeeping/issues)
|
||||
|
||||
If you would like to contribute code, you can fork the repository and open a pull request.
|
||||
Want to contribute code? Feel free to fork and send a pull request.
|
||||
|
||||
Improvements to documentation, feature suggestions, and other forms of feedback are also appreciated.
|
||||
Contributions of all kinds — bug reports, feature suggestions, documentation improvements, or code — are highly appreciated.
|
||||
|
||||
You can view existing contributors on the [Contributor Graph](https://github.com/mayswind/ezbookkeeping/graphs/contributors).
|
||||
Check out our [Contributor Graph](https://github.com/mayswind/ezbookkeeping/graphs/contributors) to see the amazing people who've already helped.
|
||||
|
||||
## Translating
|
||||
Help make ezBookkeeping accessible to users around the world. We welcome help to improve existing translations or add new ones. If you would like to contribute a translation, please refer to the [translation guide](https://ezbookkeeping.mayswind.net/translating).
|
||||
Help make ezBookkeeping accessible to users around the world. If you want to contribute a translation, please refer to our [translation guide](https://ezbookkeeping.mayswind.net/translating).
|
||||
|
||||
Currently available translations:
|
||||
|
||||
@@ -133,27 +129,24 @@ 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) |
|
||||
| pt-BR | Português (Brasil) | [@thecodergus](https://github.com/thecodergus), [@balaios](https://github.com/balaios) |
|
||||
| ru | Русский | [@artegoser](https://github.com/artegoser), [@dshemin](https://github.com/dshemin) |
|
||||
| sl | Slovenščina | [@thehijacker](https://github.com/thehijacker) |
|
||||
| ta | தமிழ் | [@hhharsha36](https://github.com/hhharsha36) |
|
||||
| nl | Nederlands | [@automagic](https://github.com/automagics) |
|
||||
| pt-BR | Português (Brasil) | [@thecodergus](https://github.com/thecodergus) |
|
||||
| ru | Русский | [@artegoser](https://github.com/artegoser) |
|
||||
| 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 | 中文 (简体) | / |
|
||||
| zh-Hant | 中文 (繁體) | / |
|
||||
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
-51
@@ -162,62 +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.AnthropicCompatibleAPIKey != "" {
|
||||
clonedConfig.ReceiptImageRecognitionLLMConfig.AnthropicCompatibleAPIKey = "****"
|
||||
}
|
||||
|
||||
if clonedConfig.ReceiptImageRecognitionLLMConfig.AnthropicAPIKey != "" {
|
||||
clonedConfig.ReceiptImageRecognitionLLMConfig.AnthropicAPIKey = "****"
|
||||
}
|
||||
|
||||
if clonedConfig.ReceiptImageRecognitionLLMConfig.OpenRouterAPIKey != "" {
|
||||
clonedConfig.ReceiptImageRecognitionLLMConfig.OpenRouterAPIKey = "****"
|
||||
}
|
||||
|
||||
if clonedConfig.ReceiptImageRecognitionLLMConfig.LMStudioToken != "" {
|
||||
clonedConfig.ReceiptImageRecognitionLLMConfig.LMStudioToken = "****"
|
||||
}
|
||||
|
||||
if clonedConfig.ReceiptImageRecognitionLLMConfig.GoogleAIAPIKey != "" {
|
||||
clonedConfig.ReceiptImageRecognitionLLMConfig.GoogleAIAPIKey = "****"
|
||||
}
|
||||
}
|
||||
|
||||
if clonedConfig.OAuth2ClientSecret != "" {
|
||||
clonedConfig.OAuth2ClientSecret = "****"
|
||||
clonedConfig.ReceiptImageRecognitionLLMConfig.OpenAIAPIKey = "****"
|
||||
clonedConfig.ReceiptImageRecognitionLLMConfig.OpenAICompatibleAPIKey = "****"
|
||||
clonedConfig.ReceiptImageRecognitionLLMConfig.OpenRouterAPIKey = "****"
|
||||
}
|
||||
|
||||
return clonedConfig
|
||||
|
||||
+4
-16
@@ -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")
|
||||
|
||||
+14
-85
@@ -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,12 +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.APITokenIpLimit(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))
|
||||
@@ -339,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))
|
||||
@@ -384,20 +346,17 @@ 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 {
|
||||
apiV1Route.POST("/transactions/parse_custom_file.json", bindApi(api.Transactions.TransactionParseImportCustomFileDataHandler))
|
||||
apiV1Route.POST("/transactions/parse_dsv_file.json", bindApi(api.Transactions.TransactionParseImportDsvFileDataHandler))
|
||||
apiV1Route.POST("/transactions/parse_import.json", bindApi(api.Transactions.TransactionParseImportFileHandler))
|
||||
apiV1Route.POST("/transactions/import.json", bindApi(api.Transactions.TransactionImportHandler))
|
||||
apiV1Route.GET("/transactions/import/process.json", bindApi(api.Transactions.TransactionImportProcessHandler))
|
||||
@@ -419,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))
|
||||
@@ -446,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 {
|
||||
@@ -501,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)
|
||||
|
||||
+10
-105
@@ -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
|
||||
@@ -169,7 +172,7 @@ transaction_from_ai_image_recognition = false
|
||||
max_ai_recognition_picture_size = 10485760
|
||||
|
||||
[llm_image_recognition]
|
||||
# Large Language Model (LLM) provider for receipt image recognition, supports the following types: "openai", "openai_compatible", "anthropic", "anthropic_compatible", "openrouter", "ollama", "lm_studio", "google_ai"
|
||||
# Large Language Model (LLM) provider for receipt image recognition, supports the following types: "openai", "openai_compatible", "openrouter", "ollama", "google_ai"
|
||||
llm_provider =
|
||||
|
||||
# For "openai" llm provider only, OpenAI API secret key, please visit https://platform.openai.com/api-keys for more information
|
||||
@@ -187,30 +190,6 @@ openai_compatible_api_key =
|
||||
# For "openai_compatible" llm provider only, receipt image recognition model for creating transactions from images
|
||||
openai_compatible_model_id =
|
||||
|
||||
# For "anthropic" llm provider only, Anthropic API key, please visit https://platform.claude.com/settings/keys for more information
|
||||
anthropic_api_key =
|
||||
|
||||
# For "anthropic" llm provider only, receipt image recognition model for creating transactions from images
|
||||
anthropic_model_id =
|
||||
|
||||
# For "anthropic" llm provider only, maximum allowed number of generated tokens for creating transactions from images, default is 1024
|
||||
anthropic_max_tokens = 1024
|
||||
|
||||
# For "anthropic_compatible" llm provider only, Anthropic compatible API base url, e.g. "https://api.anthropic.com/v1/"
|
||||
anthropic_compatible_base_url =
|
||||
|
||||
# For "anthropic_compatible" llm provider only, Anthropic compatible API version, e.g. "2023-06-01". If the LLM service does not require API versioning, leave it blank
|
||||
anthropic_compatible_api_version =
|
||||
|
||||
# For "anthropic_compatible" llm provider only, Anthropic compatible API secret key
|
||||
anthropic_compatible_api_key =
|
||||
|
||||
# For "anthropic_compatible" llm provider only, receipt image recognition model for creating transactions from images
|
||||
anthropic_compatible_model_id =
|
||||
|
||||
# For "anthropic_compatible" llm provider only, maximum allowed number of generated tokens for creating transactions from images, default is 1024
|
||||
anthropic_compatible_max_tokens = 1024
|
||||
|
||||
# For "openrouter" llm provider only, OpenRouter API key, please visit https://openrouter.ai/settings/keys for more information
|
||||
openrouter_api_key =
|
||||
|
||||
@@ -223,15 +202,6 @@ ollama_server_url =
|
||||
# For "ollama" llm provider only, receipt image recognition model for creating transactions from images
|
||||
ollama_model_id =
|
||||
|
||||
# For "lm_studio" llm provider only, LM Studio server url, e.g. "http://127.0.0.1:1234/"
|
||||
lm_studio_server_url =
|
||||
|
||||
# For "lm_studio" llm provider only, LM Studio API token, if "require authentication" is not enabled in LM Studio, leave it blank
|
||||
lm_studio_token =
|
||||
|
||||
# For "lm_studio" llm provider only, receipt image recognition model for creating transactions from images
|
||||
lm_studio_model_id =
|
||||
|
||||
# For "google_ai" llm provider only, Google AI Studio API key, please visit https://aistudio.google.com/apikey for more information
|
||||
google_ai_api_key =
|
||||
|
||||
@@ -293,12 +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
|
||||
|
||||
# Allowed remote IPs for using the API token, a comma-separated list of allowed remote IPs (asterisk * for any addresses, e.g. 192.168.1.* means any IPs in the 192.168.1.x subnet), leave blank to allow all remote IPs
|
||||
api_token_allowed_remote_ips =
|
||||
|
||||
# Maximum count of password / token check failures (0 - 4294967295) per IP per minute (use the above duplicate checker), default is 5, set to 0 to disable
|
||||
max_failures_per_ip_per_minute = 5
|
||||
|
||||
@@ -306,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
|
||||
@@ -415,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]
|
||||
@@ -529,6 +432,7 @@ custom_map_tile_server_default_zoom_level = 14
|
||||
|
||||
[exchange_rates]
|
||||
# Exchange rates data source, supports the following types:
|
||||
# "reserve_bank_of_australia": https://www.rba.gov.au/statistics/frequency/exchange-rates.html
|
||||
# "bank_of_canada": https://www.bankofcanada.ca/rates/exchange/daily-exchange-rates/
|
||||
# "czech_national_bank": https://www.cnb.cz/en/financial-markets/foreign-exchange-market/central-bank-exchange-rate-fixing/central-bank-exchange-rate-fixing/
|
||||
# "danmarks_national_bank": https://www.nationalbanken.dk/en/what-we-do/stable-prices-monetary-policy-and-the-danish-economy/exchange-rates
|
||||
@@ -544,6 +448,7 @@ custom_map_tile_server_default_zoom_level = 14
|
||||
# "swiss_national_bank": https://www.snb.ch/en/the-snb/mandates-goals/statistics/statistics-pub/current_interest_exchange_rates
|
||||
# "national_bank_of_ukraine": https://bank.gov.ua/ua/markets/exchangerates
|
||||
# "central_bank_of_uzbekistan": https://cbu.uz/en/arkhiv-kursov-valyut/
|
||||
# "international_monetary_fund": https://www.imf.org/external/np/fin/data/param_rms_mth.aspx
|
||||
# "user_custom": users set their own exchange rates data in the UI
|
||||
data_source = euro_central_bank
|
||||
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
{
|
||||
"code": [
|
||||
"jiangshengwu",
|
||||
"vigdail",
|
||||
"f97",
|
||||
"Miguelonlonlon",
|
||||
"seb26",
|
||||
"nktlitvinenko",
|
||||
"lvdou-bing",
|
||||
"dshemin",
|
||||
"lucdsouza",
|
||||
"OuIChien",
|
||||
"RasterCrow"
|
||||
],
|
||||
"translators": {
|
||||
"de": [
|
||||
"chrgm"
|
||||
],
|
||||
"en": [],
|
||||
"es": [
|
||||
"Miguelonlonlon",
|
||||
"abrugues",
|
||||
"AndresTeller",
|
||||
"diegofercri"
|
||||
],
|
||||
"fr": [
|
||||
"brieucdlf"
|
||||
],
|
||||
"it": [
|
||||
"waron97"
|
||||
],
|
||||
"ja": [
|
||||
"tkymmm"
|
||||
],
|
||||
"kn": [
|
||||
"Darshanbm05"
|
||||
],
|
||||
"ko": [
|
||||
"overworks"
|
||||
],
|
||||
"nl": [
|
||||
"automagics"
|
||||
],
|
||||
"pt-BR": [
|
||||
"thecodergus",
|
||||
"balaios"
|
||||
],
|
||||
"ru": [
|
||||
"artegoser",
|
||||
"dshemin"
|
||||
],
|
||||
"sl": [
|
||||
"thehijacker"
|
||||
],
|
||||
"ta": [
|
||||
"hhharsha36"
|
||||
],
|
||||
"th": [
|
||||
"natthavat28"
|
||||
],
|
||||
"tr": [
|
||||
"aydnykn"
|
||||
],
|
||||
"uk": [
|
||||
"nktlitvinenko"
|
||||
],
|
||||
"vi": [
|
||||
"f97"
|
||||
],
|
||||
"zh-Hans": [],
|
||||
"zh-Hant": []
|
||||
}
|
||||
}
|
||||
@@ -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,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
|
||||
|
||||
+4
-4
@@ -10,7 +10,7 @@ import (
|
||||
"github.com/urfave/cli/v3"
|
||||
|
||||
"github.com/mayswind/ezbookkeeping/cmd"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||
)
|
||||
|
||||
@@ -26,9 +26,9 @@ var (
|
||||
)
|
||||
|
||||
func main() {
|
||||
core.Version = Version
|
||||
core.CommitHash = CommitHash
|
||||
core.BuildTime = BuildUnixTime
|
||||
settings.Version = Version
|
||||
settings.CommitHash = CommitHash
|
||||
settings.BuildTime = BuildUnixTime
|
||||
|
||||
cmd := &cli.Command{
|
||||
Name: "ezBookkeeping",
|
||||
|
||||
@@ -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.19.1
|
||||
github.com/go-playground/validator/v10 v10.30.1
|
||||
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.1
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||
github.com/invopop/jsonschema v0.13.0
|
||||
github.com/lib/pq v1.11.1
|
||||
github.com/mattn/go-sqlite3 v1.14.33
|
||||
github.com/minio/minio-go/v7 v7.0.98
|
||||
github.com/lib/pq v1.10.9
|
||||
github.com/mattn/go-sqlite3 v1.14.32
|
||||
github.com/minio/minio-go/v7 v7.0.95
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||
github.com/pquerna/otp v1.5.0
|
||||
github.com/sirupsen/logrus v1.9.4
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/urfave/cli/v3 v3.6.2
|
||||
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.47.0
|
||||
golang.org/x/net v0.49.0
|
||||
golang.org/x/oauth2 v0.34.0
|
||||
golang.org/x/text v0.33.0
|
||||
gopkg.in/ini.v1 v1.67.1
|
||||
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.12 // 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.2 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/klauspost/crc32 v1.3.0 // indirect
|
||||
github.com/klauspost/compress v1.18.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.1 // 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,21 +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/tinylib/msgp v1.6.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
|
||||
go.yaml.in/yaml/v3 v3.0.4 // 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.31.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
golang.org/x/tools v0.40.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
|
||||
)
|
||||
|
||||
@@ -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,36 +40,32 @@ 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.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
||||
github.com/gabriel-vasile/mimetype v1.4.12/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.19.1 h1:B4iLeA0NB/2iO3EKQ7NfKn5KsQgZfjb2fkvoZJU3yBI=
|
||||
github.com/go-co-op/gocron/v2 v2.19.1/go.mod h1:5lEiCKk1oVJV39Zg7/YG10OnaVrDAV5GGR6O0663k6U=
|
||||
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.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
||||
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
||||
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.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
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=
|
||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||
@@ -90,41 +83,40 @@ github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7X
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
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=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/lib/pq v1.11.1 h1:wuChtj2hfsGmmx3nf1m7xC2XpK6OtelS2shMY+bGMtI=
|
||||
github.com/lib/pq v1.11.1/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.33 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K73s0=
|
||||
github.com/mattn/go-sqlite3 v1.14.33/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
|
||||
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.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI=
|
||||
github.com/minio/crc64nvme v1.1.1/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.98 h1:MeAVKjLVz+XJ28zFcuYyImNSAh8Mq725uNW4beRisi0=
|
||||
github.com/minio/minio-go/v7 v7.0.98/go.mod h1:cY0Y+W7yozf0mdIclrttzo1Iiu7mEf9y7nk2uXqMOvM=
|
||||
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=
|
||||
@@ -155,79 +143,67 @@ github.com/robfig/go-cache v0.0.0-20130306151617-9fc39e0dbf62/go.mod h1:65XQgovT
|
||||
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
|
||||
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
|
||||
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/tinylib/msgp v1.6.1 h1:ESRv8eL3u+DNHUoSAAQRE50Hm162zqAnBoGv9PzScPY=
|
||||
github.com/tinylib/msgp v1.6.1/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
|
||||
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.2 h1:lQuqiPrZ1cIz8hz+HcrG0TNZFxU70dPZ3Yl+pSrH9A8=
|
||||
github.com/urfave/cli/v3 v3.6.2/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=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
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.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
||||
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
||||
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.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
|
||||
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||
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.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.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.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||
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=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/ini.v1 v1.67.1 h1:tVBILHy0R6e4wkYOn3XmiITt/hEVH4TFMYvAX2Ytz6k=
|
||||
gopkg.in/ini.v1 v1.67.1/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk=
|
||||
gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
@@ -239,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=
|
||||
|
||||
Generated
+2464
-2942
File diff suppressed because it is too large
Load Diff
+30
-31
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ezbookkeeping",
|
||||
"version": "1.4.0",
|
||||
"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.4",
|
||||
"@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.3",
|
||||
"echarts": "^5.5.1",
|
||||
"framework7": "^8.3.4",
|
||||
"framework7-icons": "^5.0.5",
|
||||
"framework7-vue": "^9.0.3",
|
||||
"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.1.0",
|
||||
"swiper": "^10.2.0",
|
||||
"ua-parser-js": "^1.0.39",
|
||||
"vue": "^3.5.27",
|
||||
"vue-echarts": "^8.0.1",
|
||||
"vue-i18n": "^11.2.8",
|
||||
"vue-router": "^5.0.2",
|
||||
"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.8"
|
||||
"vuetify": "^3.9.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jest/globals": "^30.2.0",
|
||||
"@tsconfig/node24": "^24.0.4",
|
||||
"@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.4",
|
||||
"@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.7.0",
|
||||
"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": "^11.1.3",
|
||||
"sass": "^1.97.3",
|
||||
"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.3.1",
|
||||
"vite-plugin-checker": "^0.12.0",
|
||||
"vite-plugin-pwa": "^1.2.0",
|
||||
"vite-plugin-vuetify": "^2.1.3",
|
||||
"vue-tsc": "^3.2.4"
|
||||
"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.0.6"
|
||||
},
|
||||
"browserslist": [
|
||||
"last 5 Chrome versions",
|
||||
|
||||
+16
-27
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
+3
-2
@@ -3,6 +3,7 @@ package api
|
||||
import (
|
||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||
)
|
||||
|
||||
// HealthsApi represents health api
|
||||
@@ -17,8 +18,8 @@ var (
|
||||
func (a *HealthsApi) HealthStatusHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
result := make(map[string]string)
|
||||
|
||||
result["version"] = core.Version
|
||||
result["commit"] = core.CommitHash
|
||||
result["version"] = settings.Version
|
||||
result["commit"] = settings.CommitHash
|
||||
result["status"] = "ok"
|
||||
|
||||
return result, nil
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,8 +102,8 @@ func (a *ModelContextProtocolAPI) InitializeHandler(c *core.WebContext, jsonRPCR
|
||||
},
|
||||
ServerInfo: &mcp.MCPImplementation{
|
||||
Name: mcpServerName,
|
||||
Title: core.ApplicationName,
|
||||
Version: core.Version,
|
||||
Title: a.CurrentConfig().AppName,
|
||||
Version: settings.Version,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
+5
-4
@@ -3,6 +3,7 @@ package api
|
||||
import (
|
||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||
)
|
||||
|
||||
// SystemsApi represents system api
|
||||
@@ -17,11 +18,11 @@ var (
|
||||
func (a *SystemsApi) VersionHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
result := make(map[string]string)
|
||||
|
||||
result["version"] = core.Version
|
||||
result["commitHash"] = core.CommitHash
|
||||
result["version"] = settings.Version
|
||||
result["commitHash"] = settings.CommitHash
|
||||
|
||||
if core.BuildTime != "" {
|
||||
result["buildTime"] = core.BuildTime
|
||||
if settings.BuildTime != "" {
|
||||
result["buildTime"] = settings.BuildTime
|
||||
}
|
||||
|
||||
return result, nil
|
||||
|
||||
+4
-54
@@ -69,10 +69,8 @@ func (a *TokensApi) TokenListHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
tokenResp.IsCurrent = true
|
||||
}
|
||||
|
||||
if token.TokenType == core.USER_TOKEN_TYPE_API && token.UserAgent != core.TokenUserAgentCreatedViaCli {
|
||||
tokenResp.UserAgent = core.TokenUserAgentForAPI
|
||||
} else if token.TokenType == core.USER_TOKEN_TYPE_MCP && token.UserAgent != core.TokenUserAgentCreatedViaCli {
|
||||
tokenResp.UserAgent = core.TokenUserAgentForMCP
|
||||
if token.TokenType == core.USER_TOKEN_TYPE_MCP && token.UserAgent != services.TokenUserAgentCreatedViaCli {
|
||||
tokenResp.UserAgent = services.TokenUserAgentForMCP
|
||||
}
|
||||
|
||||
tokenResps[i] = tokenResp
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -12,15 +12,13 @@ import (
|
||||
|
||||
// TransactionTagsApi represents transaction tag api
|
||||
type TransactionTagsApi struct {
|
||||
tags *services.TransactionTagService
|
||||
tagGroups *services.TransactionTagGroupService
|
||||
tags *services.TransactionTagService
|
||||
}
|
||||
|
||||
// Initialize a transaction tag api singleton instance
|
||||
var (
|
||||
TransactionTags = &TransactionTagsApi{
|
||||
tags: services.TransactionTags,
|
||||
tagGroups: services.TransactionTagGroups,
|
||||
tags: services.TransactionTags,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -80,21 +78,7 @@ func (a *TransactionTagsApi) TagCreateHandler(c *core.WebContext) (any, *errs.Er
|
||||
|
||||
uid := c.GetCurrentUid()
|
||||
|
||||
if tagCreateReq.GroupId > 0 {
|
||||
tagGroup, err := a.tagGroups.GetTagGroupByTagGroupId(c, uid, tagCreateReq.GroupId)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transaction_tags.TagCreateHandler] failed to get tag group \"id:%d\" for user \"uid:%d\", because %s", tagCreateReq.GroupId, uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
if tagGroup == nil {
|
||||
log.Warnf(c, "[transaction_tags.TagCreateHandler] the tag group \"id:%d\" does not exist for user \"uid:%d\"", tagCreateReq.GroupId, uid)
|
||||
return nil, errs.ErrTransactionTagGroupNotFound
|
||||
}
|
||||
}
|
||||
|
||||
maxOrderId, err := a.tags.GetMaxDisplayOrder(c, uid, tagCreateReq.GroupId)
|
||||
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())
|
||||
@@ -127,30 +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()
|
||||
|
||||
if tagCreateBatchReq.GroupId > 0 {
|
||||
tagGroup, err := a.tagGroups.GetTagGroupByTagGroupId(c, uid, tagCreateBatchReq.GroupId)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transaction_tags.TagCreateBatchHandler] failed to get tag group \"id:%d\" for user \"uid:%d\", because %s", tagCreateBatchReq.GroupId, uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
if tagGroup == nil {
|
||||
log.Warnf(c, "[transaction_tags.TagCreateBatchHandler] the tag group \"id:%d\" does not exist for user \"uid:%d\"", tagCreateBatchReq.GroupId, uid)
|
||||
return nil, errs.ErrTransactionTagGroupNotFound
|
||||
}
|
||||
}
|
||||
|
||||
maxOrderId, err := a.tags.GetMaxDisplayOrder(c, uid, tagCreateBatchReq.GroupId)
|
||||
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())
|
||||
@@ -197,46 +160,17 @@ func (a *TransactionTagsApi) TagModifyHandler(c *core.WebContext) (any, *errs.Er
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
if tagModifyReq.GroupId != tag.TagGroupId && tagModifyReq.GroupId > 0 {
|
||||
tagGroup, err := a.tagGroups.GetTagGroupByTagGroupId(c, uid, tagModifyReq.GroupId)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transaction_tags.TagModifyHandler] failed to get tag group \"id:%d\" for user \"uid:%d\", because %s", tagModifyReq.GroupId, uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
if tagGroup == nil {
|
||||
log.Warnf(c, "[transaction_tags.TagModifyHandler] the tag group \"id:%d\" does not exist for user \"uid:%d\"", tagModifyReq.GroupId, uid)
|
||||
return nil, errs.ErrTransactionTagGroupNotFound
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
@@ -246,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
|
||||
@@ -336,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,
|
||||
}
|
||||
}
|
||||
@@ -347,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
|
||||
}
|
||||
|
||||
|
||||
+100
-448
@@ -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,15 +190,7 @@ func (a *TransactionsApi) TransactionListHandler(c *core.WebContext) (any, *errs
|
||||
transactions = transactions[:transactionListReq.Count]
|
||||
}
|
||||
|
||||
accountMap, categoryMap, tagMap, allTransactionTagIds, pictureInfoMap, err := a.getTransactionEssentialDataByTransactionIds(c, user, transactions, transactionListReq.WithPictures, transactionListReq.TrimCategory, transactionListReq.TrimTag)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transactions.TransactionListHandler] failed to get essential data for assembling transaction result for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
transactions = a.filterTransactions(c, uid, transactions, accountMap)
|
||||
transactionResult, err := a.getTransactionResponseListResult(c, user, transactions, accountMap, categoryMap, tagMap, allTransactionTagIds, pictureInfoMap, clientTimezone, transactionListReq.WithPictures, transactionListReq.TrimAccount, transactionListReq.TrimCategory, transactionListReq.TrimTag)
|
||||
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())
|
||||
@@ -232,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
|
||||
}
|
||||
|
||||
@@ -264,34 +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)
|
||||
}
|
||||
|
||||
accountMap, categoryMap, tagMap, allTransactionTagIds, pictureInfoMap, err := a.getTransactionEssentialDataByTransactionIds(c, user, transactions, transactionListReq.WithPictures, transactionListReq.TrimCategory, transactionListReq.TrimTag)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transactions.TransactionMonthListHandler] failed to get essential data for assembling transaction result for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
transactions = a.filterTransactions(c, uid, transactions, accountMap)
|
||||
transactionResult, err := a.getTransactionResponseListResult(c, user, transactions, accountMap, categoryMap, tagMap, allTransactionTagIds, pictureInfoMap, clientTimezone, transactionListReq.WithPictures, transactionListReq.TrimAccount, transactionListReq.TrimCategory, transactionListReq.TrimTag)
|
||||
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())
|
||||
@@ -306,106 +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)
|
||||
}
|
||||
|
||||
var accountMap map[int64]*models.Account
|
||||
var categoryMap map[int64]*models.TransactionCategory
|
||||
var tagMap map[int64]*models.TransactionTag
|
||||
var allTransactionTagIds map[int64][]int64
|
||||
var pictureInfoMap map[int64][]*models.TransactionPictureInfo
|
||||
|
||||
if minTransactionTime == 0 && maxTransactionTime == math.MaxInt64 && len(allCategoryIds) < 1 && len(allAccountIds) < 1 && len(tagFilters) < 1 && transactionAllListReq.AmountFilter == "" && transactionAllListReq.Keyword == "" {
|
||||
accountMap, categoryMap, tagMap, allTransactionTagIds, pictureInfoMap, err = a.getTransactionAllEssentialData(c, user, transactionAllListReq.WithPictures, transactionAllListReq.TrimCategory, transactionAllListReq.TrimTag)
|
||||
} else {
|
||||
accountMap, categoryMap, tagMap, allTransactionTagIds, pictureInfoMap, err = a.getTransactionEssentialDataByTransactionIds(c, user, allTransactions, transactionAllListReq.WithPictures, transactionAllListReq.TrimCategory, transactionAllListReq.TrimTag)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transactions.TransactionListAllHandler] failed to get essential data for assembling transaction result for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
allTransactions = a.filterTransactions(c, uid, allTransactions, accountMap)
|
||||
transactionResult, err := a.getTransactionResponseListResult(c, user, allTransactions, accountMap, categoryMap, tagMap, allTransactionTagIds, pictureInfoMap, clientTimezone, transactionAllListReq.WithPictures, transactionAllListReq.TrimAccount, transactionAllListReq.TrimCategory, transactionAllListReq.TrimTag)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transactions.TransactionListAllHandler] failed to assemble transaction result for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
return transactionResult, nil
|
||||
}
|
||||
|
||||
// TransactionReconciliationStatementHandler returns transaction reconciliation statement list of current user
|
||||
func (a *TransactionsApi) TransactionReconciliationStatementHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
var reconciliationStatementRequest models.TransactionReconciliationStatementRequest
|
||||
@@ -416,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
|
||||
}
|
||||
|
||||
@@ -458,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())
|
||||
@@ -475,24 +357,7 @@ func (a *TransactionsApi) TransactionReconciliationStatementHandler(c *core.WebC
|
||||
transactionAccountBalanceMap[transactionWithBalance.RelatedId] = transactionWithBalance
|
||||
}
|
||||
|
||||
allAccountIds := make([]int64, 0, len(transactions)*2)
|
||||
|
||||
for i := 0; i < len(transactions); i++ {
|
||||
allAccountIds = append(allAccountIds, transactions[i].AccountId)
|
||||
|
||||
if transactions[i].Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN || transactions[i].Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT {
|
||||
allAccountIds = append(allAccountIds, transactions[i].RelatedAccountId)
|
||||
}
|
||||
}
|
||||
|
||||
allAccounts, err := a.accounts.GetAccountsByAccountIds(c, uid, utils.ToUniqueInt64Slice(allAccountIds))
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transactions.TransactionReconciliationStatementHandler] failed to get essential data for assembling transaction result for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
transactionResult, err := a.getTransactionResponseListResult(c, user, transactions, allAccounts, nil, nil, nil, nil, clientTimezone, false, true, true, true)
|
||||
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())
|
||||
@@ -541,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())
|
||||
@@ -582,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
|
||||
@@ -602,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
|
||||
}
|
||||
|
||||
@@ -616,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())
|
||||
@@ -652,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)
|
||||
@@ -667,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
|
||||
@@ -778,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
|
||||
}
|
||||
|
||||
@@ -800,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())
|
||||
@@ -881,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
|
||||
}
|
||||
|
||||
@@ -970,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)
|
||||
|
||||
@@ -1011,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 {
|
||||
@@ -1075,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
|
||||
@@ -1148,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 {
|
||||
@@ -1268,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
|
||||
@@ -1338,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
|
||||
@@ -1405,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
|
||||
}
|
||||
|
||||
@@ -1435,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
|
||||
@@ -1452,13 +1171,13 @@ func (a *TransactionsApi) TransactionDeleteHandler(c *core.WebContext) (any, *er
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// TransactionParseImportCustomFileDataHandler returns the parsed file data by request parameters for current user
|
||||
func (a *TransactionsApi) TransactionParseImportCustomFileDataHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
// TransactionParseImportDsvFileDataHandler returns the parsed file data by request parameters for current user
|
||||
func (a *TransactionsApi) TransactionParseImportDsvFileDataHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
uid := c.GetCurrentUid()
|
||||
form, err := c.MultipartForm()
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transactions.TransactionParseImportCustomFileDataHandler] failed to get multi-part form data for user \"uid:%d\", because %s", uid, err.Error())
|
||||
log.Errorf(c, "[transactions.TransactionParseImportDsvFileDataHandler] failed to get multi-part form data for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.ErrParameterInvalid
|
||||
}
|
||||
|
||||
@@ -1470,18 +1189,18 @@ func (a *TransactionsApi) TransactionParseImportCustomFileDataHandler(c *core.We
|
||||
|
||||
fileType := fileTypes[0]
|
||||
|
||||
if !converters.IsCustomFileFormatFileType(fileType) {
|
||||
if !converters.IsCustomDelimiterSeparatedValuesFileType(fileType) {
|
||||
return nil, errs.Or(err, errs.ErrImportFileTypeNotSupported)
|
||||
}
|
||||
|
||||
fileEncodings := form.Value["fileEncoding"]
|
||||
fileEncoding := ""
|
||||
|
||||
if len(fileEncodings) > 0 {
|
||||
fileEncoding = fileEncodings[0]
|
||||
if len(fileEncodings) < 1 || fileEncodings[0] == "" {
|
||||
return nil, errs.ErrImportFileEncodingIsEmpty
|
||||
}
|
||||
|
||||
dataParser, err := converters.CreateNewCustomFileFormatTransactionDataParser(fileType, fileEncoding)
|
||||
fileEncoding := fileEncodings[0]
|
||||
dataParser, err := converters.CreateNewDelimiterSeparatedValuesDataParser(fileType, fileEncoding)
|
||||
|
||||
if err != nil {
|
||||
return nil, errs.Or(err, errs.ErrImportFileTypeNotSupported)
|
||||
@@ -1490,24 +1209,24 @@ func (a *TransactionsApi) TransactionParseImportCustomFileDataHandler(c *core.We
|
||||
importFiles := form.File["file"]
|
||||
|
||||
if len(importFiles) < 1 {
|
||||
log.Warnf(c, "[transactions.TransactionParseImportCustomFileDataHandler] there is no import file in request for user \"uid:%d\"", uid)
|
||||
log.Warnf(c, "[transactions.TransactionParseImportDsvFileDataHandler] there is no import file in request for user \"uid:%d\"", uid)
|
||||
return nil, errs.ErrNoFilesUpload
|
||||
}
|
||||
|
||||
if importFiles[0].Size < 1 {
|
||||
log.Warnf(c, "[transactions.TransactionParseImportCustomFileDataHandler] the size of import file in request is zero for user \"uid:%d\"", uid)
|
||||
log.Warnf(c, "[transactions.TransactionParseImportDsvFileDataHandler] the size of import file in request is zero for user \"uid:%d\"", uid)
|
||||
return nil, errs.ErrUploadedFileEmpty
|
||||
}
|
||||
|
||||
if importFiles[0].Size > int64(a.CurrentConfig().MaxImportFileSize) {
|
||||
log.Warnf(c, "[transactions.TransactionParseImportCustomFileDataHandler] the upload file size \"%d\" exceeds the maximum size \"%d\" of import file for user \"uid:%d\"", importFiles[0].Size, a.CurrentConfig().MaxImportFileSize, uid)
|
||||
log.Warnf(c, "[transactions.TransactionParseImportDsvFileDataHandler] the upload file size \"%d\" exceeds the maximum size \"%d\" of import file for user \"uid:%d\"", importFiles[0].Size, a.CurrentConfig().MaxImportFileSize, uid)
|
||||
return nil, errs.ErrExceedMaxUploadFileSize
|
||||
}
|
||||
|
||||
importFile, err := importFiles[0].Open()
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transactions.TransactionParseImportCustomFileDataHandler] failed to get import file from request for user \"uid:%d\", because %s", uid, err.Error())
|
||||
log.Errorf(c, "[transactions.TransactionParseImportDsvFileDataHandler] failed to get import file from request for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.ErrOperationFailed
|
||||
}
|
||||
|
||||
@@ -1515,14 +1234,14 @@ func (a *TransactionsApi) TransactionParseImportCustomFileDataHandler(c *core.We
|
||||
fileData, err := io.ReadAll(importFile)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transactions.TransactionParseImportCustomFileDataHandler] failed to read import file data for user \"uid:%d\", because %s", uid, err.Error())
|
||||
log.Errorf(c, "[transactions.TransactionParseImportDsvFileDataHandler] failed to read import file data for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
allLines, err := dataParser.ParseDataLines(c, fileData)
|
||||
allLines, err := dataParser.ParseDsvFileLines(c, fileData)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transactions.TransactionParseImportCustomFileDataHandler] failed to parse import file data for user \"uid:%d\", because %s", uid, err.Error())
|
||||
log.Errorf(c, "[transactions.TransactionParseImportDsvFileDataHandler] failed to parse import file data for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
@@ -1539,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
|
||||
}
|
||||
|
||||
@@ -1554,25 +1273,17 @@ 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.IsCustomFileFormatFileType(fileType) {
|
||||
if converters.IsCustomDelimiterSeparatedValuesFileType(fileType) {
|
||||
fileEncodings := form.Value["fileEncoding"]
|
||||
fileEncoding := ""
|
||||
|
||||
if len(fileEncodings) > 0 {
|
||||
fileEncoding = fileEncodings[0]
|
||||
if len(fileEncodings) < 1 || fileEncodings[0] == "" {
|
||||
return nil, errs.ErrImportFileEncodingIsEmpty
|
||||
}
|
||||
|
||||
fileEncoding := fileEncodings[0]
|
||||
|
||||
columnMappings := form.Value["columnMapping"]
|
||||
|
||||
if len(columnMappings) < 1 || columnMappings[0] == "" {
|
||||
@@ -1656,7 +1367,7 @@ func (a *TransactionsApi) TransactionParseImportFileHandler(c *core.WebContext)
|
||||
transactionTagSeparator = transactionTagSeparators[0]
|
||||
}
|
||||
|
||||
dataImporter, err = converters.CreateNewCustomTransactionDataImporter(fileType, fileEncoding, columnIndexMapping, transactionTypeNameMapping, hasHeaderLine, timeFormats[0], timezoneFormat, amountDecimalSeparator, amountDigitGroupingSymbol, geoLocationSeparator, geoLocationOrder, transactionTagSeparator)
|
||||
dataImporter, err = converters.CreateNewDelimiterSeparatedValuesDataImporter(fileType, fileEncoding, columnIndexMapping, transactionTypeNameMapping, hasHeaderLine, timeFormats[0], timezoneFormat, amountDecimalSeparator, amountDigitGroupingSymbol, geoLocationSeparator, geoLocationOrder, transactionTagSeparator)
|
||||
} else {
|
||||
dataImporter, err = converters.GetTransactionDataImporter(fileType)
|
||||
}
|
||||
@@ -1738,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())
|
||||
@@ -1769,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 != "" {
|
||||
@@ -1861,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
|
||||
@@ -1978,61 +1682,7 @@ func (a *TransactionsApi) getTransactionTagInfoResponses(tagIds []int64, allTran
|
||||
return allTags
|
||||
}
|
||||
|
||||
func (a *TransactionsApi) getTransactionAllEssentialData(c *core.WebContext, user *models.User, withPictures bool, trimCategory bool, trimTag bool) (accountMap map[int64]*models.Account, categoryMap map[int64]*models.TransactionCategory, tagMap map[int64]*models.TransactionTag, allTransactionTagIds map[int64][]int64, pictureInfoMap map[int64][]*models.TransactionPictureInfo, err error) {
|
||||
uid := user.Uid
|
||||
allAccounts, err := a.accounts.GetAllAccountsByUid(c, uid)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transactions.getTransactionAllEssentialData] failed to get all accounts for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
accountMap = a.accounts.GetAccountMapByList(allAccounts)
|
||||
|
||||
allTagIndexes, err := a.transactionTags.GetAllTagIdsOfAllTransactions(c, uid)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transactions.getTransactionAllEssentialData] failed to get all transactions tag ids for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
allTransactionTagIds = a.transactionTags.GetGroupedTransactionTagIds(allTagIndexes)
|
||||
|
||||
if !trimCategory {
|
||||
allCategories, err := a.transactionCategories.GetAllCategoriesByUid(c, uid, 0, -1)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transactions.getTransactionAllEssentialData] failed to get all transactions categories for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
categoryMap = a.transactionCategories.GetCategoryMapByList(allCategories)
|
||||
}
|
||||
|
||||
if !trimTag {
|
||||
allTags, err := a.transactionTags.GetAllTagsByUid(c, uid)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transactions.getTransactionAllEssentialData] failed to get all transactions tags for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
tagMap = a.transactionTags.GetTagMapByList(allTags)
|
||||
}
|
||||
|
||||
if withPictures && a.CurrentConfig().EnableTransactionPictures {
|
||||
pictureInfoMap, err = a.transactionPictures.GetAllPictureInfosOfAllTransactions(c, uid)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transactions.getTransactionAllEssentialData] failed to get all transactions pictures for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, nil, nil, nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return accountMap, categoryMap, tagMap, allTransactionTagIds, pictureInfoMap, nil
|
||||
}
|
||||
|
||||
func (a *TransactionsApi) getTransactionEssentialDataByTransactionIds(c *core.WebContext, user *models.User, transactions []*models.Transaction, withPictures bool, trimCategory bool, trimTag bool) (accountMap map[int64]*models.Account, categoryMap map[int64]*models.TransactionCategory, tagMap map[int64]*models.TransactionTag, allTransactionTagIds map[int64][]int64, pictureInfoMap map[int64][]*models.TransactionPictureInfo, err error) {
|
||||
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)
|
||||
@@ -2055,26 +1705,32 @@ func (a *TransactionsApi) getTransactionEssentialDataByTransactionIds(c *core.We
|
||||
categoryIds = append(categoryIds, transactions[i].CategoryId)
|
||||
}
|
||||
|
||||
accountMap, err = a.accounts.GetAccountsByAccountIds(c, uid, utils.ToUniqueInt64Slice(accountIds))
|
||||
allAccounts, err := a.accounts.GetAccountsByAccountIds(c, uid, utils.ToUniqueInt64Slice(accountIds))
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transactions.getTransactionEssentialDataByTransactionIds] failed to get accounts for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, nil, nil, nil, nil, err
|
||||
log.Errorf(c, "[transactions.getTransactionResponseListResult] failed to get accounts for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
allTransactionTagIds, err = a.transactionTags.GetAllTagIdsOfTransactions(c, uid, transactionIds)
|
||||
transactions = a.filterTransactions(c, uid, transactions, allAccounts)
|
||||
|
||||
allTransactionTagIds, err := a.transactionTags.GetAllTagIdsOfTransactions(c, uid, transactionIds)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transactions.getTransactionEssentialDataByTransactionIds] failed to get transactions tag ids for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, nil, nil, nil, nil, err
|
||||
log.Errorf(c, "[transactions.getTransactionResponseListResult] failed to get transactions tag ids for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var categoryMap map[int64]*models.TransactionCategory
|
||||
var tagMap map[int64]*models.TransactionTag
|
||||
var pictureInfoMap map[int64][]*models.TransactionPictureInfo
|
||||
|
||||
if !trimCategory {
|
||||
categoryMap, err = a.transactionCategories.GetCategoriesByCategoryIds(c, uid, utils.ToUniqueInt64Slice(categoryIds))
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transactions.getTransactionEssentialDataByTransactionIds] failed to get transactions categories for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, nil, nil, nil, nil, err
|
||||
log.Errorf(c, "[transactions.getTransactionResponseListResult] failed to get transactions categories for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2082,8 +1738,8 @@ func (a *TransactionsApi) getTransactionEssentialDataByTransactionIds(c *core.We
|
||||
tagMap, err = a.transactionTags.GetTagsByTagIds(c, uid, utils.ToUniqueInt64Slice(a.transactionTags.GetTransactionTagIds(allTransactionTagIds)))
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transactions.getTransactionEssentialDataByTransactionIds] failed to get transactions tags for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, nil, nil, nil, nil, err
|
||||
log.Errorf(c, "[transactions.getTransactionResponseListResult] failed to get transactions tags for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2091,15 +1747,11 @@ func (a *TransactionsApi) getTransactionEssentialDataByTransactionIds(c *core.We
|
||||
pictureInfoMap, err = a.transactionPictures.GetPictureInfosByTransactionIds(c, uid, utils.ToUniqueInt64Slice(a.transactions.GetTransactionIds(transactions)))
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transactions.getTransactionEssentialDataByTransactionIds] failed to get transactions pictures for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, nil, nil, nil, nil, err
|
||||
log.Errorf(c, "[transactions.getTransactionResponseListResult] failed to get transactions pictures for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return accountMap, categoryMap, tagMap, allTransactionTagIds, pictureInfoMap, nil
|
||||
}
|
||||
|
||||
func (a *TransactionsApi) getTransactionResponseListResult(c *core.WebContext, user *models.User, transactions []*models.Transaction, allAccounts map[int64]*models.Account, categoryMap map[int64]*models.TransactionCategory, tagMap map[int64]*models.TransactionTag, allTransactionTagIds map[int64][]int64, pictureInfoMap map[int64][]*models.TransactionPictureInfo, clientTimezone *time.Location, withPictures bool, trimAccount bool, trimCategory bool, trimTag bool) (models.TransactionInfoResponseSlice, error) {
|
||||
result := make(models.TransactionInfoResponseSlice, len(transactions))
|
||||
|
||||
for i := 0; i < len(transactions); i++ {
|
||||
@@ -2109,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)
|
||||
|
||||
@@ -2123,17 +1775,17 @@ func (a *TransactionsApi) getTransactionResponseListResult(c *core.WebContext, u
|
||||
}
|
||||
}
|
||||
|
||||
if !trimCategory && categoryMap != nil {
|
||||
if !trimCategory {
|
||||
if category := categoryMap[transaction.CategoryId]; category != nil {
|
||||
result[i].Category = category.ToTransactionCategoryInfoResponse()
|
||||
}
|
||||
}
|
||||
|
||||
if !trimTag && tagMap != nil && transactionTagIds != nil {
|
||||
if !trimTag {
|
||||
result[i].Tags = a.getTransactionTagInfoResponses(transactionTagIds, tagMap)
|
||||
}
|
||||
|
||||
if withPictures && a.CurrentConfig().EnableTransactionPictures && pictureInfoMap != nil {
|
||||
if withPictures && a.CurrentConfig().EnableTransactionPictures {
|
||||
pictureInfos, exists := pictureInfoMap[transaction.TransactionId]
|
||||
|
||||
if exists {
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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, core.GetOutgoingUserAgent(), 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
+9
-18
@@ -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())
|
||||
@@ -150,7 +149,7 @@ func (l *UserDataCli) ModifyUserPassword(c *core.CliContext, username string, pa
|
||||
Password: password,
|
||||
}
|
||||
|
||||
err = l.users.UpdateUserPassword(c, userNew)
|
||||
_, _, err = l.users.UpdateUser(c, userNew, false)
|
||||
|
||||
if err != nil {
|
||||
log.CliErrorf(c, "[user_data.ModifyUserPassword] failed to update user \"%s\" password, 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)
|
||||
}
|
||||
|
||||
@@ -9,20 +9,11 @@ const (
|
||||
CAMT_INDICATOR_DEBIT camtCreditDebitIndicator = "DBIT"
|
||||
)
|
||||
|
||||
type camt052File struct {
|
||||
XMLName xml.Name `xml:"Document"`
|
||||
BankToCustomerAccountReport *camtBankToCustomerAccountReport `xml:"BkToCstmrAcctRpt"`
|
||||
}
|
||||
|
||||
type camt053File struct {
|
||||
XMLName xml.Name `xml:"Document"`
|
||||
BankToCustomerStatement *camtBankToCustomerStatement `xml:"BkToCstmrStmt"`
|
||||
}
|
||||
|
||||
type camtBankToCustomerAccountReport struct {
|
||||
Statements []*camtStatement `xml:"Rpt"`
|
||||
}
|
||||
|
||||
type camtBankToCustomerStatement struct {
|
||||
Statements []*camtStatement `xml:"Stmt"`
|
||||
}
|
||||
|
||||
@@ -10,30 +10,11 @@ import (
|
||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||
)
|
||||
|
||||
// camt052FileReader defines the structure of camt.052 file reader
|
||||
type camt052FileReader struct {
|
||||
xmlDecoder *xml.Decoder
|
||||
}
|
||||
|
||||
// camt053FileReader defines the structure of camt.053 file reader
|
||||
type camt053FileReader struct {
|
||||
xmlDecoder *xml.Decoder
|
||||
}
|
||||
|
||||
// read returns the imported camt.052 data
|
||||
// Reference: https://www.iso20022.org/message-set/1196/download
|
||||
func (r *camt052FileReader) read(ctx core.Context) (*camt052File, error) {
|
||||
file := &camt052File{}
|
||||
|
||||
err := r.xmlDecoder.Decode(&file)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return file, nil
|
||||
}
|
||||
|
||||
// read returns the imported camt.053 data
|
||||
// Reference: https://www.iso20022.org/message-set/1196/download
|
||||
func (r *camt053FileReader) read(ctx core.Context) (*camt053File, error) {
|
||||
@@ -48,19 +29,6 @@ func (r *camt053FileReader) read(ctx core.Context) (*camt053File, error) {
|
||||
return file, nil
|
||||
}
|
||||
|
||||
func createNewCamt052FileReader(data []byte) (*camt052FileReader, error) {
|
||||
if len(data) > 5 && data[0] == 0x3C && data[1] == 0x3F && data[2] == 0x78 && data[3] == 0x6D && data[4] == 0x6C { // <?xml
|
||||
xmlDecoder := xml.NewDecoder(bytes.NewReader(data))
|
||||
xmlDecoder.CharsetReader = charset.NewReaderLabel
|
||||
|
||||
return &camt052FileReader{
|
||||
xmlDecoder: xmlDecoder,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, errs.ErrInvalidXmlFile
|
||||
}
|
||||
|
||||
func createNewCamt053FileReader(data []byte) (*camt053FileReader, error) {
|
||||
if len(data) > 5 && data[0] == 0x3C && data[1] == 0x3F && data[2] == 0x78 && data[3] == 0x6D && data[4] == 0x6C { // <?xml
|
||||
xmlDecoder := xml.NewDecoder(bytes.NewReader(data))
|
||||
|
||||
@@ -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
|
||||
@@ -303,12 +303,12 @@ func (t *camtStatementTransactionDataRowIterator) parseTransaction(ctx core.Cont
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func createNewCamtStatementTransactionDataTable(camtStatements []*camtStatement) (*camtStatementTransactionDataTable, error) {
|
||||
if len(camtStatements) == 0 {
|
||||
func createNewCamtStatementTransactionDataTable(file *camt053File) (*camtStatementTransactionDataTable, error) {
|
||||
if file == nil || file.BankToCustomerStatement == nil || len(file.BankToCustomerStatement.Statements) == 0 {
|
||||
return nil, errs.ErrNotFoundTransactionDataInFile
|
||||
}
|
||||
|
||||
return &camtStatementTransactionDataTable{
|
||||
allStatements: camtStatements,
|
||||
allStatements: file.BankToCustomerStatement.Statements,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
package camt
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"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"
|
||||
)
|
||||
@@ -16,51 +13,17 @@ var camtTransactionTypeNameMapping = map[models.TransactionType]string{
|
||||
models.TRANSACTION_TYPE_TRANSFER: utils.IntToString(int(models.TRANSACTION_TYPE_TRANSFER)),
|
||||
}
|
||||
|
||||
// camt052TransactionDataImporter defines the structure of camt.052 file importer for transaction data
|
||||
type camt052TransactionDataImporter struct {
|
||||
}
|
||||
|
||||
// camt053TransactionDataImporter defines the structure of camt.053 file importer for transaction data
|
||||
type camt053TransactionDataImporter struct {
|
||||
}
|
||||
|
||||
// Initialize camt.052 and camt.053 transaction data importer singleton instances
|
||||
// Initialize a camt.053 transaction data importer singleton instance
|
||||
var (
|
||||
Camt052TransactionDataImporter = &camt052TransactionDataImporter{}
|
||||
Camt053TransactionDataImporter = &camt053TransactionDataImporter{}
|
||||
)
|
||||
|
||||
// ParseImportedData returns the imported data by parsing the camt.052 file transaction data
|
||||
func (c *camt052TransactionDataImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezone *time.Location, additionalOptions converter.TransactionDataImporterOptions, accountMap map[string]*models.Account, expenseCategoryMap map[string]map[string]*models.TransactionCategory, incomeCategoryMap map[string]map[string]*models.TransactionCategory, transferCategoryMap map[string]map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionTag, error) {
|
||||
camt052DataReader, err := createNewCamt052FileReader(data)
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
camt052Data, err := camt052DataReader.read(ctx)
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
if camt052Data.BankToCustomerAccountReport == nil || camt052Data.BankToCustomerAccountReport.Statements == nil {
|
||||
return nil, nil, nil, nil, nil, nil, errs.ErrNotFoundTransactionDataInFile
|
||||
}
|
||||
|
||||
transactionDataTable, err := createNewCamtStatementTransactionDataTable(camt052Data.BankToCustomerAccountReport.Statements)
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
dataTableImporter := converter.CreateNewSimpleImporterWithTypeNameMapping(camtTransactionTypeNameMapping)
|
||||
|
||||
return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezone, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
|
||||
}
|
||||
|
||||
// ParseImportedData returns the imported data by parsing the camt.053 file transaction data
|
||||
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 {
|
||||
@@ -73,11 +36,7 @@ func (c *camt053TransactionDataImporter) ParseImportedData(ctx core.Context, use
|
||||
return nil, nil, nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
if camt053Data.BankToCustomerStatement == nil || camt053Data.BankToCustomerStatement.Statements == nil {
|
||||
return nil, nil, nil, nil, nil, nil, errs.ErrNotFoundTransactionDataInFile
|
||||
}
|
||||
|
||||
transactionDataTable, err := createNewCamtStatementTransactionDataTable(camt053Data.BankToCustomerStatement.Statements)
|
||||
transactionDataTable, err := createNewCamtStatementTransactionDataTable(camt053Data)
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, nil, err
|
||||
@@ -85,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,122 +2,17 @@ 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"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||
)
|
||||
|
||||
func TestCamt052TransactionDataFileParseImportedData_MinimumValidData(t *testing.T) {
|
||||
importer := Camt052TransactionDataImporter
|
||||
context := core.NewNullContext()
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1234567890,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := importer.ParseImportedData(context, user, []byte(
|
||||
`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.052.001.02">
|
||||
<BkToCstmrAcctRpt>
|
||||
<Rpt>
|
||||
<Acct>
|
||||
<Id>
|
||||
<IBAN>123</IBAN>
|
||||
</Id>
|
||||
<Ccy>CNY</Ccy>
|
||||
</Acct>
|
||||
<Ntry>
|
||||
<BookgDt>
|
||||
<DtTm>2024-09-01T01:23:45+08:00</DtTm>
|
||||
</BookgDt>
|
||||
<CdtDbtInd>CRDT</CdtDbtInd>
|
||||
<Amt Ccy="CNY">123.45</Amt>
|
||||
</Ntry>
|
||||
<Ntry>
|
||||
<BookgDt>
|
||||
<DtTm>2024-09-01T12:34:56+08:00</DtTm>
|
||||
</BookgDt>
|
||||
<CdtDbtInd>DBIT</CdtDbtInd>
|
||||
<Amt Ccy="CNY">0.12</Amt>
|
||||
</Ntry>
|
||||
</Rpt>
|
||||
<Rpt>
|
||||
<Acct>
|
||||
<Id>
|
||||
<Othr>
|
||||
<Id>456</Id>
|
||||
</Othr>
|
||||
</Id>
|
||||
<Ccy>USD</Ccy>
|
||||
</Acct>
|
||||
<Ntry>
|
||||
<BookgDt>
|
||||
<DtTm>2024-09-01T23:59:59+08:00</DtTm>
|
||||
</BookgDt>
|
||||
<CdtDbtInd>CRDT</CdtDbtInd>
|
||||
<Amt Ccy="USD">1.23</Amt>
|
||||
</Ntry>
|
||||
</Rpt>
|
||||
</BkToCstmrAcctRpt>
|
||||
</Document>`), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
|
||||
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, 3, len(allNewTransactions))
|
||||
assert.Equal(t, 2, len(allNewAccounts))
|
||||
assert.Equal(t, 1, len(allNewSubExpenseCategories))
|
||||
assert.Equal(t, 1, len(allNewSubIncomeCategories))
|
||||
assert.Equal(t, 0, len(allNewSubTransferCategories))
|
||||
assert.Equal(t, 0, len(allNewTags))
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewTransactions[0].Uid)
|
||||
assert.Equal(t, models.TRANSACTION_DB_TYPE_INCOME, allNewTransactions[0].Type)
|
||||
assert.Equal(t, int64(1725125025), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
|
||||
assert.Equal(t, int64(12345), allNewTransactions[0].Amount)
|
||||
assert.Equal(t, "123", allNewTransactions[0].OriginalSourceAccountName)
|
||||
assert.Equal(t, "CNY", allNewTransactions[0].OriginalSourceAccountCurrency)
|
||||
assert.Equal(t, "", allNewTransactions[0].OriginalCategoryName)
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewTransactions[1].Uid)
|
||||
assert.Equal(t, models.TRANSACTION_DB_TYPE_EXPENSE, allNewTransactions[1].Type)
|
||||
assert.Equal(t, int64(1725165296), utils.GetUnixTimeFromTransactionTime(allNewTransactions[1].TransactionTime))
|
||||
assert.Equal(t, int64(12), allNewTransactions[1].Amount)
|
||||
assert.Equal(t, "123", allNewTransactions[1].OriginalSourceAccountName)
|
||||
assert.Equal(t, "CNY", allNewTransactions[1].OriginalSourceAccountCurrency)
|
||||
assert.Equal(t, "", allNewTransactions[1].OriginalCategoryName)
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewTransactions[2].Uid)
|
||||
assert.Equal(t, models.TRANSACTION_DB_TYPE_INCOME, allNewTransactions[2].Type)
|
||||
assert.Equal(t, int64(1725206399), utils.GetUnixTimeFromTransactionTime(allNewTransactions[2].TransactionTime))
|
||||
assert.Equal(t, int64(123), allNewTransactions[2].Amount)
|
||||
assert.Equal(t, "456", allNewTransactions[2].OriginalSourceAccountName)
|
||||
assert.Equal(t, "USD", allNewTransactions[2].OriginalSourceAccountCurrency)
|
||||
assert.Equal(t, "", allNewTransactions[2].OriginalCategoryName)
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewAccounts[0].Uid)
|
||||
assert.Equal(t, "123", allNewAccounts[0].Name)
|
||||
assert.Equal(t, "CNY", allNewAccounts[0].Currency)
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewAccounts[1].Uid)
|
||||
assert.Equal(t, "456", allNewAccounts[1].Name)
|
||||
assert.Equal(t, "USD", allNewAccounts[1].Currency)
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewSubExpenseCategories[0].Uid)
|
||||
assert.Equal(t, "", allNewSubExpenseCategories[0].Name)
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewSubIncomeCategories[0].Uid)
|
||||
assert.Equal(t, "", allNewSubIncomeCategories[0].Name)
|
||||
}
|
||||
|
||||
func TestCamt053TransactionDataFileParseImportedData_MinimumValidData(t *testing.T) {
|
||||
importer := Camt053TransactionDataImporter
|
||||
converter := Camt053TransactionDataImporter
|
||||
context := core.NewNullContext()
|
||||
|
||||
user := &models.User{
|
||||
@@ -125,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>
|
||||
@@ -169,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)
|
||||
|
||||
@@ -220,7 +115,7 @@ func TestCamt053TransactionDataFileParseImportedData_MinimumValidData(t *testing
|
||||
}
|
||||
|
||||
func TestCamt053TransactionDataFileParseImportedData_ParseValidTransactionTime(t *testing.T) {
|
||||
importer := Camt053TransactionDataImporter
|
||||
converter := Camt053TransactionDataImporter
|
||||
context := core.NewNullContext()
|
||||
|
||||
user := &models.User{
|
||||
@@ -228,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>
|
||||
@@ -262,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))
|
||||
@@ -273,7 +168,7 @@ func TestCamt053TransactionDataFileParseImportedData_ParseValidTransactionTime(t
|
||||
}
|
||||
|
||||
func TestCamt053TransactionDataFileParseImportedData_ParseInvalidTransactionTime(t *testing.T) {
|
||||
importer := Camt053TransactionDataImporter
|
||||
converter := Camt053TransactionDataImporter
|
||||
context := core.NewNullContext()
|
||||
|
||||
user := &models.User{
|
||||
@@ -281,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>
|
||||
@@ -301,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>
|
||||
@@ -324,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>
|
||||
@@ -347,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>
|
||||
@@ -370,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{
|
||||
@@ -383,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>
|
||||
@@ -419,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))
|
||||
@@ -428,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>
|
||||
@@ -470,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))
|
||||
@@ -479,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>
|
||||
@@ -508,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>
|
||||
@@ -535,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))
|
||||
@@ -544,7 +439,7 @@ func TestCamt053TransactionDataFileParseImportedData_ParseTransactionValidAmount
|
||||
}
|
||||
|
||||
func TestCamt053TransactionDataFileParseImportedData_ParseTransactionInvalidAmountAndCurrency(t *testing.T) {
|
||||
importer := Camt053TransactionDataImporter
|
||||
converter := Camt053TransactionDataImporter
|
||||
context := core.NewNullContext()
|
||||
|
||||
user := &models.User{
|
||||
@@ -552,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>
|
||||
@@ -572,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>
|
||||
@@ -603,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>
|
||||
@@ -634,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{
|
||||
@@ -647,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>
|
||||
@@ -677,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>
|
||||
@@ -712,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>
|
||||
@@ -739,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))
|
||||
@@ -747,7 +642,7 @@ func TestCamt053TransactionDataFileParseImportedData_ParseDescription(t *testing
|
||||
}
|
||||
|
||||
func TestCamt053TransactionDataFileParseImportedData_MissingAccountNode(t *testing.T) {
|
||||
importer := Camt053TransactionDataImporter
|
||||
converter := Camt053TransactionDataImporter
|
||||
context := core.NewNullContext()
|
||||
|
||||
user := &models.User{
|
||||
@@ -755,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>
|
||||
@@ -769,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{
|
||||
@@ -782,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>
|
||||
@@ -799,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>
|
||||
@@ -821,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>
|
||||
@@ -843,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>
|
||||
@@ -865,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)
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package custom
|
||||
|
||||
import "github.com/mayswind/ezbookkeeping/pkg/core"
|
||||
|
||||
// CustomTransactionDataParser represents the parser for custom transaction data files
|
||||
type CustomTransactionDataParser interface {
|
||||
ParseDataLines(ctx core.Context, data []byte) ([][]string, error)
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
package custom
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mayswind/ezbookkeeping/pkg/converters/converter"
|
||||
csvconverter "github.com/mayswind/ezbookkeeping/pkg/converters/csv"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/converters/excel"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||
)
|
||||
|
||||
const customOOXMLExcelFileType = "custom_xlsx"
|
||||
const customMSCFBExcelFileType = "custom_xls"
|
||||
|
||||
// customTransactionDataExcelFileImporter defines the structure of custom excel importer for transaction data
|
||||
type customTransactionDataExcelFileImporter struct {
|
||||
fileType string
|
||||
columnIndexMapping map[datatable.TransactionDataTableColumn]int
|
||||
transactionTypeNameMapping map[string]models.TransactionType
|
||||
hasHeaderLine bool
|
||||
timeFormat string
|
||||
timezoneFormat string
|
||||
amountDecimalSeparator string
|
||||
amountDigitGroupingSymbol string
|
||||
geoLocationSeparator string
|
||||
geoLocationOrder converter.TransactionGeoLocationOrder
|
||||
transactionTagSeparator string
|
||||
}
|
||||
|
||||
// ParseDataLines returns the parsed file lines for specified the excel file data
|
||||
func (c *customTransactionDataExcelFileImporter) ParseDataLines(ctx core.Context, data []byte) ([][]string, error) {
|
||||
var excelDataTable datatable.BasicDataTable
|
||||
var err error
|
||||
|
||||
if c.fileType == customOOXMLExcelFileType {
|
||||
excelDataTable, err = excel.CreateNewExcelOOXMLFileBasicDataTable(data, false)
|
||||
} else if c.fileType == customMSCFBExcelFileType {
|
||||
excelDataTable, err = excel.CreateNewExcelMSCFBFileBasicDataTable(data, false)
|
||||
} else {
|
||||
return nil, errs.ErrImportFileTypeNotSupported
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
iterator := excelDataTable.DataRowIterator()
|
||||
allLines := make([][]string, 0)
|
||||
|
||||
for iterator.HasNext() {
|
||||
row := iterator.Next()
|
||||
items := make([]string, row.ColumnCount())
|
||||
|
||||
for i := 0; i < row.ColumnCount(); i++ {
|
||||
items[i] = strings.Trim(row.GetData(i), " ")
|
||||
}
|
||||
|
||||
allLines = append(allLines, items)
|
||||
}
|
||||
|
||||
return allLines, nil
|
||||
}
|
||||
|
||||
// ParseImportedData returns the imported data by parsing the custom transaction dsv data
|
||||
func (c *customTransactionDataExcelFileImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezone *time.Location, additionalOptions converter.TransactionDataImporterOptions, accountMap map[string]*models.Account, expenseCategoryMap map[string]map[string]*models.TransactionCategory, incomeCategoryMap map[string]map[string]*models.TransactionCategory, transferCategoryMap map[string]map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionTag, error) {
|
||||
allLines, err := c.ParseDataLines(ctx, data)
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
dataTable := csvconverter.CreateNewCustomCsvBasicDataTable(allLines, c.hasHeaderLine)
|
||||
transactionDataTable := CreateNewCustomPlainTextDataTable(dataTable, c.columnIndexMapping, c.transactionTypeNameMapping, c.timeFormat, c.timezoneFormat, c.amountDecimalSeparator, c.amountDigitGroupingSymbol)
|
||||
dataTableImporter := converter.CreateNewImporterWithTypeNameMapping(customTransactionTypeNameMapping, c.geoLocationSeparator, c.geoLocationOrder, c.transactionTagSeparator)
|
||||
|
||||
return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezone, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
|
||||
}
|
||||
|
||||
// IsCustomExcelFileType returns whether the file type is the custom excel file type
|
||||
func IsCustomExcelFileType(fileType string) bool {
|
||||
return fileType == customOOXMLExcelFileType || fileType == customMSCFBExcelFileType
|
||||
}
|
||||
|
||||
// CreateNewCustomTransactionDataExcelFileParser returns a new custom transaction data parser
|
||||
func CreateNewCustomTransactionDataExcelFileParser(fileType string) (CustomTransactionDataParser, error) {
|
||||
if fileType != customOOXMLExcelFileType && fileType != customMSCFBExcelFileType {
|
||||
return nil, errs.ErrImportFileTypeNotSupported
|
||||
}
|
||||
|
||||
return &customTransactionDataExcelFileImporter{
|
||||
fileType: fileType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CreateNewCustomTransactionDataExcelFileImporter returns a new custom excel importer for transaction data
|
||||
func CreateNewCustomTransactionDataExcelFileImporter(fileType string, columnIndexMapping map[datatable.TransactionDataTableColumn]int, transactionTypeNameMapping map[string]models.TransactionType, hasHeaderLine bool, timeFormat string, timezoneFormat string, amountDecimalSeparator string, amountDigitGroupingSymbol string, geoLocationSeparator string, geoLocationOrder string, transactionTagSeparator string) (converter.TransactionDataImporter, error) {
|
||||
if fileType != customOOXMLExcelFileType && fileType != customMSCFBExcelFileType {
|
||||
return nil, errs.ErrImportFileTypeNotSupported
|
||||
}
|
||||
|
||||
if geoLocationOrder == "" {
|
||||
geoLocationOrder = string(converter.TRANSACTION_GEO_LOCATION_ORDER_LONGITUDE_LATITUDE)
|
||||
} else if geoLocationOrder != string(converter.TRANSACTION_GEO_LOCATION_ORDER_LONGITUDE_LATITUDE) &&
|
||||
geoLocationOrder != string(converter.TRANSACTION_GEO_LOCATION_ORDER_LATITUDE_LONGITUDE) {
|
||||
return nil, errs.ErrImportFileTypeNotSupported
|
||||
}
|
||||
|
||||
if _, exists := columnIndexMapping[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME]; !exists {
|
||||
return nil, errs.ErrMissingRequiredFieldInHeaderRow
|
||||
}
|
||||
|
||||
if _, exists := columnIndexMapping[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE]; !exists {
|
||||
return nil, errs.ErrMissingRequiredFieldInHeaderRow
|
||||
}
|
||||
|
||||
if _, exists := columnIndexMapping[datatable.TRANSACTION_DATA_TABLE_AMOUNT]; !exists {
|
||||
return nil, errs.ErrMissingRequiredFieldInHeaderRow
|
||||
}
|
||||
|
||||
return &customTransactionDataExcelFileImporter{
|
||||
fileType: fileType,
|
||||
columnIndexMapping: columnIndexMapping,
|
||||
transactionTypeNameMapping: transactionTypeNameMapping,
|
||||
hasHeaderLine: hasHeaderLine,
|
||||
timeFormat: timeFormat,
|
||||
timezoneFormat: timezoneFormat,
|
||||
amountDecimalSeparator: amountDecimalSeparator,
|
||||
amountDigitGroupingSymbol: amountDigitGroupingSymbol,
|
||||
geoLocationSeparator: geoLocationSeparator,
|
||||
geoLocationOrder: converter.TransactionGeoLocationOrder(geoLocationOrder),
|
||||
transactionTagSeparator: transactionTagSeparator,
|
||||
}, nil
|
||||
}
|
||||
@@ -1,254 +0,0 @@
|
||||
package custom
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestIsCustomExcelFileType(t *testing.T) {
|
||||
assert.True(t, IsCustomExcelFileType("custom_xlsx"))
|
||||
assert.True(t, IsCustomExcelFileType("custom_xls"))
|
||||
|
||||
assert.False(t, IsCustomExcelFileType("xlsx"))
|
||||
assert.False(t, IsCustomExcelFileType("xls"))
|
||||
assert.False(t, IsCustomExcelFileType("excel"))
|
||||
}
|
||||
|
||||
func TestCustomTransactionDataParser_ParseOOXMLExcelDataLines_EmptyData(t *testing.T) {
|
||||
importer, err := CreateNewCustomTransactionDataExcelFileParser("custom_xlsx")
|
||||
assert.Nil(t, err)
|
||||
|
||||
context := core.NewNullContext()
|
||||
|
||||
testdata, err := os.ReadFile("../../../testdata/empty_excel_file.xlsx")
|
||||
assert.Nil(t, err)
|
||||
|
||||
allLines, err := importer.ParseDataLines(context, testdata)
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, 0, len(allLines))
|
||||
}
|
||||
|
||||
func TestCustomTransactionDataParser_ParseOOXMLExcelDataLines_SingleSheet(t *testing.T) {
|
||||
importer, err := CreateNewCustomTransactionDataExcelFileParser("custom_xlsx")
|
||||
assert.Nil(t, err)
|
||||
|
||||
context := core.NewNullContext()
|
||||
|
||||
testdata, err := os.ReadFile("../../../testdata/simple_excel_file.xlsx")
|
||||
assert.Nil(t, err)
|
||||
|
||||
allLines, err := importer.ParseDataLines(context, testdata)
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, 3, len(allLines))
|
||||
|
||||
assert.Equal(t, 3, len(allLines[0]))
|
||||
assert.Equal(t, "A1", allLines[0][0])
|
||||
assert.Equal(t, "B1", allLines[0][1])
|
||||
assert.Equal(t, "C1", allLines[0][2])
|
||||
|
||||
assert.Equal(t, 3, len(allLines[1]))
|
||||
assert.Equal(t, "A2", allLines[1][0])
|
||||
assert.Equal(t, "B2", allLines[1][1])
|
||||
assert.Equal(t, "C2", allLines[1][2])
|
||||
|
||||
assert.Equal(t, 3, len(allLines[2]))
|
||||
assert.Equal(t, "A3", allLines[2][0])
|
||||
assert.Equal(t, "B3", allLines[2][1])
|
||||
assert.Equal(t, "C3", allLines[2][2])
|
||||
}
|
||||
|
||||
func TestCustomTransactionDataParser_ParseOOXMLExcelDataLines_MultipleSheet(t *testing.T) {
|
||||
importer, err := CreateNewCustomTransactionDataExcelFileParser("custom_xlsx")
|
||||
assert.Nil(t, err)
|
||||
|
||||
context := core.NewNullContext()
|
||||
|
||||
testdata, err := os.ReadFile("../../../testdata/multiple_sheets_excel_file.xlsx")
|
||||
assert.Nil(t, err)
|
||||
|
||||
allLines, err := importer.ParseDataLines(context, testdata)
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, 9, len(allLines))
|
||||
|
||||
assert.Equal(t, 3, len(allLines[0]))
|
||||
assert.Equal(t, "A1", allLines[0][0])
|
||||
assert.Equal(t, "B1", allLines[0][1])
|
||||
assert.Equal(t, "C1", allLines[0][2])
|
||||
|
||||
assert.Equal(t, 3, len(allLines[1]))
|
||||
assert.Equal(t, "1-A2", allLines[1][0])
|
||||
assert.Equal(t, "1-B2", allLines[1][1])
|
||||
assert.Equal(t, "1-C2", allLines[1][2])
|
||||
|
||||
assert.Equal(t, 3, len(allLines[2]))
|
||||
assert.Equal(t, "1-A3", allLines[2][0])
|
||||
assert.Equal(t, "1-B3", allLines[2][1])
|
||||
assert.Equal(t, "1-C3", allLines[2][2])
|
||||
|
||||
assert.Equal(t, 3, len(allLines[3]))
|
||||
assert.Equal(t, "A1", allLines[3][0])
|
||||
assert.Equal(t, "B1", allLines[3][1])
|
||||
assert.Equal(t, "C1", allLines[3][2])
|
||||
|
||||
assert.Equal(t, 2, len(allLines[4]))
|
||||
assert.Equal(t, "3-A2", allLines[4][0])
|
||||
assert.Equal(t, "3-B2", allLines[4][1])
|
||||
|
||||
assert.Equal(t, 3, len(allLines[5]))
|
||||
assert.Equal(t, "A1", allLines[5][0])
|
||||
assert.Equal(t, "B1", allLines[5][1])
|
||||
assert.Equal(t, "C1", allLines[5][2])
|
||||
|
||||
assert.Equal(t, 3, len(allLines[6]))
|
||||
assert.Equal(t, "A1", allLines[6][0])
|
||||
assert.Equal(t, "B1", allLines[6][1])
|
||||
assert.Equal(t, "C1", allLines[6][2])
|
||||
|
||||
assert.Equal(t, 3, len(allLines[7]))
|
||||
assert.Equal(t, "5-A2", allLines[7][0])
|
||||
assert.Equal(t, "5-B2", allLines[7][1])
|
||||
assert.Equal(t, "5-C2", allLines[7][2])
|
||||
|
||||
assert.Equal(t, 3, len(allLines[8]))
|
||||
assert.Equal(t, "5-A3", allLines[8][0])
|
||||
assert.Equal(t, "5-B3", allLines[8][1])
|
||||
assert.Equal(t, "5-C3", allLines[8][2])
|
||||
}
|
||||
|
||||
func TestCustomTransactionDataParser_ParseOOXMLExcelDataLines_MultipleSheetWithDifferentColumnCount(t *testing.T) {
|
||||
importer, err := CreateNewCustomTransactionDataExcelFileParser("custom_xlsx")
|
||||
assert.Nil(t, err)
|
||||
|
||||
context := core.NewNullContext()
|
||||
|
||||
testdata, err := os.ReadFile("../../../testdata/multiple_sheets_with_different_header_row_excel_file.xlsx")
|
||||
assert.Nil(t, err)
|
||||
|
||||
_, err = importer.ParseDataLines(context, testdata)
|
||||
assert.EqualError(t, err, errs.ErrFieldsInMultiTableAreDifferent.Message)
|
||||
}
|
||||
|
||||
func TestCustomTransactionDataParser_ParseMSCFBExcelDataLines_EmptyData(t *testing.T) {
|
||||
importer, err := CreateNewCustomTransactionDataExcelFileParser("custom_xls")
|
||||
assert.Nil(t, err)
|
||||
|
||||
context := core.NewNullContext()
|
||||
|
||||
testdata, err := os.ReadFile("../../../testdata/empty_excel_file.xls")
|
||||
assert.Nil(t, err)
|
||||
|
||||
allLines, err := importer.ParseDataLines(context, testdata)
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, 0, len(allLines))
|
||||
}
|
||||
|
||||
func TestCustomTransactionDataParser_ParseMSCFBExcelDataLines_SingleSheet(t *testing.T) {
|
||||
importer, err := CreateNewCustomTransactionDataExcelFileParser("custom_xls")
|
||||
assert.Nil(t, err)
|
||||
|
||||
context := core.NewNullContext()
|
||||
|
||||
testdata, err := os.ReadFile("../../../testdata/simple_excel_file.xls")
|
||||
assert.Nil(t, err)
|
||||
|
||||
allLines, err := importer.ParseDataLines(context, testdata)
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, 3, len(allLines))
|
||||
|
||||
assert.Equal(t, 3, len(allLines[0]))
|
||||
assert.Equal(t, "A1", allLines[0][0])
|
||||
assert.Equal(t, "B1", allLines[0][1])
|
||||
assert.Equal(t, "C1", allLines[0][2])
|
||||
|
||||
assert.Equal(t, 3, len(allLines[1]))
|
||||
assert.Equal(t, "A2", allLines[1][0])
|
||||
assert.Equal(t, "B2", allLines[1][1])
|
||||
assert.Equal(t, "C2", allLines[1][2])
|
||||
|
||||
assert.Equal(t, 3, len(allLines[2]))
|
||||
assert.Equal(t, "A3", allLines[2][0])
|
||||
assert.Equal(t, "B3", allLines[2][1])
|
||||
assert.Equal(t, "C3", allLines[2][2])
|
||||
}
|
||||
|
||||
func TestCustomTransactionDataParser_ParseMSCFBExcelDataLines_MultipleSheet(t *testing.T) {
|
||||
importer, err := CreateNewCustomTransactionDataExcelFileParser("custom_xls")
|
||||
assert.Nil(t, err)
|
||||
|
||||
context := core.NewNullContext()
|
||||
|
||||
testdata, err := os.ReadFile("../../../testdata/multiple_sheets_excel_file.xls")
|
||||
assert.Nil(t, err)
|
||||
|
||||
allLines, err := importer.ParseDataLines(context, testdata)
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, 9, len(allLines))
|
||||
|
||||
assert.Equal(t, 3, len(allLines[0]))
|
||||
assert.Equal(t, "A1", allLines[0][0])
|
||||
assert.Equal(t, "B1", allLines[0][1])
|
||||
assert.Equal(t, "C1", allLines[0][2])
|
||||
|
||||
assert.Equal(t, 3, len(allLines[1]))
|
||||
assert.Equal(t, "1-A2", allLines[1][0])
|
||||
assert.Equal(t, "1-B2", allLines[1][1])
|
||||
assert.Equal(t, "1-C2", allLines[1][2])
|
||||
|
||||
assert.Equal(t, 3, len(allLines[2]))
|
||||
assert.Equal(t, "1-A3", allLines[2][0])
|
||||
assert.Equal(t, "1-B3", allLines[2][1])
|
||||
assert.Equal(t, "1-C3", allLines[2][2])
|
||||
|
||||
assert.Equal(t, 3, len(allLines[3]))
|
||||
assert.Equal(t, "A1", allLines[3][0])
|
||||
assert.Equal(t, "B1", allLines[3][1])
|
||||
assert.Equal(t, "C1", allLines[3][2])
|
||||
|
||||
assert.Equal(t, 3, len(allLines[4]))
|
||||
assert.Equal(t, "3-A2", allLines[4][0])
|
||||
assert.Equal(t, "3-B2", allLines[4][1])
|
||||
assert.Equal(t, "", allLines[4][2])
|
||||
|
||||
assert.Equal(t, 3, len(allLines[5]))
|
||||
assert.Equal(t, "A1", allLines[5][0])
|
||||
assert.Equal(t, "B1", allLines[5][1])
|
||||
assert.Equal(t, "C1", allLines[5][2])
|
||||
|
||||
assert.Equal(t, 3, len(allLines[6]))
|
||||
assert.Equal(t, "A1", allLines[6][0])
|
||||
assert.Equal(t, "B1", allLines[6][1])
|
||||
assert.Equal(t, "C1", allLines[6][2])
|
||||
|
||||
assert.Equal(t, 3, len(allLines[7]))
|
||||
assert.Equal(t, "5-A2", allLines[7][0])
|
||||
assert.Equal(t, "5-B2", allLines[7][1])
|
||||
assert.Equal(t, "5-C2", allLines[7][2])
|
||||
|
||||
assert.Equal(t, 3, len(allLines[8]))
|
||||
assert.Equal(t, "5-A3", allLines[8][0])
|
||||
assert.Equal(t, "5-B3", allLines[8][1])
|
||||
assert.Equal(t, "5-C3", allLines[8][2])
|
||||
}
|
||||
|
||||
func TestCustomTransactionDataParser_ParseMSCFBExcelDataLines_MultipleSheetWithDifferentColumnCount(t *testing.T) {
|
||||
importer, err := CreateNewCustomTransactionDataExcelFileParser("custom_xls")
|
||||
assert.Nil(t, err)
|
||||
|
||||
context := core.NewNullContext()
|
||||
|
||||
testdata, err := os.ReadFile("../../../testdata/multiple_sheets_with_different_header_row_excel_file.xls")
|
||||
assert.Nil(t, err)
|
||||
|
||||
_, err = importer.ParseDataLines(context, testdata)
|
||||
assert.EqualError(t, err, errs.ErrFieldsInMultiTableAreDifferent.Message)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
+63
-71
@@ -1,11 +1,10 @@
|
||||
package custom
|
||||
package dsv
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/csv"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/text/encoding"
|
||||
"golang.org/x/text/encoding/charmap"
|
||||
@@ -14,7 +13,6 @@ import (
|
||||
"golang.org/x/text/encoding/simplifiedchinese"
|
||||
"golang.org/x/text/encoding/traditionalchinese"
|
||||
"golang.org/x/text/encoding/unicode"
|
||||
"golang.org/x/text/encoding/unicode/utf32"
|
||||
"golang.org/x/text/transform"
|
||||
|
||||
"github.com/mayswind/ezbookkeeping/pkg/converters/converter"
|
||||
@@ -30,61 +28,59 @@ import (
|
||||
var supportedFileTypeSeparators = map[string]rune{
|
||||
"custom_csv": ',',
|
||||
"custom_tsv": '\t',
|
||||
"custom_ssv": ';',
|
||||
}
|
||||
|
||||
var supportedFileEncodings = map[string]encoding.Encoding{
|
||||
"utf-8": unicode.UTF8BOM, // UTF-8
|
||||
"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-32le": utf32.UTF32(utf32.LittleEndian, utf32.UseBOM), // UTF-32 Little Endian
|
||||
"utf-32be": utf32.UTF32(utf32.BigEndian, utf32.UseBOM), // UTF-32 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)
|
||||
"cp1047": charmap.CodePage1047, // IBM EBCDIC Open Systems (CP-1047)
|
||||
"cp1140": charmap.CodePage1140, // IBM EBCDIC US/Canada with Euro (CP-1140)
|
||||
"iso-8859-1": charmap.ISO8859_1, // Western European (ISO-8859-1)
|
||||
"cp850": charmap.CodePage850, // Western European (CP-850)
|
||||
"cp858": charmap.CodePage858, // Western European with Euro (CP-858)
|
||||
"windows-1252": charmap.Windows1252, // Western European (Windows-1252)
|
||||
"iso-8859-15": charmap.ISO8859_15, // Western European (ISO-8859-15)
|
||||
"iso-8859-4": charmap.ISO8859_4, // North European (ISO-8859-4)
|
||||
"iso-8859-10": charmap.ISO8859_10, // North European (ISO-8859-10)
|
||||
"cp865": charmap.CodePage865, // North European (CP-865)
|
||||
"iso-8859-2": charmap.ISO8859_2, // Central European (ISO-8859-2)
|
||||
"cp852": charmap.CodePage852, // Central European (CP-852)
|
||||
"windows-1250": charmap.Windows1250, // Central European (Windows-1250)
|
||||
"iso-8859-14": charmap.ISO8859_14, // Celtic (ISO-8859-14)
|
||||
"iso-8859-3": charmap.ISO8859_3, // South European (ISO-8859-3)
|
||||
"cp860": charmap.CodePage860, // Portuguese (CP-860)
|
||||
"iso-8859-7": charmap.ISO8859_7, // Greek (ISO-8859-7)
|
||||
"windows-1253": charmap.Windows1253, // Greek (Windows-1253)
|
||||
"iso-8859-9": charmap.ISO8859_9, // Turkish (ISO-8859-9)
|
||||
"windows-1254": charmap.Windows1254, // Turkish (Windows-1254)
|
||||
"iso-8859-13": charmap.ISO8859_13, // Baltic (ISO-8859-13)
|
||||
"windows-1257": charmap.Windows1257, // Baltic (Windows-1257)
|
||||
"iso-8859-16": charmap.ISO8859_16, // South-Eastern European (ISO-8859-16)
|
||||
"iso-8859-5": charmap.ISO8859_5, // Cyrillic (ISO-8859-5)
|
||||
"cp855": charmap.CodePage855, // Cyrillic (CP-855)
|
||||
"cp866": charmap.CodePage866, // Cyrillic (CP-866)
|
||||
"windows-1251": charmap.Windows1251, // Cyrillic (Windows-1251)
|
||||
"koi8r": charmap.KOI8R, // Cyrillic (KOI8-R)
|
||||
"koi8u": charmap.KOI8U, // Cyrillic (KOI8-U)
|
||||
"iso-8859-6": charmap.ISO8859_6, // Arabic (ISO-8859-6)
|
||||
"windows-1256": charmap.Windows1256, // Arabic (Windows-1256)
|
||||
"iso-8859-8": charmap.ISO8859_8, // Hebrew (ISO-8859-8)
|
||||
"cp862": charmap.CodePage862, // Hebrew (CP-862)
|
||||
"windows-1255": charmap.Windows1255, // Hebrew (Windows-1255)
|
||||
"windows-874": charmap.Windows874, // Thai (Windows-874)
|
||||
"windows-1258": charmap.Windows1258, // Vietnamese (Windows-1258)
|
||||
"gb18030": simplifiedchinese.GB18030, // Chinese (Simplified, GB18030)
|
||||
"gbk": simplifiedchinese.GBK, // Chinese (Simplified, GBK)
|
||||
"big5": traditionalchinese.Big5, // Chinese (Traditional, Big5)
|
||||
"euc-kr": korean.EUCKR, // Korean (EUC-KR)
|
||||
"euc-jp": japanese.EUCJP, // Japanese (EUC-JP)
|
||||
"iso-2022-jp": japanese.ISO2022JP, // Japanese (ISO-2022-JP)
|
||||
"shift_jis": japanese.ShiftJIS, // Japanese (Shift JIS)
|
||||
"utf-8": unicode.UTF8, // UTF-8
|
||||
"utf-8-bom": unicode.UTF8BOM, // UTF-8 with BOM
|
||||
"utf-16le": unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM), // UTF-16 Little Endian
|
||||
"utf-16be": unicode.UTF16(unicode.BigEndian, unicode.IgnoreBOM), // UTF-16 Big Endian
|
||||
"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)
|
||||
"cp1047": charmap.CodePage1047, // IBM EBCDIC Open Systems (CP-1047)
|
||||
"cp1140": charmap.CodePage1140, // IBM EBCDIC US/Canada with Euro (CP-1140)
|
||||
"iso-8859-1": charmap.ISO8859_1, // Western European (ISO-8859-1)
|
||||
"cp850": charmap.CodePage850, // Western European (CP-850)
|
||||
"cp858": charmap.CodePage858, // Western European with Euro (CP-858)
|
||||
"windows-1252": charmap.Windows1252, // Western European (Windows-1252)
|
||||
"iso-8859-15": charmap.ISO8859_15, // Western European (ISO-8859-15)
|
||||
"iso-8859-4": charmap.ISO8859_4, // North European (ISO-8859-4)
|
||||
"iso-8859-10": charmap.ISO8859_10, // North European (ISO-8859-10)
|
||||
"cp865": charmap.CodePage865, // North European (CP-865)
|
||||
"iso-8859-2": charmap.ISO8859_2, // Central European (ISO-8859-2)
|
||||
"cp852": charmap.CodePage852, // Central European (CP-852)
|
||||
"windows-1250": charmap.Windows1250, // Central European (Windows-1250)
|
||||
"iso-8859-14": charmap.ISO8859_14, // Celtic (ISO-8859-14)
|
||||
"iso-8859-3": charmap.ISO8859_3, // South European (ISO-8859-3)
|
||||
"cp860": charmap.CodePage860, // Portuguese (CP-860)
|
||||
"iso-8859-7": charmap.ISO8859_7, // Greek (ISO-8859-7)
|
||||
"windows-1253": charmap.Windows1253, // Greek (Windows-1253)
|
||||
"iso-8859-9": charmap.ISO8859_9, // Turkish (ISO-8859-9)
|
||||
"windows-1254": charmap.Windows1254, // Turkish (Windows-1254)
|
||||
"iso-8859-13": charmap.ISO8859_13, // Baltic (ISO-8859-13)
|
||||
"windows-1257": charmap.Windows1257, // Baltic (Windows-1257)
|
||||
"iso-8859-16": charmap.ISO8859_16, // South-Eastern European (ISO-8859-16)
|
||||
"iso-8859-5": charmap.ISO8859_5, // Cyrillic (ISO-8859-5)
|
||||
"cp855": charmap.CodePage855, // Cyrillic (CP-855)
|
||||
"cp866": charmap.CodePage866, // Cyrillic (CP-866)
|
||||
"windows-1251": charmap.Windows1251, // Cyrillic (Windows-1251)
|
||||
"koi8r": charmap.KOI8R, // Cyrillic (KOI8-R)
|
||||
"koi8u": charmap.KOI8U, // Cyrillic (KOI8-U)
|
||||
"iso-8859-6": charmap.ISO8859_6, // Arabic (ISO-8859-6)
|
||||
"windows-1256": charmap.Windows1256, // Arabic (Windows-1256)
|
||||
"iso-8859-8": charmap.ISO8859_8, // Hebrew (ISO-8859-8)
|
||||
"cp862": charmap.CodePage862, // Hebrew (CP-862)
|
||||
"windows-1255": charmap.Windows1255, // Hebrew (Windows-1255)
|
||||
"windows-874": charmap.Windows874, // Thai (Windows-874)
|
||||
"windows-1258": charmap.Windows1258, // Vietnamese (Windows-1258)
|
||||
"gb18030": simplifiedchinese.GB18030, // Chinese (Simplified, GB18030)
|
||||
"gbk": simplifiedchinese.GBK, // Chinese (Simplified, GBK)
|
||||
"big5": traditionalchinese.Big5, // Chinese (Traditional, Big5)
|
||||
"euc-kr": korean.EUCKR, // Korean (EUC-KR)
|
||||
"euc-jp": japanese.EUCJP, // Japanese (EUC-JP)
|
||||
"iso-2022-jp": japanese.ISO2022JP, // Japanese (ISO-2022-JP)
|
||||
"shift_jis": japanese.ShiftJIS, // Japanese (Shift JIS)
|
||||
}
|
||||
|
||||
var customTransactionTypeNameMapping = map[models.TransactionType]string{
|
||||
@@ -94,6 +90,10 @@ var customTransactionTypeNameMapping = map[models.TransactionType]string{
|
||||
models.TRANSACTION_TYPE_TRANSFER: utils.IntToString(int(models.TRANSACTION_TYPE_TRANSFER)),
|
||||
}
|
||||
|
||||
type CustomTransactionDataDsvFileParser interface {
|
||||
ParseDsvFileLines(ctx core.Context, data []byte) ([][]string, error)
|
||||
}
|
||||
|
||||
// customTransactionDataDsvFileImporter defines the structure of custom dsv importer for transaction data
|
||||
type customTransactionDataDsvFileImporter struct {
|
||||
fileEncoding encoding.Encoding
|
||||
@@ -110,8 +110,8 @@ type customTransactionDataDsvFileImporter struct {
|
||||
transactionTagSeparator string
|
||||
}
|
||||
|
||||
// ParseDataLines returns the parsed file lines for specified the dsv file data
|
||||
func (c *customTransactionDataDsvFileImporter) ParseDataLines(ctx core.Context, data []byte) ([][]string, error) {
|
||||
// ParseDsvFileLines returns the parsed file lines for specified the dsv file data
|
||||
func (c *customTransactionDataDsvFileImporter) ParseDsvFileLines(ctx core.Context, data []byte) ([][]string, error) {
|
||||
reader := transform.NewReader(bytes.NewReader(data), c.fileEncoding.NewDecoder())
|
||||
csvReader := csv.NewReader(reader)
|
||||
csvReader.Comma = c.separator
|
||||
@@ -127,7 +127,7 @@ func (c *customTransactionDataDsvFileImporter) ParseDataLines(ctx core.Context,
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(ctx, "[custom_transaction_data_dsv_file_importer.ParseDataLines] cannot parse dsv data, because %s", err.Error())
|
||||
log.Errorf(ctx, "[custom_transaction_data_dsv_file_importer.ParseDsvFileLines] cannot parse dsv data, because %s", err.Error())
|
||||
return nil, errs.ErrInvalidCSVFile
|
||||
}
|
||||
|
||||
@@ -146,8 +146,8 @@ func (c *customTransactionDataDsvFileImporter) ParseDataLines(ctx core.Context,
|
||||
}
|
||||
|
||||
// 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) {
|
||||
allLines, err := c.ParseDataLines(ctx, data)
|
||||
func (c *customTransactionDataDsvFileImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezoneOffset int16, accountMap map[string]*models.Account, expenseCategoryMap map[string]map[string]*models.TransactionCategory, incomeCategoryMap map[string]map[string]*models.TransactionCategory, transferCategoryMap map[string]map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionTag, error) {
|
||||
allLines, err := c.ParseDsvFileLines(ctx, data)
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, nil, err
|
||||
@@ -157,7 +157,7 @@ func (c *customTransactionDataDsvFileImporter) ParseImportedData(ctx core.Contex
|
||||
transactionDataTable := CreateNewCustomPlainTextDataTable(dataTable, c.columnIndexMapping, c.transactionTypeNameMapping, c.timeFormat, c.timezoneFormat, c.amountDecimalSeparator, c.amountDigitGroupingSymbol)
|
||||
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
|
||||
@@ -166,18 +166,14 @@ func IsDelimiterSeparatedValuesFileType(fileType string) bool {
|
||||
return exists
|
||||
}
|
||||
|
||||
// CreateNewCustomTransactionDataDsvFileParser returns a new custom transaction data parser
|
||||
func CreateNewCustomTransactionDataDsvFileParser(fileType string, fileEncoding string) (CustomTransactionDataParser, error) {
|
||||
// CreateNewCustomTransactionDataDsvFileParser returns a new custom dsv parser for transaction data
|
||||
func CreateNewCustomTransactionDataDsvFileParser(fileType string, fileEncoding string) (CustomTransactionDataDsvFileParser, error) {
|
||||
separator, exists := supportedFileTypeSeparators[fileType]
|
||||
|
||||
if !exists {
|
||||
return nil, errs.ErrImportFileTypeNotSupported
|
||||
}
|
||||
|
||||
if fileEncoding == "" {
|
||||
return nil, errs.ErrImportFileEncodingIsEmpty
|
||||
}
|
||||
|
||||
enc, exists := supportedFileEncodings[fileEncoding]
|
||||
|
||||
if !exists {
|
||||
@@ -198,10 +194,6 @@ func CreateNewCustomTransactionDataDsvFileImporter(fileType string, fileEncoding
|
||||
return nil, errs.ErrImportFileTypeNotSupported
|
||||
}
|
||||
|
||||
if fileEncoding == "" {
|
||||
return nil, errs.ErrImportFileEncodingIsEmpty
|
||||
}
|
||||
|
||||
enc, exists := supportedFileEncodings[fileEncoding]
|
||||
|
||||
if !exists {
|
||||
+119
-199
@@ -1,12 +1,10 @@
|
||||
package custom
|
||||
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_ParseDataLines(t *testing.T) {
|
||||
importer, err := CreateNewCustomTransactionDataDsvFileParser("custom_csv", "utf-8")
|
||||
func TestCustomTransactionDataDsvFileParser_ParseDsvFileLines(t *testing.T) {
|
||||
converter, err := CreateNewCustomTransactionDataDsvFileParser("custom_csv", "utf-8")
|
||||
assert.Nil(t, err)
|
||||
|
||||
context := core.NewNullContext()
|
||||
|
||||
allLines, err := importer.ParseDataLines(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_ParseDataLines(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.ParseDataLines(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_ParseDataLines(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.ParseDataLines(context, []byte(
|
||||
"2024-09-01 12:34:56;E;1.00\n"+
|
||||
"2024-09-01 23:59:59;T;0.05"))
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, 2, len(allLines))
|
||||
|
||||
assert.Equal(t, 3, len(allLines[0]))
|
||||
assert.Equal(t, "2024-09-01 12:34:56", allLines[0][0])
|
||||
assert.Equal(t, "E", allLines[0][1])
|
||||
assert.Equal(t, "1.00", allLines[0][2])
|
||||
|
||||
assert.Equal(t, 3, len(allLines[1]))
|
||||
assert.Equal(t, "2024-09-01 23:59:59", allLines[1][0])
|
||||
assert.Equal(t, "T", allLines[1][1])
|
||||
assert.Equal(t, "0.05", allLines[1][2])
|
||||
}
|
||||
|
||||
func TestCustomTransactionDataDsvFileImporter_MinimumValidData(t *testing.T) {
|
||||
@@ -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,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)
|
||||
|
||||
// figure 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)
|
||||
}
|
||||
|
||||
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))
|
||||
+2
-23
@@ -1,4 +1,4 @@
|
||||
package custom
|
||||
package dsv
|
||||
|
||||
import (
|
||||
"strings"
|
||||
@@ -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 != "." {
|
||||
@@ -86,7 +86,7 @@ func (t *ExcelMSCFBFileBasicDataTable) DataRowIterator() datatable.BasicDataTabl
|
||||
// ColumnCount returns the total count of column in this data row
|
||||
func (r *ExcelMSCFBFileBasicDataTableRow) ColumnCount() int {
|
||||
row := r.sheet.Row(r.rowIndex)
|
||||
return row.LastCol()
|
||||
return row.LastCol() + 1
|
||||
}
|
||||
|
||||
// GetData returns the data in the specified column index
|
||||
@@ -195,10 +195,7 @@ func CreateNewExcelMSCFBFileBasicDataTable(data []byte, hasTitleLine bool) (data
|
||||
}
|
||||
|
||||
if i == 0 {
|
||||
// row.LastCol() returns "colMac" in the "Row" struct, that is an unsigned integer that specifies the one-based index of the last column.
|
||||
// But row.FirstCol() returns "colMic" in the "Row" struct, that is an unsigned integer that specifies the zero-based index of the first column.
|
||||
// Reference: https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-xls/4aab09eb-49ed-4d01-a3b1-1d726247d3c2
|
||||
for j := 0; j < row.LastCol(); j++ {
|
||||
for j := 0; j <= row.LastCol(); j++ {
|
||||
headerItem := row.Col(j)
|
||||
|
||||
if headerItem == "" {
|
||||
@@ -208,7 +205,7 @@ func CreateNewExcelMSCFBFileBasicDataTable(data []byte, hasTitleLine bool) (data
|
||||
firstRowItems = append(firstRowItems, headerItem)
|
||||
}
|
||||
} else {
|
||||
for j := 0; j < min(row.LastCol(), len(firstRowItems)); j++ {
|
||||
for j := 0; j <= min(row.LastCol(), len(firstRowItems)-1); j++ {
|
||||
headerItem := row.Col(j)
|
||||
|
||||
if headerItem != firstRowItems[j] {
|
||||
|
||||
@@ -300,10 +300,10 @@ func TestExcelMSCFBFileBasicDataRowColumnCount(t *testing.T) {
|
||||
iterator := datatable.DataRowIterator()
|
||||
|
||||
row1 := iterator.Next()
|
||||
assert.EqualValues(t, 3, row1.ColumnCount())
|
||||
assert.EqualValues(t, 4, row1.ColumnCount())
|
||||
|
||||
row2 := iterator.Next()
|
||||
assert.EqualValues(t, 3, row2.ColumnCount())
|
||||
assert.EqualValues(t, 4, row2.ColumnCount())
|
||||
}
|
||||
|
||||
func TestExcelMSCFBFileBasicDataRowGetData(t *testing.T) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
+65
-95
@@ -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)
|
||||
}
|
||||
|
||||
+2
-7
@@ -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)
|
||||
}
|
||||
|
||||
+2
-3
@@ -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))
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user