Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7a7892a403 |
@@ -0,0 +1,13 @@
|
||||
module.exports = {
|
||||
'root': true,
|
||||
'env': {
|
||||
'node': true
|
||||
},
|
||||
'extends': [
|
||||
'eslint:recommended',
|
||||
'plugin:vue/vue3-essential'
|
||||
],
|
||||
'rules': {
|
||||
'vue/no-use-v-if-with-v-for': 'off'
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
name: Deploy Docker Image
|
||||
|
||||
on:
|
||||
workflow_dispatch
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Execute custom script
|
||||
run: |
|
||||
cat >> deploy.sh <<EOF
|
||||
#!/bin/sh
|
||||
${{ vars.CUSTOM_DEPLOY_SCRIPTS }}
|
||||
EOF
|
||||
chmod +x deploy.sh
|
||||
./deploy.sh
|
||||
@@ -10,11 +10,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
${{ secrets.DOCKER_REPO }}/mayswind/ezbookkeeping
|
||||
@@ -24,16 +24,13 @@ jobs:
|
||||
type=raw,value=latest
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
with:
|
||||
image: tonistiigi/binfmt:qemu-v8.1.5
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Set up the environment
|
||||
run: |
|
||||
sed -r -i 's#FROM( --.*)? (.*:.*)?#FROM\1 ${{ secrets.DOCKER_REPO }}/mirrors/\2#g' Dockerfile
|
||||
cat >> docker/custom-backend-pre-setup.sh <<EOF
|
||||
#!/bin/sh
|
||||
${{ vars.CUSTOM_BACKEND_PRE_SETUP }}
|
||||
@@ -46,7 +43,7 @@ jobs:
|
||||
chmod +x docker/custom-frontend-pre-setup.sh
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
file: Dockerfile
|
||||
context: .
|
||||
@@ -54,6 +51,5 @@ jobs:
|
||||
push: true
|
||||
build-args: |
|
||||
RELEASE_BUILD=1
|
||||
SKIP_TESTS=${{ vars.SKIP_TESTS }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
@@ -10,11 +10,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
${{ secrets.DOCKER_REPO }}/mayswind/ezbookkeeping
|
||||
@@ -24,16 +24,14 @@ jobs:
|
||||
type=sha,format=short,prefix=SNAPSHOT-
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
with:
|
||||
image: tonistiigi/binfmt:qemu-v8.1.5
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Set up the environment
|
||||
run: |
|
||||
sed -r -i 's#FROM( --.*)? (.*:.*)?#FROM\1 ${{ secrets.DOCKER_REPO }}/mirrors/\2#g' Dockerfile
|
||||
sed -i 's#FROM #FROM ${{ secrets.DOCKER_REPO }}/mirrors/#g' Dockerfile
|
||||
cat >> docker/custom-backend-pre-setup.sh <<EOF
|
||||
#!/bin/sh
|
||||
${{ vars.CUSTOM_BACKEND_PRE_SETUP }}
|
||||
@@ -46,13 +44,11 @@ jobs:
|
||||
chmod +x docker/custom-frontend-pre-setup.sh
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
file: Dockerfile
|
||||
context: .
|
||||
platforms: ${{ vars.BUILD_SNAPSHOT_PLATFORMS }}
|
||||
push: true
|
||||
build-args: |
|
||||
SKIP_TESTS=${{ vars.SKIP_TESTS }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
@@ -10,11 +10,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
${{ secrets.DOCKER_USERNAME }}/ezbookkeeping
|
||||
@@ -24,21 +24,19 @@ jobs:
|
||||
type=raw,value=latest
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
with:
|
||||
image: tonistiigi/binfmt:qemu-v8.1.5
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
file: Dockerfile
|
||||
context: .
|
||||
@@ -50,6 +48,5 @@ jobs:
|
||||
push: true
|
||||
build-args: |
|
||||
RELEASE_BUILD=1
|
||||
SKIP_TESTS=${{ vars.SKIP_TESTS }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
@@ -10,11 +10,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
${{ secrets.DOCKER_USERNAME }}/ezbookkeeping
|
||||
@@ -23,21 +23,19 @@ jobs:
|
||||
type=raw,value=latest-snapshot
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
with:
|
||||
image: tonistiigi/binfmt:qemu-v8.1.5
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
file: Dockerfile
|
||||
context: .
|
||||
@@ -47,7 +45,5 @@ jobs:
|
||||
linux/arm/v7
|
||||
linux/arm/v6
|
||||
push: true
|
||||
build-args: |
|
||||
SKIP_TESTS=${{ vars.SKIP_TESTS }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
@@ -11,20 +11,18 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v2
|
||||
-
|
||||
name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
-
|
||||
name: Build
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
file: Dockerfile
|
||||
context: .
|
||||
platforms: linux/amd64
|
||||
push: false
|
||||
build-args: |
|
||||
SKIP_TESTS=${{ vars.SKIP_TESTS }}
|
||||
push: false
|
||||
+4
-7
@@ -1,9 +1,7 @@
|
||||
# Build backend binary file
|
||||
FROM golang:1.24.0-alpine3.21 AS be-builder
|
||||
FROM golang:1.20.8-alpine3.17 AS be-builder
|
||||
ARG RELEASE_BUILD
|
||||
ARG SKIP_TESTS
|
||||
ENV RELEASE_BUILD=$RELEASE_BUILD
|
||||
ENV SKIP_TESTS=$SKIP_TESTS
|
||||
WORKDIR /go/src/github.com/mayswind/ezbookkeeping
|
||||
COPY . .
|
||||
RUN docker/backend-build-pre-setup.sh
|
||||
@@ -11,7 +9,7 @@ RUN apk add git gcc g++ libc-dev
|
||||
RUN ./build.sh backend
|
||||
|
||||
# Build frontend files
|
||||
FROM --platform=$BUILDPLATFORM node:22.14.0-alpine3.21 AS fe-builder
|
||||
FROM node:18.17.1-alpine3.17 AS fe-builder
|
||||
ARG RELEASE_BUILD
|
||||
ENV RELEASE_BUILD=$RELEASE_BUILD
|
||||
WORKDIR /go/src/github.com/mayswind/ezbookkeeping
|
||||
@@ -21,7 +19,7 @@ RUN apk add git
|
||||
RUN ./build.sh frontend
|
||||
|
||||
# Package docker image
|
||||
FROM alpine:3.21.3
|
||||
FROM alpine:3.17.5
|
||||
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
|
||||
@@ -29,8 +27,7 @@ COPY docker/docker-entrypoint.sh /docker-entrypoint.sh
|
||||
RUN chmod +x /docker-entrypoint.sh
|
||||
RUN mkdir -p /ezbookkeeping && chown 1000:1000 /ezbookkeeping \
|
||||
&& mkdir -p /ezbookkeeping/data && chown 1000:1000 /ezbookkeeping/data \
|
||||
&& mkdir -p /ezbookkeeping/log && chown 1000:1000 /ezbookkeeping/log \
|
||||
&& mkdir -p /ezbookkeeping/storage && chown 1000:1000 /ezbookkeeping/storage
|
||||
&& mkdir -p /ezbookkeeping/log && chown 1000:1000 /ezbookkeeping/log
|
||||
WORKDIR /ezbookkeeping
|
||||
COPY --from=be-builder --chown=1000:1000 /go/src/github.com/mayswind/ezbookkeeping/ezbookkeeping /ezbookkeeping/ezbookkeeping
|
||||
COPY --from=fe-builder --chown=1000:1000 /go/src/github.com/mayswind/ezbookkeeping/dist /ezbookkeeping/public
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020-2025 MaysWind (i@mayswind.net)
|
||||
Copyright (c) 2020-2023 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
|
||||
|
||||
@@ -6,9 +6,7 @@
|
||||
[](https://github.com/mayswind/ezbookkeeping/releases)
|
||||
|
||||
## Introduction
|
||||
ezBookkeeping is a lightweight personal bookkeeping app hosted by yourself. It can be deployed on almost all platforms, including Windows, macOS and Linux on x86, amd64 and ARM architectures. You can even deploy it on an raspberry device. It also supports many different databases, including SQLite, MySQL and PostgreSQL. With docker, you can just deploy it via one command without complicated configuration.
|
||||
|
||||
Online Demo: [https://ezbookkeeping-demo.mayswind.net](https://ezbookkeeping-demo.mayswind.net)
|
||||
ezBookkeeping is a lightweight personal bookkeeping app hosted by yourself. It can be deployed on almost all platforms, including Windows, macOS and Linux on x86, amd64 and ARM architectures. You can even deploy it on an raspberry device. It also supports many different databases, including sqlite and mysql. With docker, you can just deploy it via one command without complicated configuration.
|
||||
|
||||
## Features
|
||||
1. Open source & Self-hosted
|
||||
@@ -31,7 +29,7 @@ Online Demo: [https://ezbookkeeping-demo.mayswind.net](https://ezbookkeeping-dem
|
||||
7. Multi-language support
|
||||
8. Two-factor authentication
|
||||
9. Application lock (PIN code / WebAuthn)
|
||||
10. Data export & import (OFX, QFX, QIF, IIF, CSV, GnuCash, FireFly III, etc.)
|
||||
10. Data export
|
||||
|
||||
## Screenshots
|
||||
### Desktop Version
|
||||
@@ -55,40 +53,24 @@ Latest Daily Build:
|
||||
### Install from binary
|
||||
Latest release: [https://github.com/mayswind/ezbookkeeping/releases](https://github.com/mayswind/ezbookkeeping/releases)
|
||||
|
||||
**Linux / macOS**
|
||||
|
||||
$ ./ezbookkeeping server run
|
||||
|
||||
**Windows**
|
||||
|
||||
> .\ezbookkeeping.exe server run
|
||||
|
||||
ezBookkeeping will listen at port 8080 as default. Then you can visit `http://{YOUR_HOST_ADDRESS}:8080/` .
|
||||
ezBookkeeping will listen at port 8080 as default. Then you can visit http://{YOUR_HOST_ADDRESS}:8080/ .
|
||||
|
||||
### Build from source
|
||||
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**
|
||||
|
||||
$ ./build.sh package -o ezbookkeeping.tar.gz
|
||||
|
||||
All the files will be packaged in `ezbookkeeping.tar.gz`.
|
||||
|
||||
**Windows**
|
||||
|
||||
> .\build.bat package -o ezbookkeeping.zip
|
||||
|
||||
All the files will be packaged in `ezbookkeeping.zip`.
|
||||
|
||||
You can also build docker image, make sure you have [docker](https://www.docker.com/) installed, then follow these steps:
|
||||
|
||||
**Linux**
|
||||
|
||||
$ ./build.sh docker
|
||||
|
||||
## Documents
|
||||
1. [English](http://ezbookkeeping.mayswind.net)
|
||||
1. [中文 (简体)](http://ezbookkeeping.mayswind.net/zh_Hans)
|
||||
1. [简体中文 (Simplified Chinese)](http://ezbookkeeping.mayswind.net/zh_Hans)
|
||||
|
||||
## License
|
||||
[MIT](https://github.com/mayswind/ezbookkeeping/blob/master/LICENSE)
|
||||
|
||||
@@ -1,271 +0,0 @@
|
||||
@echo off
|
||||
|
||||
set "TYPE="
|
||||
set "NO_LINT=0"
|
||||
set "NO_TEST=0"
|
||||
set "SKIP_TESTS=%SKIP_TESTS%"
|
||||
set "RELEASE=%RELEASE_BUILD%"
|
||||
set "RELEASE_TYPE=unknown"
|
||||
set "VERSION="
|
||||
set "COMMIT_HASH="
|
||||
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"
|
||||
|
||||
if "%~1"=="" call :show_help & goto :end
|
||||
goto :pre_parse_args
|
||||
|
||||
:echo_red
|
||||
echo %ESC%[91m%~1%ESC%[0m
|
||||
goto :eof
|
||||
|
||||
:set_unixtime
|
||||
setlocal enableextensions
|
||||
for /f %%x in ('wmic path win32_utctime get /format:list ^| findstr "="') do set %%x
|
||||
set /a z=(14-100%Month%%%100)/12, y=10000%Year%%%10000-z
|
||||
set /a ut=y*365+y/4-y/100+y/400+(153*(100%Month%%%100+12*z-3)+2)/5+Day-719469
|
||||
set /a ut=ut*86400+100%Hour%%%100*3600+100%Minute%%%100*60+100%Second%%%100
|
||||
endlocal & set "%1=%ut%" & goto :eof
|
||||
|
||||
:set_date
|
||||
setlocal enableextensions
|
||||
for /f %%x in ('wmic path win32_localtime get /format:list ^| findstr "="') do set %%x
|
||||
if %Month% lss 10 set "Month=0%Month%"
|
||||
if %Day% lss 10 set "Day=0%Day%"
|
||||
endlocal & set "%1=%Year%%Month%%Day%" & goto :eof
|
||||
|
||||
:check_dependency
|
||||
if "%~1"=="" goto :eof
|
||||
where /q %~1 || call :echo_red "Error: "%~1" is required." && goto :end
|
||||
|
||||
shift
|
||||
goto :check_dependency
|
||||
|
||||
:show_help
|
||||
echo ezBookkeeping build script for Windows
|
||||
echo.
|
||||
echo Usage:
|
||||
echo build.cmd type [options]
|
||||
echo.
|
||||
echo Types:
|
||||
echo backend Build backend binary file
|
||||
echo frontend Build frontend files
|
||||
echo package Build package archive
|
||||
echo.
|
||||
echo Options:
|
||||
echo /r, --release Build release (The script will use environment variable "RELEASE_BUILD" to detect whether this is release building by default)
|
||||
echo /o, --output ^<filename^> Package file name (For "package" type only)
|
||||
echo --no-lint Do not execute lint check before building
|
||||
echo --no-test Do not execute unit testing before building (You can use environment variable "SKIP_TESTS" to skip specified tests)
|
||||
echo /h, --help Show help
|
||||
goto :eof
|
||||
|
||||
:pre_parse_args
|
||||
if "%~1"=="" goto :post_parse_args
|
||||
|
||||
if /i "%~1"=="backend" set "TYPE=%~1" & shift
|
||||
if /i "%~1"=="frontend" set "TYPE=%~1" & shift
|
||||
if /i "%~1"=="package" set "TYPE=%~1" & shift
|
||||
|
||||
:parse_args
|
||||
if "%~1"=="" goto :post_parse_args
|
||||
|
||||
if /i "%~1"=="/r" set "RELEASE=1" & shift & goto :parse_args
|
||||
if /i "%~1"=="-r" set "RELEASE=1" & shift & goto :parse_args
|
||||
if /i "%~1"=="--release" set "RELEASE=1" & shift & goto :parse_args
|
||||
|
||||
if /i "%~1"=="/o" set "PACKAGE_FILENAME=%~2" & shift & shift & goto :parse_args
|
||||
if /i "%~1"=="-o" set "PACKAGE_FILENAME=%~2" & shift & shift & goto :parse_args
|
||||
if /i "%~1"=="--output" set "PACKAGE_FILENAME=%~2" & shift & shift & goto :parse_args
|
||||
|
||||
if /i "%~1"=="--no-lint" set "NO_LINT=1" & shift & goto :parse_args
|
||||
if /i "%~1"=="--no-test" set "NO_TEST=1" & shift & goto :parse_args
|
||||
|
||||
if /i "%~1"=="/h" call :show_help & goto :end
|
||||
if /i "%~1"=="-h" call :show_help & goto :end
|
||||
if /i "%~1"=="--help" call :show_help & goto :end
|
||||
|
||||
call :echo_red "Invalid argument: %~1" & call :show_help & goto :end
|
||||
|
||||
:post_parse_args
|
||||
if "%RELEASE%"=="" set "RELEASE=0"
|
||||
|
||||
if "%RELEASE%"=="0" (
|
||||
set "RELEASE_TYPE=snapshot"
|
||||
) else (
|
||||
set "RELEASE_TYPE=release"
|
||||
)
|
||||
|
||||
:check_type_dependencies
|
||||
if not defined TYPE call :echo_red "Error: No specified type" & call :show_help & goto :end
|
||||
|
||||
call :check_dependency "git"
|
||||
if "%TYPE%"=="backend" call :check_dependency "go" "gcc"
|
||||
if "%TYPE%"=="frontend" call :check_dependency "node" "npm"
|
||||
if "%TYPE%"=="package" call :check_dependency "go" "gcc" "node" "npm" "7z"
|
||||
|
||||
if not "%errorlevel%"=="0" goto :end
|
||||
|
||||
:set_build_parameters
|
||||
for /f "tokens=2 delims=:" %%x in ('findstr "\"version\": \"*\"," package.json') do set "VERSION=%%x"
|
||||
set VERSION=%VERSION: =%
|
||||
set VERSION=%VERSION:,=%
|
||||
set VERSION=%VERSION:"=%
|
||||
for /f %%x in ('git rev-parse --short HEAD') do set "COMMIT_HASH=%%x"
|
||||
call :set_unixtime BUILD_UNIXTIME
|
||||
call :set_date BUILD_DATE
|
||||
|
||||
:main
|
||||
if "%TYPE%"=="backend" call :build_backend & goto :end
|
||||
if "%TYPE%"=="frontend" call :build_frontend & goto :end
|
||||
if "%TYPE%"=="package" call :build_package & goto :end
|
||||
goto :end
|
||||
|
||||
:build_backend
|
||||
setlocal enabledelayedexpansion
|
||||
echo Pulling backend dependencies...
|
||||
call go get .
|
||||
|
||||
if "%NO_LINT%"=="0" (
|
||||
echo Executing backend lint checking...
|
||||
call go vet -v .\...
|
||||
|
||||
if !errorlevel! neq 0 (
|
||||
call :echo_red "Error: Failed to pass lint checking"
|
||||
goto :end
|
||||
)
|
||||
)
|
||||
|
||||
if "%NO_TEST%"=="0" (
|
||||
echo Executing backend unit testing...
|
||||
call go clean -cache
|
||||
|
||||
if "%SKIP_TESTS%"=="" (
|
||||
call go test .\... -v
|
||||
) else (
|
||||
echo (Skip unit test "%SKIP_TESTS%")
|
||||
call go test .\... -v -skip "%SKIP_TESTS%"
|
||||
)
|
||||
|
||||
if !errorlevel! neq 0 (
|
||||
call :echo_red "Error: Failed to pass unit testing"
|
||||
goto :end
|
||||
)
|
||||
)
|
||||
|
||||
endlocal
|
||||
|
||||
set "CGO_ENABLED=1"
|
||||
|
||||
setlocal
|
||||
set "backend_build_extra_arguments=-X main.Version=%VERSION%"
|
||||
set "backend_build_extra_arguments=%backend_build_extra_arguments% -X main.CommitHash=%COMMIT_HASH%"
|
||||
|
||||
if "%RELEASE%"=="0" (
|
||||
set "backend_build_extra_arguments=%backend_build_extra_arguments% -X main.BuildUnixTime=%BUILD_UNIXTIME%"
|
||||
)
|
||||
|
||||
echo Building backend binary file (%RELEASE_TYPE%)...
|
||||
|
||||
call go build -a -v -trimpath -tags timetzdata -ldflags "-w -s -linkmode external -extldflags '-static' %backend_build_extra_arguments%" -o ezbookkeeping.exe ezbookkeeping.go
|
||||
endlocal
|
||||
|
||||
set "CGO_ENABLED="
|
||||
|
||||
goto :eof
|
||||
|
||||
:build_frontend
|
||||
setlocal enabledelayedexpansion
|
||||
echo Pulling frontend dependencies...
|
||||
call npm install
|
||||
|
||||
if "%NO_LINT%"=="0" (
|
||||
echo Executing frontend lint checking...
|
||||
|
||||
call npm run lint
|
||||
|
||||
if !errorlevel! neq 0 (
|
||||
call :echo_red "Error: Failed to pass lint checking"
|
||||
goto :end
|
||||
)
|
||||
)
|
||||
|
||||
endlocal
|
||||
|
||||
echo Building frontend files(%RELEASE_TYPE%)...
|
||||
|
||||
if "%RELEASE%"=="0" (
|
||||
set "buildUnixTime=%BUILD_UNIXTIME%"
|
||||
call npm run build
|
||||
set "buildUnixTime="
|
||||
) else (
|
||||
call npm run build
|
||||
)
|
||||
|
||||
goto :eof
|
||||
|
||||
:build_package
|
||||
setlocal enabledelayedexpansion
|
||||
set "package_file_name=%VERSION%"
|
||||
|
||||
if "%RELEASE%"=="0" (
|
||||
set "build_date="
|
||||
set "package_file_name=%package_file_name%-%build_date%"
|
||||
)
|
||||
|
||||
set "package_file_name=ezbookkeeping-%package_file_name%-windows.zip"
|
||||
|
||||
if defined PACKAGE_FILENAME set "package_file_name=%PACKAGE_FILENAME%"
|
||||
|
||||
echo Building package archive "%package_file_name%" (%RELEASE_TYPE%)...
|
||||
|
||||
call :build_backend
|
||||
|
||||
if !errorlevel! neq 0 (
|
||||
goto :end
|
||||
)
|
||||
|
||||
call :build_frontend
|
||||
|
||||
if !errorlevel! neq 0 (
|
||||
goto :end
|
||||
)
|
||||
|
||||
rmdir package /s /q
|
||||
mkdir package
|
||||
mkdir package\data
|
||||
mkdir package\storage
|
||||
mkdir package\log
|
||||
xcopy ezbookkeeping.exe package\
|
||||
xcopy dist package\public /e /i
|
||||
xcopy conf package\conf /e /i
|
||||
xcopy templates package\templates /e /i
|
||||
xcopy LICENSE package\
|
||||
|
||||
cd package
|
||||
|
||||
if !errorlevel! neq 0 (
|
||||
call :echo_red "Error: Build Failed"
|
||||
goto :end
|
||||
)
|
||||
|
||||
call 7z a -r -tzip -mx9 ..\%package_file_name% package *
|
||||
|
||||
cd ..
|
||||
endlocal
|
||||
|
||||
goto :eof
|
||||
|
||||
:end
|
||||
set "TYPE="
|
||||
set "NO_LINT="
|
||||
set "NO_TEST="
|
||||
set "RELEASE="
|
||||
set "RELEASE_TYPE="
|
||||
set "VERSION="
|
||||
set "COMMIT_HASH="
|
||||
set "BUILD_UNIXTIME="
|
||||
set "BUILD_DATE="
|
||||
set "PACKAGE_FILENAME="
|
||||
exit /B
|
||||
@@ -3,7 +3,6 @@
|
||||
TYPE=""
|
||||
NO_LINT="0"
|
||||
NO_TEST="0"
|
||||
SKIP_TESTS="${SKIP_TESTS}"
|
||||
RELEASE=${RELEASE_BUILD:-"0"}
|
||||
RELEASE_TYPE="unknown"
|
||||
VERSION=""
|
||||
@@ -44,7 +43,7 @@ Options:
|
||||
-o, --output <filename> Package file name (For "package" type only)
|
||||
-t, --tag Docker tag (For "docker" type only)
|
||||
--no-lint Do not execute lint check before building
|
||||
--no-test Do not execute unit testing before building (You can use environment variable "SKIP_TESTS" to skip specified tests)
|
||||
--no-test Do not execute unit testing before building
|
||||
-h, --help Show help
|
||||
EOF
|
||||
}
|
||||
@@ -138,13 +137,7 @@ build_backend() {
|
||||
if [ "$NO_TEST" = "0" ]; then
|
||||
echo "Executing backend unit testing..."
|
||||
go clean -cache
|
||||
|
||||
if [ -z "$SKIP_TESTS" ]; then
|
||||
go test ./... -v
|
||||
else
|
||||
echo "(Skip unit test \"$SKIP_TESTS\")"
|
||||
go test ./... -v -skip "$SKIP_TESTS"
|
||||
fi
|
||||
go test ./... -v
|
||||
|
||||
if [ "$?" != "0" ]; then
|
||||
echo_red "Error: Failed to pass unit testing"
|
||||
@@ -209,12 +202,10 @@ build_package() {
|
||||
rm -rf package
|
||||
mkdir package
|
||||
mkdir package/data
|
||||
mkdir package/storage
|
||||
mkdir package/log
|
||||
cp ezbookkeeping package/
|
||||
cp -R dist package/public
|
||||
cp -R conf package/conf
|
||||
cp -R templates package/templates
|
||||
cp LICENSE package/
|
||||
|
||||
cd package || { echo_red "Error: Build Failed"; exit 1; }
|
||||
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||
)
|
||||
|
||||
func bindAction(fn core.CliHandlerFunc) cli.ActionFunc {
|
||||
return func(cliCtx *cli.Context) error {
|
||||
c := core.WrapCilContext(cliCtx)
|
||||
return fn(c)
|
||||
}
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/cron"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/log"
|
||||
)
|
||||
|
||||
// CronJobs represents the cron command
|
||||
var CronJobs = &cli.Command{
|
||||
Name: "cron",
|
||||
Usage: "ezBookkeeping cron job utilities",
|
||||
Subcommands: []*cli.Command{
|
||||
{
|
||||
Name: "list",
|
||||
Usage: "List all enabled cron jobs",
|
||||
Action: bindAction(listAllCronJobs),
|
||||
},
|
||||
{
|
||||
Name: "run",
|
||||
Usage: "Run specified cron job",
|
||||
Action: bindAction(runCronJob),
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "name",
|
||||
Aliases: []string{"n"},
|
||||
Required: true,
|
||||
Usage: "Cron job name",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func listAllCronJobs(c *core.CliContext) error {
|
||||
config, err := initializeSystem(c)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = cron.InitializeCronJobSchedulerContainer(c, config, false)
|
||||
|
||||
if err != nil {
|
||||
log.CliErrorf(c, "[cron_jobs.listAllCronJobs] initializes cron job scheduler failed, because %s", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
cronJobs := cron.Container.GetAllJobs()
|
||||
|
||||
if len(cronJobs) < 1 {
|
||||
log.CliErrorf(c, "[cron_jobs.listAllCronJobs] there are no enabled cron jobs")
|
||||
return err
|
||||
}
|
||||
|
||||
for i := 0; i < len(cronJobs); i++ {
|
||||
if i > 0 {
|
||||
fmt.Printf("---\n")
|
||||
}
|
||||
|
||||
cronJob := cronJobs[i]
|
||||
|
||||
fmt.Printf("[Name] %s\n", cronJob.Name)
|
||||
fmt.Printf("[Description] %s\n", cronJob.Description)
|
||||
fmt.Printf("[Interval] Every %s\n", cronJob.Period.GetInterval())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func runCronJob(c *core.CliContext) error {
|
||||
config, err := initializeSystem(c)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = cron.InitializeCronJobSchedulerContainer(c, config, false)
|
||||
|
||||
if err != nil {
|
||||
log.CliErrorf(c, "[cron_jobs.runCronJob] initializes cron job scheduler failed, because %s", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
jobName := c.String("name")
|
||||
err = cron.Container.SyncRunJobNow(jobName)
|
||||
|
||||
if err != nil {
|
||||
log.CliErrorf(c, "[cron_jobs.runCronJob] failed to run cron job \"%s\", because %s", jobName, err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
log.CliInfof(c, "[cron_jobs.runCronJob] run cron job \"%s\" successfully", jobName)
|
||||
|
||||
return nil
|
||||
}
|
||||
+16
-33
@@ -3,7 +3,6 @@ package cmd
|
||||
import (
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/datastore"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/log"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||
@@ -17,32 +16,32 @@ var Database = &cli.Command{
|
||||
{
|
||||
Name: "update",
|
||||
Usage: "Update database structure",
|
||||
Action: bindAction(updateDatabaseStructure),
|
||||
Action: updateDatabaseStructure,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func updateDatabaseStructure(c *core.CliContext) error {
|
||||
func updateDatabaseStructure(c *cli.Context) error {
|
||||
_, err := initializeSystem(c)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.CliInfof(c, "[database.updateDatabaseStructure] starting maintaining")
|
||||
log.BootInfof("[database.updateDatabaseStructure] starting maintaining")
|
||||
|
||||
err = updateAllDatabaseTablesStructure(c)
|
||||
err = updateAllDatabaseTablesStructure()
|
||||
|
||||
if err != nil {
|
||||
log.CliErrorf(c, "[database.updateDatabaseStructure] update database table structure failed, because %s", err.Error())
|
||||
log.BootErrorf("[database.updateDatabaseStructure] update database table structure failed, because %s", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
log.CliInfof(c, "[database.updateDatabaseStructure] all tables maintained successfully")
|
||||
log.BootInfof("[database.updateDatabaseStructure] all tables maintained successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateAllDatabaseTablesStructure(c *core.CliContext) error {
|
||||
func updateAllDatabaseTablesStructure() error {
|
||||
var err error
|
||||
|
||||
err = datastore.Container.UserStore.SyncStructs(new(models.User))
|
||||
@@ -51,7 +50,7 @@ func updateAllDatabaseTablesStructure(c *core.CliContext) error {
|
||||
return err
|
||||
}
|
||||
|
||||
log.BootInfof(c, "[database.updateAllDatabaseTablesStructure] user table maintained successfully")
|
||||
log.BootInfof("[database.updateAllDatabaseTablesStructure] user table maintained successfully")
|
||||
|
||||
err = datastore.Container.UserStore.SyncStructs(new(models.TwoFactor))
|
||||
|
||||
@@ -59,7 +58,7 @@ func updateAllDatabaseTablesStructure(c *core.CliContext) error {
|
||||
return err
|
||||
}
|
||||
|
||||
log.BootInfof(c, "[database.updateAllDatabaseTablesStructure] two-factor table maintained successfully")
|
||||
log.BootInfof("[database.updateAllDatabaseTablesStructure] two factor table maintained successfully")
|
||||
|
||||
err = datastore.Container.UserStore.SyncStructs(new(models.TwoFactorRecoveryCode))
|
||||
|
||||
@@ -67,7 +66,7 @@ func updateAllDatabaseTablesStructure(c *core.CliContext) error {
|
||||
return err
|
||||
}
|
||||
|
||||
log.BootInfof(c, "[database.updateAllDatabaseTablesStructure] two-factor recovery code table maintained successfully")
|
||||
log.BootInfof("[database.updateAllDatabaseTablesStructure] two factor recovery code table maintained successfully")
|
||||
|
||||
err = datastore.Container.TokenStore.SyncStructs(new(models.TokenRecord))
|
||||
|
||||
@@ -75,7 +74,7 @@ func updateAllDatabaseTablesStructure(c *core.CliContext) error {
|
||||
return err
|
||||
}
|
||||
|
||||
log.BootInfof(c, "[database.updateAllDatabaseTablesStructure] token record table maintained successfully")
|
||||
log.BootInfof("[database.updateAllDatabaseTablesStructure] token record table maintained successfully")
|
||||
|
||||
err = datastore.Container.UserDataStore.SyncStructs(new(models.Account))
|
||||
|
||||
@@ -83,7 +82,7 @@ func updateAllDatabaseTablesStructure(c *core.CliContext) error {
|
||||
return err
|
||||
}
|
||||
|
||||
log.BootInfof(c, "[database.updateAllDatabaseTablesStructure] account table maintained successfully")
|
||||
log.BootInfof("[database.updateAllDatabaseTablesStructure] account table maintained successfully")
|
||||
|
||||
err = datastore.Container.UserDataStore.SyncStructs(new(models.Transaction))
|
||||
|
||||
@@ -91,7 +90,7 @@ func updateAllDatabaseTablesStructure(c *core.CliContext) error {
|
||||
return err
|
||||
}
|
||||
|
||||
log.BootInfof(c, "[database.updateAllDatabaseTablesStructure] transaction table maintained successfully")
|
||||
log.BootInfof("[database.updateAllDatabaseTablesStructure] transaction table maintained successfully")
|
||||
|
||||
err = datastore.Container.UserDataStore.SyncStructs(new(models.TransactionCategory))
|
||||
|
||||
@@ -99,7 +98,7 @@ func updateAllDatabaseTablesStructure(c *core.CliContext) error {
|
||||
return err
|
||||
}
|
||||
|
||||
log.BootInfof(c, "[database.updateAllDatabaseTablesStructure] transaction category table maintained successfully")
|
||||
log.BootInfof("[database.updateAllDatabaseTablesStructure] transaction category table maintained successfully")
|
||||
|
||||
err = datastore.Container.UserDataStore.SyncStructs(new(models.TransactionTag))
|
||||
|
||||
@@ -107,7 +106,7 @@ func updateAllDatabaseTablesStructure(c *core.CliContext) error {
|
||||
return err
|
||||
}
|
||||
|
||||
log.BootInfof(c, "[database.updateAllDatabaseTablesStructure] transaction tag table maintained successfully")
|
||||
log.BootInfof("[database.updateAllDatabaseTablesStructure] transaction tag table maintained successfully")
|
||||
|
||||
err = datastore.Container.UserDataStore.SyncStructs(new(models.TransactionTagIndex))
|
||||
|
||||
@@ -115,23 +114,7 @@ func updateAllDatabaseTablesStructure(c *core.CliContext) error {
|
||||
return err
|
||||
}
|
||||
|
||||
log.BootInfof(c, "[database.updateAllDatabaseTablesStructure] transaction tag index table maintained successfully")
|
||||
|
||||
err = datastore.Container.UserDataStore.SyncStructs(new(models.TransactionTemplate))
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.BootInfof(c, "[database.updateAllDatabaseTablesStructure] transaction template table maintained successfully")
|
||||
|
||||
err = datastore.Container.UserDataStore.SyncStructs(new(models.TransactionPictureInfo))
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.BootInfof(c, "[database.updateAllDatabaseTablesStructure] transaction picture table maintained successfully")
|
||||
log.BootInfof("[database.updateAllDatabaseTablesStructure] transaction tag index table maintained successfully")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
+14
-49
@@ -4,20 +4,18 @@ import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
|
||||
"github.com/mayswind/ezbookkeeping/pkg/avatars"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"github.com/mayswind/ezbookkeeping/pkg/datastore"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/duplicatechecker"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/exchangerates"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/log"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/mail"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/storage"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/uuid"
|
||||
)
|
||||
|
||||
func initializeSystem(c *core.CliContext) (*settings.Config, error) {
|
||||
func initializeSystem(c *cli.Context) (*settings.Config, error) {
|
||||
var err error
|
||||
configFilePath := c.String("conf-path")
|
||||
isDisableBootLog := c.Bool("no-boot-log")
|
||||
@@ -25,26 +23,26 @@ func initializeSystem(c *core.CliContext) (*settings.Config, error) {
|
||||
if configFilePath != "" {
|
||||
if _, err = os.Stat(configFilePath); err != nil {
|
||||
if !isDisableBootLog {
|
||||
log.BootErrorf(c, "[initializer.initializeSystem] cannot load configuration from custom config path %s, because file not exists", configFilePath)
|
||||
log.BootErrorf("[initializer.initializeSystem] cannot load configuration from custom config path %s, because file not exists", configFilePath)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !isDisableBootLog {
|
||||
log.BootInfof(c, "[initializer.initializeSystem] will loading configuration from custom config path %s", configFilePath)
|
||||
log.BootInfof("[initializer.initializeSystem] will loading configuration from custom config path %s", configFilePath)
|
||||
}
|
||||
} else {
|
||||
configFilePath, err = settings.GetDefaultConfigFilePath()
|
||||
|
||||
if err != nil {
|
||||
if !isDisableBootLog {
|
||||
log.BootErrorf(c, "[initializer.initializeSystem] cannot get default configuration path, because %s", err.Error())
|
||||
log.BootErrorf("[initializer.initializeSystem] cannot get default configuration path, because %s", err.Error())
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !isDisableBootLog {
|
||||
log.BootInfof(c, "[initializer.initializeSystem] will load configuration from default config path %s", configFilePath)
|
||||
log.BootInfof("[initializer.initializeSystem] will load configuration from default config path %s", configFilePath)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,22 +50,18 @@ func initializeSystem(c *core.CliContext) (*settings.Config, error) {
|
||||
|
||||
if err != nil {
|
||||
if !isDisableBootLog {
|
||||
log.BootErrorf(c, "[initializer.initializeSystem] cannot load configuration, because %s", err.Error())
|
||||
log.BootErrorf("[initializer.initializeSystem] cannot load configuration, because %s", err.Error())
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if config.SecretKeyNoSet {
|
||||
log.BootWarnf(c, "[initializer.initializeSystem] \"secret_key\" in config file is not set, please change it to keep your user data safe")
|
||||
}
|
||||
|
||||
settings.SetCurrentConfig(config)
|
||||
|
||||
err = datastore.InitializeDataStore(config)
|
||||
|
||||
if err != nil {
|
||||
if !isDisableBootLog {
|
||||
log.BootErrorf(c, "[initializer.initializeSystem] initializes data store failed, because %s", err.Error())
|
||||
log.BootErrorf("[initializer.initializeSystem] initializes data store failed, because %s", err.Error())
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
@@ -76,16 +70,7 @@ func initializeSystem(c *core.CliContext) (*settings.Config, error) {
|
||||
|
||||
if err != nil {
|
||||
if !isDisableBootLog {
|
||||
log.BootErrorf(c, "[initializer.initializeSystem] sets logger configuration failed, because %s", err.Error())
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = storage.InitializeStorageContainer(config)
|
||||
|
||||
if err != nil {
|
||||
if !isDisableBootLog {
|
||||
log.BootErrorf(c, "[initializer.initializeSystem] initializes object storage failed, because %s", err.Error())
|
||||
log.BootErrorf("[initializer.initializeSystem] sets logger configuration failed, because %s", err.Error())
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
@@ -94,25 +79,7 @@ func initializeSystem(c *core.CliContext) (*settings.Config, error) {
|
||||
|
||||
if err != nil {
|
||||
if !isDisableBootLog {
|
||||
log.BootErrorf(c, "[initializer.initializeSystem] initializes uuid generator failed, because %s", err.Error())
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = duplicatechecker.InitializeDuplicateChecker(config)
|
||||
|
||||
if err != nil {
|
||||
if !isDisableBootLog {
|
||||
log.BootErrorf(c, "[initializer.initializeSystem] initializes duplicate checker failed, because %s", err.Error())
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = avatars.InitializeAvatarProvider(config)
|
||||
|
||||
if err != nil {
|
||||
if !isDisableBootLog {
|
||||
log.BootErrorf(c, "[initializer.initializeSystem] initializes avatar provider failed, because %s", err.Error())
|
||||
log.BootErrorf("[initializer.initializeSystem] initializes uuid generator failed, because %s", err.Error())
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
@@ -121,7 +88,7 @@ func initializeSystem(c *core.CliContext) (*settings.Config, error) {
|
||||
|
||||
if err != nil {
|
||||
if !isDisableBootLog {
|
||||
log.BootErrorf(c, "[initializer.initializeSystem] initializes mailer failed, because %s", err.Error())
|
||||
log.BootErrorf("[initializer.initializeSystem] initializes mailer failed, because %s", err.Error())
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
@@ -130,7 +97,7 @@ func initializeSystem(c *core.CliContext) (*settings.Config, error) {
|
||||
|
||||
if err != nil {
|
||||
if !isDisableBootLog {
|
||||
log.BootErrorf(c, "[initializer.initializeSystem] initializes exchange rates data source failed, because %s", err.Error())
|
||||
log.BootErrorf("[initializer.initializeSystem] initializes exchange rates data source failed, because %s", err.Error())
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
@@ -138,7 +105,7 @@ func initializeSystem(c *core.CliContext) (*settings.Config, error) {
|
||||
cfgJson, _ := json.Marshal(getConfigWithoutSensitiveData(config))
|
||||
|
||||
if !isDisableBootLog {
|
||||
log.BootInfof(c, "[initializer.initializeSystem] has loaded configuration %s", cfgJson)
|
||||
log.BootInfof("[initializer.initializeSystem] has loaded configuration %s", cfgJson)
|
||||
}
|
||||
|
||||
return config, nil
|
||||
@@ -154,9 +121,7 @@ func getConfigWithoutSensitiveData(config *settings.Config) *settings.Config {
|
||||
|
||||
clonedConfig.DatabaseConfig.DatabasePassword = "****"
|
||||
clonedConfig.SMTPConfig.SMTPPasswd = "****"
|
||||
clonedConfig.MinIOConfig.SecretAccessKey = "****"
|
||||
clonedConfig.SecretKey = "****"
|
||||
clonedConfig.AmapApplicationSecret = "****"
|
||||
|
||||
return clonedConfig
|
||||
}
|
||||
|
||||
+2
-3
@@ -5,7 +5,6 @@ import (
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||
)
|
||||
|
||||
@@ -17,7 +16,7 @@ var SecurityUtils = &cli.Command{
|
||||
{
|
||||
Name: "gen-secret-key",
|
||||
Usage: "Generate a random secret key",
|
||||
Action: bindAction(genSecretKey),
|
||||
Action: genSecretKey,
|
||||
Flags: []cli.Flag{
|
||||
&cli.IntFlag{
|
||||
Name: "length",
|
||||
@@ -31,7 +30,7 @@ var SecurityUtils = &cli.Command{
|
||||
},
|
||||
}
|
||||
|
||||
func genSecretKey(c *core.CliContext) error {
|
||||
func genSecretKey(c *cli.Context) error {
|
||||
length := c.Int("length")
|
||||
|
||||
if length <= 0 {
|
||||
|
||||
+65
-367
@@ -2,15 +2,13 @@ package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||
"os"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
clis "github.com/mayswind/ezbookkeeping/pkg/cli"
|
||||
"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/utils"
|
||||
)
|
||||
|
||||
@@ -22,7 +20,7 @@ var UserData = &cli.Command{
|
||||
{
|
||||
Name: "user-add",
|
||||
Usage: "Add new user",
|
||||
Action: bindAction(addNewUser),
|
||||
Action: addNewUser,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "username",
|
||||
@@ -59,7 +57,7 @@ var UserData = &cli.Command{
|
||||
{
|
||||
Name: "user-get",
|
||||
Usage: "Get specified user info",
|
||||
Action: bindAction(getUserInfo),
|
||||
Action: getUserInfo,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "username",
|
||||
@@ -72,7 +70,7 @@ var UserData = &cli.Command{
|
||||
{
|
||||
Name: "user-modify-password",
|
||||
Usage: "Modify user password",
|
||||
Action: bindAction(modifyUserPassword),
|
||||
Action: modifyUserPassword,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "username",
|
||||
@@ -91,7 +89,7 @@ var UserData = &cli.Command{
|
||||
{
|
||||
Name: "user-enable",
|
||||
Usage: "Enable specified user",
|
||||
Action: bindAction(enableUser),
|
||||
Action: enableUser,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "username",
|
||||
@@ -104,7 +102,7 @@ var UserData = &cli.Command{
|
||||
{
|
||||
Name: "user-disable",
|
||||
Usage: "Disable specified user",
|
||||
Action: bindAction(disableUser),
|
||||
Action: disableUser,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "username",
|
||||
@@ -114,67 +112,10 @@ var UserData = &cli.Command{
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "user-set-restrict-features",
|
||||
Usage: "Set restrictions of user features",
|
||||
Action: bindAction(setUserFeatureRestriction),
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "username",
|
||||
Aliases: []string{"n"},
|
||||
Required: true,
|
||||
Usage: "Specific user name",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "features",
|
||||
Aliases: []string{"t"},
|
||||
Required: true,
|
||||
Usage: "Specific feature types (feature types separated by commas)",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "user-add-restrict-features",
|
||||
Usage: "Add restrictions of user features",
|
||||
Action: bindAction(addUserFeatureRestriction),
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "username",
|
||||
Aliases: []string{"n"},
|
||||
Required: true,
|
||||
Usage: "Specific user name",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "features",
|
||||
Aliases: []string{"t"},
|
||||
Required: true,
|
||||
Usage: "Specific feature types (feature types separated by commas)",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "user-remove-restrict-features",
|
||||
Usage: "Remove restrictions of user features",
|
||||
Action: bindAction(removeUserFeatureRestriction),
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "username",
|
||||
Aliases: []string{"n"},
|
||||
Required: true,
|
||||
Usage: "Specific user name",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "features",
|
||||
Aliases: []string{"t"},
|
||||
Required: true,
|
||||
Usage: "Specific feature types (feature types separated by commas)",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "user-resend-verify-email",
|
||||
Usage: "Resend user verify email",
|
||||
Action: bindAction(resendUserVerifyEmail),
|
||||
Action: resendUserVerifyEmail,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "username",
|
||||
@@ -187,7 +128,7 @@ var UserData = &cli.Command{
|
||||
{
|
||||
Name: "user-set-email-verified",
|
||||
Usage: "Set user email address verified",
|
||||
Action: bindAction(setUserEmailVerified),
|
||||
Action: setUserEmailVerified,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "username",
|
||||
@@ -200,7 +141,7 @@ var UserData = &cli.Command{
|
||||
{
|
||||
Name: "user-set-email-unverified",
|
||||
Usage: "Set user email address unverified",
|
||||
Action: bindAction(setUserEmailUnverified),
|
||||
Action: setUserEmailUnverified,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "username",
|
||||
@@ -213,7 +154,7 @@ var UserData = &cli.Command{
|
||||
{
|
||||
Name: "user-delete",
|
||||
Usage: "Delete specified user",
|
||||
Action: bindAction(deleteUser),
|
||||
Action: deleteUser,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "username",
|
||||
@@ -226,7 +167,7 @@ var UserData = &cli.Command{
|
||||
{
|
||||
Name: "user-2fa-disable",
|
||||
Usage: "Disable user 2fa setting",
|
||||
Action: bindAction(disableUser2FA),
|
||||
Action: disableUser2FA,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "username",
|
||||
@@ -239,20 +180,7 @@ var UserData = &cli.Command{
|
||||
{
|
||||
Name: "user-session-list",
|
||||
Usage: "List all user sessions",
|
||||
Action: bindAction(listUserTokens),
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "username",
|
||||
Aliases: []string{"n"},
|
||||
Required: true,
|
||||
Usage: "Specific user name",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "user-session-new",
|
||||
Usage: "Create new session for user",
|
||||
Action: bindAction(createNewUserToken),
|
||||
Action: listUserTokens,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "username",
|
||||
@@ -265,7 +193,7 @@ var UserData = &cli.Command{
|
||||
{
|
||||
Name: "user-session-clear",
|
||||
Usage: "Clear user all sessions",
|
||||
Action: bindAction(clearUserTokens),
|
||||
Action: clearUserTokens,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "username",
|
||||
@@ -278,7 +206,7 @@ var UserData = &cli.Command{
|
||||
{
|
||||
Name: "send-password-reset-mail",
|
||||
Usage: "Send password reset mail",
|
||||
Action: bindAction(sendPasswordResetMail),
|
||||
Action: sendPasswordResetMail,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "username",
|
||||
@@ -291,7 +219,7 @@ var UserData = &cli.Command{
|
||||
{
|
||||
Name: "transaction-check",
|
||||
Usage: "Check whether user all transactions and accounts are correct",
|
||||
Action: bindAction(checkUserTransactionAndAccount),
|
||||
Action: checkUserTransactionAndAccount,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "username",
|
||||
@@ -301,48 +229,10 @@ var UserData = &cli.Command{
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "transaction-tag-index-fix-transaction-time",
|
||||
Usage: "Fix the transaction tag index data which does not have transaction time",
|
||||
Action: bindAction(fixTransactionTagIndexNotHaveTransactionTime),
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "username",
|
||||
Aliases: []string{"n"},
|
||||
Required: true,
|
||||
Usage: "Specific user name",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "transaction-import",
|
||||
Usage: "Import transactions to specified user",
|
||||
Action: bindAction(importUserTransaction),
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "username",
|
||||
Aliases: []string{"n"},
|
||||
Required: true,
|
||||
Usage: "Specific user name",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "file",
|
||||
Aliases: []string{"f"},
|
||||
Required: true,
|
||||
Usage: "Specific import file path (e.g. transaction.csv)",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "type",
|
||||
Aliases: []string{"t"},
|
||||
Required: true,
|
||||
Usage: "Import file type (supports \"ezbookkeeping_csv\", \"ezbookkeeping_tsv\")",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "transaction-export",
|
||||
Usage: "Export user all transactions to file",
|
||||
Action: bindAction(exportUserTransaction),
|
||||
Usage: "Export user all transactions to csv file",
|
||||
Action: exportUserTransaction,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "username",
|
||||
@@ -356,18 +246,12 @@ var UserData = &cli.Command{
|
||||
Required: true,
|
||||
Usage: "Specific exported file path (e.g. transaction.csv)",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "type",
|
||||
Aliases: []string{"t"},
|
||||
Required: false,
|
||||
Usage: "Export file type, support csv or tsv, default is csv",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func addNewUser(c *core.CliContext) error {
|
||||
func addNewUser(c *cli.Context) error {
|
||||
_, err := initializeSystem(c)
|
||||
|
||||
if err != nil {
|
||||
@@ -383,7 +267,7 @@ func addNewUser(c *core.CliContext) error {
|
||||
user, err := clis.UserData.AddNewUser(c, username, email, nickname, password, defaultCurrency)
|
||||
|
||||
if err != nil {
|
||||
log.CliErrorf(c, "[user_data.addNewUser] error occurs when adding new user")
|
||||
log.BootErrorf("[user_data.addNewUser] error occurs when adding new user")
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -392,7 +276,7 @@ func addNewUser(c *core.CliContext) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func getUserInfo(c *core.CliContext) error {
|
||||
func getUserInfo(c *cli.Context) error {
|
||||
_, err := initializeSystem(c)
|
||||
|
||||
if err != nil {
|
||||
@@ -403,7 +287,7 @@ func getUserInfo(c *core.CliContext) error {
|
||||
user, err := clis.UserData.GetUserByUsername(c, username)
|
||||
|
||||
if err != nil {
|
||||
log.CliErrorf(c, "[user_data.getUserInfo] error occurs when getting user data")
|
||||
log.BootErrorf("[user_data.getUserInfo] error occurs when getting user data")
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -412,7 +296,7 @@ func getUserInfo(c *core.CliContext) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func modifyUserPassword(c *core.CliContext) error {
|
||||
func modifyUserPassword(c *cli.Context) error {
|
||||
_, err := initializeSystem(c)
|
||||
|
||||
if err != nil {
|
||||
@@ -424,16 +308,16 @@ func modifyUserPassword(c *core.CliContext) error {
|
||||
err = clis.UserData.ModifyUserPassword(c, username, password)
|
||||
|
||||
if err != nil {
|
||||
log.CliErrorf(c, "[user_data.modifyUserPassword] error occurs when modifying user password")
|
||||
log.BootErrorf("[user_data.modifyUserPassword] error occurs when modifying user password")
|
||||
return err
|
||||
}
|
||||
|
||||
log.CliInfof(c, "[user_data.modifyUserPassword] password of user \"%s\" has been changed", username)
|
||||
log.BootInfof("[user_data.modifyUserPassword] password of user \"%s\" has been changed", username)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func sendPasswordResetMail(c *core.CliContext) error {
|
||||
func sendPasswordResetMail(c *cli.Context) error {
|
||||
_, err := initializeSystem(c)
|
||||
|
||||
if err != nil {
|
||||
@@ -444,16 +328,16 @@ func sendPasswordResetMail(c *core.CliContext) error {
|
||||
err = clis.UserData.SendPasswordResetMail(c, username)
|
||||
|
||||
if err != nil {
|
||||
log.CliErrorf(c, "[user_data.sendPasswordResetMail] error occurs when sending password reset email")
|
||||
log.BootErrorf("[user_data.sendPasswordResetMail] error occurs when sending password reset email")
|
||||
return err
|
||||
}
|
||||
|
||||
log.CliInfof(c, "[user_data.sendPasswordResetMail] a password reset email for user \"%s\" has been sent", username)
|
||||
log.BootInfof("[user_data.sendPasswordResetMail] a password reset email for user \"%s\" has been sent", username)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func enableUser(c *core.CliContext) error {
|
||||
func enableUser(c *cli.Context) error {
|
||||
_, err := initializeSystem(c)
|
||||
|
||||
if err != nil {
|
||||
@@ -464,16 +348,16 @@ func enableUser(c *core.CliContext) error {
|
||||
err = clis.UserData.EnableUser(c, username)
|
||||
|
||||
if err != nil {
|
||||
log.CliErrorf(c, "[user_data.enableUser] error occurs when setting user enabled")
|
||||
log.BootErrorf("[user_data.enableUser] error occurs when setting user enabled")
|
||||
return err
|
||||
}
|
||||
|
||||
log.CliInfof(c, "[user_data.enableUser] user \"%s\" has been set enabled", username)
|
||||
log.BootInfof("[user_data.enableUser] user \"%s\" has been set enabled", username)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func disableUser(c *core.CliContext) error {
|
||||
func disableUser(c *cli.Context) error {
|
||||
_, err := initializeSystem(c)
|
||||
|
||||
if err != nil {
|
||||
@@ -484,91 +368,16 @@ func disableUser(c *core.CliContext) error {
|
||||
err = clis.UserData.DisableUser(c, username)
|
||||
|
||||
if err != nil {
|
||||
log.CliErrorf(c, "[user_data.disableUser] error occurs when setting user disabled")
|
||||
log.BootErrorf("[user_data.disableUser] error occurs when setting user disabled")
|
||||
return err
|
||||
}
|
||||
|
||||
log.CliInfof(c, "[user_data.disableUser] user \"%s\" has been set disabled", username)
|
||||
log.BootInfof("[user_data.disableUser] user \"%s\" has been set disabled", username)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func setUserFeatureRestriction(c *core.CliContext) error {
|
||||
_, err := initializeSystem(c)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
username := c.String("username")
|
||||
featureRestriction := core.ParseUserFeatureRestrictions(c.String("features"))
|
||||
err = clis.UserData.SetUserFeatureRestrictions(c, username, featureRestriction)
|
||||
|
||||
if err != nil {
|
||||
log.CliErrorf(c, "[user_data.setUserFeatureRestriction] error occurs when setting user feature restriction")
|
||||
return err
|
||||
}
|
||||
|
||||
log.CliInfof(c, "[user_data.setUserFeatureRestriction] user \"%s\" has been set new feature restriction", username)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func addUserFeatureRestriction(c *core.CliContext) error {
|
||||
_, err := initializeSystem(c)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
username := c.String("username")
|
||||
featureRestriction := core.ParseUserFeatureRestrictions(c.String("features"))
|
||||
|
||||
if featureRestriction < 1 {
|
||||
log.CliErrorf(c, "[user_data.addUserFeatureRestriction] nothing has been modified")
|
||||
return nil
|
||||
}
|
||||
|
||||
err = clis.UserData.AddUserFeatureRestrictions(c, username, featureRestriction)
|
||||
|
||||
if err != nil {
|
||||
log.CliErrorf(c, "[user_data.addUserFeatureRestriction] error occurs when adding user feature restriction")
|
||||
return err
|
||||
}
|
||||
|
||||
log.CliInfof(c, "[user_data.addUserFeatureRestriction] user \"%s\" has been add new feature restriction", username)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func removeUserFeatureRestriction(c *core.CliContext) error {
|
||||
_, err := initializeSystem(c)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
username := c.String("username")
|
||||
featureRestriction := core.ParseUserFeatureRestrictions(c.String("features"))
|
||||
|
||||
if featureRestriction < 1 {
|
||||
log.CliErrorf(c, "[user_data.removeUserFeatureRestriction] nothing has been modified")
|
||||
return nil
|
||||
}
|
||||
|
||||
err = clis.UserData.RemoveUserFeatureRestrictions(c, username, featureRestriction)
|
||||
|
||||
if err != nil {
|
||||
log.CliErrorf(c, "[user_data.removeUserFeatureRestriction] error occurs when removing user feature restriction")
|
||||
return err
|
||||
}
|
||||
|
||||
log.CliInfof(c, "[user_data.removeUserFeatureRestriction] user \"%s\" has been removed new feature restriction", username)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func resendUserVerifyEmail(c *core.CliContext) error {
|
||||
func resendUserVerifyEmail(c *cli.Context) error {
|
||||
_, err := initializeSystem(c)
|
||||
|
||||
if err != nil {
|
||||
@@ -579,16 +388,16 @@ func resendUserVerifyEmail(c *core.CliContext) error {
|
||||
err = clis.UserData.ResendVerifyEmail(c, username)
|
||||
|
||||
if err != nil {
|
||||
log.CliErrorf(c, "[user_data.resendUserVerifyEmail] error occurs when resending user verify email")
|
||||
log.BootErrorf("[user_data.resendUserVerifyEmail] error occurs when resending user verify email")
|
||||
return err
|
||||
}
|
||||
|
||||
log.CliInfof(c, "[user_data.resendUserVerifyEmail] verify email for user \"%s\" has been resent", username)
|
||||
log.BootInfof("[user_data.resendUserVerifyEmail] verify email for user \"%s\" has been resent", username)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func setUserEmailVerified(c *core.CliContext) error {
|
||||
func setUserEmailVerified(c *cli.Context) error {
|
||||
_, err := initializeSystem(c)
|
||||
|
||||
if err != nil {
|
||||
@@ -599,16 +408,16 @@ func setUserEmailVerified(c *core.CliContext) error {
|
||||
err = clis.UserData.SetUserEmailVerified(c, username)
|
||||
|
||||
if err != nil {
|
||||
log.CliErrorf(c, "[user_data.setUserEmailVerified] error occurs when setting user email address verified")
|
||||
log.BootErrorf("[user_data.setUserEmailVerified] error occurs when setting user email address verified")
|
||||
return err
|
||||
}
|
||||
|
||||
log.CliInfof(c, "[user_data.setUserEmailVerified] user \"%s\" email address has been set verified", username)
|
||||
log.BootInfof("[user_data.setUserEmailVerified] user \"%s\" email address has been set verified", username)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func setUserEmailUnverified(c *core.CliContext) error {
|
||||
func setUserEmailUnverified(c *cli.Context) error {
|
||||
_, err := initializeSystem(c)
|
||||
|
||||
if err != nil {
|
||||
@@ -619,16 +428,16 @@ func setUserEmailUnverified(c *core.CliContext) error {
|
||||
err = clis.UserData.SetUserEmailUnverified(c, username)
|
||||
|
||||
if err != nil {
|
||||
log.CliErrorf(c, "[user_data.setUserEmailUnverified] error occurs when setting user email address unverified")
|
||||
log.BootErrorf("[user_data.setUserEmailUnverified] error occurs when setting user email address unverified")
|
||||
return err
|
||||
}
|
||||
|
||||
log.CliInfof(c, "[user_data.setUserEmailUnverified] user \"%s\" email address has been set unverified", username)
|
||||
log.BootInfof("[user_data.setUserEmailUnverified] user \"%s\" email address has been set unverified", username)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func deleteUser(c *core.CliContext) error {
|
||||
func deleteUser(c *cli.Context) error {
|
||||
_, err := initializeSystem(c)
|
||||
|
||||
if err != nil {
|
||||
@@ -639,16 +448,16 @@ func deleteUser(c *core.CliContext) error {
|
||||
err = clis.UserData.DeleteUser(c, username)
|
||||
|
||||
if err != nil {
|
||||
log.CliErrorf(c, "[user_data.deleteUser] error occurs when deleting user")
|
||||
log.BootErrorf("[user_data.deleteUser] error occurs when deleting user")
|
||||
return err
|
||||
}
|
||||
|
||||
log.CliInfof(c, "[user_data.deleteUser] user \"%s\" has been deleted", username)
|
||||
log.BootInfof("[user_data.deleteUser] user \"%s\" has been deleted", username)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func disableUser2FA(c *core.CliContext) error {
|
||||
func disableUser2FA(c *cli.Context) error {
|
||||
_, err := initializeSystem(c)
|
||||
|
||||
if err != nil {
|
||||
@@ -659,16 +468,16 @@ func disableUser2FA(c *core.CliContext) error {
|
||||
err = clis.UserData.DisableUserTwoFactorAuthorization(c, username)
|
||||
|
||||
if err != nil {
|
||||
log.CliErrorf(c, "[user_data.disableUser2FA] error occurs when disabling user two-factor authorization")
|
||||
log.BootErrorf("[user_data.disableUser2FA] error occurs when disabling user two factor authorization")
|
||||
return err
|
||||
}
|
||||
|
||||
log.CliInfof(c, "[user_data.disableUser2FA] two-factor authorization of user \"%s\" has been disabled", username)
|
||||
log.BootInfof("[user_data.disableUser2FA] two factor authorization of user \"%s\" has been disabled", username)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func listUserTokens(c *core.CliContext) error {
|
||||
func listUserTokens(c *cli.Context) error {
|
||||
_, err := initializeSystem(c)
|
||||
|
||||
if err != nil {
|
||||
@@ -679,7 +488,7 @@ func listUserTokens(c *core.CliContext) error {
|
||||
tokens, err := clis.UserData.ListUserTokens(c, username)
|
||||
|
||||
if err != nil {
|
||||
log.CliErrorf(c, "[user_data.listUserTokens] error occurs when getting user tokens")
|
||||
log.BootErrorf("[user_data.listUserTokens] error occurs when getting user tokens")
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -694,28 +503,7 @@ func listUserTokens(c *core.CliContext) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func createNewUserToken(c *core.CliContext) error {
|
||||
_, err := initializeSystem(c)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
username := c.String("username")
|
||||
token, tokenString, err := clis.UserData.CreateNewUserToken(c, username)
|
||||
|
||||
if err != nil {
|
||||
log.CliErrorf(c, "[user_data.createNewUserToken] error occurs when creating user token")
|
||||
return err
|
||||
}
|
||||
|
||||
printTokenInfo(token)
|
||||
fmt.Printf("[NewToken] %s\n", tokenString)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func clearUserTokens(c *core.CliContext) error {
|
||||
func clearUserTokens(c *cli.Context) error {
|
||||
_, err := initializeSystem(c)
|
||||
|
||||
if err != nil {
|
||||
@@ -726,16 +514,16 @@ func clearUserTokens(c *core.CliContext) error {
|
||||
err = clis.UserData.ClearUserTokens(c, username)
|
||||
|
||||
if err != nil {
|
||||
log.CliErrorf(c, "[user_data.clearUserTokens] error occurs when clearing user tokens")
|
||||
log.BootErrorf("[user_data.clearUserTokens] error occurs when clearing user tokens")
|
||||
return err
|
||||
}
|
||||
|
||||
log.CliInfof(c, "[user_data.clearUserTokens] all tokens of user \"%s\" has been cleared", username)
|
||||
log.BootInfof("[user_data.clearUserTokens] all tokens of user \"%s\" has been cleared", username)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkUserTransactionAndAccount(c *core.CliContext) error {
|
||||
func checkUserTransactionAndAccount(c *cli.Context) error {
|
||||
_, err := initializeSystem(c)
|
||||
|
||||
if err != nil {
|
||||
@@ -744,44 +532,21 @@ func checkUserTransactionAndAccount(c *core.CliContext) error {
|
||||
|
||||
username := c.String("username")
|
||||
|
||||
log.CliInfof(c, "[user_data.checkUserTransactionAndAccount] starting checking user \"%s\" data", username)
|
||||
log.BootInfof("[user_data.checkUserTransactionAndAccount] starting checking user \"%s\" data", username)
|
||||
|
||||
_, err = clis.UserData.CheckTransactionAndAccount(c, username)
|
||||
|
||||
if err != nil {
|
||||
log.CliErrorf(c, "[user_data.checkUserTransactionAndAccount] error occurs when checking user data")
|
||||
log.BootErrorf("[user_data.checkUserTransactionAndAccount] error occurs when checking user data")
|
||||
return err
|
||||
}
|
||||
|
||||
log.CliInfof(c, "[user_data.checkUserTransactionAndAccount] user transactions and accounts data has been checked successfully, there is no problem with user data")
|
||||
log.BootInfof("[user_data.checkUserTransactionAndAccount] user transactions and accounts data has been checked successfully, there is no problem with user data")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func fixTransactionTagIndexNotHaveTransactionTime(c *core.CliContext) error {
|
||||
_, err := initializeSystem(c)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
username := c.String("username")
|
||||
|
||||
log.CliInfof(c, "[user_data.fixTransactionTagIndexNotHaveTransactionTime] starting fixing user \"%s\" transaction tag index data", username)
|
||||
|
||||
_, err = clis.UserData.FixTransactionTagIndexWithTransactionTime(c, username)
|
||||
|
||||
if err != nil {
|
||||
log.CliErrorf(c, "[user_data.fixTransactionTagIndexNotHaveTransactionTime] error occurs when fixing user data")
|
||||
return err
|
||||
}
|
||||
|
||||
log.CliInfof(c, "[user_data.fixTransactionTagIndexNotHaveTransactionTime] user transaction tag index data has been fixed successfully")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func exportUserTransaction(c *core.CliContext) error {
|
||||
func exportUserTransaction(c *cli.Context) error {
|
||||
_, err := initializeSystem(c)
|
||||
|
||||
if err != nil {
|
||||
@@ -790,95 +555,36 @@ func exportUserTransaction(c *core.CliContext) error {
|
||||
|
||||
username := c.String("username")
|
||||
filePath := c.String("file")
|
||||
fileType := c.String("type")
|
||||
|
||||
if fileType == "" {
|
||||
fileType = "csv"
|
||||
}
|
||||
|
||||
if fileType != "csv" && fileType != "tsv" {
|
||||
log.CliErrorf(c, "[user_data.exportUserTransaction] export file type is not supported")
|
||||
return errs.ErrNotSupported
|
||||
}
|
||||
|
||||
if filePath == "" {
|
||||
log.CliErrorf(c, "[user_data.exportUserTransaction] export file path is unspecified")
|
||||
log.BootErrorf("[user_data.exportUserTransaction] export file path is not specified")
|
||||
return os.ErrNotExist
|
||||
}
|
||||
|
||||
fileExists, err := utils.IsExists(filePath)
|
||||
|
||||
if fileExists {
|
||||
log.CliErrorf(c, "[user_data.exportUserTransaction] specified file path already exists")
|
||||
log.BootErrorf("[user_data.exportUserTransaction] specified file path already exists")
|
||||
return os.ErrExist
|
||||
}
|
||||
|
||||
log.CliInfof(c, "[user_data.exportUserTransaction] starting exporting user \"%s\" data", username)
|
||||
log.BootInfof("[user_data.exportUserTransaction] starting exporting user \"%s\" data", username)
|
||||
|
||||
content, err := clis.UserData.ExportTransaction(c, username, fileType)
|
||||
content, err := clis.UserData.ExportTransaction(c, username)
|
||||
|
||||
if err != nil {
|
||||
log.CliErrorf(c, "[user_data.exportUserTransaction] error occurs when exporting user data")
|
||||
log.BootErrorf("[user_data.exportUserTransaction] error occurs when exporting user data")
|
||||
return err
|
||||
}
|
||||
|
||||
err = utils.WriteFile(filePath, content)
|
||||
|
||||
if err != nil {
|
||||
log.CliErrorf(c, "[user_data.exportUserTransaction] failed to write to %s", filePath)
|
||||
log.BootErrorf("[user_data.exportUserTransaction] failed to write to %s", filePath)
|
||||
return err
|
||||
}
|
||||
|
||||
log.CliInfof(c, "[user_data.exportUserTransaction] user transactions have been exported to %s", filePath)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func importUserTransaction(c *core.CliContext) error {
|
||||
_, err := initializeSystem(c)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
username := c.String("username")
|
||||
filePath := c.String("file")
|
||||
filetype := c.String("type")
|
||||
|
||||
if filePath == "" {
|
||||
log.CliErrorf(c, "[user_data.importUserTransaction] import file path is not specified")
|
||||
return os.ErrNotExist
|
||||
}
|
||||
|
||||
fileExists, err := utils.IsExists(filePath)
|
||||
|
||||
if !fileExists {
|
||||
log.CliErrorf(c, "[user_data.importUserTransaction] import file does not exist")
|
||||
return os.ErrExist
|
||||
}
|
||||
|
||||
if filetype != "ezbookkeeping_csv" && filetype != "ezbookkeeping_tsv" {
|
||||
log.CliErrorf(c, "[user_data.importUserTransaction] unknown file type \"%s\"", filetype)
|
||||
return errs.ErrImportFileTypeNotSupported
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(filePath)
|
||||
|
||||
if err != nil {
|
||||
log.CliErrorf(c, "[user_data.importUserTransaction] failed to load import file")
|
||||
return err
|
||||
}
|
||||
|
||||
log.CliInfof(c, "[user_data.importUserTransaction] start importing transactions to user \"%s\"", username)
|
||||
|
||||
err = clis.UserData.ImportTransaction(c, username, filetype, data)
|
||||
|
||||
if err != nil {
|
||||
log.CliErrorf(c, "[user_data.importUserTransaction] error occurs when importing user data")
|
||||
return err
|
||||
}
|
||||
|
||||
log.CliInfof(c, "[user_data.importUserTransaction] transactions have been imported to user \"%s\"", username)
|
||||
log.BootInfof("[user_data.exportUserTransaction] user transactions have been exported to %s", filePath)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -899,13 +605,6 @@ func printUserInfo(user *models.User) {
|
||||
fmt.Printf("[ShortDateFormat] %s (%d)\n", user.ShortDateFormat, user.ShortDateFormat)
|
||||
fmt.Printf("[LongTimeFormat] %s (%d)\n", user.LongTimeFormat, user.LongTimeFormat)
|
||||
fmt.Printf("[ShortTimeFormat] %s (%d)\n", user.ShortTimeFormat, user.ShortTimeFormat)
|
||||
fmt.Printf("[DecimalSeparator] %s (%d)\n", user.DecimalSeparator, user.DecimalSeparator)
|
||||
fmt.Printf("[DigitGroupingSymbol] %s (%d)\n", user.DigitGroupingSymbol, user.DigitGroupingSymbol)
|
||||
fmt.Printf("[DigitGrouping] %s (%d)\n", user.DigitGrouping, user.DigitGrouping)
|
||||
fmt.Printf("[CurrencyDisplayType] %s (%d)\n", user.CurrencyDisplayType, user.CurrencyDisplayType)
|
||||
fmt.Printf("[ExpenseAmountColor] %s (%d)\n", user.ExpenseAmountColor, user.ExpenseAmountColor)
|
||||
fmt.Printf("[IncomeAmountColor] %s (%d)\n", user.IncomeAmountColor, user.IncomeAmountColor)
|
||||
fmt.Printf("[FeatureRestriction] %s (%d)\n", user.FeatureRestriction, user.FeatureRestriction)
|
||||
fmt.Printf("[Deleted] %t\n", user.Deleted)
|
||||
fmt.Printf("[EmailVerified] %t\n", user.EmailVerified)
|
||||
fmt.Printf("[CreatedAt] %s (%d)\n", utils.FormatUnixTimeToLongDateTimeInServerTimezone(user.CreatedUnixTime), user.CreatedUnixTime)
|
||||
@@ -926,6 +625,5 @@ func printUserInfo(user *models.User) {
|
||||
func printTokenInfo(token *models.TokenRecord) {
|
||||
fmt.Printf("[CreatedAt] %s (%d)\n", utils.FormatUnixTimeToLongDateTimeInServerTimezone(token.CreatedUnixTime), token.CreatedUnixTime)
|
||||
fmt.Printf("[ExpiredAt] %s (%d)\n", utils.FormatUnixTimeToLongDateTimeInServerTimezone(token.ExpiredUnixTime), token.ExpiredUnixTime)
|
||||
fmt.Printf("[LastSeen] %s (%d)\n", utils.FormatUnixTimeToLongDateTimeInServerTimezone(token.LastSeenUnixTime), token.LastSeenUnixTime)
|
||||
fmt.Printf("[UserAgent] %s\n", token.UserAgent)
|
||||
}
|
||||
|
||||
+8
-10
@@ -7,7 +7,6 @@ import (
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/mail"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/requestid"
|
||||
@@ -22,7 +21,7 @@ var Utilities = &cli.Command{
|
||||
{
|
||||
Name: "parse-default-request-id",
|
||||
Usage: "Parse a request id which is generated by default request generator and show the details",
|
||||
Action: bindAction(parseRequestId),
|
||||
Action: parseRequestId,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "id",
|
||||
@@ -34,7 +33,7 @@ var Utilities = &cli.Command{
|
||||
{
|
||||
Name: "send-test-mail",
|
||||
Usage: "Send an email to specified e-mail address",
|
||||
Action: bindAction(sendTestMail),
|
||||
Action: sendTestMail,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "to",
|
||||
@@ -46,15 +45,15 @@ var Utilities = &cli.Command{
|
||||
},
|
||||
}
|
||||
|
||||
func parseRequestId(c *core.CliContext) error {
|
||||
func parseRequestId(c *cli.Context) error {
|
||||
config, err := initializeSystem(c)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = requestid.InitializeRequestIdGenerator(c, config)
|
||||
defaultGenerator, err := requestid.NewDefaultRequestIdGenerator(c, config)
|
||||
err = requestid.InitializeRequestIdGenerator(config)
|
||||
defaultGenerator, err := requestid.NewDefaultRequestIdGenerator(config)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -67,14 +66,14 @@ func parseRequestId(c *core.CliContext) error {
|
||||
return err
|
||||
}
|
||||
|
||||
newRequestId := defaultGenerator.GenerateRequestId(net.IPv4zero.String(), 0)
|
||||
newRequestId := defaultGenerator.GenerateRequestId(net.IPv4zero.String())
|
||||
newRequestIdInfo, err := defaultGenerator.ParseRequestIdInfo(newRequestId)
|
||||
printRequestIdInfo(requestId, requestIdInfo, newRequestIdInfo)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func sendTestMail(c *core.CliContext) error {
|
||||
func sendTestMail(c *cli.Context) error {
|
||||
config, err := initializeSystem(c)
|
||||
|
||||
if err != nil {
|
||||
@@ -115,6 +114,7 @@ func printRequestIdInfo(requestId string, requestIdInfo *requestid.RequestIdInfo
|
||||
fmt.Printf("[SecondsElapsedToday] %d\n", requestIdInfo.SecondsElapsedToday)
|
||||
}
|
||||
|
||||
fmt.Printf("[RandomNumber] %d\n", requestIdInfo.RandomNumber)
|
||||
fmt.Printf("[RequestSeqId] %d\n", requestIdInfo.RequestSeqId)
|
||||
fmt.Printf("[IsClientIpv6] %t\n", requestIdInfo.IsClientIpv6)
|
||||
|
||||
@@ -125,6 +125,4 @@ func printRequestIdInfo(requestId string, requestIdInfo *requestid.RequestIdInfo
|
||||
binary.BigEndian.PutUint32(ip, requestIdInfo.ClientIp)
|
||||
fmt.Printf("[ClientIpv4] %s\n", ip.String())
|
||||
}
|
||||
|
||||
fmt.Printf("[ClientPort] %d\n", requestIdInfo.ClientPort)
|
||||
}
|
||||
|
||||
+40
-136
@@ -15,7 +15,6 @@ import (
|
||||
|
||||
"github.com/mayswind/ezbookkeeping/pkg/api"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/cron"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/log"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/middlewares"
|
||||
@@ -33,40 +32,33 @@ var WebServer = &cli.Command{
|
||||
{
|
||||
Name: "run",
|
||||
Usage: "Run ezBookkeeping web server",
|
||||
Action: bindAction(startWebServer),
|
||||
Action: startWebServer,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func startWebServer(c *core.CliContext) error {
|
||||
func startWebServer(c *cli.Context) error {
|
||||
config, err := initializeSystem(c)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.BootInfof(c, "[webserver.startWebServer] static root path is %s", config.StaticRootPath)
|
||||
log.BootInfof("[server.startWebServer] static root path is %s", config.StaticRootPath)
|
||||
|
||||
if config.AutoUpdateDatabase {
|
||||
err = updateAllDatabaseTablesStructure(c)
|
||||
err = updateAllDatabaseTablesStructure()
|
||||
|
||||
if err != nil {
|
||||
log.BootErrorf(c, "[webserver.startWebServer] update database table structure failed, because %s", err.Error())
|
||||
log.BootErrorf("[server.startWebServer] update database table structure failed, because %s", err.Error())
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = requestid.InitializeRequestIdGenerator(c, config)
|
||||
err = requestid.InitializeRequestIdGenerator(config)
|
||||
|
||||
if err != nil {
|
||||
log.BootErrorf(c, "[webserver.startWebServer] initializes requestid generator failed, because %s", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
err = cron.InitializeCronJobSchedulerContainer(c, config, true)
|
||||
|
||||
if err != nil {
|
||||
log.BootErrorf(c, "[webserver.startWebServer] initializes cron job scheduler failed, because %s", err.Error())
|
||||
log.BootErrorf("[server.startWebServer] initializes requestid generator failed, because %s", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -76,7 +68,7 @@ func startWebServer(c *core.CliContext) error {
|
||||
uuidServerInfo = fmt.Sprintf(", current uuid server id is %d", config.UuidServerId)
|
||||
}
|
||||
|
||||
log.BootInfof(c, "[webserver.startWebServer] %s%s", serverInfo, uuidServerInfo)
|
||||
log.BootInfof("[server.startWebServer] %s%s", serverInfo, uuidServerInfo)
|
||||
|
||||
if config.Mode == settings.MODE_PRODUCTION {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
@@ -97,14 +89,11 @@ func startWebServer(c *core.CliContext) error {
|
||||
_ = v.RegisterValidation("validEmail", validators.ValidEmail)
|
||||
_ = v.RegisterValidation("validCurrency", validators.ValidCurrency)
|
||||
_ = v.RegisterValidation("validHexRGBColor", validators.ValidHexRGBColor)
|
||||
_ = v.RegisterValidation("validAmountFilter", validators.ValidAmountFilter)
|
||||
}
|
||||
|
||||
router.NoRoute(bindApi(api.Default.ApiNotFound))
|
||||
router.NoMethod(bindApi(api.Default.MethodNotAllowed))
|
||||
|
||||
serverSettingsCacheStore := persistence.NewInMemoryStore(time.Minute)
|
||||
|
||||
router.StaticFile("/", filepath.Join(config.StaticRootPath, "index.html"))
|
||||
router.Static("/js", filepath.Join(config.StaticRootPath, "js"))
|
||||
router.Static("/css", filepath.Join(config.StaticRootPath, "css"))
|
||||
@@ -116,14 +105,12 @@ func startWebServer(c *core.CliContext) error {
|
||||
router.StaticFile("favicon.png", filepath.Join(config.StaticRootPath, "favicon.png"))
|
||||
router.StaticFile("touchicon.png", filepath.Join(config.StaticRootPath, "touchicon.png"))
|
||||
router.StaticFile("manifest.json", filepath.Join(config.StaticRootPath, "manifest.json"))
|
||||
router.StaticFile("sw.js", filepath.Join(config.StaticRootPath, "sw.js"))
|
||||
router.GET("/server_settings.js", bindCachedJs(api.ServerSettings.ServerSettingsJavascriptHandler, serverSettingsCacheStore))
|
||||
|
||||
for i := 0; i < len(workboxFileNames); i++ {
|
||||
router.StaticFile("/"+workboxFileNames[i], filepath.Join(config.StaticRootPath, workboxFileNames[i]))
|
||||
mobileEntryRoute := router.Group("/mobile")
|
||||
mobileEntryRoute.Use(bindMiddleware(middlewares.ServerSettingsCookie(config)))
|
||||
{
|
||||
mobileEntryRoute.StaticFile("/", filepath.Join(config.StaticRootPath, "mobile.html"))
|
||||
}
|
||||
|
||||
router.StaticFile("/mobile", filepath.Join(config.StaticRootPath, "mobile.html"))
|
||||
router.Static("/mobile/js", filepath.Join(config.StaticRootPath, "js"))
|
||||
router.Static("/mobile/css", filepath.Join(config.StaticRootPath, "css"))
|
||||
router.Static("/mobile/img", filepath.Join(config.StaticRootPath, "img"))
|
||||
@@ -133,13 +120,16 @@ func startWebServer(c *core.CliContext) error {
|
||||
router.StaticFile("/mobile/touchicon.png", filepath.Join(config.StaticRootPath, "touchicon.png"))
|
||||
router.StaticFile("/mobile/manifest.json", filepath.Join(config.StaticRootPath, "manifest.json"))
|
||||
router.StaticFile("/mobile/sw.js", filepath.Join(config.StaticRootPath, "sw.js"))
|
||||
router.GET("/mobile/server_settings.js", bindCachedJs(api.ServerSettings.ServerSettingsJavascriptHandler, serverSettingsCacheStore))
|
||||
|
||||
for i := 0; i < len(workboxFileNames); i++ {
|
||||
router.StaticFile("/mobile/"+workboxFileNames[i], filepath.Join(config.StaticRootPath, workboxFileNames[i]))
|
||||
}
|
||||
|
||||
router.StaticFile("/desktop", filepath.Join(config.StaticRootPath, "desktop.html"))
|
||||
desktopEntryRoute := router.Group("/desktop")
|
||||
desktopEntryRoute.Use(bindMiddleware(middlewares.ServerSettingsCookie(config)))
|
||||
{
|
||||
desktopEntryRoute.StaticFile("/", filepath.Join(config.StaticRootPath, "desktop.html"))
|
||||
}
|
||||
router.Static("/desktop/js", filepath.Join(config.StaticRootPath, "js"))
|
||||
router.Static("/desktop/css", filepath.Join(config.StaticRootPath, "css"))
|
||||
router.Static("/desktop/img", filepath.Join(config.StaticRootPath, "img"))
|
||||
@@ -149,30 +139,18 @@ func startWebServer(c *core.CliContext) error {
|
||||
router.StaticFile("/desktop/touchicon.png", filepath.Join(config.StaticRootPath, "touchicon.png"))
|
||||
router.StaticFile("/desktop/manifest.json", filepath.Join(config.StaticRootPath, "manifest.json"))
|
||||
router.StaticFile("/desktop/sw.js", filepath.Join(config.StaticRootPath, "sw.js"))
|
||||
router.GET("/desktop/server_settings.js", bindCachedJs(api.ServerSettings.ServerSettingsJavascriptHandler, serverSettingsCacheStore))
|
||||
|
||||
for i := 0; i < len(workboxFileNames); i++ {
|
||||
router.StaticFile("/desktop/"+workboxFileNames[i], filepath.Join(config.StaticRootPath, workboxFileNames[i]))
|
||||
}
|
||||
|
||||
if config.AvatarProvider == core.USER_AVATAR_PROVIDER_INTERNAL {
|
||||
avatarRoute := router.Group("/avatar")
|
||||
avatarRoute.Use(bindMiddleware(middlewares.JWTAuthorizationByQueryString))
|
||||
{
|
||||
avatarRoute.GET("/:fileName", bindImage(api.Users.UserGetAvatarHandler))
|
||||
}
|
||||
}
|
||||
|
||||
if config.EnableTransactionPictures {
|
||||
pictureRoute := router.Group("/pictures")
|
||||
pictureRoute.Use(bindMiddleware(middlewares.JWTAuthorizationByQueryString))
|
||||
{
|
||||
pictureRoute.GET("/:fileName", bindImage(api.TransactionPictures.TransactionPictureGetHandler))
|
||||
}
|
||||
}
|
||||
|
||||
router.GET("/healthz.json", bindApi(api.Healths.HealthStatusHandler))
|
||||
|
||||
if config.Mode == settings.MODE_DEVELOPMENT {
|
||||
devRoute := router.Group("/dev")
|
||||
devRoute.GET("/cookies", bindMiddleware(middlewares.ServerSettingsCookie(config)))
|
||||
}
|
||||
|
||||
proxyRoute := router.Group("/proxy")
|
||||
proxyRoute.Use(bindMiddleware(middlewares.JWTAuthorizationByQueryString))
|
||||
{
|
||||
@@ -182,17 +160,9 @@ func startWebServer(c *core.CliContext) error {
|
||||
config.MapProvider == settings.OpenTopoMapProvider ||
|
||||
config.MapProvider == settings.OPNVKarteMapProvider ||
|
||||
config.MapProvider == settings.CyclOSMMapProvider ||
|
||||
config.MapProvider == settings.CartoDBMapProvider ||
|
||||
config.MapProvider == settings.TomTomMapProvider ||
|
||||
config.MapProvider == settings.TianDiTuProvider ||
|
||||
config.MapProvider == settings.CustomProvider {
|
||||
config.MapProvider == settings.TomTomMapProvider {
|
||||
proxyRoute.GET("/map/tile/:zoomLevel/:coordinateX/:fileName", bindProxy(api.MapImages.MapTileImageProxyHandler))
|
||||
}
|
||||
|
||||
if config.MapProvider == settings.TianDiTuProvider ||
|
||||
(config.MapProvider == settings.CustomProvider && config.CustomMapTileServerAnnotationLayerUrl != "") {
|
||||
proxyRoute.GET("/map/annotation/:zoomLevel/:coordinateX/:fileName", bindProxy(api.MapImages.MapAnnotationImageProxyHandler))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,7 +178,7 @@ func startWebServer(c *core.CliContext) error {
|
||||
qrCodeRoute.Use(bindMiddleware(middlewares.RequestId(config)))
|
||||
{
|
||||
qrCodeCacheStore := persistence.NewInMemoryStore(time.Minute)
|
||||
qrCodeRoute.GET("/mobile_url.png", bindCachedImage(api.QrCodes.MobileUrlQrCodeHandler, qrCodeCacheStore))
|
||||
qrCodeRoute.GET("/mobile_url.png", bindCachedPngImage(api.QrCodes.MobileUrlQrCodeHandler, qrCodeCacheStore))
|
||||
}
|
||||
|
||||
apiRoute := router.Group("/api")
|
||||
@@ -266,16 +236,11 @@ func startWebServer(c *core.CliContext) error {
|
||||
apiV1Route.GET("/users/profile/get.json", bindApi(api.Users.UserProfileHandler))
|
||||
apiV1Route.POST("/users/profile/update.json", bindApiWithTokenUpdate(api.Users.UserUpdateProfileHandler, config))
|
||||
|
||||
if config.AvatarProvider == core.USER_AVATAR_PROVIDER_INTERNAL {
|
||||
apiV1Route.POST("/users/avatar/update.json", bindApi(api.Users.UserUpdateAvatarHandler))
|
||||
apiV1Route.POST("/users/avatar/remove.json", bindApi(api.Users.UserRemoveAvatarHandler))
|
||||
}
|
||||
|
||||
if config.EnableUserVerifyEmail {
|
||||
apiV1Route.POST("/users/verify_email/resend.json", bindApi(api.Users.UserSendVerifyEmailByLoginedUserHandler))
|
||||
}
|
||||
|
||||
// Two-Factor Authorization
|
||||
// Two Factor Authorization
|
||||
if config.EnableTwoFactor {
|
||||
apiV1Route.GET("/users/2fa/status.json", bindApi(api.TwoFactorAuthorizations.TwoFactorStatusHandler))
|
||||
apiV1Route.POST("/users/2fa/enable/request.json", bindApi(api.TwoFactorAuthorizations.TwoFactorEnableRequestHandler))
|
||||
@@ -289,8 +254,7 @@ func startWebServer(c *core.CliContext) error {
|
||||
apiV1Route.POST("/data/clear.json", bindApi(api.DataManagements.ClearDataHandler))
|
||||
|
||||
if config.EnableDataExport {
|
||||
apiV1Route.GET("/data/export.csv", bindCsv(api.DataManagements.ExportDataToEzbookkeepingCSVHandler))
|
||||
apiV1Route.GET("/data/export.tsv", bindTsv(api.DataManagements.ExportDataToEzbookkeepingTSVHandler))
|
||||
apiV1Route.GET("/data/export.csv", bindCsv(api.DataManagements.ExportDataHandler))
|
||||
}
|
||||
|
||||
// Accounts
|
||||
@@ -307,25 +271,13 @@ func startWebServer(c *core.CliContext) error {
|
||||
apiV1Route.GET("/transactions/list.json", bindApi(api.Transactions.TransactionListHandler))
|
||||
apiV1Route.GET("/transactions/list/by_month.json", bindApi(api.Transactions.TransactionMonthListHandler))
|
||||
apiV1Route.GET("/transactions/statistics.json", bindApi(api.Transactions.TransactionStatisticsHandler))
|
||||
apiV1Route.GET("/transactions/statistics/trends.json", bindApi(api.Transactions.TransactionStatisticsTrendsHandler))
|
||||
apiV1Route.GET("/transactions/amounts.json", bindApi(api.Transactions.TransactionAmountsHandler))
|
||||
apiV1Route.GET("/transactions/amounts/by_month.json", bindApi(api.Transactions.TransactionMonthAmountsHandler))
|
||||
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/delete.json", bindApi(api.Transactions.TransactionDeleteHandler))
|
||||
|
||||
if config.EnableDataImport {
|
||||
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))
|
||||
}
|
||||
|
||||
// Transaction Pictures
|
||||
if config.EnableTransactionPictures {
|
||||
apiV1Route.POST("/transaction/pictures/upload.json", bindApi(api.TransactionPictures.TransactionPictureUploadHandler))
|
||||
apiV1Route.POST("/transaction/pictures/remove_unused.json", bindApi(api.TransactionPictures.TransactionPictureRemoveUnusedHandler))
|
||||
}
|
||||
|
||||
// Transaction Categories
|
||||
apiV1Route.GET("/transaction/categories/list.json", bindApi(api.TransactionCategories.CategoryListHandler))
|
||||
apiV1Route.GET("/transaction/categories/get.json", bindApi(api.TransactionCategories.CategoryGetHandler))
|
||||
@@ -345,15 +297,6 @@ func startWebServer(c *core.CliContext) error {
|
||||
apiV1Route.POST("/transaction/tags/move.json", bindApi(api.TransactionTags.TagMoveHandler))
|
||||
apiV1Route.POST("/transaction/tags/delete.json", bindApi(api.TransactionTags.TagDeleteHandler))
|
||||
|
||||
// Transaction Templates
|
||||
apiV1Route.GET("/transaction/templates/list.json", bindApi(api.TransactionTemplates.TemplateListHandler))
|
||||
apiV1Route.GET("/transaction/templates/get.json", bindApi(api.TransactionTemplates.TemplateGetHandler))
|
||||
apiV1Route.POST("/transaction/templates/add.json", bindApi(api.TransactionTemplates.TemplateCreateHandler))
|
||||
apiV1Route.POST("/transaction/templates/modify.json", bindApi(api.TransactionTemplates.TemplateModifyHandler))
|
||||
apiV1Route.POST("/transaction/templates/hide.json", bindApi(api.TransactionTemplates.TemplateHideHandler))
|
||||
apiV1Route.POST("/transaction/templates/move.json", bindApi(api.TransactionTemplates.TemplateMoveHandler))
|
||||
apiV1Route.POST("/transaction/templates/delete.json", bindApi(api.TransactionTemplates.TemplateDeleteHandler))
|
||||
|
||||
// Exchange Rates
|
||||
apiV1Route.GET("/exchange_rates/latest.json", bindApi(api.ExchangeRates.LatestExchangeRateHandler))
|
||||
}
|
||||
@@ -362,20 +305,20 @@ func startWebServer(c *core.CliContext) error {
|
||||
listenAddr := fmt.Sprintf("%s:%d", config.HttpAddr, config.HttpPort)
|
||||
|
||||
if config.Protocol == settings.SCHEME_SOCKET {
|
||||
log.BootInfof(c, "[webserver.startWebServer] will run at socks:%s", config.UnixSocketPath)
|
||||
log.BootInfof("[server.startWebServer] will run at socks:%s", config.UnixSocketPath)
|
||||
err = router.RunUnix(config.UnixSocketPath)
|
||||
} else if config.Protocol == settings.SCHEME_HTTP {
|
||||
log.BootInfof(c, "[webserver.startWebServer] will run at http://%s", listenAddr)
|
||||
log.BootInfof("[server.startWebServer] will run at http://%s", listenAddr)
|
||||
err = router.Run(listenAddr)
|
||||
} else if config.Protocol == settings.SCHEME_HTTPS {
|
||||
log.BootInfof(c, "[webserver.startWebServer] will run at https://%s", listenAddr)
|
||||
log.BootInfof("[server.startWebServer] will run at https://%s", listenAddr)
|
||||
err = router.RunTLS(listenAddr, config.CertFile, config.CertKeyFile)
|
||||
} else {
|
||||
err = errs.ErrInvalidProtocol
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.BootErrorf(c, "[webserver.startWebServer] cannot start, because %s", err)
|
||||
log.BootErrorf("[server.startWebServer] cannot start, because %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -384,13 +327,13 @@ func startWebServer(c *core.CliContext) error {
|
||||
|
||||
func bindMiddleware(fn core.MiddlewareHandlerFunc) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
fn(core.WrapWebContext(c))
|
||||
fn(core.WrapContext(c))
|
||||
}
|
||||
}
|
||||
|
||||
func bindApi(fn core.ApiHandlerFunc) gin.HandlerFunc {
|
||||
return func(ginCtx *gin.Context) {
|
||||
c := core.WrapWebContext(ginCtx)
|
||||
c := core.WrapContext(ginCtx)
|
||||
result, err := fn(c)
|
||||
|
||||
if err != nil {
|
||||
@@ -403,7 +346,7 @@ func bindApi(fn core.ApiHandlerFunc) gin.HandlerFunc {
|
||||
|
||||
func bindApiWithTokenUpdate(fn core.ApiHandlerFunc, config *settings.Config) gin.HandlerFunc {
|
||||
return func(ginCtx *gin.Context) {
|
||||
c := core.WrapWebContext(ginCtx)
|
||||
c := core.WrapContext(ginCtx)
|
||||
result, err := fn(c)
|
||||
|
||||
if err == nil && config.MapProvider == settings.AmapProvider && config.AmapSecurityVerificationMethod == settings.AmapSecurityVerificationInternalProxyMethod {
|
||||
@@ -418,22 +361,9 @@ func bindApiWithTokenUpdate(fn core.ApiHandlerFunc, config *settings.Config) gin
|
||||
}
|
||||
}
|
||||
|
||||
func bindCachedJs(fn core.DataHandlerFunc, store persistence.CacheStore) gin.HandlerFunc {
|
||||
return cache.CachePage(store, time.Minute, func(ginCtx *gin.Context) {
|
||||
c := core.WrapWebContext(ginCtx)
|
||||
result, _, err := fn(c)
|
||||
|
||||
if err != nil {
|
||||
utils.PrintDataErrorResult(c, "text/javascript", err)
|
||||
} else {
|
||||
utils.PrintDataSuccessResult(c, "text/javascript", "", result)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func bindCsv(fn core.DataHandlerFunc) gin.HandlerFunc {
|
||||
return func(ginCtx *gin.Context) {
|
||||
c := core.WrapWebContext(ginCtx)
|
||||
c := core.WrapContext(ginCtx)
|
||||
result, fileName, err := fn(c)
|
||||
|
||||
if err != nil {
|
||||
@@ -444,48 +374,22 @@ func bindCsv(fn core.DataHandlerFunc) gin.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
func bindTsv(fn core.DataHandlerFunc) gin.HandlerFunc {
|
||||
return func(ginCtx *gin.Context) {
|
||||
c := core.WrapWebContext(ginCtx)
|
||||
result, fileName, err := fn(c)
|
||||
|
||||
if err != nil {
|
||||
utils.PrintDataErrorResult(c, "text/text", err)
|
||||
} else {
|
||||
utils.PrintDataSuccessResult(c, "text/tab-separated-values", fileName, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func bindImage(fn core.ImageHandlerFunc) gin.HandlerFunc {
|
||||
return func(ginCtx *gin.Context) {
|
||||
c := core.WrapWebContext(ginCtx)
|
||||
result, contentType, err := fn(c)
|
||||
|
||||
if err != nil {
|
||||
utils.PrintDataErrorResult(c, "text/text", err)
|
||||
} else {
|
||||
utils.PrintDataSuccessResult(c, contentType, "", result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func bindCachedImage(fn core.ImageHandlerFunc, store persistence.CacheStore) gin.HandlerFunc {
|
||||
func bindCachedPngImage(fn core.DataHandlerFunc, store persistence.CacheStore) gin.HandlerFunc {
|
||||
return cache.CachePage(store, time.Minute, func(ginCtx *gin.Context) {
|
||||
c := core.WrapWebContext(ginCtx)
|
||||
result, contentType, err := fn(c)
|
||||
c := core.WrapContext(ginCtx)
|
||||
result, _, err := fn(c)
|
||||
|
||||
if err != nil {
|
||||
utils.PrintDataErrorResult(c, "text/text", err)
|
||||
} else {
|
||||
utils.PrintDataSuccessResult(c, contentType, "", result)
|
||||
utils.PrintDataSuccessResult(c, "img/png", "", result)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func bindProxy(fn core.ProxyHandlerFunc) gin.HandlerFunc {
|
||||
return func(ginCtx *gin.Context) {
|
||||
c := core.WrapWebContext(ginCtx)
|
||||
c := core.WrapContext(ginCtx)
|
||||
proxy, err := fn(c)
|
||||
|
||||
if err != nil {
|
||||
|
||||
+28
-198
@@ -25,7 +25,7 @@ root_url = %(protocol)s://%(domain)s:%(http_port)s/
|
||||
cert_file =
|
||||
cert_key_file =
|
||||
|
||||
# Unix socket path, for "socket" protocol only
|
||||
# Unix socket path, for "socket" only
|
||||
unix_socket =
|
||||
|
||||
# Static file root path (relative or absolute path)
|
||||
@@ -47,10 +47,10 @@ name = ezbookkeeping
|
||||
user = root
|
||||
passwd =
|
||||
|
||||
# For "postgres" database only, Either "disable", "require" or "verify-full"
|
||||
# For "postgres" only, Either "disable", "require" or "verify-full"
|
||||
ssl_mode = disable
|
||||
|
||||
# For "sqlite3" database only, database file path (relative or absolute path)
|
||||
# For "sqlite3" only, db file path (relative or absolute path)
|
||||
db_path = data/ezbookkeeping.db
|
||||
|
||||
# Max idle connection number (0 - 65535, 0 means no idle connections are retained), default is 2
|
||||
@@ -89,103 +89,35 @@ mode = console file
|
||||
# Either "debug", "info", "warn", "error", default is "info"
|
||||
level = info
|
||||
|
||||
# For "file" mode only, log file path (relative or absolute path)
|
||||
# For "file" only, log file path (relative or absolute path)
|
||||
log_path = log/ezbookkeeping.log
|
||||
|
||||
# For "file" only, request log file path (relative or absolute path). Leave blank if you want to write request log in default log file
|
||||
request_log_path =
|
||||
|
||||
# For "file" only, query log file path (relative or absolute path). Leave blank if you want to write query log in default log file
|
||||
query_log_path =
|
||||
|
||||
# For "file" only, whether rotate the log files
|
||||
log_file_rotate = false
|
||||
|
||||
# For "file" only, maximum size (1 - 4294967295 bytes) of the log file before it gets rotated
|
||||
log_file_max_size = 104857600
|
||||
|
||||
# For "file" only, maximum number of days to retain old log files. Set to 0 to retain all logs
|
||||
log_file_max_days = 7
|
||||
|
||||
[storage]
|
||||
# Object storage type, supports "local_filesystem" and "minio" currently
|
||||
type = local_filesystem
|
||||
|
||||
# For "local_filesystem" storage only, the storage root path (relative or absolute path)
|
||||
local_filesystem_path = storage/
|
||||
|
||||
# For "minio" storage only, the minio connection configuration
|
||||
minio_endpoint = 127.0.0.1:9000
|
||||
minio_location =
|
||||
minio_access_key_id =
|
||||
minio_secret_access_key =
|
||||
|
||||
# For "minio" storage only, whether enable ssl for minio connection
|
||||
minio_use_ssl = false
|
||||
|
||||
# For "minio" storage only, set to true to skip tls verification when connect minio
|
||||
minio_skip_tls_verify = false
|
||||
|
||||
# For "minio" storage only, the minio bucket
|
||||
minio_bucket = ezbookkeeping
|
||||
|
||||
# For "minio" storage only, the root path to store files in minio
|
||||
minio_root_path = /
|
||||
|
||||
[uuid]
|
||||
# Uuid generator type, supports "internal" currently
|
||||
generator_type = internal
|
||||
|
||||
# For "internal" uuid generator only, each server must have unique id (0 - 255)
|
||||
# For "internal" only, each server must have unique id (0 - 255)
|
||||
server_id = 0
|
||||
|
||||
[duplicate_checker]
|
||||
# Duplicate checker type, supports "in_memory" currently
|
||||
checker_type = in_memory
|
||||
|
||||
# For "in_memory" duplicate checker only, cleanup expired data interval seconds (1 - 4294967295), default is 60 (1 minutes)
|
||||
cleanup_interval = 60
|
||||
|
||||
# The minimum interval seconds (0 - 4294967295) between duplicate submissions on the same page (exiting and re-entering the edit page / edit dialog is considered as a new session)
|
||||
# Set to 0 to disable duplicate checker for new data submissions, default is 300 (5 minutes)
|
||||
duplicate_submissions_interval = 300
|
||||
|
||||
[cron]
|
||||
# Set to true to clean up expired tokens periodically
|
||||
enable_remove_expired_tokens = true
|
||||
|
||||
# Set to true to create scheduled transactions based on the user's templates
|
||||
enable_create_scheduled_transaction = true
|
||||
|
||||
[security]
|
||||
# Used for signing, you must change it to keep your user data safe before you first run ezBookkeeping
|
||||
secret_key =
|
||||
|
||||
# Set to true to enable two-factor authorization
|
||||
# Set to true to enable two factor authorization
|
||||
enable_two_factor = true
|
||||
|
||||
# Token expired seconds (60 - 4294967295), default is 2592000 (30 days)
|
||||
# Token expired seconds (0 - 4294967295), default is 2592000 (30 days)
|
||||
token_expired_time = 2592000
|
||||
|
||||
# Token minimum refresh interval (0 - 4294967295), the value should be less than token expired time
|
||||
# Set to 0 to refresh the token every time when refreshing the front end, default is 86400 (1 day)
|
||||
token_min_refresh_interval = 86400
|
||||
|
||||
# Temporary token expired seconds (60 - 4294967295), default is 300 (5 minutes)
|
||||
# Temporary token expired seconds (0 - 4294967295), default is 300 (5 minutes)
|
||||
temporary_token_expired_time = 300
|
||||
|
||||
# Email verify token expired seconds (60 - 4294967295), default is 3600 (60 minutes)
|
||||
# Email verify token expired seconds (0 - 4294967295), default is 3600 (60 minutes)
|
||||
email_verify_token_expired_time = 3600
|
||||
|
||||
# Password reset token expired seconds (60 - 4294967295), default is 3600 (60 minutes)
|
||||
# Password reset token expired seconds (0 - 4294967295), default is 3600 (60 minutes)
|
||||
password_reset_token_expired_time = 3600
|
||||
|
||||
# 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
|
||||
|
||||
# Maximum count of password / token check failures (0 - 4294967295) per user per minute (use the above duplicate checker), default is 5, set to 0 to disable
|
||||
max_failures_per_user_per_minute = 5
|
||||
|
||||
# Add X-Request-Id header to response to track user request or error, default is true
|
||||
request_id_header = true
|
||||
|
||||
@@ -205,79 +137,15 @@ enable_forget_password = true
|
||||
# Set to true to require email must be verified when use forget password
|
||||
forget_password_require_email_verify = false
|
||||
|
||||
# Set to true to allow users to upload transaction pictures
|
||||
enable_transaction_picture = true
|
||||
|
||||
# Maximum allowed transaction picture file size (1 - 4294967295 bytes)
|
||||
max_transaction_picture_size = 10485760
|
||||
|
||||
# Set to true to allow users to create scheduled transaction
|
||||
enable_scheduled_transaction = true
|
||||
|
||||
# User avatar provider, supports the following types:
|
||||
# "internal": Use the internal object storage to store user avatar (refer to "storage" settings), supports updating avatar by user self
|
||||
# "gravatar": https://gravatar.com
|
||||
# Leave blank if you want to disable user avatar
|
||||
avatar_provider = internal
|
||||
|
||||
# For "internal" avatar provider only, maximum allowed user avatar file size (1 - 4294967295 bytes)
|
||||
max_user_avatar_size = 1048576
|
||||
|
||||
# The default feature restrictions after user registration (feature types separated by commas), leave blank for no restrictions
|
||||
# Supports the following feature types:
|
||||
# 1: Update Password
|
||||
# 2: Update Email
|
||||
# 3: Update Profile Basic Info
|
||||
# 4: Update Avatar
|
||||
# 5: Logout Other Session
|
||||
# 6: Enable Two-Factor Authentication
|
||||
# 7: Disable Enable Two-Factor Authentication
|
||||
# 8: Forget Password
|
||||
# 9: Import Transactions
|
||||
# 10: Export Transactions
|
||||
# 11: Clear All Data
|
||||
default_feature_restrictions =
|
||||
avatar_provider =
|
||||
|
||||
[data]
|
||||
# Set to true to allow users to export their data
|
||||
enable_export = true
|
||||
|
||||
# Set to true to allow users to import their data
|
||||
enable_import = true
|
||||
|
||||
# Maximum allowed import file size (1 - 4294967295 bytes)
|
||||
max_import_file_size = 10485760
|
||||
|
||||
[tip]
|
||||
# Set to true to display custom tips in login page
|
||||
enable_tips_in_login_page = false
|
||||
|
||||
# The custom tips displayed in login page, it supports multi-language configuration
|
||||
# Add an underscore and a language tag after the setting key to configure the notification content in that language, the same below
|
||||
# For example, login_page_tips_content_zh_hans means the notification content in Chinese (Simplified)
|
||||
login_page_tips_content =
|
||||
|
||||
[notification]
|
||||
# Set to true to display custom notification in home page every time users register
|
||||
enable_notification_after_register = false
|
||||
|
||||
# The notification content displayed each time users register, it supports multi-language configuration
|
||||
# Add an underscore and a language tag after the setting key to configure the notification content in that language, the same below
|
||||
# For example, after_login_notification_content_zh_hans means the notification content in Chinese (Simplified)
|
||||
after_register_notification_content =
|
||||
|
||||
# Set to true to display custom notification in home page every time users login
|
||||
enable_notification_after_login = false
|
||||
|
||||
# The notification content displayed each time users log in, it supports multi-language configuration
|
||||
after_login_notification_content =
|
||||
|
||||
# Set to true to display custom notification in home page every time users open the app
|
||||
enable_notification_after_open = false
|
||||
|
||||
# The notification content displayed each time users open the app, it supports multi-language configuration
|
||||
after_open_notification_content =
|
||||
|
||||
[map]
|
||||
# Map provider, supports the following types:
|
||||
# "openstreetmap": https://www.openstreetmap.org
|
||||
@@ -285,91 +153,53 @@ after_open_notification_content =
|
||||
# "opentopomap": https://opentopomap.org
|
||||
# "opnvkarte": https://publictransportmap.org
|
||||
# "cyclosm": https://www.cyclosm.org
|
||||
# "cartodb": https://carto.com/basemaps
|
||||
# "tomtom": https://www.tomtom.com
|
||||
# "tianditu": https://www.tianditu.gov.cn
|
||||
# "googlemap": https://map.google.com
|
||||
# "baidumap": https://map.baidu.com
|
||||
# "amap": https://amap.com
|
||||
# "custom": custom map tile server url
|
||||
# Leave blank if you want to disable map
|
||||
map_provider = openstreetmap
|
||||
|
||||
# Set to true to use the ezbookkeeping server to forward map data requests, for "openstreetmap", "openstreetmap_humanitarian", "opentopomap", "opnvkarte", "cyclosm", "cartodb", "tomtom", "tianditu" or "custom"
|
||||
# Set to true to use the ezbookkeeping server to proxy map data requests, for "openstreetmap", "openstreetmap_humanitarian", "opentopomap", "opnvkarte", "cyclosm" or "tomtom"
|
||||
map_data_fetch_proxy = false
|
||||
|
||||
# Proxy for ezbookkeeping server requesting original map data when map_data_fetch_proxy is set to true, supports "system" (use system proxy), "none" (do not use proxy), or proxy URL which starts with "http://", "https://" or "socks5://", default is "system"
|
||||
proxy = system
|
||||
|
||||
# For "tomtom" map provider only, TomTom map API key, please visit https://developer.tomtom.com/how-to-get-tomtom-api-key for more information
|
||||
# For "tomtom" only, TomTom map API key, please visit https://developer.tomtom.com/how-to-get-tomtom-api-key
|
||||
tomtom_map_api_key =
|
||||
|
||||
# For "tianditu" map provider only, TianDiTu map application key, please visit https://console.tianditu.gov.cn/api/register for more information
|
||||
tianditu_map_app_key =
|
||||
|
||||
# For "googlemap" map provider only, Google map JavaScript API key, please visit https://developers.google.com/maps/get-started for more information
|
||||
# For "googlemap" only, Google map JavaScript API key, please visit https://developers.google.com/maps/get-started for more information
|
||||
google_map_api_key =
|
||||
|
||||
# For "baidumap" map provider only, Baidu map JavaScript API application key, please visit https://lbsyun.baidu.com/index.php?title=jspopular3.0/guide/getkey for more information
|
||||
# For "baidumap" only, Baidu map JavaScript API application key, please visit https://lbsyun.baidu.com/index.php?title=jspopular3.0/guide/getkey for more information
|
||||
baidu_map_ak =
|
||||
|
||||
# For "amap" map provider only, Amap JavaScript API application key, please visit https://lbs.amap.com/api/javascript-api/guide/abc/prepare for more information
|
||||
# For "amap" only, Amap JavaScript API application key, please visit https://lbs.amap.com/api/javascript-api/guide/abc/prepare for more information
|
||||
amap_application_key =
|
||||
|
||||
# For "amap" map provider only, Amap JavaScript API security verification method, supports the following methods:
|
||||
# For "amap" only, Amap JavaScript API security verification method, supports the following methods:
|
||||
# "internal_proxy": use the internal proxy to request amap api with amap application secret (default)
|
||||
# "external_proxy": use an external proxy to request amap api (amap application secret should be set by external proxy)
|
||||
# "plain_text": append amap application secret to frontend request directly (insecurity for public network)
|
||||
# Please visit https://developer.amap.com/api/jsapi-v2/guide/abc/load for more information
|
||||
amap_security_verification_method = internal_proxy
|
||||
amap_security_verification_method = plain_text
|
||||
|
||||
# For "amap" map provider only, Amap JavaScript API application secret, this setting must be provided when "amap_security_verification_method" is set to "internal_proxy" or "plain_text", please visit https://lbs.amap.com/api/javascript-api/guide/abc/prepare for more information
|
||||
# For "amap" only, Amap JavaScript API application secret, this setting must be provided when "amap_security_verification_method" is set to "internal_proxy" or "plain_text", please visit https://lbs.amap.com/api/javascript-api/guide/abc/prepare for more information
|
||||
amap_application_secret =
|
||||
|
||||
# For "amap" map provider only, Amap JavaScript API external proxy url, this setting must be provided when "amap_security_verification_method" is set to "external_proxy"
|
||||
# For "amap" only, Amap JavaScript API external proxy url, this setting must be provided when "amap_security_verification_method" is set to "external_proxy"
|
||||
amap_api_external_proxy_url =
|
||||
|
||||
# For "custom" map provider only, the tile layer url of custom map tile server, supports {x}, {y} (coordinates) and {z} (zoom level) placeholders, like "https://tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
custom_map_tile_server_url =
|
||||
|
||||
# For "custom" map provider only, the optional annotation layer url of custom map tile server, supports {x}, {y} (coordinates) and {z} (zoom level) placeholders
|
||||
custom_map_tile_server_annotation_url =
|
||||
|
||||
# For "custom" map provider only, the min zoom level (0 - 255) for custom map tile server, default is 1
|
||||
custom_map_tile_server_min_zoom_level = 1
|
||||
|
||||
# For "custom" map provider only, the max zoom level (0 - 255) for custom map tile server, default is 18
|
||||
custom_map_tile_server_max_zoom_level = 18
|
||||
|
||||
# For "custom" map provider only, the default zoom level (0 - 255) for custom map tile server, default is 14
|
||||
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
|
||||
# "euro_central_bank": https://www.ecb.europa.eu/stats/policy_and_exchange_rates/euro_reference_exchange_rates/html/index.en.html
|
||||
# "national_bank_of_georgia": https://nbg.gov.ge/en/monetary-policy/currency
|
||||
# "central_bank_of_hungary": https://www.mnb.hu/en/arfolyamok
|
||||
# "bank_of_israel": https://www.boi.org.il/en/economic-roles/financial-markets/exchange-rates/
|
||||
# "central_bank_of_myanmar": https://forex.cbm.gov.mm/index.php/fxrate
|
||||
# "norges_bank": https://www.norges-bank.no/en/topics/Statistics/exchange_rates/
|
||||
# "national_bank_of_poland": https://nbp.pl/en/statistic-and-financial-reporting/rates/
|
||||
# "national_bank_of_romania": https://www.bnr.ro/Exchange-rates-1224.aspx
|
||||
# "bank_of_russia": https://www.cbr.ru/eng/currency_base/daily/
|
||||
# "swiss_national_bank": https://www.snb.ch/en/the-snb/mandates-goals/statistics/statistics-pub/current_interest_exchange_rates
|
||||
# "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
|
||||
# "euro_central_bank"
|
||||
# "bank_of_canada"
|
||||
# "reserve_bank_of_australia",
|
||||
# "czech_national_bank"
|
||||
# "national_bank_of_poland"
|
||||
# "monetary_authority_of_singapore"
|
||||
data_source = euro_central_bank
|
||||
|
||||
# Requesting exchange rates data timeout (0 - 4294967295 milliseconds)
|
||||
# Set to 0 to disable timeout for requesting exchange rates data, default is 10000 (10 seconds)
|
||||
# Requesting exchange rates data timeout (0 - 4294967295 milliseconds), default is 10000 (10 seconds)
|
||||
request_timeout = 10000
|
||||
|
||||
# Proxy for ezbookkeeping server requesting exchange rates data, supports "system" (use system proxy), "none" (do not use proxy), or proxy URL which starts with "http://", "https://" or "socks5://", default is "system"
|
||||
proxy = system
|
||||
|
||||
# Set to true to skip tls verification when request exchange rates data
|
||||
# Set to true skip tls verification when request exchange rates data
|
||||
skip_tls_verify = false
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import pluginVue from 'eslint-plugin-vue';
|
||||
import vueTsEslintConfig from '@vue/eslint-config-typescript';
|
||||
|
||||
export default [
|
||||
...pluginVue.configs['flat/essential'],
|
||||
...vueTsEslintConfig(),
|
||||
{
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
projectService: true,
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
ignores: [
|
||||
'dist/**',
|
||||
'**/*.{js,jsx,cjs,mjs}'
|
||||
]
|
||||
},
|
||||
{
|
||||
files: [
|
||||
'**/*.{vue,ts,tsx,mts,js,jsx,cjs,mjs}'
|
||||
],
|
||||
rules: {
|
||||
'vue/valid-v-slot': ['error', {
|
||||
allowModifiers: true
|
||||
}]
|
||||
}
|
||||
},
|
||||
];
|
||||
@@ -36,7 +36,6 @@ func main() {
|
||||
cmd.WebServer,
|
||||
cmd.Database,
|
||||
cmd.UserData,
|
||||
cmd.CronJobs,
|
||||
cmd.SecurityUtils,
|
||||
cmd.Utilities,
|
||||
},
|
||||
|
||||
@@ -1,86 +1,65 @@
|
||||
module github.com/mayswind/ezbookkeeping
|
||||
|
||||
go 1.24
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/boombuler/barcode v1.0.2
|
||||
github.com/extrame/xls v0.0.2-0.20200426124601-4a6cf263071b
|
||||
github.com/gin-contrib/cache v1.3.1
|
||||
github.com/gin-contrib/gzip v1.2.2
|
||||
github.com/gin-gonic/gin v1.10.0
|
||||
github.com/go-co-op/gocron/v2 v2.15.0
|
||||
github.com/go-playground/validator/v10 v10.24.0
|
||||
github.com/go-sql-driver/mysql v1.8.1
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||
github.com/boombuler/barcode v1.0.1
|
||||
github.com/gin-contrib/cache v1.2.0
|
||||
github.com/gin-contrib/gzip v0.0.6
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
github.com/go-playground/validator/v10 v10.15.1
|
||||
github.com/go-sql-driver/mysql v1.7.1
|
||||
github.com/golang-jwt/jwt/v5 v5.0.0
|
||||
github.com/lib/pq v1.10.9
|
||||
github.com/mattn/go-sqlite3 v1.14.24
|
||||
github.com/minio/minio-go/v7 v7.0.85
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||
github.com/mattn/go-sqlite3 v1.14.16
|
||||
github.com/pquerna/otp v1.4.0
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/urfave/cli/v2 v2.27.5
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/urfave/cli/v2 v2.25.7
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8
|
||||
golang.org/x/crypto v0.33.0
|
||||
golang.org/x/net v0.34.0
|
||||
golang.org/x/text v0.22.0
|
||||
golang.org/x/crypto v0.12.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.9
|
||||
xorm.io/xorm v1.3.2
|
||||
)
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||
github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 // indirect
|
||||
github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d // indirect
|
||||
github.com/buger/jsonparser v1.1.1 // indirect
|
||||
github.com/bytedance/sonic v1.12.7 // indirect
|
||||
github.com/bytedance/sonic/loader v0.2.2 // 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.4 // indirect
|
||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
|
||||
github.com/bytedance/sonic v1.9.1 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // 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.8 // indirect
|
||||
github.com/gin-contrib/sse v1.0.0 // indirect
|
||||
github.com/go-ini/ini v1.67.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // 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.4 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // 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.4.0 // indirect
|
||||
github.com/gomodule/redigo v1.8.9 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.17.11 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
|
||||
github.com/leodido/go-urn v1.2.4 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/memcachier/mc/v3 v3.0.3 // 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
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||
github.com/robfig/go-cache v0.0.0-20130306151617-9fc39e0dbf62 // indirect
|
||||
github.com/rs/xid v1.6.0 // indirect
|
||||
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/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
|
||||
golang.org/x/arch v0.13.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
google.golang.org/protobuf v1.36.2 // indirect
|
||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||
golang.org/x/arch v0.3.0 // indirect
|
||||
golang.org/x/net v0.10.0 // indirect
|
||||
golang.org/x/sys v0.11.0 // indirect
|
||||
golang.org/x/text v0.12.0 // indirect
|
||||
google.golang.org/protobuf v1.30.0 // indirect
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
xorm.io/builder v0.3.12 // indirect
|
||||
)
|
||||
|
||||
@@ -1,201 +1,778 @@
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s=
|
||||
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU=
|
||||
gitee.com/travelliu/dm v1.8.11192/go.mod h1:DHTzyhCrM843x9VdKVbZ+GKXGRbKM2sJ4LxihRxShkE=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
|
||||
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
|
||||
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
|
||||
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
|
||||
github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
|
||||
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
||||
github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
||||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
|
||||
github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
|
||||
github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
||||
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
|
||||
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
|
||||
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/boombuler/barcode v1.0.2 h1:79yrbttoZrLGkL/oOI8hBrUKucwOL0oOjUgEguGMcJ4=
|
||||
github.com/boombuler/barcode v1.0.2/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 h1:N7oVaKyGp8bttX0bfZGmcGkjz7DLQXhAn3DNd3T0ous=
|
||||
github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c=
|
||||
github.com/boombuler/barcode v1.0.1 h1:NDBbPmhS+EqABEs5Kg3n/5ZNjy73Pz7SIV+KCeqyXcs=
|
||||
github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d h1:pVrfxiGfwelyab6n21ZBkbkmbevaf+WvMIiR7sr97hw=
|
||||
github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
|
||||
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/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.12.7 h1:CQU8pxOy9HToxhndH0Kx/S1qU/CuS9GnKYrGioDcU1Q=
|
||||
github.com/bytedance/sonic v1.12.7/go.mod h1:tnbal4mxOMju17EGfknm2XyYcpyCnIROYOEYuemj13I=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/bytedance/sonic/loader v0.2.2 h1:jxAJuN9fOot/cyz5Q6dUuMJF5OqQ6+5GfA8FjjQ0R4o=
|
||||
github.com/bytedance/sonic/loader v0.2.2/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
|
||||
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
||||
github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||
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.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
|
||||
github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
|
||||
github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/extrame/goyymmdd v0.0.0-20210114090516-7cc815f00d1a h1:c5k29baTzznteWs+9dxrtqpNxgtQ3V5NbU8d6laLK9Q=
|
||||
github.com/extrame/goyymmdd v0.0.0-20210114090516-7cc815f00d1a/go.mod h1:xbpgo9r3xURoPa/l3sLKLGcnWlkz9UkfFsQ7lW0S6h8=
|
||||
github.com/extrame/ole2 v0.0.0-20160812065207-d69429661ad7 h1:n+nk0bNe2+gVbRI8WRbLFVwwcBQ0rr5p+gzkKb6ol8c=
|
||||
github.com/extrame/ole2 v0.0.0-20160812065207-d69429661ad7/go.mod h1:GPpMrAfHdb8IdQ1/R2uIRBsNfnPnwsYE9YYI5WyY1zw=
|
||||
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/denisenkom/go-mssqldb v0.10.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
|
||||
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
|
||||
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
|
||||
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
|
||||
github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
|
||||
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||
github.com/gin-contrib/cache v1.3.1 h1:EWjkOaLocs5fGt9feQaI7rt1GZbDyatFXEUh2/s3ZI8=
|
||||
github.com/gin-contrib/cache v1.3.1/go.mod h1:6Tme0p3QEF/Ck/KUcq7h/OAqZvUDjHRH1DtQbNgfIX0=
|
||||
github.com/gin-contrib/gzip v1.2.2 h1:iUU/EYCM8ENfkjmZaVrxbjF/ZC267Iqv5S0MMCMEliI=
|
||||
github.com/gin-contrib/gzip v1.2.2/go.mod h1:C1a5cacjlDsS20cKnHlZRCPUu57D3qH6B2pV0rl+Y/s=
|
||||
github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E=
|
||||
github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0=
|
||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/go-co-op/gocron/v2 v2.15.0 h1:Kpvo71VSihE+RImmpA+3ta5CcMhoRzMGw4dJawrj4zo=
|
||||
github.com/go-co-op/gocron/v2 v2.15.0/go.mod h1:ZF70ZwEqz0OO4RBXE1sNxnANy/zvwLcattWEFsqpKig=
|
||||
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/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/gin-contrib/cache v1.2.0 h1:WA+AJR4kmHDTaLLShCHo/IeWVmmGRZ3Lsr3JQ46tFlE=
|
||||
github.com/gin-contrib/cache v1.2.0/go.mod h1:2KkFL8PSnPF3Tt5E2Jpc3HWuBAUKqGZnClCFMm0tXQI=
|
||||
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
|
||||
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
|
||||
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
||||
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
|
||||
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.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
|
||||
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.24.0 h1:KHQckvo8G6hlWnrPX4NJJ+aBfWNAE/HH+qdL2cBpCmg=
|
||||
github.com/go-playground/validator/v10 v10.24.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus=
|
||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM=
|
||||
github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
|
||||
github.com/go-playground/validator/v10 v10.14.1 h1:9c50NUPC30zyuKprjL3vNZ0m5oG+jU0zvx4AqHGnv4k=
|
||||
github.com/go-playground/validator/v10 v10.14.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||
github.com/go-playground/validator/v10 v10.15.1 h1:BSe8uhN+xQ4r5guV/ywQI4gO59C2raYcGffYWZEjZzM=
|
||||
github.com/go-playground/validator/v10 v10.15.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
|
||||
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/goccy/go-json v0.8.1/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||
github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE=
|
||||
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/gomodule/redigo v1.9.2 h1:HrutZBLhSIU8abiSfW8pj8mPhOyMYjZT/wcA4/L9L9s=
|
||||
github.com/gomodule/redigo v1.9.2/go.mod h1:KsU3hiK/Ay8U42qpaJk+kuNa3C+spxapWpM+ywhcgtw=
|
||||
github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws=
|
||||
github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||
github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE=
|
||||
github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
|
||||
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
||||
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
|
||||
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
|
||||
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
|
||||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
|
||||
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
|
||||
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
|
||||
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
|
||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4=
|
||||
github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc=
|
||||
github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
|
||||
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
|
||||
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
|
||||
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
|
||||
github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
|
||||
github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
|
||||
github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
|
||||
github.com/jackc/pgconn v1.4.0/go.mod h1:Y2O3ZDF0q4mMacyWV3AstPJpeHXWGEetiFttmq5lahk=
|
||||
github.com/jackc/pgconn v1.5.0/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI=
|
||||
github.com/jackc/pgconn v1.5.1-0.20200601181101-fa742c524853/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI=
|
||||
github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
|
||||
github.com/jackc/pgconn v1.8.1/go.mod h1:JV6m6b6jhjdmzchES0drzCcYcAHS1OPD5xu3OZ/lE2g=
|
||||
github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=
|
||||
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
|
||||
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
|
||||
github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
|
||||
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
|
||||
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
|
||||
github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
|
||||
github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
|
||||
github.com/jackc/pgproto3/v2 v2.0.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||
github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||
github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||
github.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
|
||||
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
|
||||
github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
|
||||
github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
|
||||
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
|
||||
github.com/jackc/pgtype v1.2.0/go.mod h1:5m2OfMh1wTK7x+Fk952IDmI4nw3nPrvtQdM0ZT4WpC0=
|
||||
github.com/jackc/pgtype v1.3.1-0.20200510190516-8cd94a14c75a/go.mod h1:vaogEUkALtxZMCH411K+tKzNpwzCKU+AnPzBKZ+I+Po=
|
||||
github.com/jackc/pgtype v1.3.1-0.20200606141011-f6355165a91c/go.mod h1:cvk9Bgu/VzJ9/lxTO5R5sf80p0DiucVtN7ZxvaC4GmQ=
|
||||
github.com/jackc/pgtype v1.7.0/go.mod h1:ZnHF+rMePVqDKaOfJVI4Q8IVvAQMryDlDkZnKOI75BE=
|
||||
github.com/jackc/pgtype v1.8.0/go.mod h1:PqDKcEBtllAtk/2p6z6SHdXW5UB+MhE75tUol2OKexE=
|
||||
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
|
||||
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
|
||||
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
|
||||
github.com/jackc/pgx/v4 v4.5.0/go.mod h1:EpAKPLdnTorwmPUUsqrPxy5fphV18j9q3wrfRXgo+kA=
|
||||
github.com/jackc/pgx/v4 v4.6.1-0.20200510190926-94ba730bb1e9/go.mod h1:t3/cdRQl6fOLDxqtlyhe9UWgfIi9R8+8v8GKV5TRA/o=
|
||||
github.com/jackc/pgx/v4 v4.6.1-0.20200606145419-4e5062306904/go.mod h1:ZDaNWkt9sW1JMiNn0kdYBaLelIhw7Pg4qd+Vk6tw7Hg=
|
||||
github.com/jackc/pgx/v4 v4.11.0/go.mod h1:i62xJgdrtVDsnL3U8ekyrQXEwGNTRoG7/8r+CIdYfcc=
|
||||
github.com/jackc/pgx/v4 v4.12.0/go.mod h1:fE547h6VulLPA3kySjfnSG/e2D861g/50JlVUa/ub60=
|
||||
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
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.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
||||
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
|
||||
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
|
||||
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
|
||||
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/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
|
||||
github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
|
||||
github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
|
||||
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.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
|
||||
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
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/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.85 h1:9psTLS/NTvC3MWoyjhjXpwcKoNbkongaCSF3PNpSuXo=
|
||||
github.com/minio/minio-go/v7 v7.0.85/go.mod h1:57YXpvc5l3rjPdhqNrDsvVlY0qPI6UTk1bflAe+9doY=
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
||||
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
|
||||
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
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 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
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/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=
|
||||
github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU=
|
||||
github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k=
|
||||
github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w=
|
||||
github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
|
||||
github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
|
||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||
github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs=
|
||||
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
|
||||
github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
|
||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
|
||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
|
||||
github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis=
|
||||
github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
|
||||
github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
||||
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
||||
github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA=
|
||||
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
|
||||
github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
|
||||
github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
|
||||
github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
|
||||
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
|
||||
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
|
||||
github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=
|
||||
github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
|
||||
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||
github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg=
|
||||
github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/robfig/go-cache v0.0.0-20130306151617-9fc39e0dbf62 h1:pyecQtsPmlkCsMkYhT5iZ+sUXuwee+OvfuJjinEA3ko=
|
||||
github.com/robfig/go-cache v0.0.0-20130306151617-9fc39e0dbf62/go.mod h1:65XQgovT59RWatovFwnwocoUxiI/eENTnOY5GK3STuY=
|
||||
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
|
||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
||||
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
|
||||
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
|
||||
github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
||||
github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
|
||||
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
|
||||
github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
|
||||
github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
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.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.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.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
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/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
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.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w=
|
||||
github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
|
||||
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
|
||||
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
|
||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
||||
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs=
|
||||
github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
|
||||
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 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
|
||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
||||
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
|
||||
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
|
||||
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
|
||||
go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
|
||||
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
|
||||
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
|
||||
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.13.0 h1:KCkqVVV1kGg0X87TFysjCJ8MxtZEIU4Ja/yXGeoECdA=
|
||||
golang.org/x/arch v0.13.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY=
|
||||
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI=
|
||||
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
||||
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
|
||||
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
|
||||
golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
|
||||
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210902050250-f475640dd07b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
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.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
google.golang.org/protobuf v1.36.2 h1:R8FeyR1/eLmkutZOM5CWghmo5itiG9z0ktFlTVLuTmU=
|
||||
google.golang.org/protobuf v1.36.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
|
||||
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
|
||||
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
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/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
|
||||
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
|
||||
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/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
||||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
lukechampine.com/uint128 v1.1.1 h1:pnxCASz787iMf+02ssImqk6OLt+Z5QHMoZyUXR4z6JU=
|
||||
lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
|
||||
modernc.org/cc/v3 v3.33.6/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/cc/v3 v3.33.9/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/cc/v3 v3.33.11/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/cc/v3 v3.34.0/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/cc/v3 v3.35.0/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/cc/v3 v3.35.4/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/cc/v3 v3.35.5/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/cc/v3 v3.35.7/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/cc/v3 v3.35.8/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/cc/v3 v3.35.10/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/cc/v3 v3.35.15/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/cc/v3 v3.35.16/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/cc/v3 v3.35.17/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/cc/v3 v3.35.18 h1:rMZhRcWrba0y3nVmdiQ7kxAgOOSq2m2f2VzjHLgEs6U=
|
||||
modernc.org/cc/v3 v3.35.18/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/ccgo/v3 v3.9.5/go.mod h1:umuo2EP2oDSBnD3ckjaVUXMrmeAw8C8OSICVa0iFf60=
|
||||
modernc.org/ccgo/v3 v3.10.0/go.mod h1:c0yBmkRFi7uW4J7fwx/JiijwOjeAeR2NoSaRVFPmjMw=
|
||||
modernc.org/ccgo/v3 v3.11.0/go.mod h1:dGNposbDp9TOZ/1KBxghxtUp/bzErD0/0QW4hhSaBMI=
|
||||
modernc.org/ccgo/v3 v3.11.1/go.mod h1:lWHxfsn13L3f7hgGsGlU28D9eUOf6y3ZYHKoPaKU0ag=
|
||||
modernc.org/ccgo/v3 v3.11.3/go.mod h1:0oHunRBMBiXOKdaglfMlRPBALQqsfrCKXgw9okQ3GEw=
|
||||
modernc.org/ccgo/v3 v3.12.4/go.mod h1:Bk+m6m2tsooJchP/Yk5ji56cClmN6R1cqc9o/YtbgBQ=
|
||||
modernc.org/ccgo/v3 v3.12.6/go.mod h1:0Ji3ruvpFPpz+yu+1m0wk68pdr/LENABhTrDkMDWH6c=
|
||||
modernc.org/ccgo/v3 v3.12.8/go.mod h1:Hq9keM4ZfjCDuDXxaHptpv9N24JhgBZmUG5q60iLgUo=
|
||||
modernc.org/ccgo/v3 v3.12.11/go.mod h1:0jVcmyDwDKDGWbcrzQ+xwJjbhZruHtouiBEvDfoIsdg=
|
||||
modernc.org/ccgo/v3 v3.12.14/go.mod h1:GhTu1k0YCpJSuWwtRAEHAol5W7g1/RRfS4/9hc9vF5I=
|
||||
modernc.org/ccgo/v3 v3.12.18/go.mod h1:jvg/xVdWWmZACSgOiAhpWpwHWylbJaSzayCqNOJKIhs=
|
||||
modernc.org/ccgo/v3 v3.12.20/go.mod h1:aKEdssiu7gVgSy/jjMastnv/q6wWGRbszbheXgWRHc8=
|
||||
modernc.org/ccgo/v3 v3.12.21/go.mod h1:ydgg2tEprnyMn159ZO/N4pLBqpL7NOkJ88GT5zNU2dE=
|
||||
modernc.org/ccgo/v3 v3.12.22/go.mod h1:nyDVFMmMWhMsgQw+5JH6B6o4MnZ+UQNw1pp52XYFPRk=
|
||||
modernc.org/ccgo/v3 v3.12.25/go.mod h1:UaLyWI26TwyIT4+ZFNjkyTbsPsY3plAEB6E7L/vZV3w=
|
||||
modernc.org/ccgo/v3 v3.12.29/go.mod h1:FXVjG7YLf9FetsS2OOYcwNhcdOLGt8S9bQ48+OP75cE=
|
||||
modernc.org/ccgo/v3 v3.12.36/go.mod h1:uP3/Fiezp/Ga8onfvMLpREq+KUjUmYMxXPO8tETHtA8=
|
||||
modernc.org/ccgo/v3 v3.12.38/go.mod h1:93O0G7baRST1vNj4wnZ49b1kLxt0xCW5Hsa2qRaZPqc=
|
||||
modernc.org/ccgo/v3 v3.12.43/go.mod h1:k+DqGXd3o7W+inNujK15S5ZYuPoWYLpF5PYougCmthU=
|
||||
modernc.org/ccgo/v3 v3.12.46/go.mod h1:UZe6EvMSqOxaJ4sznY7b23/k13R8XNlyWsO5bAmSgOE=
|
||||
modernc.org/ccgo/v3 v3.12.47/go.mod h1:m8d6p0zNps187fhBwzY/ii6gxfjob1VxWb919Nk1HUk=
|
||||
modernc.org/ccgo/v3 v3.12.50/go.mod h1:bu9YIwtg+HXQxBhsRDE+cJjQRuINuT9PUK4orOco/JI=
|
||||
modernc.org/ccgo/v3 v3.12.51/go.mod h1:gaIIlx4YpmGO2bLye04/yeblmvWEmE4BBBls4aJXFiE=
|
||||
modernc.org/ccgo/v3 v3.12.53/go.mod h1:8xWGGTFkdFEWBEsUmi+DBjwu/WLy3SSOrqEmKUjMeEg=
|
||||
modernc.org/ccgo/v3 v3.12.54/go.mod h1:yANKFTm9llTFVX1FqNKHE0aMcQb1fuPJx6p8AcUx+74=
|
||||
modernc.org/ccgo/v3 v3.12.55/go.mod h1:rsXiIyJi9psOwiBkplOaHye5L4MOOaCjHg1Fxkj7IeU=
|
||||
modernc.org/ccgo/v3 v3.12.56/go.mod h1:ljeFks3faDseCkr60JMpeDb2GSO3TKAmrzm7q9YOcMU=
|
||||
modernc.org/ccgo/v3 v3.12.57/go.mod h1:hNSF4DNVgBl8wYHpMvPqQWDQx8luqxDnNGCMM4NFNMc=
|
||||
modernc.org/ccgo/v3 v3.12.60/go.mod h1:k/Nn0zdO1xHVWjPYVshDeWKqbRWIfif5dtsIOCUVMqM=
|
||||
modernc.org/ccgo/v3 v3.12.65/go.mod h1:D6hQtKxPNZiY6wDBtehSGKFKmyXn53F8nGTpH+POmS4=
|
||||
modernc.org/ccgo/v3 v3.12.66/go.mod h1:jUuxlCFZTUZLMV08s7B1ekHX5+LIAurKTTaugUr/EhQ=
|
||||
modernc.org/ccgo/v3 v3.12.67/go.mod h1:Bll3KwKvGROizP2Xj17GEGOTrlvB1XcVaBrC90ORO84=
|
||||
modernc.org/ccgo/v3 v3.12.73/go.mod h1:hngkB+nUUqzOf3iqsM48Gf1FZhY599qzVg1iX+BT3cQ=
|
||||
modernc.org/ccgo/v3 v3.12.81/go.mod h1:p2A1duHoBBg1mFtYvnhAnQyI6vL0uw5PGYLSIgF6rYY=
|
||||
modernc.org/ccgo/v3 v3.12.82 h1:wudcnJyjLj1aQQCXF3IM9Gz2X6UNjw+afIghzdtn0v8=
|
||||
modernc.org/ccgo/v3 v3.12.82/go.mod h1:ApbflUfa5BKadjHynCficldU1ghjen84tuM5jRynB7w=
|
||||
modernc.org/ccorpus v1.11.1/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
|
||||
modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
|
||||
modernc.org/libc v1.9.8/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w=
|
||||
modernc.org/libc v1.9.11/go.mod h1:NyF3tsA5ArIjJ83XB0JlqhjTabTCHm9aX4XMPHyQn0Q=
|
||||
modernc.org/libc v1.11.0/go.mod h1:2lOfPmj7cz+g1MrPNmX65QCzVxgNq2C5o0jdLY2gAYg=
|
||||
modernc.org/libc v1.11.2/go.mod h1:ioIyrl3ETkugDO3SGZ+6EOKvlP3zSOycUETe4XM4n8M=
|
||||
modernc.org/libc v1.11.5/go.mod h1:k3HDCP95A6U111Q5TmG3nAyUcp3kR5YFZTeDS9v8vSU=
|
||||
modernc.org/libc v1.11.6/go.mod h1:ddqmzR6p5i4jIGK1d/EiSw97LBcE3dK24QEwCFvgNgE=
|
||||
modernc.org/libc v1.11.11/go.mod h1:lXEp9QOOk4qAYOtL3BmMve99S5Owz7Qyowzvg6LiZso=
|
||||
modernc.org/libc v1.11.13/go.mod h1:ZYawJWlXIzXy2Pzghaf7YfM8OKacP3eZQI81PDLFdY8=
|
||||
modernc.org/libc v1.11.16/go.mod h1:+DJquzYi+DMRUtWI1YNxrlQO6TcA5+dRRiq8HWBWRC8=
|
||||
modernc.org/libc v1.11.19/go.mod h1:e0dgEame6mkydy19KKaVPBeEnyJB4LGNb0bBH1EtQ3I=
|
||||
modernc.org/libc v1.11.24/go.mod h1:FOSzE0UwookyT1TtCJrRkvsOrX2k38HoInhw+cSCUGk=
|
||||
modernc.org/libc v1.11.26/go.mod h1:SFjnYi9OSd2W7f4ct622o/PAYqk7KHv6GS8NZULIjKY=
|
||||
modernc.org/libc v1.11.27/go.mod h1:zmWm6kcFXt/jpzeCgfvUNswM0qke8qVwxqZrnddlDiE=
|
||||
modernc.org/libc v1.11.28/go.mod h1:Ii4V0fTFcbq3qrv3CNn+OGHAvzqMBvC7dBNyC4vHZlg=
|
||||
modernc.org/libc v1.11.31/go.mod h1:FpBncUkEAtopRNJj8aRo29qUiyx5AvAlAxzlx9GNaVM=
|
||||
modernc.org/libc v1.11.34/go.mod h1:+Tzc4hnb1iaX/SKAutJmfzES6awxfU1BPvrrJO0pYLg=
|
||||
modernc.org/libc v1.11.37/go.mod h1:dCQebOwoO1046yTrfUE5nX1f3YpGZQKNcITUYWlrAWo=
|
||||
modernc.org/libc v1.11.39/go.mod h1:mV8lJMo2S5A31uD0k1cMu7vrJbSA3J3waQJxpV4iqx8=
|
||||
modernc.org/libc v1.11.42/go.mod h1:yzrLDU+sSjLE+D4bIhS7q1L5UwXDOw99PLSX0BlZvSQ=
|
||||
modernc.org/libc v1.11.44/go.mod h1:KFq33jsma7F5WXiYelU8quMJasCCTnHK0mkri4yPHgA=
|
||||
modernc.org/libc v1.11.45/go.mod h1:Y192orvfVQQYFzCNsn+Xt0Hxt4DiO4USpLNXBlXg/tM=
|
||||
modernc.org/libc v1.11.47/go.mod h1:tPkE4PzCTW27E6AIKIR5IwHAQKCAtudEIeAV1/SiyBg=
|
||||
modernc.org/libc v1.11.49/go.mod h1:9JrJuK5WTtoTWIFQ7QjX2Mb/bagYdZdscI3xrvHbXjE=
|
||||
modernc.org/libc v1.11.51/go.mod h1:R9I8u9TS+meaWLdbfQhq2kFknTW0O3aw3kEMqDDxMaM=
|
||||
modernc.org/libc v1.11.53/go.mod h1:5ip5vWYPAoMulkQ5XlSJTy12Sz5U6blOQiYasilVPsU=
|
||||
modernc.org/libc v1.11.54/go.mod h1:S/FVnskbzVUrjfBqlGFIPA5m7UwB3n9fojHhCNfSsnw=
|
||||
modernc.org/libc v1.11.55/go.mod h1:j2A5YBRm6HjNkoSs/fzZrSxCuwWqcMYTDPLNx0URn3M=
|
||||
modernc.org/libc v1.11.56/go.mod h1:pakHkg5JdMLt2OgRadpPOTnyRXm/uzu+Yyg/LSLdi18=
|
||||
modernc.org/libc v1.11.58/go.mod h1:ns94Rxv0OWyoQrDqMFfWwka2BcaF6/61CqJRK9LP7S8=
|
||||
modernc.org/libc v1.11.70/go.mod h1:DUOmMYe+IvKi9n6Mycyx3DbjfzSKrdr/0Vgt3j7P5gw=
|
||||
modernc.org/libc v1.11.71/go.mod h1:DUOmMYe+IvKi9n6Mycyx3DbjfzSKrdr/0Vgt3j7P5gw=
|
||||
modernc.org/libc v1.11.75/go.mod h1:dGRVugT6edz361wmD9gk6ax1AbDSe0x5vji0dGJiPT0=
|
||||
modernc.org/libc v1.11.82/go.mod h1:NF+Ek1BOl2jeC7lw3a7Jj5PWyHPwWD4aq3wVKxqV1fI=
|
||||
modernc.org/libc v1.11.86/go.mod h1:ePuYgoQLmvxdNT06RpGnaDKJmDNEkV7ZPKI2jnsvZoE=
|
||||
modernc.org/libc v1.11.87 h1:PzIzOqtlzMDDcCzJ5cUP6h/Ku6Fa9iyflP2ccTY64aE=
|
||||
modernc.org/libc v1.11.87/go.mod h1:Qvd5iXTeLhI5PS0XSyqMY99282y+3euapQFxM7jYnpY=
|
||||
modernc.org/mathutil v1.1.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/mathutil v1.4.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/mathutil v1.4.1 h1:ij3fYGe8zBF4Vu+g0oT7mB06r8sqGWKuJu1yXeR4by8=
|
||||
modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/memory v1.0.4/go.mod h1:nV2OApxradM3/OVbs2/0OsP6nPfakXpi50C7dcoHXlc=
|
||||
modernc.org/memory v1.0.5 h1:XRch8trV7GgvTec2i7jc33YlUI0RKVDBvZ5eZ5m8y14=
|
||||
modernc.org/memory v1.0.5/go.mod h1:B7OYswTRnfGg+4tDH1t1OeUNnsy2viGTdME4tzd+IjM=
|
||||
modernc.org/opt v0.1.1 h1:/0RX92k9vwVeDXj+Xn23DKp2VJubL7k8qNffND6qn3A=
|
||||
modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||
modernc.org/sqlite v1.14.2 h1:ohsW2+e+Qe2To1W6GNezzKGwjXwSax6R+CrhRxVaFbE=
|
||||
modernc.org/sqlite v1.14.2/go.mod h1:yqfn85u8wVOE6ub5UT8VI9JjhrwBUUCNyTACN0h6Sx8=
|
||||
modernc.org/strutil v1.1.1 h1:xv+J1BXY3Opl2ALrBwyfEikFAj8pmqcpnfmuwUwcozs=
|
||||
modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw=
|
||||
modernc.org/tcl v1.8.13/go.mod h1:V+q/Ef0IJaNUSECieLU4o+8IScapxnMyFV6i/7uQlAY=
|
||||
modernc.org/token v1.0.0 h1:a0jaWiNMDhDUtqOj09wvjWWAqd3q7WpBulmL9H2egsk=
|
||||
modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
modernc.org/z v1.2.19/go.mod h1:+ZpP0pc4zz97eukOzW3xagV/lS82IpPN9NGG5pNF9vY=
|
||||
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.9 h1:TUovzS0ko+IQ1XnNLfs5dqK1cJl1H5uHpWbWqAQ04nU=
|
||||
xorm.io/xorm v1.3.9/go.mod h1:LsCCffeeYp63ssk0pKumP6l96WZcHix7ChpurcLNuMw=
|
||||
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
|
||||
sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=
|
||||
xorm.io/builder v0.3.11-0.20220531020008-1bd24a7dc978/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
|
||||
xorm.io/builder v0.3.12 h1:ASZYX7fQmy+o8UJdhlLHSW57JDOkM8DNhcAF5d0LiJM=
|
||||
xorm.io/builder v0.3.12/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
|
||||
xorm.io/xorm v1.3.2 h1:uTRRKF2jYzbZ5nsofXVUx6ncMaek+SHjWYtCXyZo1oM=
|
||||
xorm.io/xorm v1.3.2/go.mod h1:9NbjqdnjX6eyjRRhh01GHm64r6N9shTb/8Ak3YRt8Nw=
|
||||
|
||||
Generated
+3761
-5634
File diff suppressed because it is too large
Load Diff
+29
-37
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ezbookkeeping",
|
||||
"version": "0.8.0",
|
||||
"version": "0.4.0",
|
||||
"private": true,
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -15,58 +15,50 @@
|
||||
"serve": "cross-env NODE_ENV=development vite",
|
||||
"build": "cross-env NODE_ENV=production vite build",
|
||||
"serve:dist": "vite preview",
|
||||
"lint": "tsc --noEmit && eslint . --fix"
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mdi/js": "^7.4.47",
|
||||
"@vuepic/vue-datepicker": "^11.0.1",
|
||||
"axios": "^1.7.9",
|
||||
"@mdi/js": "^7.2.96",
|
||||
"@vuepic/vue-datepicker": "^5.4.0",
|
||||
"axios": "^1.4.0",
|
||||
"cbor-js": "^0.1.0",
|
||||
"clipboard": "^2.0.11",
|
||||
"crypto-js": "^4.2.0",
|
||||
"crypto-js": "^4.1.1",
|
||||
"dom7": "^4.0.6",
|
||||
"echarts": "^5.6.0",
|
||||
"framework7": "^8.3.4",
|
||||
"echarts": "^5.4.3",
|
||||
"framework7": "^8.3.0",
|
||||
"framework7-icons": "^5.0.5",
|
||||
"framework7-vue": "^8.3.4",
|
||||
"framework7-vue": "^8.3.0",
|
||||
"js-cookie": "^3.0.5",
|
||||
"leaflet": "^1.9.4",
|
||||
"line-awesome": "^1.3.0",
|
||||
"moment": "^2.30.1",
|
||||
"moment-timezone": "^0.5.47",
|
||||
"pinia": "^2.3.1",
|
||||
"moment": "^2.29.4",
|
||||
"moment-timezone": "^0.5.43",
|
||||
"pinia": "^2.1.6",
|
||||
"register-service-worker": "^1.7.2",
|
||||
"skeleton-elements": "^4.0.1",
|
||||
"swiper": "^10.2.0",
|
||||
"ua-parser-js": "^1.0.39",
|
||||
"vue": "^3.5.13",
|
||||
"vue-echarts": "^7.0.3",
|
||||
"vue-i18n": "^11.1.1",
|
||||
"vue-router": "^4.5.0",
|
||||
"vue3-perfect-scrollbar": "^2.0.0",
|
||||
"ua-parser-js": "^1.0.35",
|
||||
"vue": "^3.3.4",
|
||||
"vue-echarts": "^6.6.1",
|
||||
"vue-i18n": "^9.2.2",
|
||||
"vue-router": "^4.2.4",
|
||||
"vue3-perfect-scrollbar": "^1.6.1",
|
||||
"vuedraggable": "^4.1.0",
|
||||
"vuetify": "^3.7.11"
|
||||
"vuetify": "^3.3.16"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/node22": "^22.0.0",
|
||||
"@types/cbor-js": "^0.1.1",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/git-rev-sync": "^2.0.2",
|
||||
"@types/node": "^22.12.0",
|
||||
"@types/ua-parser-js": "^0.7.39",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"@vue/eslint-config-typescript": "^14.3.0",
|
||||
"@vue/tsconfig": "^0.7.0",
|
||||
"@vitejs/plugin-vue": "^4.3.1",
|
||||
"@vue/compiler-sfc": "^3.3.4",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^9.20.0",
|
||||
"eslint-plugin-vue": "^9.32.0",
|
||||
"eslint": "^8.47.0",
|
||||
"eslint-plugin-vue": "^9.17.0",
|
||||
"git-rev-sync": "^3.0.2",
|
||||
"postcss-preset-env": "^10.1.3",
|
||||
"sass": "^1.84.0",
|
||||
"typescript": "^5.7.3",
|
||||
"vite": "^6.1.0",
|
||||
"vite-plugin-pwa": "^0.21.1",
|
||||
"vite-plugin-vuetify": "^2.1.0",
|
||||
"vue-tsc": "^2.2.0"
|
||||
"postcss-preset-env": "^9.1.1",
|
||||
"sass": "^1.66.1",
|
||||
"vite": "^4.4.9",
|
||||
"vite-plugin-pwa": "^0.16.4",
|
||||
"vite-plugin-vuetify": "^1.0.2"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
|
||||
+50
-175
@@ -4,46 +4,32 @@ import (
|
||||
"sort"
|
||||
|
||||
"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"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/services"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/validators"
|
||||
)
|
||||
|
||||
// AccountsApi represents account api
|
||||
type AccountsApi struct {
|
||||
ApiUsingConfig
|
||||
ApiUsingDuplicateChecker
|
||||
accounts *services.AccountService
|
||||
}
|
||||
|
||||
// Initialize an account api singleton instance
|
||||
var (
|
||||
Accounts = &AccountsApi{
|
||||
ApiUsingConfig: ApiUsingConfig{
|
||||
container: settings.Container,
|
||||
},
|
||||
ApiUsingDuplicateChecker: ApiUsingDuplicateChecker{
|
||||
ApiUsingConfig: ApiUsingConfig{
|
||||
container: settings.Container,
|
||||
},
|
||||
container: duplicatechecker.Container,
|
||||
},
|
||||
accounts: services.Accounts,
|
||||
}
|
||||
)
|
||||
|
||||
// AccountListHandler returns accounts list of current user
|
||||
func (a *AccountsApi) AccountListHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
func (a *AccountsApi) AccountListHandler(c *core.Context) (any, *errs.Error) {
|
||||
var accountListReq models.AccountListRequest
|
||||
err := c.ShouldBindQuery(&accountListReq)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[accounts.AccountListHandler] parse request failed, because %s", err.Error())
|
||||
log.WarnfWithRequestId(c, "[accounts.AccountListHandler] parse request failed, because %s", err.Error())
|
||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||
}
|
||||
|
||||
@@ -51,7 +37,7 @@ func (a *AccountsApi) AccountListHandler(c *core.WebContext) (any, *errs.Error)
|
||||
accounts, err := a.accounts.GetAllAccountsByUid(c, uid)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[accounts.AccountListHandler] failed to get all accounts for user \"uid:%d\", because %s", uid, err.Error())
|
||||
log.ErrorfWithRequestId(c, "[accounts.AccountListHandler] failed to get all accounts for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
@@ -98,12 +84,12 @@ func (a *AccountsApi) AccountListHandler(c *core.WebContext) (any, *errs.Error)
|
||||
}
|
||||
|
||||
// AccountGetHandler returns one specific account of current user
|
||||
func (a *AccountsApi) AccountGetHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
func (a *AccountsApi) AccountGetHandler(c *core.Context) (any, *errs.Error) {
|
||||
var accountGetReq models.AccountGetRequest
|
||||
err := c.ShouldBindQuery(&accountGetReq)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[accounts.AccountGetHandler] parse request failed, because %s", err.Error())
|
||||
log.WarnfWithRequestId(c, "[accounts.AccountGetHandler] parse request failed, because %s", err.Error())
|
||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||
}
|
||||
|
||||
@@ -111,7 +97,7 @@ func (a *AccountsApi) AccountGetHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
accountAndSubAccounts, err := a.accounts.GetAccountAndSubAccountsByAccountId(c, uid, accountGetReq.Id)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[accounts.AccountGetHandler] failed to get account \"id:%d\" for user \"uid:%d\", because %s", accountGetReq.Id, uid, err.Error())
|
||||
log.ErrorfWithRequestId(c, "[accounts.AccountGetHandler] failed to get account \"id:%d\" for user \"uid:%d\", because %s", accountGetReq.Id, uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
@@ -141,60 +127,45 @@ func (a *AccountsApi) AccountGetHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
}
|
||||
|
||||
// AccountCreateHandler saves a new account by request parameters for current user
|
||||
func (a *AccountsApi) AccountCreateHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
func (a *AccountsApi) AccountCreateHandler(c *core.Context) (any, *errs.Error) {
|
||||
var accountCreateReq models.AccountCreateRequest
|
||||
err := c.ShouldBindJSON(&accountCreateReq)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[accounts.AccountCreateHandler] parse request failed, because %s", err.Error())
|
||||
log.WarnfWithRequestId(c, "[accounts.AccountCreateHandler] parse request failed, because %s", err.Error())
|
||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||
}
|
||||
|
||||
utcOffset, err := c.GetClientTimezoneOffset()
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[accounts.AccountCreateHandler] cannot get client timezone offset, because %s", err.Error())
|
||||
log.WarnfWithRequestId(c, "[accounts.AccountCreateHandler] cannot get client timezone offset, because %s", err.Error())
|
||||
return nil, errs.ErrClientTimezoneOffsetInvalid
|
||||
}
|
||||
|
||||
if accountCreateReq.Category < models.ACCOUNT_CATEGORY_CASH || accountCreateReq.Category > models.ACCOUNT_CATEGORY_CERTIFICATE_OF_DEPOSIT {
|
||||
log.Warnf(c, "[accounts.AccountCreateHandler] account category invalid, category is %d", accountCreateReq.Category)
|
||||
return nil, errs.ErrAccountCategoryInvalid
|
||||
}
|
||||
|
||||
if accountCreateReq.Category != models.ACCOUNT_CATEGORY_CREDIT_CARD && accountCreateReq.CreditCardStatementDate != 0 {
|
||||
log.Warnf(c, "[accounts.AccountCreateHandler] cannot set statement date with category \"%d\"", accountCreateReq.Category)
|
||||
return nil, errs.ErrCannotSetStatementDateForNonCreditCard
|
||||
}
|
||||
|
||||
if accountCreateReq.Type == models.ACCOUNT_TYPE_SINGLE_ACCOUNT {
|
||||
if len(accountCreateReq.SubAccounts) > 0 {
|
||||
log.Warnf(c, "[accounts.AccountCreateHandler] account cannot have any sub-accounts")
|
||||
log.WarnfWithRequestId(c, "[accounts.AccountCreateHandler] account cannot have any sub accounts")
|
||||
return nil, errs.ErrAccountCannotHaveSubAccounts
|
||||
}
|
||||
|
||||
if accountCreateReq.Currency == validators.ParentAccountCurrencyPlaceholder {
|
||||
log.Warnf(c, "[accounts.AccountCreateHandler] account cannot set currency placeholder")
|
||||
log.WarnfWithRequestId(c, "[accounts.AccountCreateHandler] account cannot set currency placeholder")
|
||||
return nil, errs.ErrAccountCurrencyInvalid
|
||||
}
|
||||
|
||||
if accountCreateReq.Balance != 0 && accountCreateReq.BalanceTime <= 0 {
|
||||
log.Warnf(c, "[accounts.AccountCreateHandler] account balance time is not set")
|
||||
return nil, errs.ErrAccountBalanceTimeNotSet
|
||||
}
|
||||
} else if accountCreateReq.Type == models.ACCOUNT_TYPE_MULTI_SUB_ACCOUNTS {
|
||||
if len(accountCreateReq.SubAccounts) < 1 {
|
||||
log.Warnf(c, "[accounts.AccountCreateHandler] account does not have any sub-accounts")
|
||||
log.WarnfWithRequestId(c, "[accounts.AccountCreateHandler] account does not have any sub accounts")
|
||||
return nil, errs.ErrAccountHaveNoSubAccount
|
||||
}
|
||||
|
||||
if accountCreateReq.Currency != validators.ParentAccountCurrencyPlaceholder {
|
||||
log.Warnf(c, "[accounts.AccountCreateHandler] parent account cannot set currency")
|
||||
log.WarnfWithRequestId(c, "[accounts.AccountCreateHandler] parent account cannot set currency")
|
||||
return nil, errs.ErrParentAccountCannotSetCurrency
|
||||
}
|
||||
|
||||
if accountCreateReq.Balance != 0 {
|
||||
log.Warnf(c, "[accounts.AccountCreateHandler] parent account cannot set balance")
|
||||
log.WarnfWithRequestId(c, "[accounts.AccountCreateHandler] parent account cannot set balance")
|
||||
return nil, errs.ErrParentAccountCannotSetBalance
|
||||
}
|
||||
|
||||
@@ -202,32 +173,22 @@ func (a *AccountsApi) AccountCreateHandler(c *core.WebContext) (any, *errs.Error
|
||||
subAccount := accountCreateReq.SubAccounts[i]
|
||||
|
||||
if subAccount.Category != accountCreateReq.Category {
|
||||
log.Warnf(c, "[accounts.AccountCreateHandler] category of sub-account#%d not equals to parent", i)
|
||||
log.WarnfWithRequestId(c, "[accounts.AccountCreateHandler] category of sub account not equals to parent")
|
||||
return nil, errs.ErrSubAccountCategoryNotEqualsToParent
|
||||
}
|
||||
|
||||
if subAccount.Type != models.ACCOUNT_TYPE_SINGLE_ACCOUNT {
|
||||
log.Warnf(c, "[accounts.AccountCreateHandler] sub-account#%d type invalid", i)
|
||||
log.WarnfWithRequestId(c, "[accounts.AccountCreateHandler] sub account type invalid")
|
||||
return nil, errs.ErrSubAccountTypeInvalid
|
||||
}
|
||||
|
||||
if subAccount.Currency == validators.ParentAccountCurrencyPlaceholder {
|
||||
log.Warnf(c, "[accounts.AccountCreateHandler] sub-account#%d cannot set currency placeholder", i)
|
||||
log.WarnfWithRequestId(c, "[accounts.AccountCreateHandler] sub account cannot set currency placeholder")
|
||||
return nil, errs.ErrAccountCurrencyInvalid
|
||||
}
|
||||
|
||||
if subAccount.Balance != 0 && subAccount.BalanceTime <= 0 {
|
||||
log.Warnf(c, "[accounts.AccountCreateHandler] sub-account#%d balance time is not set", i)
|
||||
return nil, errs.ErrAccountBalanceTimeNotSet
|
||||
}
|
||||
|
||||
if subAccount.CreditCardStatementDate != 0 {
|
||||
log.Warnf(c, "[accounts.AccountCreateHandler] sub-account#%d cannot set statement date", i)
|
||||
return nil, errs.ErrCannotSetStatementDateForSubAccount
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.Warnf(c, "[accounts.AccountCreateHandler] account type invalid, type is %d", accountCreateReq.Type)
|
||||
log.WarnfWithRequestId(c, "[accounts.AccountCreateHandler] account type invalid, type is %d", accountCreateReq.Type)
|
||||
return nil, errs.ErrAccountTypeInvalid
|
||||
}
|
||||
|
||||
@@ -235,59 +196,22 @@ func (a *AccountsApi) AccountCreateHandler(c *core.WebContext) (any, *errs.Error
|
||||
maxOrderId, err := a.accounts.GetMaxDisplayOrder(c, uid, accountCreateReq.Category)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[accounts.AccountCreateHandler] failed to get max display order for user \"uid:%d\", because %s", uid, err.Error())
|
||||
log.ErrorfWithRequestId(c, "[accounts.AccountCreateHandler] failed to get max display order for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
mainAccount := a.createNewAccountModel(uid, &accountCreateReq, false, maxOrderId+1)
|
||||
childrenAccounts, childrenAccountBalanceTimes := a.createSubAccountModels(uid, &accountCreateReq)
|
||||
mainAccount := a.createNewAccountModel(uid, &accountCreateReq, maxOrderId+1)
|
||||
childrenAccounts := a.createSubAccountModels(uid, &accountCreateReq)
|
||||
|
||||
if a.CurrentConfig().EnableDuplicateSubmissionsCheck && accountCreateReq.ClientSessionId != "" {
|
||||
found, remark := a.GetSubmissionRemark(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_ACCOUNT, uid, accountCreateReq.ClientSessionId)
|
||||
|
||||
if found {
|
||||
log.Infof(c, "[accounts.AccountCreateHandler] another account \"id:%s\" has been created for user \"uid:%d\"", remark, uid)
|
||||
accountId, err := utils.StringToInt64(remark)
|
||||
|
||||
if err == nil {
|
||||
accountAndSubAccounts, err := a.accounts.GetAccountAndSubAccountsByAccountId(c, uid, accountId)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[accounts.AccountCreateHandler] failed to get existed account \"id:%d\" for user \"uid:%d\", because %s", accountId, uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
accountMap := a.accounts.GetAccountMapByList(accountAndSubAccounts)
|
||||
mainAccount, exists := accountMap[accountId]
|
||||
|
||||
if !exists {
|
||||
return nil, errs.ErrOperationFailed
|
||||
}
|
||||
|
||||
accountInfoResp := mainAccount.ToAccountInfoResponse()
|
||||
|
||||
for i := 0; i < len(accountAndSubAccounts); i++ {
|
||||
if accountAndSubAccounts[i].ParentAccountId == mainAccount.AccountId {
|
||||
subAccountResp := accountAndSubAccounts[i].ToAccountInfoResponse()
|
||||
accountInfoResp.SubAccounts = append(accountInfoResp.SubAccounts, subAccountResp)
|
||||
}
|
||||
}
|
||||
|
||||
return accountInfoResp, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = a.accounts.CreateAccounts(c, mainAccount, accountCreateReq.BalanceTime, childrenAccounts, childrenAccountBalanceTimes, utcOffset)
|
||||
err = a.accounts.CreateAccounts(c, mainAccount, childrenAccounts, 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())
|
||||
log.ErrorfWithRequestId(c, "[accounts.AccountCreateHandler] failed to create account \"id:%d\" for user \"uid:%d\", because %s", mainAccount.AccountId, uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
log.Infof(c, "[accounts.AccountCreateHandler] user \"uid:%d\" has created a new account \"id:%d\" successfully", uid, mainAccount.AccountId)
|
||||
log.InfofWithRequestId(c, "[accounts.AccountCreateHandler] user \"uid:%d\" has created a new account \"id:%d\" successfully", uid, mainAccount.AccountId)
|
||||
|
||||
a.SetSubmissionRemark(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_ACCOUNT, uid, accountCreateReq.ClientSessionId, utils.Int64ToString(mainAccount.AccountId))
|
||||
accountInfoResp := mainAccount.ToAccountInfoResponse()
|
||||
|
||||
if len(childrenAccounts) > 0 {
|
||||
@@ -302,32 +226,26 @@ func (a *AccountsApi) AccountCreateHandler(c *core.WebContext) (any, *errs.Error
|
||||
}
|
||||
|
||||
// AccountModifyHandler saves an existed account by request parameters for current user
|
||||
func (a *AccountsApi) AccountModifyHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
func (a *AccountsApi) AccountModifyHandler(c *core.Context) (any, *errs.Error) {
|
||||
var accountModifyReq models.AccountModifyRequest
|
||||
err := c.ShouldBindJSON(&accountModifyReq)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[accounts.AccountModifyHandler] parse request failed, because %s", err.Error())
|
||||
log.WarnfWithRequestId(c, "[accounts.AccountModifyHandler] parse request failed, because %s", err.Error())
|
||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||
}
|
||||
|
||||
if accountModifyReq.Category < models.ACCOUNT_CATEGORY_CASH || accountModifyReq.Category > models.ACCOUNT_CATEGORY_CERTIFICATE_OF_DEPOSIT {
|
||||
log.Warnf(c, "[accounts.AccountModifyHandler] account category invalid, category is %d", accountModifyReq.Category)
|
||||
return nil, errs.ErrAccountCategoryInvalid
|
||||
}
|
||||
|
||||
uid := c.GetCurrentUid()
|
||||
accountAndSubAccounts, err := a.accounts.GetAccountAndSubAccountsByAccountId(c, uid, accountModifyReq.Id)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[accounts.AccountModifyHandler] failed to get account \"id:%d\" for user \"uid:%d\", because %s", accountModifyReq.Id, uid, err.Error())
|
||||
log.ErrorfWithRequestId(c, "[accounts.AccountModifyHandler] failed to get account \"id:%d\" for user \"uid:%d\", because %s", accountModifyReq.Id, uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
accountMap := a.accounts.GetAccountMapByList(accountAndSubAccounts)
|
||||
mainAccount, exists := accountMap[accountModifyReq.Id]
|
||||
|
||||
if !exists {
|
||||
if _, exists := accountMap[accountModifyReq.Id]; !exists {
|
||||
return nil, errs.ErrAccountNotFound
|
||||
}
|
||||
|
||||
@@ -335,26 +253,10 @@ func (a *AccountsApi) AccountModifyHandler(c *core.WebContext) (any, *errs.Error
|
||||
return nil, errs.ErrCannotAddOrDeleteSubAccountsWhenModify
|
||||
}
|
||||
|
||||
if accountModifyReq.Category != models.ACCOUNT_CATEGORY_CREDIT_CARD && accountModifyReq.CreditCardStatementDate != 0 {
|
||||
log.Warnf(c, "[accounts.AccountModifyHandler] cannot set statement date with category \"%d\"", accountModifyReq.Category)
|
||||
return nil, errs.ErrCannotSetStatementDateForNonCreditCard
|
||||
}
|
||||
|
||||
if mainAccount.Type == models.ACCOUNT_TYPE_MULTI_SUB_ACCOUNTS {
|
||||
for i := 0; i < len(accountModifyReq.SubAccounts); i++ {
|
||||
subAccount := accountModifyReq.SubAccounts[i]
|
||||
|
||||
if subAccount.CreditCardStatementDate != 0 {
|
||||
log.Warnf(c, "[accounts.AccountModifyHandler] sub-account#%d cannot set statement date", i)
|
||||
return nil, errs.ErrCannotSetStatementDateForSubAccount
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
anythingUpdate := false
|
||||
var toUpdateAccounts []*models.Account
|
||||
|
||||
toUpdateAccount := a.getToUpdateAccount(uid, &accountModifyReq, mainAccount, false)
|
||||
toUpdateAccount := a.getToUpdateAccount(uid, &accountModifyReq, accountMap[accountModifyReq.Id])
|
||||
|
||||
if toUpdateAccount != nil {
|
||||
anythingUpdate = true
|
||||
@@ -368,7 +270,7 @@ func (a *AccountsApi) AccountModifyHandler(c *core.WebContext) (any, *errs.Error
|
||||
return nil, errs.ErrAccountNotFound
|
||||
}
|
||||
|
||||
toUpdateSubAccount := a.getToUpdateAccount(uid, subAccountReq, accountMap[subAccountReq.Id], true)
|
||||
toUpdateSubAccount := a.getToUpdateAccount(uid, subAccountReq, accountMap[subAccountReq.Id])
|
||||
|
||||
if toUpdateSubAccount != nil {
|
||||
anythingUpdate = true
|
||||
@@ -383,11 +285,11 @@ func (a *AccountsApi) AccountModifyHandler(c *core.WebContext) (any, *errs.Error
|
||||
err = a.accounts.ModifyAccounts(c, uid, toUpdateAccounts)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[accounts.AccountModifyHandler] failed to update account \"id:%d\" for user \"uid:%d\", because %s", accountModifyReq.Id, uid, err.Error())
|
||||
log.ErrorfWithRequestId(c, "[accounts.AccountModifyHandler] failed to update account \"id:%d\" for user \"uid:%d\", because %s", accountModifyReq.Id, uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
log.Infof(c, "[accounts.AccountModifyHandler] user \"uid:%d\" has updated account \"id:%d\" successfully", uid, accountModifyReq.Id)
|
||||
log.InfofWithRequestId(c, "[accounts.AccountModifyHandler] user \"uid:%d\" has updated account \"id:%d\" successfully", uid, accountModifyReq.Id)
|
||||
|
||||
accountRespMap := make(map[int64]*models.AccountInfoResponse)
|
||||
|
||||
@@ -430,12 +332,12 @@ func (a *AccountsApi) AccountModifyHandler(c *core.WebContext) (any, *errs.Error
|
||||
}
|
||||
|
||||
// AccountHideHandler hides an existed account by request parameters for current user
|
||||
func (a *AccountsApi) AccountHideHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
func (a *AccountsApi) AccountHideHandler(c *core.Context) (any, *errs.Error) {
|
||||
var accountHideReq models.AccountHideRequest
|
||||
err := c.ShouldBindJSON(&accountHideReq)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[accounts.AccountHideHandler] parse request failed, because %s", err.Error())
|
||||
log.WarnfWithRequestId(c, "[accounts.AccountHideHandler] parse request failed, because %s", err.Error())
|
||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||
}
|
||||
|
||||
@@ -443,21 +345,21 @@ func (a *AccountsApi) AccountHideHandler(c *core.WebContext) (any, *errs.Error)
|
||||
err = a.accounts.HideAccount(c, uid, []int64{accountHideReq.Id}, accountHideReq.Hidden)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[accounts.AccountHideHandler] failed to hide account \"id:%d\" for user \"uid:%d\", because %s", accountHideReq.Id, uid, err.Error())
|
||||
log.ErrorfWithRequestId(c, "[accounts.AccountHideHandler] failed to hide account \"id:%d\" for user \"uid:%d\", because %s", accountHideReq.Id, uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
log.Infof(c, "[accounts.AccountHideHandler] user \"uid:%d\" has hidden account \"id:%d\"", uid, accountHideReq.Id)
|
||||
log.InfofWithRequestId(c, "[accounts.AccountHideHandler] user \"uid:%d\" has hidden account \"id:%d\"", uid, accountHideReq.Id)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// AccountMoveHandler moves display order of existed accounts by request parameters for current user
|
||||
func (a *AccountsApi) AccountMoveHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
func (a *AccountsApi) AccountMoveHandler(c *core.Context) (any, *errs.Error) {
|
||||
var accountMoveReq models.AccountMoveRequest
|
||||
err := c.ShouldBindJSON(&accountMoveReq)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[accounts.AccountMoveHandler] parse request failed, because %s", err.Error())
|
||||
log.WarnfWithRequestId(c, "[accounts.AccountMoveHandler] parse request failed, because %s", err.Error())
|
||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||
}
|
||||
|
||||
@@ -478,21 +380,21 @@ func (a *AccountsApi) AccountMoveHandler(c *core.WebContext) (any, *errs.Error)
|
||||
err = a.accounts.ModifyAccountDisplayOrders(c, uid, accounts)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[accounts.AccountMoveHandler] failed to move accounts for user \"uid:%d\", because %s", uid, err.Error())
|
||||
log.ErrorfWithRequestId(c, "[accounts.AccountMoveHandler] failed to move accounts for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
log.Infof(c, "[accounts.AccountMoveHandler] user \"uid:%d\" has moved accounts", uid)
|
||||
log.InfofWithRequestId(c, "[accounts.AccountMoveHandler] user \"uid:%d\" has moved accounts", uid)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// AccountDeleteHandler deletes an existed account by request parameters for current user
|
||||
func (a *AccountsApi) AccountDeleteHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
func (a *AccountsApi) AccountDeleteHandler(c *core.Context) (any, *errs.Error) {
|
||||
var accountDeleteReq models.AccountDeleteRequest
|
||||
err := c.ShouldBindJSON(&accountDeleteReq)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[accounts.AccountDeleteHandler] parse request failed, because %s", err.Error())
|
||||
log.WarnfWithRequestId(c, "[accounts.AccountDeleteHandler] parse request failed, because %s", err.Error())
|
||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||
}
|
||||
|
||||
@@ -500,21 +402,15 @@ func (a *AccountsApi) AccountDeleteHandler(c *core.WebContext) (any, *errs.Error
|
||||
err = a.accounts.DeleteAccount(c, uid, accountDeleteReq.Id)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[accounts.AccountDeleteHandler] failed to delete account \"id:%d\" for user \"uid:%d\", because %s", accountDeleteReq.Id, uid, err.Error())
|
||||
log.ErrorfWithRequestId(c, "[accounts.AccountDeleteHandler] failed to delete account \"id:%d\" for user \"uid:%d\", because %s", accountDeleteReq.Id, uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
log.Infof(c, "[accounts.AccountDeleteHandler] user \"uid:%d\" has deleted account \"id:%d\"", uid, accountDeleteReq.Id)
|
||||
log.InfofWithRequestId(c, "[accounts.AccountDeleteHandler] user \"uid:%d\" has deleted account \"id:%d\"", uid, accountDeleteReq.Id)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (a *AccountsApi) createNewAccountModel(uid int64, accountCreateReq *models.AccountCreateRequest, isSubAccount bool, order int32) *models.Account {
|
||||
accountExtend := &models.AccountExtend{}
|
||||
|
||||
if !isSubAccount && accountCreateReq.Category == models.ACCOUNT_CATEGORY_CREDIT_CARD {
|
||||
accountExtend.CreditCardStatementDate = &accountCreateReq.CreditCardStatementDate
|
||||
}
|
||||
|
||||
func (a *AccountsApi) createNewAccountModel(uid int64, accountCreateReq *models.AccountCreateRequest, order int32) *models.Account {
|
||||
return &models.Account{
|
||||
Uid: uid,
|
||||
Name: accountCreateReq.Name,
|
||||
@@ -526,33 +422,24 @@ func (a *AccountsApi) createNewAccountModel(uid int64, accountCreateReq *models.
|
||||
Currency: accountCreateReq.Currency,
|
||||
Balance: accountCreateReq.Balance,
|
||||
Comment: accountCreateReq.Comment,
|
||||
Extend: accountExtend,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AccountsApi) createSubAccountModels(uid int64, accountCreateReq *models.AccountCreateRequest) ([]*models.Account, []int64) {
|
||||
func (a *AccountsApi) createSubAccountModels(uid int64, accountCreateReq *models.AccountCreateRequest) []*models.Account {
|
||||
if len(accountCreateReq.SubAccounts) <= 0 {
|
||||
return nil, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
childrenAccounts := make([]*models.Account, len(accountCreateReq.SubAccounts))
|
||||
childrenAccountBalanceTimes := make([]int64, len(accountCreateReq.SubAccounts))
|
||||
|
||||
for i := int32(0); i < int32(len(accountCreateReq.SubAccounts)); i++ {
|
||||
childrenAccounts[i] = a.createNewAccountModel(uid, accountCreateReq.SubAccounts[i], true, i+1)
|
||||
childrenAccountBalanceTimes[i] = accountCreateReq.SubAccounts[i].BalanceTime
|
||||
childrenAccounts[i] = a.createNewAccountModel(uid, accountCreateReq.SubAccounts[i], i+1)
|
||||
}
|
||||
|
||||
return childrenAccounts, childrenAccountBalanceTimes
|
||||
return childrenAccounts
|
||||
}
|
||||
|
||||
func (a *AccountsApi) getToUpdateAccount(uid int64, accountModifyReq *models.AccountModifyRequest, oldAccount *models.Account, isSubAccount bool) *models.Account {
|
||||
newAccountExtend := &models.AccountExtend{}
|
||||
|
||||
if !isSubAccount && accountModifyReq.Category == models.ACCOUNT_CATEGORY_CREDIT_CARD {
|
||||
newAccountExtend.CreditCardStatementDate = &accountModifyReq.CreditCardStatementDate
|
||||
}
|
||||
|
||||
func (a *AccountsApi) getToUpdateAccount(uid int64, accountModifyReq *models.AccountModifyRequest, oldAccount *models.Account) *models.Account {
|
||||
newAccount := &models.Account{
|
||||
AccountId: oldAccount.AccountId,
|
||||
Uid: uid,
|
||||
@@ -561,7 +448,6 @@ func (a *AccountsApi) getToUpdateAccount(uid int64, accountModifyReq *models.Acc
|
||||
Icon: accountModifyReq.Icon,
|
||||
Color: accountModifyReq.Color,
|
||||
Comment: accountModifyReq.Comment,
|
||||
Extend: newAccountExtend,
|
||||
Hidden: accountModifyReq.Hidden,
|
||||
}
|
||||
|
||||
@@ -574,16 +460,5 @@ func (a *AccountsApi) getToUpdateAccount(uid int64, accountModifyReq *models.Acc
|
||||
return newAccount
|
||||
}
|
||||
|
||||
if (newAccount.Extend != nil && oldAccount.Extend == nil) ||
|
||||
(newAccount.Extend == nil && oldAccount.Extend != nil) {
|
||||
return newAccount
|
||||
}
|
||||
|
||||
oldAccountExtend := oldAccount.Extend
|
||||
|
||||
if newAccountExtend.CreditCardStatementDate != oldAccountExtend.CreditCardStatementDate {
|
||||
return newAccount
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -18,20 +18,15 @@ const amapRestApiUrl = "https://restapi.amap.com/"
|
||||
|
||||
// AmapApiProxy represents amap api proxy
|
||||
type AmapApiProxy struct {
|
||||
ApiUsingConfig
|
||||
}
|
||||
|
||||
// Initialize a amap api proxy singleton instance
|
||||
var (
|
||||
AmapApis = &AmapApiProxy{
|
||||
ApiUsingConfig: ApiUsingConfig{
|
||||
container: settings.Container,
|
||||
},
|
||||
}
|
||||
AmapApis = &AmapApiProxy{}
|
||||
)
|
||||
|
||||
// AmapApiProxyHandler returns amap api response
|
||||
func (p *AmapApiProxy) AmapApiProxyHandler(c *core.WebContext) (*httputil.ReverseProxy, *errs.Error) {
|
||||
func (p *AmapApiProxy) AmapApiProxyHandler(c *core.Context) (*httputil.ReverseProxy, *errs.Error) {
|
||||
var targetUrl string
|
||||
|
||||
if strings.HasPrefix(c.Request.RequestURI, "/_AMapService/v4/map/styles") {
|
||||
@@ -43,7 +38,7 @@ func (p *AmapApiProxy) AmapApiProxyHandler(c *core.WebContext) (*httputil.Revers
|
||||
}
|
||||
|
||||
director := func(req *http.Request) {
|
||||
targetRawUrl := fmt.Sprintf("%s?%s&jscode=%s", targetUrl, req.URL.RawQuery, p.CurrentConfig().AmapApplicationSecret)
|
||||
targetRawUrl := fmt.Sprintf("%s?%s&jscode=%s", targetUrl, req.URL.RawQuery, settings.Container.Current.AmapApplicationSecret)
|
||||
targetUrl, _ := url.Parse(targetRawUrl)
|
||||
|
||||
oldCookies := req.Cookies()
|
||||
|
||||
+41
-111
@@ -3,9 +3,7 @@ package api
|
||||
import (
|
||||
"github.com/pquerna/otp/totp"
|
||||
|
||||
"github.com/mayswind/ezbookkeeping/pkg/avatars"
|
||||
"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,9 +13,6 @@ import (
|
||||
|
||||
// AuthorizationsApi represents authorization api
|
||||
type AuthorizationsApi struct {
|
||||
ApiUsingConfig
|
||||
ApiUsingDuplicateChecker
|
||||
ApiWithUserInfo
|
||||
users *services.UserService
|
||||
tokens *services.TokenService
|
||||
twoFactorAuthorizations *services.TwoFactorAuthorizationService
|
||||
@@ -26,23 +21,6 @@ type AuthorizationsApi struct {
|
||||
// Initialize a authorization api singleton instance
|
||||
var (
|
||||
Authorizations = &AuthorizationsApi{
|
||||
ApiUsingConfig: ApiUsingConfig{
|
||||
container: settings.Container,
|
||||
},
|
||||
ApiUsingDuplicateChecker: ApiUsingDuplicateChecker{
|
||||
ApiUsingConfig: ApiUsingConfig{
|
||||
container: settings.Container,
|
||||
},
|
||||
container: duplicatechecker.Container,
|
||||
},
|
||||
ApiWithUserInfo: ApiWithUserInfo{
|
||||
ApiUsingConfig: ApiUsingConfig{
|
||||
container: settings.Container,
|
||||
},
|
||||
ApiUsingAvatarProvider: ApiUsingAvatarProvider{
|
||||
container: avatars.Container,
|
||||
},
|
||||
},
|
||||
users: services.Users,
|
||||
tokens: services.Tokens,
|
||||
twoFactorAuthorizations: services.TwoFactorAuthorizations,
|
||||
@@ -50,52 +28,36 @@ var (
|
||||
)
|
||||
|
||||
// AuthorizeHandler verifies and authorizes current login request
|
||||
func (a *AuthorizationsApi) AuthorizeHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
func (a *AuthorizationsApi) AuthorizeHandler(c *core.Context) (any, *errs.Error) {
|
||||
var credential models.UserLoginRequest
|
||||
err := c.ShouldBindJSON(&credential)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[authorizations.AuthorizeHandler] parse request failed, because %s", err.Error())
|
||||
log.WarnfWithRequestId(c, "[authorizations.AuthorizeHandler] parse request failed, because %s", err.Error())
|
||||
return nil, errs.ErrLoginNameOrPasswordInvalid
|
||||
}
|
||||
|
||||
err = a.CheckFailureCount(c, 0)
|
||||
user, err := a.users.GetUserByUsernameOrEmailAndPassword(c, credential.LoginName, credential.Password)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[authorizations.AuthorizeHandler] cannot login for user \"%s\", because %s", credential.LoginName, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrFailureCountLimitReached)
|
||||
}
|
||||
|
||||
user, uid, err := a.users.GetUserByUsernameOrEmailAndPassword(c, credential.LoginName, credential.Password)
|
||||
|
||||
if errs.IsCustomError(err) {
|
||||
failureCheckErr := a.CheckAndIncreaseFailureCount(c, uid)
|
||||
|
||||
if failureCheckErr != nil {
|
||||
log.Warnf(c, "[authorizations.AuthorizeHandler] cannot login for user \"%s\", because %s", credential.LoginName, failureCheckErr.Error())
|
||||
return nil, errs.Or(failureCheckErr, errs.ErrFailureCountLimitReached)
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[authorizations.AuthorizeHandler] login failed for user \"%s\", because %s", credential.LoginName, err.Error())
|
||||
log.WarnfWithRequestId(c, "[authorizations.AuthorizeHandler] login failed for user \"%s\", because %s", credential.LoginName, err.Error())
|
||||
return nil, errs.ErrLoginNameOrPasswordWrong
|
||||
}
|
||||
|
||||
if user.Disabled {
|
||||
log.Warnf(c, "[authorizations.AuthorizeHandler] login failed for user \"%s\", because user is disabled", credential.LoginName)
|
||||
log.WarnfWithRequestId(c, "[authorizations.AuthorizeHandler] login failed for user \"%s\", because user is disabled", credential.LoginName)
|
||||
return nil, errs.ErrUserIsDisabled
|
||||
}
|
||||
|
||||
if a.CurrentConfig().EnableUserForceVerifyEmail && !user.EmailVerified {
|
||||
if settings.Container.Current.EnableUserForceVerifyEmail && !user.EmailVerified {
|
||||
hasValidEmailVerifyToken, err := a.tokens.ExistsValidTokenByType(c, user.Uid, core.USER_TOKEN_TYPE_EMAIL_VERIFY)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[authorizations.AuthorizeHandler] failed check whether user \"uid:%d\" has valid verify email token, because %s", user.Uid, err.Error())
|
||||
log.WarnfWithRequestId(c, "[authorizations.AuthorizeHandler] failed check whether user \"uid:%d\" has valid verify email token, because %s", user.Uid, err.Error())
|
||||
hasValidEmailVerifyToken = false
|
||||
}
|
||||
|
||||
log.Warnf(c, "[authorizations.AuthorizeHandler] login failed for user \"%s\", because user has not verified email", credential.LoginName)
|
||||
log.WarnfWithRequestId(c, "[authorizations.AuthorizeHandler] login failed for user \"%s\", because user has not verified email", credential.LoginName)
|
||||
|
||||
return nil, errs.NewErrorWithContext(errs.ErrEmailIsNotVerified, map[string]any{
|
||||
"email": user.Email,
|
||||
@@ -106,7 +68,7 @@ func (a *AuthorizationsApi) AuthorizeHandler(c *core.WebContext) (any, *errs.Err
|
||||
err = a.users.UpdateUserLastLoginTime(c, user.Uid)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[authorizations.AuthorizeHandler] failed to update last login time for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
log.WarnfWithRequestId(c, "[authorizations.AuthorizeHandler] failed to update last login time for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
}
|
||||
|
||||
twoFactorEnable := a.tokens.CurrentConfig().EnableTwoFactor
|
||||
@@ -115,7 +77,7 @@ func (a *AuthorizationsApi) AuthorizeHandler(c *core.WebContext) (any, *errs.Err
|
||||
twoFactorEnable, err = a.twoFactorAuthorizations.ExistsTwoFactorSetting(c, user.Uid)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[authorizations.AuthorizeHandler] failed to check two-factor setting for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
log.ErrorfWithRequestId(c, "[authorizations.AuthorizeHandler] failed to check two factor setting for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrSystemError)
|
||||
}
|
||||
}
|
||||
@@ -130,7 +92,7 @@ func (a *AuthorizationsApi) AuthorizeHandler(c *core.WebContext) (any, *errs.Err
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[authorizations.AuthorizeHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
log.ErrorfWithRequestId(c, "[authorizations.AuthorizeHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
return nil, errs.ErrTokenGenerating
|
||||
}
|
||||
|
||||
@@ -140,64 +102,49 @@ func (a *AuthorizationsApi) AuthorizeHandler(c *core.WebContext) (any, *errs.Err
|
||||
|
||||
c.SetTokenClaims(claims)
|
||||
|
||||
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)
|
||||
log.InfofWithRequestId(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)
|
||||
authResp := a.getAuthResponse(token, twoFactorEnable, user)
|
||||
return authResp, nil
|
||||
}
|
||||
|
||||
// TwoFactorAuthorizeHandler verifies and authorizes current 2fa login by passcode
|
||||
func (a *AuthorizationsApi) TwoFactorAuthorizeHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
func (a *AuthorizationsApi) TwoFactorAuthorizeHandler(c *core.Context) (any, *errs.Error) {
|
||||
var credential models.TwoFactorLoginRequest
|
||||
err := c.ShouldBindJSON(&credential)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[authorizations.TwoFactorAuthorizeHandler] parse request failed, because %s", err.Error())
|
||||
log.WarnfWithRequestId(c, "[authorizations.TwoFactorAuthorizeHandler] parse request failed, because %s", err.Error())
|
||||
return nil, errs.ErrPasscodeInvalid
|
||||
}
|
||||
|
||||
uid := c.GetCurrentUid()
|
||||
err = a.CheckFailureCount(c, uid)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[authorizations.TwoFactorAuthorizeHandler] cannot auth for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrFailureCountLimitReached)
|
||||
}
|
||||
|
||||
twoFactorSetting, err := a.twoFactorAuthorizations.GetUserTwoFactorSettingByUid(c, uid)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[authorizations.TwoFactorAuthorizeHandler] failed to get two-factor setting for user \"uid:%d\", because %s", uid, err.Error())
|
||||
log.ErrorfWithRequestId(c, "[authorizations.TwoFactorAuthorizeHandler] failed to get two factor setting for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrSystemError)
|
||||
}
|
||||
|
||||
if !totp.Validate(credential.Passcode, twoFactorSetting.Secret) {
|
||||
log.Warnf(c, "[authorizations.TwoFactorAuthorizeHandler] passcode is invalid for user \"uid:%d\"", uid)
|
||||
|
||||
err = a.CheckAndIncreaseFailureCount(c, uid)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[authorizations.TwoFactorAuthorizeHandler] cannot auth for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrFailureCountLimitReached)
|
||||
}
|
||||
|
||||
log.WarnfWithRequestId(c, "[authorizations.TwoFactorAuthorizeHandler] passcode is invalid for user \"uid:%d\"", uid)
|
||||
return nil, errs.ErrPasscodeInvalid
|
||||
}
|
||||
|
||||
user, err := a.users.GetUserById(c, uid)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[authorizations.TwoFactorAuthorizeHandler] failed to get user \"uid:%d\" info, because %s", user.Uid, err.Error())
|
||||
log.ErrorfWithRequestId(c, "[authorizations.TwoFactorAuthorizeHandler] failed to get user \"uid:%d\" info, because %s", user.Uid, err.Error())
|
||||
return nil, errs.ErrUserNotFound
|
||||
}
|
||||
|
||||
if user.Disabled {
|
||||
log.Warnf(c, "[authorizations.TwoFactorAuthorizeHandler] user \"uid:%d\" is disabled", user.Uid)
|
||||
log.WarnfWithRequestId(c, "[authorizations.TwoFactorAuthorizeHandler] user \"uid:%d\" is disabled", user.Uid)
|
||||
return nil, errs.ErrUserIsDisabled
|
||||
}
|
||||
|
||||
if a.CurrentConfig().EnableUserForceVerifyEmail && !user.EmailVerified {
|
||||
log.Warnf(c, "[authorizations.TwoFactorAuthorizeHandler] user \"uid:%d\" has not verified email", user.Uid)
|
||||
if settings.Container.Current.EnableUserForceVerifyEmail && !user.EmailVerified {
|
||||
log.WarnfWithRequestId(c, "[authorizations.TwoFactorAuthorizeHandler] user \"uid:%d\" has not verified email", user.Uid)
|
||||
return nil, errs.ErrEmailIsNotVerified
|
||||
}
|
||||
|
||||
@@ -205,47 +152,40 @@ func (a *AuthorizationsApi) TwoFactorAuthorizeHandler(c *core.WebContext) (any,
|
||||
err = a.tokens.DeleteTokenByClaims(c, oldTokenClaims)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[authorizations.TwoFactorAuthorizeHandler] failed to revoke temporary token \"utid:%s\" for user \"uid:%d\", because %s", oldTokenClaims.UserTokenId, user.Uid, err.Error())
|
||||
log.WarnfWithRequestId(c, "[authorizations.TwoFactorAuthorizeHandler] failed to revoke temporary token \"utid:%s\" for user \"uid:%d\", because %s", oldTokenClaims.UserTokenId, user.Uid, err.Error())
|
||||
}
|
||||
|
||||
token, claims, err := a.tokens.CreateToken(c, user)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[authorizations.TwoFactorAuthorizeHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
log.ErrorfWithRequestId(c, "[authorizations.TwoFactorAuthorizeHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
return nil, errs.ErrTokenGenerating
|
||||
}
|
||||
|
||||
c.SetTextualToken(token)
|
||||
c.SetTokenClaims(claims)
|
||||
|
||||
log.Infof(c, "[authorizations.TwoFactorAuthorizeHandler] user \"uid:%d\" has authorized two-factor via passcode, token will be expired at %d", user.Uid, claims.ExpiresAt)
|
||||
log.InfofWithRequestId(c, "[authorizations.TwoFactorAuthorizeHandler] user \"uid:%d\" has authorized two factor via passcode, token will be expired at %d", user.Uid, claims.ExpiresAt)
|
||||
|
||||
authResp := a.getAuthResponse(c, token, false, user)
|
||||
authResp := a.getAuthResponse(token, false, user)
|
||||
return authResp, nil
|
||||
}
|
||||
|
||||
// TwoFactorAuthorizeByRecoveryCodeHandler verifies and authorizes current 2fa login by recovery code
|
||||
func (a *AuthorizationsApi) TwoFactorAuthorizeByRecoveryCodeHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
func (a *AuthorizationsApi) TwoFactorAuthorizeByRecoveryCodeHandler(c *core.Context) (any, *errs.Error) {
|
||||
var credential models.TwoFactorRecoveryCodeLoginRequest
|
||||
err := c.ShouldBindJSON(&credential)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] parse request failed, because %s", err.Error())
|
||||
log.WarnfWithRequestId(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] parse request failed, because %s", err.Error())
|
||||
return nil, errs.ErrTwoFactorRecoveryCodeInvalid
|
||||
}
|
||||
|
||||
uid := c.GetCurrentUid()
|
||||
err = a.CheckFailureCount(c, uid)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] cannot auth for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrFailureCountLimitReached)
|
||||
}
|
||||
|
||||
enableTwoFactor, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(c, uid)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] failed to get two-factor setting for user \"uid:%d\", because %s", uid, err.Error())
|
||||
log.WarnfWithRequestId(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] failed to get two factor setting for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrSystemError)
|
||||
}
|
||||
|
||||
@@ -256,33 +196,24 @@ 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", user.Uid, err.Error())
|
||||
log.ErrorfWithRequestId(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] failed to get user \"uid:%d\" info, because %s", user.Uid, err.Error())
|
||||
return nil, errs.ErrUserNotFound
|
||||
}
|
||||
|
||||
if user.Disabled {
|
||||
log.Warnf(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] user \"uid:%d\" is disabled", user.Uid)
|
||||
log.WarnfWithRequestId(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] user \"uid:%d\" is disabled", user.Uid)
|
||||
return nil, errs.ErrUserIsDisabled
|
||||
}
|
||||
|
||||
if a.CurrentConfig().EnableUserForceVerifyEmail && !user.EmailVerified {
|
||||
log.Warnf(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] user \"uid:%d\" has not verified email", user.Uid)
|
||||
if settings.Container.Current.EnableUserForceVerifyEmail && !user.EmailVerified {
|
||||
log.WarnfWithRequestId(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] user \"uid:%d\" has not verified email", user.Uid)
|
||||
return nil, errs.ErrEmailIsNotVerified
|
||||
}
|
||||
|
||||
err = a.twoFactorAuthorizations.GetAndUseUserTwoFactorRecoveryCode(c, uid, credential.RecoveryCode, user.Salt)
|
||||
|
||||
if errs.IsCustomError(err) {
|
||||
failureCheckErr := a.CheckAndIncreaseFailureCount(c, uid)
|
||||
|
||||
if failureCheckErr != nil {
|
||||
log.Warnf(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] cannot auth for user \"uid:%d\", because %s", uid, failureCheckErr.Error())
|
||||
return nil, errs.Or(failureCheckErr, errs.ErrFailureCountLimitReached)
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] failed to get two-factor recovery code for user \"uid:%d\", because %s", uid, err.Error())
|
||||
log.WarnfWithRequestId(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] failed to get two factor recovery code for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrTwoFactorRecoveryCodeNotExist)
|
||||
}
|
||||
|
||||
@@ -290,30 +221,29 @@ func (a *AuthorizationsApi) TwoFactorAuthorizeByRecoveryCodeHandler(c *core.WebC
|
||||
err = a.tokens.DeleteTokenByClaims(c, oldTokenClaims)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] failed to revoke temporary token \"utid:%s\" for user \"uid:%d\", because %s", oldTokenClaims.UserTokenId, user.Uid, err.Error())
|
||||
log.WarnfWithRequestId(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] failed to revoke temporary token \"utid:%s\" for user \"uid:%d\", because %s", oldTokenClaims.UserTokenId, user.Uid, err.Error())
|
||||
}
|
||||
|
||||
token, claims, err := a.tokens.CreateToken(c, user)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
log.ErrorfWithRequestId(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
return nil, errs.ErrTokenGenerating
|
||||
}
|
||||
|
||||
c.SetTextualToken(token)
|
||||
c.SetTokenClaims(claims)
|
||||
|
||||
log.Infof(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] user \"uid:%d\" has authorized two-factor via recovery code \"%s\", token will be expired at %d", user.Uid, credential.RecoveryCode, claims.ExpiresAt)
|
||||
log.InfofWithRequestId(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] user \"uid:%d\" has authorized two factor via recovery code \"%s\", token will be expired at %d", user.Uid, credential.RecoveryCode, claims.ExpiresAt)
|
||||
|
||||
authResp := a.getAuthResponse(c, token, false, user)
|
||||
authResp := a.getAuthResponse(token, false, user)
|
||||
return authResp, nil
|
||||
}
|
||||
|
||||
func (a *AuthorizationsApi) getAuthResponse(c *core.WebContext, token string, need2FA bool, user *models.User) *models.AuthResponse {
|
||||
func (a *AuthorizationsApi) getAuthResponse(token string, need2FA bool, user *models.User) *models.AuthResponse {
|
||||
return &models.AuthResponse{
|
||||
Token: token,
|
||||
Need2FA: need2FA,
|
||||
User: a.GetUserBasicInfo(user),
|
||||
NotificationContent: a.GetAfterLoginNotificationContent(user.Language, c.GetClientLocale()),
|
||||
Token: token,
|
||||
Need2FA: need2FA,
|
||||
User: user.ToUserBasicInfo(),
|
||||
}
|
||||
}
|
||||
|
||||
-201
@@ -1,201 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/mayswind/ezbookkeeping/pkg/avatars"
|
||||
"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"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||
)
|
||||
|
||||
const internalTransactionPictureUrlFormat = "%spictures/%d.%s"
|
||||
|
||||
// ApiUsingConfig represents an api that need to use config
|
||||
type ApiUsingConfig struct {
|
||||
container *settings.ConfigContainer
|
||||
}
|
||||
|
||||
// CurrentConfig returns the current config
|
||||
func (a *ApiUsingConfig) CurrentConfig() *settings.Config {
|
||||
return a.container.Current
|
||||
}
|
||||
|
||||
// GetTransactionPictureInfoResponse returns the view-object of transaction picture basic info according to the transaction picture model
|
||||
func (a *ApiUsingConfig) GetTransactionPictureInfoResponse(pictureInfo *models.TransactionPictureInfo) *models.TransactionPictureInfoBasicResponse {
|
||||
originalUrl := fmt.Sprintf(internalTransactionPictureUrlFormat, a.CurrentConfig().RootUrl, pictureInfo.PictureId, pictureInfo.PictureExtension)
|
||||
return pictureInfo.ToTransactionPictureInfoBasicResponse(originalUrl)
|
||||
}
|
||||
|
||||
// GetTransactionPictureInfoResponseList returns the view-object list of transaction picture basic info according to the transaction picture model
|
||||
func (a *ApiUsingConfig) GetTransactionPictureInfoResponseList(pictureInfos []*models.TransactionPictureInfo) models.TransactionPictureInfoBasicResponseSlice {
|
||||
pictureInfoResps := make(models.TransactionPictureInfoBasicResponseSlice, len(pictureInfos))
|
||||
|
||||
for i := 0; i < len(pictureInfos); i++ {
|
||||
pictureInfoResps[i] = a.GetTransactionPictureInfoResponse(pictureInfos[i])
|
||||
}
|
||||
|
||||
sort.Sort(pictureInfoResps)
|
||||
|
||||
return pictureInfoResps
|
||||
}
|
||||
|
||||
// GetAfterRegisterNotificationContent returns the notification content displayed each time users register
|
||||
func (a *ApiUsingConfig) GetAfterRegisterNotificationContent(userLanguage string, clientLanguage string) string {
|
||||
language := userLanguage
|
||||
|
||||
if language == "" {
|
||||
language = clientLanguage
|
||||
}
|
||||
|
||||
if !a.container.Current.AfterRegisterNotification.Enabled {
|
||||
return ""
|
||||
}
|
||||
|
||||
if multiLanguageContent, exists := a.container.Current.AfterRegisterNotification.MultiLanguageContent[language]; exists {
|
||||
return multiLanguageContent
|
||||
}
|
||||
|
||||
return a.container.Current.AfterRegisterNotification.DefaultContent
|
||||
}
|
||||
|
||||
// GetAfterLoginNotificationContent returns the notification content displayed each time users log in
|
||||
func (a *ApiUsingConfig) GetAfterLoginNotificationContent(userLanguage string, clientLanguage string) string {
|
||||
language := userLanguage
|
||||
|
||||
if language == "" {
|
||||
language = clientLanguage
|
||||
}
|
||||
|
||||
if !a.container.Current.AfterLoginNotification.Enabled {
|
||||
return ""
|
||||
}
|
||||
|
||||
if multiLanguageContent, exists := a.container.Current.AfterLoginNotification.MultiLanguageContent[language]; exists {
|
||||
return multiLanguageContent
|
||||
}
|
||||
|
||||
return a.container.Current.AfterLoginNotification.DefaultContent
|
||||
}
|
||||
|
||||
// GetAfterOpenNotificationContent returns the notification content displayed each time users open the app
|
||||
func (a *ApiUsingConfig) GetAfterOpenNotificationContent(userLanguage string, clientLanguage string) string {
|
||||
language := userLanguage
|
||||
|
||||
if language == "" {
|
||||
language = clientLanguage
|
||||
}
|
||||
|
||||
if !a.container.Current.AfterOpenNotification.Enabled {
|
||||
return ""
|
||||
}
|
||||
|
||||
if multiLanguageContent, exists := a.container.Current.AfterOpenNotification.MultiLanguageContent[language]; exists {
|
||||
return multiLanguageContent
|
||||
}
|
||||
|
||||
return a.container.Current.AfterOpenNotification.DefaultContent
|
||||
}
|
||||
|
||||
// ApiUsingDuplicateChecker represents an api that need to use duplicate checker
|
||||
type ApiUsingDuplicateChecker struct {
|
||||
ApiUsingConfig
|
||||
container *duplicatechecker.DuplicateCheckerContainer
|
||||
}
|
||||
|
||||
// GetSubmissionRemark returns whether the same submission has been processed and related remark by the current duplicate checker
|
||||
func (a *ApiUsingDuplicateChecker) GetSubmissionRemark(checkerType duplicatechecker.DuplicateCheckerType, uid int64, identification string) (bool, string) {
|
||||
return a.container.GetSubmissionRemark(checkerType, uid, identification)
|
||||
}
|
||||
|
||||
// SetSubmissionRemark saves the identification and remark to in-memory cache by the current duplicate checker
|
||||
func (a *ApiUsingDuplicateChecker) SetSubmissionRemark(checkerType duplicatechecker.DuplicateCheckerType, uid int64, identification string, remark string) {
|
||||
a.container.SetSubmissionRemark(checkerType, uid, identification, remark)
|
||||
}
|
||||
|
||||
// CheckFailureCount returns whether the failure count of the specified IP and user has reached the limit and increases the failure count
|
||||
func (a *ApiUsingDuplicateChecker) CheckFailureCount(c *core.WebContext, uid int64) error {
|
||||
if a.CurrentConfig().MaxFailuresPerIpPerMinute > 0 {
|
||||
clientIp := c.ClientIP()
|
||||
ipFailureCount := a.container.GetFailureCount(clientIp)
|
||||
|
||||
if ipFailureCount >= a.CurrentConfig().MaxFailuresPerIpPerMinute {
|
||||
log.Warnf(c, "[base.CheckFailureCount] operation failure via IP \"%s\", current failure count: %d reached the limit", clientIp, ipFailureCount)
|
||||
return errs.ErrFailureCountLimitReached
|
||||
}
|
||||
}
|
||||
|
||||
if a.CurrentConfig().MaxFailuresPerUserPerMinute > 0 && uid > 0 {
|
||||
uidFailureCount := a.container.GetFailureCount(utils.Int64ToString(uid))
|
||||
|
||||
if uidFailureCount >= a.CurrentConfig().MaxFailuresPerUserPerMinute {
|
||||
log.Warnf(c, "[base.CheckFailureCount] operation failure via uid \"%d\", current failure count: %d reached the limit", uid, uidFailureCount)
|
||||
return errs.ErrFailureCountLimitReached
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckAndIncreaseFailureCount returns whether the failure count of the specified IP and user has reached the limit and increases the failure count
|
||||
func (a *ApiUsingDuplicateChecker) CheckAndIncreaseFailureCount(c *core.WebContext, uid int64) error {
|
||||
clientIp := c.ClientIP()
|
||||
ipFailureCount := uint32(0)
|
||||
uidFailureCount := uint32(0)
|
||||
|
||||
if a.CurrentConfig().MaxFailuresPerIpPerMinute > 0 {
|
||||
ipFailureCount = a.container.GetFailureCount(clientIp)
|
||||
}
|
||||
|
||||
if a.CurrentConfig().MaxFailuresPerUserPerMinute > 0 && uid > 0 {
|
||||
uidFailureCount = a.container.GetFailureCount(utils.Int64ToString(uid))
|
||||
}
|
||||
|
||||
if a.CurrentConfig().MaxFailuresPerIpPerMinute > 0 && ipFailureCount < a.CurrentConfig().MaxFailuresPerIpPerMinute {
|
||||
log.Warnf(c, "[base.CheckAndIncreaseFailureCount] operation failure via IP \"%s\", previous failure count: %d", clientIp, ipFailureCount)
|
||||
a.container.IncreaseFailureCount(clientIp)
|
||||
}
|
||||
|
||||
if a.CurrentConfig().MaxFailuresPerUserPerMinute > 0 && uid > 0 && uidFailureCount < a.CurrentConfig().MaxFailuresPerUserPerMinute {
|
||||
log.Warnf(c, "[base.CheckAndIncreaseFailureCount] operation failure via uid \"%d\", previous failure count: %d", uid, uidFailureCount)
|
||||
a.container.IncreaseFailureCount(utils.Int64ToString(uid))
|
||||
}
|
||||
|
||||
if a.CurrentConfig().MaxFailuresPerIpPerMinute > 0 && ipFailureCount >= a.CurrentConfig().MaxFailuresPerIpPerMinute {
|
||||
log.Warnf(c, "[base.CheckAndIncreaseFailureCount] operation failure via IP \"%s\", current failure count: %d reached the limit", clientIp, ipFailureCount)
|
||||
return errs.ErrFailureCountLimitReached
|
||||
}
|
||||
|
||||
if a.CurrentConfig().MaxFailuresPerUserPerMinute > 0 && uid > 0 && uidFailureCount >= a.CurrentConfig().MaxFailuresPerUserPerMinute {
|
||||
log.Warnf(c, "[base.CheckAndIncreaseFailureCount] operation failure via uid \"%d\", current failure count: %d reached the limit", uid, uidFailureCount)
|
||||
return errs.ErrFailureCountLimitReached
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ApiUsingAvatarProvider represents an api that need to use avatar provider
|
||||
type ApiUsingAvatarProvider struct {
|
||||
container *avatars.AvatarProviderContainer
|
||||
}
|
||||
|
||||
// GetAvatarUrl returns the avatar url by the current user avatar provider
|
||||
func (a *ApiUsingAvatarProvider) GetAvatarUrl(user *models.User) string {
|
||||
return a.container.GetAvatarUrl(user)
|
||||
}
|
||||
|
||||
// ApiWithUserInfo represents an api that can returns user info
|
||||
type ApiWithUserInfo struct {
|
||||
ApiUsingConfig
|
||||
ApiUsingAvatarProvider
|
||||
}
|
||||
|
||||
// GetUserBasicInfo returns the view-object of user basic info according to the user model
|
||||
func (a *ApiWithUserInfo) GetUserBasicInfo(user *models.User) *models.UserBasicInfo {
|
||||
return user.ToUserBasicInfo(a.CurrentConfig().AvatarProvider, a.GetAvatarUrl(user))
|
||||
}
|
||||
+109
-169
@@ -19,172 +19,31 @@ const pageCountForDataExport = 1000
|
||||
|
||||
// DataManagementsApi represents data management api
|
||||
type DataManagementsApi struct {
|
||||
ApiUsingConfig
|
||||
exporter *converters.EzBookKeepingCSVFileExporter
|
||||
tokens *services.TokenService
|
||||
users *services.UserService
|
||||
accounts *services.AccountService
|
||||
transactions *services.TransactionService
|
||||
categories *services.TransactionCategoryService
|
||||
tags *services.TransactionTagService
|
||||
pictures *services.TransactionPictureService
|
||||
templates *services.TransactionTemplateService
|
||||
}
|
||||
|
||||
// Initialize a data management api singleton instance
|
||||
var (
|
||||
DataManagements = &DataManagementsApi{
|
||||
ApiUsingConfig: ApiUsingConfig{
|
||||
container: settings.Container,
|
||||
},
|
||||
exporter: &converters.EzBookKeepingCSVFileExporter{},
|
||||
tokens: services.Tokens,
|
||||
users: services.Users,
|
||||
accounts: services.Accounts,
|
||||
transactions: services.Transactions,
|
||||
categories: services.TransactionCategories,
|
||||
tags: services.TransactionTags,
|
||||
pictures: services.TransactionPictures,
|
||||
templates: services.TransactionTemplates,
|
||||
}
|
||||
)
|
||||
|
||||
// ExportDataToEzbookkeepingCSVHandler returns exported data in csv format
|
||||
func (a *DataManagementsApi) ExportDataToEzbookkeepingCSVHandler(c *core.WebContext) ([]byte, string, *errs.Error) {
|
||||
return a.getExportedFileContent(c, "csv")
|
||||
}
|
||||
|
||||
// ExportDataToEzbookkeepingTSVHandler returns exported data in csv format
|
||||
func (a *DataManagementsApi) ExportDataToEzbookkeepingTSVHandler(c *core.WebContext) ([]byte, string, *errs.Error) {
|
||||
return a.getExportedFileContent(c, "tsv")
|
||||
}
|
||||
|
||||
// DataStatisticsHandler returns user data statistics
|
||||
func (a *DataManagementsApi) DataStatisticsHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
uid := c.GetCurrentUid()
|
||||
totalAccountCount, err := a.accounts.GetTotalAccountCountByUid(c, uid)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[data_managements.DataStatisticsHandler] failed to get total account count for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.ErrOperationFailed
|
||||
}
|
||||
|
||||
totalTransactionCategoryCount, err := a.categories.GetTotalCategoryCountByUid(c, uid)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[data_managements.DataStatisticsHandler] failed to get total transaction category count for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.ErrOperationFailed
|
||||
}
|
||||
|
||||
totalTransactionTagCount, err := a.tags.GetTotalTagCountByUid(c, uid)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[data_managements.DataStatisticsHandler] failed to get total transaction tag count for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.ErrOperationFailed
|
||||
}
|
||||
|
||||
totalTransactionCount, err := a.transactions.GetTotalTransactionCountByUid(c, uid)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[data_managements.DataStatisticsHandler] failed to get total transaction count for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.ErrOperationFailed
|
||||
}
|
||||
|
||||
totalTransactionPictureCount, err := a.pictures.GetTotalTransactionPicturesCountByUid(c, uid)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[data_managements.DataStatisticsHandler] failed to get total transaction picture count for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.ErrOperationFailed
|
||||
}
|
||||
|
||||
totalTransactionTemplateCount, err := a.templates.GetTotalNormalTemplateCountByUid(c, uid)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[data_managements.DataStatisticsHandler] failed to get total transaction template count for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.ErrOperationFailed
|
||||
}
|
||||
|
||||
totalScheduledTransactionCount, err := a.templates.GetTotalScheduledTemplateCountByUid(c, uid)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[data_managements.DataStatisticsHandler] failed to get total scheduled transaction count for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.ErrOperationFailed
|
||||
}
|
||||
|
||||
dataStatisticsResp := &models.DataStatisticsResponse{
|
||||
TotalAccountCount: totalAccountCount,
|
||||
TotalTransactionCategoryCount: totalTransactionCategoryCount,
|
||||
TotalTransactionTagCount: totalTransactionTagCount,
|
||||
TotalTransactionCount: totalTransactionCount,
|
||||
TotalTransactionPictureCount: totalTransactionPictureCount,
|
||||
TotalTransactionTemplateCount: totalTransactionTemplateCount,
|
||||
TotalScheduledTransactionCount: totalScheduledTransactionCount,
|
||||
}
|
||||
|
||||
return dataStatisticsResp, nil
|
||||
}
|
||||
|
||||
// ClearDataHandler deletes all user data
|
||||
func (a *DataManagementsApi) ClearDataHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
var clearDataReq models.ClearDataRequest
|
||||
err := c.ShouldBindJSON(&clearDataReq)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[data_managements.ClearDataHandler] 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, "[data_managements.ClearDataHandler] failed to get user for user \"uid:%d\", because %s", uid, err.Error())
|
||||
}
|
||||
|
||||
return nil, errs.ErrUserNotFound
|
||||
}
|
||||
|
||||
if !a.users.IsPasswordEqualsUserPassword(clearDataReq.Password, user) {
|
||||
return nil, errs.ErrUserPasswordWrong
|
||||
}
|
||||
|
||||
if user.FeatureRestriction.Contains(core.USER_FEATURE_RESTRICTION_TYPE_CLEAR_ALL_DATA) {
|
||||
return nil, errs.ErrNotPermittedToPerformThisAction
|
||||
}
|
||||
|
||||
err = a.templates.DeleteAllTemplates(c, uid)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[data_managements.ClearDataHandler] failed to delete all transaction templates, because %s", err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
err = a.transactions.DeleteAllTransactions(c, uid)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[data_managements.ClearDataHandler] failed to delete all transactions, because %s", err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
err = a.categories.DeleteAllCategories(c, uid)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[data_managements.ClearDataHandler] failed to delete all transaction categories, because %s", err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
err = a.tags.DeleteAllTags(c, uid)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[data_managements.ClearDataHandler] failed to delete all transaction tags, because %s", err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
log.Infof(c, "[data_managements.ClearDataHandler] user \"uid:%d\" has cleared all data", uid)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (a *DataManagementsApi) getExportedFileContent(c *core.WebContext, fileType string) ([]byte, string, *errs.Error) {
|
||||
if !a.CurrentConfig().EnableDataExport {
|
||||
// ExportDataHandler returns exported data in csv format
|
||||
func (a *DataManagementsApi) ExportDataHandler(c *core.Context) ([]byte, string, *errs.Error) {
|
||||
if !settings.Container.Current.EnableDataExport {
|
||||
return nil, "", errs.ErrDataExportNotAllowed
|
||||
}
|
||||
|
||||
@@ -192,7 +51,7 @@ func (a *DataManagementsApi) getExportedFileContent(c *core.WebContext, fileType
|
||||
utcOffset, err := c.GetClientTimezoneOffset()
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[data_managements.ExportDataHandler] cannot get client timezone offset, because %s", err.Error())
|
||||
log.WarnfWithRequestId(c, "[data_managements.ExportDataHandler] cannot get client timezone offset, because %s", err.Error())
|
||||
} else {
|
||||
timezone = time.FixedZone("Client Timezone", int(utcOffset)*60)
|
||||
}
|
||||
@@ -202,41 +61,37 @@ func (a *DataManagementsApi) getExportedFileContent(c *core.WebContext, fileType
|
||||
|
||||
if err != nil {
|
||||
if !errs.IsCustomError(err) {
|
||||
log.Warnf(c, "[data_managements.ExportDataHandler] failed to get user for user \"uid:%d\", because %s", uid, err.Error())
|
||||
log.WarnfWithRequestId(c, "[data_managements.ExportDataHandler] failed to get user for user \"uid:%d\", because %s", uid, err.Error())
|
||||
}
|
||||
|
||||
return nil, "", errs.ErrUserNotFound
|
||||
}
|
||||
|
||||
if user.FeatureRestriction.Contains(core.USER_FEATURE_RESTRICTION_TYPE_EXPORT_TRANSACTION) {
|
||||
return nil, "", errs.ErrNotPermittedToPerformThisAction
|
||||
}
|
||||
|
||||
accounts, err := a.accounts.GetAllAccountsByUid(c, uid)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[data_managements.ExportDataHandler] failed to get all accounts for user \"uid:%d\", because %s", uid, err.Error())
|
||||
log.ErrorfWithRequestId(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.ExportDataHandler] failed to get categories for user \"uid:%d\", because %s", uid, err.Error())
|
||||
log.ErrorfWithRequestId(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.ExportDataHandler] failed to get tags for user \"uid:%d\", because %s", uid, err.Error())
|
||||
log.ErrorfWithRequestId(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)
|
||||
tagIndexs, err := a.tags.GetAllTagIdsOfAllTransactions(c, uid)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[data_managements.ExportDataHandler] failed to get tag index for user \"uid:%d\", because %s", uid, err.Error())
|
||||
log.ErrorfWithRequestId(c, "[data_managements.ExportDataHandler] failed to get tag index for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, "", errs.ErrOperationFailed
|
||||
}
|
||||
|
||||
@@ -247,33 +102,118 @@ func (a *DataManagementsApi) getExportedFileContent(c *core.WebContext, fileType
|
||||
allTransactions, err := a.transactions.GetAllTransactions(c, uid, pageCountForDataExport, true)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[data_managements.ExportDataHandler] failed to all transactions user \"uid:%d\", because %s", uid, err.Error())
|
||||
log.ErrorfWithRequestId(c, "[data_managements.ExportDataHandler] failed to all transactions user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, "", errs.ErrOperationFailed
|
||||
}
|
||||
|
||||
dataExporter := converters.GetTransactionDataExporter(fileType)
|
||||
|
||||
if dataExporter == nil {
|
||||
return nil, "", errs.ErrNotImplemented
|
||||
}
|
||||
|
||||
result, err := dataExporter.ToExportedContent(c, uid, allTransactions, accountMap, categoryMap, tagMap, tagIndexes)
|
||||
result, err := a.exporter.ToExportedContent(uid, timezone, allTransactions, accountMap, categoryMap, tagMap, tagIndexs)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[data_managements.ExportDataHandler] failed to get csv format exported data for \"uid:%d\", because %s", uid, err.Error())
|
||||
log.ErrorfWithRequestId(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, timezone, fileType)
|
||||
fileName := a.getFileName(user, timezone)
|
||||
|
||||
return result, fileName, nil
|
||||
}
|
||||
|
||||
func (a *DataManagementsApi) getFileName(user *models.User, timezone *time.Location, fileExtension string) string {
|
||||
// DataStatisticsHandler returns user data statistics
|
||||
func (a *DataManagementsApi) DataStatisticsHandler(c *core.Context) (any, *errs.Error) {
|
||||
uid := c.GetCurrentUid()
|
||||
totalAccountCount, err := a.accounts.GetTotalAccountCountByUid(c, uid)
|
||||
|
||||
if err != nil {
|
||||
log.ErrorfWithRequestId(c, "[data_managements.DataStatisticsHandler] failed to get total account count for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.ErrOperationFailed
|
||||
}
|
||||
|
||||
totalTransactionCategoryCount, err := a.categories.GetTotalCategoryCountByUid(c, uid)
|
||||
|
||||
if err != nil {
|
||||
log.ErrorfWithRequestId(c, "[data_managements.DataStatisticsHandler] failed to get total transaction category count for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.ErrOperationFailed
|
||||
}
|
||||
|
||||
totalTransactionTagCount, err := a.tags.GetTotalTagCountByUid(c, uid)
|
||||
|
||||
if err != nil {
|
||||
log.ErrorfWithRequestId(c, "[data_managements.DataStatisticsHandler] failed to get total transaction tag count for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.ErrOperationFailed
|
||||
}
|
||||
|
||||
totalTransactionCount, err := a.transactions.GetTotalTransactionCountByUid(c, uid)
|
||||
|
||||
if err != nil {
|
||||
log.ErrorfWithRequestId(c, "[data_managements.DataStatisticsHandler] failed to get total transaction count for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.ErrOperationFailed
|
||||
}
|
||||
|
||||
dataStatisticsResp := &models.DataStatisticsResponse{
|
||||
TotalAccountCount: totalAccountCount,
|
||||
TotalTransactionCategoryCount: totalTransactionCategoryCount,
|
||||
TotalTransactionTagCount: totalTransactionTagCount,
|
||||
TotalTransactionCount: totalTransactionCount,
|
||||
}
|
||||
|
||||
return dataStatisticsResp, nil
|
||||
}
|
||||
|
||||
// ClearDataHandler deletes all user data
|
||||
func (a *DataManagementsApi) ClearDataHandler(c *core.Context) (any, *errs.Error) {
|
||||
var clearDataReq models.ClearDataRequest
|
||||
err := c.ShouldBindJSON(&clearDataReq)
|
||||
|
||||
if err != nil {
|
||||
log.WarnfWithRequestId(c, "[data_managements.ClearDataHandler] 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.WarnfWithRequestId(c, "[data_managements.ClearDataHandler] failed to get user for user \"uid:%d\", because %s", uid, err.Error())
|
||||
}
|
||||
|
||||
return nil, errs.ErrUserNotFound
|
||||
}
|
||||
|
||||
if !a.users.IsPasswordEqualsUserPassword(clearDataReq.Password, user) {
|
||||
return nil, errs.ErrUserPasswordWrong
|
||||
}
|
||||
|
||||
err = a.transactions.DeleteAllTransactions(c, uid)
|
||||
|
||||
if err != nil {
|
||||
log.ErrorfWithRequestId(c, "[data_managements.ClearDataHandler] failed to delete all transactions, because %s", err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
err = a.categories.DeleteAllCategories(c, uid)
|
||||
|
||||
if err != nil {
|
||||
log.ErrorfWithRequestId(c, "[data_managements.ClearDataHandler] failed to delete all transaction categories, because %s", err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
err = a.tags.DeleteAllTags(c, uid)
|
||||
|
||||
if err != nil {
|
||||
log.ErrorfWithRequestId(c, "[data_managements.ClearDataHandler] failed to delete all transaction tags, because %s", err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
log.InfofWithRequestId(c, "[data_managements.ClearDataHandler] user \"uid:%d\" has cleared all data", uid)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (a *DataManagementsApi) getFileName(user *models.User, timezone *time.Location) string {
|
||||
currentTime := utils.FormatUnixTimeToLongDateTimeWithoutSecond(time.Now().Unix(), timezone)
|
||||
currentTime = strings.Replace(currentTime, "-", "_", -1)
|
||||
currentTime = strings.Replace(currentTime, " ", "_", -1)
|
||||
currentTime = strings.Replace(currentTime, ":", "_", -1)
|
||||
|
||||
return fmt.Sprintf("%s_%s.%s", user.Username, currentTime, fileExtension)
|
||||
return fmt.Sprintf("%s_%s.csv", user.Username, currentTime)
|
||||
}
|
||||
|
||||
+2
-2
@@ -14,11 +14,11 @@ var (
|
||||
)
|
||||
|
||||
// ApiNotFound returns api not found error
|
||||
func (a *DefaultApi) ApiNotFound(c *core.WebContext) (any, *errs.Error) {
|
||||
func (a *DefaultApi) ApiNotFound(c *core.Context) (any, *errs.Error) {
|
||||
return nil, errs.ErrApiNotFound
|
||||
}
|
||||
|
||||
// MethodNotAllowed returns method not allowed error
|
||||
func (a *DefaultApi) MethodNotAllowed(c *core.WebContext) (any, *errs.Error) {
|
||||
func (a *DefaultApi) MethodNotAllowed(c *core.Context) (any, *errs.Error) {
|
||||
return nil, errs.ErrMethodNotAllowed
|
||||
}
|
||||
|
||||
+12
-38
@@ -2,7 +2,6 @@ package api
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"sort"
|
||||
@@ -14,25 +13,18 @@ import (
|
||||
"github.com/mayswind/ezbookkeeping/pkg/log"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||
)
|
||||
|
||||
// ExchangeRatesApi represents exchange rate api
|
||||
type ExchangeRatesApi struct {
|
||||
ApiUsingConfig
|
||||
}
|
||||
type ExchangeRatesApi struct{}
|
||||
|
||||
// Initialize a exchange rate api singleton instance
|
||||
var (
|
||||
ExchangeRates = &ExchangeRatesApi{
|
||||
ApiUsingConfig: ApiUsingConfig{
|
||||
container: settings.Container,
|
||||
},
|
||||
}
|
||||
ExchangeRates = &ExchangeRatesApi{}
|
||||
)
|
||||
|
||||
// LatestExchangeRateHandler returns latest exchange rate data
|
||||
func (a *ExchangeRatesApi) LatestExchangeRateHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
func (a *ExchangeRatesApi) LatestExchangeRateHandler(c *core.Context) (any, *errs.Error) {
|
||||
dataSource := exchangerates.Container.Current
|
||||
|
||||
if dataSource == nil {
|
||||
@@ -42,9 +34,8 @@ func (a *ExchangeRatesApi) LatestExchangeRateHandler(c *core.WebContext) (any, *
|
||||
uid := c.GetCurrentUid()
|
||||
|
||||
transport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
utils.SetProxyUrl(transport, a.CurrentConfig().ExchangeRatesProxy)
|
||||
|
||||
if a.CurrentConfig().ExchangeRatesSkipTLSVerify {
|
||||
if settings.Container.Current.ExchangeRatesSkipTLSVerify {
|
||||
transport.TLSClientConfig = &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
}
|
||||
@@ -52,48 +43,31 @@ func (a *ExchangeRatesApi) LatestExchangeRateHandler(c *core.WebContext) (any, *
|
||||
|
||||
client := &http.Client{
|
||||
Transport: transport,
|
||||
Timeout: time.Duration(a.CurrentConfig().ExchangeRatesRequestTimeout) * time.Millisecond,
|
||||
Timeout: time.Duration(settings.Container.Current.ExchangeRatesRequestTimeout) * time.Millisecond,
|
||||
}
|
||||
|
||||
requests, err := dataSource.BuildRequests()
|
||||
urls := dataSource.GetRequestUrls()
|
||||
exchangeRateResps := make([]*models.LatestExchangeRateResponse, 0, len(urls))
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[exchange_rates.LatestExchangeRateHandler] failed to build requests for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.ErrFailedToRequestRemoteApi
|
||||
}
|
||||
|
||||
exchangeRateResps := make([]*models.LatestExchangeRateResponse, 0, len(requests))
|
||||
|
||||
for i := 0; i < len(requests); i++ {
|
||||
req := requests[i]
|
||||
|
||||
if len(req.Header.Values("User-Agent")) < 1 {
|
||||
req.Header.Set("User-Agent", fmt.Sprintf("ezBookkeeping/%s", settings.Version))
|
||||
} else if req.Header.Get("User-Agent") == "" {
|
||||
req.Header.Del("User-Agent")
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
for i := 0; i < len(urls); i++ {
|
||||
resp, err := client.Get(urls[i])
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[exchange_rates.LatestExchangeRateHandler] failed to request latest exchange rate data for user \"uid:%d\", because %s", uid, err.Error())
|
||||
log.ErrorfWithRequestId(c, "[exchange_rates.LatestExchangeRateHandler] failed to request latest exchange rate data for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.ErrFailedToRequestRemoteApi
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
log.Errorf(c, "[exchange_rates.LatestExchangeRateHandler] failed to get latest exchange rate data response for user \"uid:%d\", because response code is not 200", uid)
|
||||
log.ErrorfWithRequestId(c, "[exchange_rates.LatestExchangeRateHandler] failed to get latest exchange rate data response for user \"uid:%d\", because response code is not 200", uid)
|
||||
return nil, errs.ErrFailedToRequestRemoteApi
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
|
||||
log.Debugf(c, "[exchange_rates.LatestExchangeRateHandler] response#%d is %s", i, body)
|
||||
|
||||
exchangeRateResp, err := dataSource.Parse(c, body)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[exchange_rates.LatestExchangeRateHandler] failed to parse response for user \"uid:%d\", because %s", uid, err.Error())
|
||||
log.ErrorfWithRequestId(c, "[exchange_rates.LatestExchangeRateHandler] failed to parse response for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrFailedToRequestRemoteApi)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,351 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/exchangerates"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||
)
|
||||
|
||||
func TestExchangeRatesApiLatestExchangeRateHandler_ReserveBankOfAustraliaDataSource(t *testing.T) {
|
||||
exchangeRateResponse := executeLatestExchangeRateHandler(t, settings.ReserveBankOfAustraliaDataSource)
|
||||
|
||||
if exchangeRateResponse == nil {
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, "AUD", exchangeRateResponse.BaseCurrency)
|
||||
|
||||
supportedCurrencyCodes := []string{"CAD", "CHF", "CNY", "EUR", "GBP", "HKD", "IDR", "INR", "JPY", "KRW",
|
||||
"MYR", "NZD", "PHP", "SGD", "THB", "TWD", "USD", "VND"}
|
||||
|
||||
checkExchangeRatesHaveSpecifiedCurrencies(t, exchangeRateResponse.BaseCurrency, supportedCurrencyCodes, exchangeRateResponse.ExchangeRates)
|
||||
}
|
||||
|
||||
func TestExchangeRatesApiLatestExchangeRateHandler_BankOfCanadaDataSource(t *testing.T) {
|
||||
exchangeRateResponse := executeLatestExchangeRateHandler(t, settings.BankOfCanadaDataSource)
|
||||
|
||||
if exchangeRateResponse == nil {
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, "CAD", exchangeRateResponse.BaseCurrency)
|
||||
|
||||
supportedCurrencyCodes := []string{"AUD", "BRL", "CHF", "CNY", "EUR", "GBP", "HKD", "IDR", "INR",
|
||||
"JPY", "KRW", "MXN", "MYR", "NOK", "NZD", "PEN", "RUB", "SAR", "SEK", "SGD", "THB", "TRY", "TWD",
|
||||
"USD", "VND", "ZAR"}
|
||||
|
||||
checkExchangeRatesHaveSpecifiedCurrencies(t, exchangeRateResponse.BaseCurrency, supportedCurrencyCodes, exchangeRateResponse.ExchangeRates)
|
||||
}
|
||||
|
||||
func TestExchangeRatesApiLatestExchangeRateHandler_CzechNationalBankDataSource(t *testing.T) {
|
||||
exchangeRateResponse := executeLatestExchangeRateHandler(t, settings.CzechNationalBankDataSource)
|
||||
|
||||
if exchangeRateResponse == nil {
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, "CZK", exchangeRateResponse.BaseCurrency)
|
||||
|
||||
supportedCurrencyCodes := []string{"AED", "AFN", "ALL", "AMD", "ANG", "AOA", "ARS", "AUD", "AWG", "AZN",
|
||||
"BAM", "BBD", "BDT", "BGN", "BHD", "BIF", "BMD", "BND", "BOB", "BRL", "BSD", "BTN", "BWP", "BYN", "BZD",
|
||||
"CAD", "CDF", "CHF", "CLP", "CNY", "COP", "CRC", "CUP", "CVE", "DJF", "DKK", "DOP", "DZD",
|
||||
"EGP", "ERN", "ETB", "EUR", "FJD", "FKP", "GBP", "GEL", "GHS", "GIP", "GMD", "GNF", "GTQ", "GYD",
|
||||
"HKD", "HNL", "HTG", "HUF", "IDR", "ILS", "INR", "IQD", "IRR", "ISK", "JMD", "JOD", "JPY",
|
||||
"KES", "KGS", "KHR", "KMF", "KPW", "KRW", "KWD", "KYD", "KZT", "LAK", "LBP", "LKR", "LRD", "LSL", "LYD",
|
||||
"MAD", "MDL", "MGA", "MKD", "MMK", "MNT", "MOP", "MRU", "MUR", "MVR", "MWK", "MXN", "MYR", "MZN",
|
||||
"NAD", "NGN", "NIO", "NOK", "NPR", "NZD", "OMR", "PAB", "PEN", "PGK", "PHP", "PKR", "PLN", "PYG",
|
||||
"QAR", "RON", "RSD", "RUB", "RWF",
|
||||
"SAR", "SBD", "SCR", "SDG", "SEK", "SGD", "SHP", "SLE", "SOS", "SRD", "SSP", "STN", "SVC", "SYP", "SZL",
|
||||
"THB", "TJS", "TMT", "TND", "TOP", "TRY", "TTD", "TWD", "TZS", "UAH", "UGX", "USD", "UYU", "UZS",
|
||||
"VND", "VUV", "WST", "XAF", "XCD", "XOF", "XPF", "YER", "ZAR", "ZMW"}
|
||||
|
||||
checkExchangeRatesHaveSpecifiedCurrencies(t, exchangeRateResponse.BaseCurrency, supportedCurrencyCodes, exchangeRateResponse.ExchangeRates)
|
||||
}
|
||||
|
||||
func TestExchangeRatesApiLatestExchangeRateHandler_DanmarksNationalbankDataSource(t *testing.T) {
|
||||
exchangeRateResponse := executeLatestExchangeRateHandler(t, settings.DanmarksNationalbankDataSource)
|
||||
|
||||
if exchangeRateResponse == nil {
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, "DKK", exchangeRateResponse.BaseCurrency)
|
||||
|
||||
supportedCurrencyCodes := []string{"AUD", "BGN", "BRL", "CAD", "CHF", "CNY", "CZK", "EUR", "GBP", "HKD", "HUF",
|
||||
"IDR", "ILS", "INR", "ISK", "JPY", "KRW", "MXN", "MYR", "NOK", "NZD", "PHP", "PLN", "RON", "SEK", "SGD",
|
||||
"THB", "TRY", "USD", "ZAR"}
|
||||
|
||||
checkExchangeRatesHaveSpecifiedCurrencies(t, exchangeRateResponse.BaseCurrency, supportedCurrencyCodes, exchangeRateResponse.ExchangeRates)
|
||||
}
|
||||
|
||||
func TestExchangeRatesApiLatestExchangeRateHandler_EuroCentralBankDataSource(t *testing.T) {
|
||||
exchangeRateResponse := executeLatestExchangeRateHandler(t, settings.EuroCentralBankDataSource)
|
||||
|
||||
if exchangeRateResponse == nil {
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, "EUR", exchangeRateResponse.BaseCurrency)
|
||||
|
||||
supportedCurrencyCodes := []string{"AUD", "BGN", "BRL", "CAD", "CHF", "CNY", "CZK", "DKK", "GBP", "HKD", "HUF",
|
||||
"IDR", "ILS", "INR", "ISK", "JPY", "KRW", "MXN", "MYR", "NOK", "NZD", "PHP", "PLN", "RON", "SEK", "SGD",
|
||||
"THB", "TRY", "USD", "ZAR"}
|
||||
|
||||
checkExchangeRatesHaveSpecifiedCurrencies(t, exchangeRateResponse.BaseCurrency, supportedCurrencyCodes, exchangeRateResponse.ExchangeRates)
|
||||
}
|
||||
|
||||
func TestExchangeRatesApiLatestExchangeRateHandler_NationalBankOfGeorgiaDataSource(t *testing.T) {
|
||||
exchangeRateResponse := executeLatestExchangeRateHandler(t, settings.NationalBankOfGeorgiaDataSource)
|
||||
|
||||
if exchangeRateResponse == nil {
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, "GEL", exchangeRateResponse.BaseCurrency)
|
||||
|
||||
supportedCurrencyCodes := []string{"AED", "AMD", "AUD", "AZN", "BGN", "BRL", "BYN", "CAD", "CHF", "CNY", "CZK",
|
||||
"DKK", "EGP", "EUR", "GBP", "HKD", "HUF", "ILS", "INR", "IRR", "ISK", "JPY", "KGS", "KRW", "KWD", "KZT",
|
||||
"MDL", "NOK", "NZD", "PLN", "QAR", "RON", "RSD", "RUB", "SEK", "SGD", "TJS", "TMT", "TRY",
|
||||
"UAH", "USD", "UZS", "ZAR"}
|
||||
|
||||
checkExchangeRatesHaveSpecifiedCurrencies(t, exchangeRateResponse.BaseCurrency, supportedCurrencyCodes, exchangeRateResponse.ExchangeRates)
|
||||
}
|
||||
|
||||
func TestExchangeRatesApiLatestExchangeRateHandler_CentralBankOfHungaryDataSource(t *testing.T) {
|
||||
exchangeRateResponse := executeLatestExchangeRateHandler(t, settings.CentralBankOfHungaryDataSource)
|
||||
|
||||
if exchangeRateResponse == nil {
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, "HUF", exchangeRateResponse.BaseCurrency)
|
||||
|
||||
supportedCurrencyCodes := []string{"AUD", "BGN", "BRL", "CAD", "CHF", "CNY", "CZK", "DKK", "EUR",
|
||||
"GBP", "HKD", "IDR", "ILS", "INR", "ISK", "JPY", "KRW", "MXN", "MYR", "NOK", "NZD",
|
||||
"PHP", "PLN", "RON", "RSD", "RUB", "SEK", "SGD", "THB", "TRY", "UAH", "USD", "ZAR"}
|
||||
|
||||
checkExchangeRatesHaveSpecifiedCurrencies(t, exchangeRateResponse.BaseCurrency, supportedCurrencyCodes, exchangeRateResponse.ExchangeRates)
|
||||
}
|
||||
|
||||
func TestExchangeRatesApiLatestExchangeRateHandler_BankOfIsraelDataSource(t *testing.T) {
|
||||
exchangeRateResponse := executeLatestExchangeRateHandler(t, settings.BankOfIsraelDataSource)
|
||||
|
||||
if exchangeRateResponse == nil {
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, "ILS", exchangeRateResponse.BaseCurrency)
|
||||
|
||||
supportedCurrencyCodes := []string{"AUD", "CAD", "CHF", "DKK", "EGP", "EUR", "GBP",
|
||||
"JOD", "JPY", "LBP", "NOK", "SEK", "USD", "ZAR"}
|
||||
|
||||
checkExchangeRatesHaveSpecifiedCurrencies(t, exchangeRateResponse.BaseCurrency, supportedCurrencyCodes, exchangeRateResponse.ExchangeRates)
|
||||
}
|
||||
|
||||
func TestExchangeRatesApiLatestExchangeRateHandler_CentralBankOfMyanmarDataSource(t *testing.T) {
|
||||
exchangeRateResponse := executeLatestExchangeRateHandler(t, settings.CentralBankOfMyanmarDataSource)
|
||||
|
||||
if exchangeRateResponse == nil {
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, "MMK", exchangeRateResponse.BaseCurrency)
|
||||
|
||||
supportedCurrencyCodes := []string{"AUD", "BDT", "BND", "BRL", "CAD", "CHF", "CNY", "CZK", "DKK",
|
||||
"EGP", "EUR", "GBP", "HKD", "IDR", "ILS", "INR", "JPY", "KES", "KHR", "KRW", "KWD", "LAK", "LKR",
|
||||
"MYR", "NOK", "NPR", "NZD", "PHP", "PKR", "RSD", "RUB", "SAR", "SEK", "SGD", "THB", "USD", "VND", "ZAR"}
|
||||
|
||||
checkExchangeRatesHaveSpecifiedCurrencies(t, exchangeRateResponse.BaseCurrency, supportedCurrencyCodes, exchangeRateResponse.ExchangeRates)
|
||||
}
|
||||
|
||||
func TestExchangeRatesApiLatestExchangeRateHandler_NorgesBankDataSource(t *testing.T) {
|
||||
exchangeRateResponse := executeLatestExchangeRateHandler(t, settings.NorgesBankDataSource)
|
||||
|
||||
if exchangeRateResponse == nil {
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, "NOK", exchangeRateResponse.BaseCurrency)
|
||||
|
||||
supportedCurrencyCodes := []string{"AUD", "BDT", "BGN", "BRL", "BYN", "CAD", "CHF", "CNY", "CZK", "DKK",
|
||||
"EUR", "GBP", "HKD", "HUF", "IDR", "ILS", "INR", "ISK", "JPY", "KRW", "MMK", "MXN", "MYR", "NZD",
|
||||
"PHP", "PKR", "PLN", "RON", "RUB", "SEK", "SGD", "THB", "TRY", "TWD", "USD", "VND", "ZAR"}
|
||||
|
||||
checkExchangeRatesHaveSpecifiedCurrencies(t, exchangeRateResponse.BaseCurrency, supportedCurrencyCodes, exchangeRateResponse.ExchangeRates)
|
||||
}
|
||||
|
||||
func TestExchangeRatesApiLatestExchangeRateHandler_NationalBankOfPolandDataSource(t *testing.T) {
|
||||
exchangeRateResponse := executeLatestExchangeRateHandler(t, settings.NationalBankOfPolandDataSource)
|
||||
|
||||
if exchangeRateResponse == nil {
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, "PLN", exchangeRateResponse.BaseCurrency)
|
||||
|
||||
supportedCurrencyCodes := []string{"AED", "AFN", "ALL", "AMD", "ANG", "AOA", "ARS", "AUD", "AWG", "AZN",
|
||||
"BAM", "BBD", "BDT", "BGN", "BHD", "BIF", "BND", "BOB", "BRL", "BSD", "BWP", "BYN", "BZD",
|
||||
"CAD", "CDF", "CHF", "CLP", "CNY", "COP", "CRC", "CUP", "CVE", "CZK",
|
||||
"DJF", "DKK", "DOP", "DZD", "EGP", "ERN", "ETB", "EUR", "FJD",
|
||||
"GBP", "GEL", "GHS", "GIP", "GMD", "GNF", "GTQ", "GYD",
|
||||
"HKD", "HNL", "HTG", "HUF", "IDR", "ILS", "INR", "IQD", "IRR", "ISK",
|
||||
"JMD", "JOD", "JPY", "KES", "KGS", "KHR", "KMF", "KRW", "KWD", "KZT",
|
||||
"LAK", "LBP", "LKR", "LRD", "LSL", "LYD",
|
||||
"MAD", "MDL", "MGA", "MKD", "MMK", "MNT", "MOP", "MRU", "MUR", "MVR", "MWK", "MXN", "MYR", "MZN",
|
||||
"NAD", "NGN", "NIO", "NOK", "NPR", "NZD", "OMR", "PAB", "PEN", "PGK", "PHP", "PKR", "PYG",
|
||||
"QAR", "RON", "RSD", "RUB", "RWF",
|
||||
"SAR", "SBD", "SCR", "SDG", "SEK", "SGD", "SLE", "SOS", "SRD", "SSP", "STN", "SVC", "SYP", "SZL",
|
||||
"THB", "TJS", "TMT", "TND", "TOP", "TRY", "TTD", "TWD", "TZS", "UAH", "UGX", "USD", "UYU", "UZS",
|
||||
"VES", "VND", "VUV", "WST", "XAF", "XCD", "XOF", "XPF", "YER", "ZAR", "ZMW", "ZWG"}
|
||||
|
||||
checkExchangeRatesHaveSpecifiedCurrencies(t, exchangeRateResponse.BaseCurrency, supportedCurrencyCodes, exchangeRateResponse.ExchangeRates)
|
||||
}
|
||||
|
||||
func TestExchangeRatesApiLatestExchangeRateHandler_NationalBankOfRomaniaDataSource(t *testing.T) {
|
||||
exchangeRateResponse := executeLatestExchangeRateHandler(t, settings.NationalBankOfRomaniaDataSource)
|
||||
|
||||
if exchangeRateResponse == nil {
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, "RON", exchangeRateResponse.BaseCurrency)
|
||||
|
||||
supportedCurrencyCodes := []string{"AED", "AUD", "BGN", "BRL", "CAD", "CHF", "CNY", "CZK", "DKK", "EGP",
|
||||
"EUR", "GBP", "HKD", "HUF", "IDR", "ILS", "INR", "ISK", "JPY", "KRW", "MDL", "MXN", "MYR",
|
||||
"NOK", "NZD", "PHP", "PLN", "RSD", "RUB", "SEK", "SGD", "THB", "TRY", "UAH", "USD", "ZAR"}
|
||||
|
||||
checkExchangeRatesHaveSpecifiedCurrencies(t, exchangeRateResponse.BaseCurrency, supportedCurrencyCodes, exchangeRateResponse.ExchangeRates)
|
||||
}
|
||||
|
||||
func TestExchangeRatesApiLatestExchangeRateHandler_BankOfRussiaDataSource(t *testing.T) {
|
||||
exchangeRateResponse := executeLatestExchangeRateHandler(t, settings.BankOfRussiaDataSource)
|
||||
|
||||
if exchangeRateResponse == nil {
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, "RUB", exchangeRateResponse.BaseCurrency)
|
||||
|
||||
supportedCurrencyCodes := []string{"AED", "AMD", "AUD", "AZN", "BGN", "BRL", "BYN", "CAD", "CHF", "CNY", "CZK",
|
||||
"DKK", "EGP", "EUR", "GBP", "GEL", "HKD", "HUF", "IDR", "INR", "JPY", "KGS", "KRW", "KZT", "MDL",
|
||||
"NOK", "NZD", "PLN", "QAR", "RON", "RSD", "SEK", "SGD", "THB", "TJS", "TMT", "TRY",
|
||||
"UAH", "USD", "UZS", "VND", "ZAR"}
|
||||
|
||||
checkExchangeRatesHaveSpecifiedCurrencies(t, exchangeRateResponse.BaseCurrency, supportedCurrencyCodes, exchangeRateResponse.ExchangeRates)
|
||||
}
|
||||
|
||||
func TestExchangeRatesApiLatestExchangeRateHandler_SwissNationalBankDataSource(t *testing.T) {
|
||||
exchangeRateResponse := executeLatestExchangeRateHandler(t, settings.SwissNationalBankDataSource)
|
||||
|
||||
if exchangeRateResponse == nil {
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, "CHF", exchangeRateResponse.BaseCurrency)
|
||||
|
||||
supportedCurrencyCodes := []string{"EUR", "GBP", "JPY", "USD"}
|
||||
|
||||
checkExchangeRatesHaveSpecifiedCurrencies(t, exchangeRateResponse.BaseCurrency, supportedCurrencyCodes, exchangeRateResponse.ExchangeRates)
|
||||
}
|
||||
|
||||
func TestExchangeRatesApiLatestExchangeRateHandler_CentralBankOfUzbekistanDataSource(t *testing.T) {
|
||||
exchangeRateResponse := executeLatestExchangeRateHandler(t, settings.CentralBankOfUzbekistanDataSource)
|
||||
|
||||
if exchangeRateResponse == nil {
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, "UZS", exchangeRateResponse.BaseCurrency)
|
||||
|
||||
supportedCurrencyCodes := []string{"AED", "AFN", "AMD", "ARS", "AUD", "AZN",
|
||||
"BDT", "BGN", "BHD", "BND", "BRL", "BYN", "CAD", "CHF", "CNY", "CUP", "CZK",
|
||||
"DKK", "DZD", "EGP", "EUR", "GBP", "GEL", "HKD", "HUF", "IDR", "ILS", "INR", "IQD", "IRR", "ISK",
|
||||
"JOD", "JPY", "KGS", "KHR", "KRW", "KWD", "KZT", "LAK", "LBP", "LYD",
|
||||
"MAD", "MDL", "MMK", "MNT", "MXN", "MYR", "NOK", "NZD", "OMR", "PHP", "PKR", "PLN",
|
||||
"QAR", "RON", "RSD", "RUB", "SAR", "SDG", "SEK", "SGD", "SYP",
|
||||
"THB", "TJS", "TMT", "TND", "TRY", "UAH", "USD", "UYU", "VES", "VND", "YER", "ZAR"}
|
||||
|
||||
checkExchangeRatesHaveSpecifiedCurrencies(t, exchangeRateResponse.BaseCurrency, supportedCurrencyCodes, exchangeRateResponse.ExchangeRates)
|
||||
}
|
||||
|
||||
func TestExchangeRatesApiLatestExchangeRateHandler_InternationalMonetaryFundDataSource(t *testing.T) {
|
||||
exchangeRateResponse := executeLatestExchangeRateHandler(t, settings.InternationalMonetaryFundDataSource)
|
||||
|
||||
if exchangeRateResponse == nil {
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, "USD", exchangeRateResponse.BaseCurrency)
|
||||
|
||||
supportedCurrencyCodes := []string{"AED", "AUD", "BND", "BRL", "BWP", "CAD", "CHF", "CLP", "CNY", "CZK",
|
||||
"DKK", "DZD", "EUR", "GBP", "ILS", "INR", "JPY", "KRW", "KWD", "MUR", "MXN", "MYR", "NOK", "NZD",
|
||||
"OMR", "PEN", "PHP", "PLN", "QAR", "RUB", "SAR", "SEK", "SGD", "THB", "TTD", "UYU", "ZAR"}
|
||||
|
||||
checkExchangeRatesHaveSpecifiedCurrencies(t, exchangeRateResponse.BaseCurrency, supportedCurrencyCodes, exchangeRateResponse.ExchangeRates)
|
||||
}
|
||||
|
||||
func executeLatestExchangeRateHandler(t *testing.T, dataSourceType string) *models.LatestExchangeRateResponse {
|
||||
config := &settings.Config{
|
||||
ExchangeRatesDataSource: dataSourceType,
|
||||
ExchangeRatesRequestTimeout: 10000,
|
||||
ExchangeRatesProxy: "system",
|
||||
ExchangeRatesSkipTLSVerify: true,
|
||||
}
|
||||
|
||||
settingsContainer := &settings.ConfigContainer{
|
||||
Current: config,
|
||||
}
|
||||
|
||||
err := exchangerates.InitializeExchangeRatesDataSource(config)
|
||||
assert.Nil(t, err)
|
||||
|
||||
exchangeRatesApi := &ExchangeRatesApi{
|
||||
ApiUsingConfig: ApiUsingConfig{
|
||||
container: settingsContainer,
|
||||
},
|
||||
}
|
||||
|
||||
ginContext, _ := gin.CreateTestContext(httptest.NewRecorder())
|
||||
|
||||
response, err := exchangeRatesApi.LatestExchangeRateHandler(&core.WebContext{
|
||||
Context: ginContext,
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
|
||||
exchangeRateResponse := response.(*models.LatestExchangeRateResponse)
|
||||
assert.NotNil(t, exchangeRateResponse)
|
||||
|
||||
return exchangeRateResponse
|
||||
}
|
||||
|
||||
func checkExchangeRatesHaveSpecifiedCurrencies(t *testing.T, baseCurrency string, currencyCodes []string, exchangeRates []*models.LatestExchangeRate) {
|
||||
assert.Equal(t, len(currencyCodes)+1, len(exchangeRates))
|
||||
|
||||
currencyCodesInExchangeRates := make(map[string]*models.LatestExchangeRate, len(exchangeRates))
|
||||
|
||||
for i := 0; i < len(exchangeRates); i++ {
|
||||
exchangeRate := exchangeRates[i]
|
||||
currencyCodesInExchangeRates[exchangeRate.Currency] = exchangeRate
|
||||
}
|
||||
|
||||
allCurrencyCodes := append(currencyCodes, baseCurrency)
|
||||
|
||||
for i := 0; i < len(allCurrencyCodes); i++ {
|
||||
exchangeRate, has := currencyCodesInExchangeRates[allCurrencyCodes[i]]
|
||||
assert.True(t, has, allCurrencyCodes[i])
|
||||
|
||||
if has {
|
||||
rate, err := utils.StringToFloat64(exchangeRate.Rate)
|
||||
assert.Nil(t, err)
|
||||
assert.Greater(t, rate, float64(0))
|
||||
}
|
||||
}
|
||||
}
|
||||
+20
-32
@@ -13,7 +13,6 @@ import (
|
||||
|
||||
// ForgetPasswordsApi represents user forget password api
|
||||
type ForgetPasswordsApi struct {
|
||||
ApiUsingConfig
|
||||
users *services.UserService
|
||||
tokens *services.TokenService
|
||||
forgetPasswords *services.ForgetPasswordService
|
||||
@@ -22,9 +21,6 @@ type ForgetPasswordsApi struct {
|
||||
// Initialize a user api singleton instance
|
||||
var (
|
||||
ForgetPasswords = &ForgetPasswordsApi{
|
||||
ApiUsingConfig: ApiUsingConfig{
|
||||
container: settings.Container,
|
||||
},
|
||||
users: services.Users,
|
||||
tokens: services.Tokens,
|
||||
forgetPasswords: services.ForgetPasswords,
|
||||
@@ -32,12 +28,12 @@ var (
|
||||
)
|
||||
|
||||
// UserForgetPasswordRequestHandler generates password reset link and send user an email with this link
|
||||
func (a *ForgetPasswordsApi) UserForgetPasswordRequestHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
func (a *ForgetPasswordsApi) UserForgetPasswordRequestHandler(c *core.Context) (any, *errs.Error) {
|
||||
var request models.ForgetPasswordRequest
|
||||
err := c.ShouldBindJSON(&request)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[forget_passwords.UserForgetPasswordRequestHandler] parse request failed, because %s", err.Error())
|
||||
log.WarnfWithRequestId(c, "[forget_passwords.UserForgetPasswordRequestHandler] parse request failed, because %s", err.Error())
|
||||
return nil, errs.ErrEmailIsEmptyOrInvalid
|
||||
}
|
||||
|
||||
@@ -45,34 +41,30 @@ func (a *ForgetPasswordsApi) UserForgetPasswordRequestHandler(c *core.WebContext
|
||||
|
||||
if err != nil {
|
||||
if !errs.IsCustomError(err) {
|
||||
log.Errorf(c, "[forget_passwords.UserForgetPasswordRequestHandler] failed to get user, because %s", err.Error())
|
||||
log.ErrorfWithRequestId(c, "[forget_passwords.UserForgetPasswordRequestHandler] failed to get user, because %s", err.Error())
|
||||
}
|
||||
|
||||
return nil, errs.ErrUserNotFound
|
||||
}
|
||||
|
||||
if user.Disabled {
|
||||
log.Warnf(c, "[forget_passwords.UserForgetPasswordRequestHandler] user \"uid:%d\" is disabled", user.Uid)
|
||||
log.WarnfWithRequestId(c, "[forget_passwords.UserForgetPasswordRequestHandler] user \"uid:%d\" is disabled", user.Uid)
|
||||
return nil, errs.ErrUserIsDisabled
|
||||
}
|
||||
|
||||
if user.FeatureRestriction.Contains(core.USER_FEATURE_RESTRICTION_TYPE_FORGET_PASSWORD) {
|
||||
return nil, errs.ErrNotPermittedToPerformThisAction
|
||||
}
|
||||
|
||||
if a.CurrentConfig().ForgetPasswordRequireVerifyEmail && !user.EmailVerified {
|
||||
log.Warnf(c, "[forget_passwords.UserForgetPasswordRequestHandler] user \"uid:%d\" has not verified email", user.Uid)
|
||||
if settings.Container.Current.ForgetPasswordRequireVerifyEmail && !user.EmailVerified {
|
||||
log.WarnfWithRequestId(c, "[forget_passwords.UserForgetPasswordRequestHandler] user \"uid:%d\" has not verified email", user.Uid)
|
||||
return nil, errs.ErrEmailIsNotVerified
|
||||
}
|
||||
|
||||
if !a.CurrentConfig().EnableSMTP {
|
||||
if !settings.Container.Current.EnableSMTP {
|
||||
return nil, errs.ErrSMTPServerNotEnabled
|
||||
}
|
||||
|
||||
token, _, err := a.tokens.CreatePasswordResetToken(c, user)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[forget_passwords.UserForgetPasswordRequestHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
log.ErrorfWithRequestId(c, "[forget_passwords.UserForgetPasswordRequestHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
return nil, errs.ErrTokenGenerating
|
||||
}
|
||||
|
||||
@@ -80,7 +72,7 @@ func (a *ForgetPasswordsApi) UserForgetPasswordRequestHandler(c *core.WebContext
|
||||
err = a.forgetPasswords.SendPasswordResetEmail(c, user, token, c.GetClientLocale())
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[forget_passwords.UserForgetPasswordRequestHandler] cannot send email to \"%s\", because %s", user.Email, err.Error())
|
||||
log.WarnfWithRequestId(c, "[forget_passwords.UserForgetPasswordRequestHandler] cannot send email to \"%s\", because %s", user.Email, err.Error())
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -88,12 +80,12 @@ func (a *ForgetPasswordsApi) UserForgetPasswordRequestHandler(c *core.WebContext
|
||||
}
|
||||
|
||||
// UserResetPasswordHandler resets user password by request parameters
|
||||
func (a *ForgetPasswordsApi) UserResetPasswordHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
func (a *ForgetPasswordsApi) UserResetPasswordHandler(c *core.Context) (any, *errs.Error) {
|
||||
var request models.PasswordResetRequest
|
||||
err := c.ShouldBindJSON(&request)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[forget_passwords.UserResetPasswordHandler] parse request failed, because %s", err.Error())
|
||||
log.WarnfWithRequestId(c, "[forget_passwords.UserResetPasswordHandler] parse request failed, because %s", err.Error())
|
||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||
}
|
||||
|
||||
@@ -102,28 +94,24 @@ func (a *ForgetPasswordsApi) UserResetPasswordHandler(c *core.WebContext) (any,
|
||||
|
||||
if err != nil {
|
||||
if !errs.IsCustomError(err) {
|
||||
log.Errorf(c, "[forget_passwords.UserResetPasswordHandler] failed to get user, because %s", err.Error())
|
||||
log.ErrorfWithRequestId(c, "[forget_passwords.UserResetPasswordHandler] failed to get user, because %s", err.Error())
|
||||
}
|
||||
|
||||
return nil, errs.ErrUserNotFound
|
||||
}
|
||||
|
||||
if user.Disabled {
|
||||
log.Warnf(c, "[forget_passwords.UserResetPasswordHandler] user \"uid:%d\" is disabled", user.Uid)
|
||||
log.WarnfWithRequestId(c, "[forget_passwords.UserResetPasswordHandler] user \"uid:%d\" is disabled", user.Uid)
|
||||
return nil, errs.ErrUserIsDisabled
|
||||
}
|
||||
|
||||
if user.FeatureRestriction.Contains(core.USER_FEATURE_RESTRICTION_TYPE_FORGET_PASSWORD) {
|
||||
return nil, errs.ErrNotPermittedToPerformThisAction
|
||||
}
|
||||
|
||||
if a.CurrentConfig().ForgetPasswordRequireVerifyEmail && !user.EmailVerified {
|
||||
log.Warnf(c, "[forget_passwords.UserResetPasswordHandler] user \"uid:%d\" has not verified email", user.Uid)
|
||||
if settings.Container.Current.ForgetPasswordRequireVerifyEmail && !user.EmailVerified {
|
||||
log.WarnfWithRequestId(c, "[forget_passwords.UserResetPasswordHandler] user \"uid:%d\" has not verified email", user.Uid)
|
||||
return nil, errs.ErrEmailIsNotVerified
|
||||
}
|
||||
|
||||
if user.Email != request.Email {
|
||||
log.Warnf(c, "[forget_passwords.UserResetPasswordHandler] request email not equals the user email")
|
||||
log.WarnfWithRequestId(c, "[forget_passwords.UserResetPasswordHandler] request email not equals the user email")
|
||||
return nil, errs.ErrEmptyIsInvalid
|
||||
}
|
||||
|
||||
@@ -132,7 +120,7 @@ func (a *ForgetPasswordsApi) UserResetPasswordHandler(c *core.WebContext) (any,
|
||||
err = a.tokens.DeleteTokenByClaims(c, oldTokenClaims)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[forget_passwords.UserResetPasswordHandler] failed to revoke password reset token \"utid:%s\" for user \"uid:%d\", because %s", oldTokenClaims.UserTokenId, user.Uid, err.Error())
|
||||
log.WarnfWithRequestId(c, "[forget_passwords.UserResetPasswordHandler] failed to revoke password reset token \"utid:%s\" for user \"uid:%d\", because %s", oldTokenClaims.UserTokenId, user.Uid, err.Error())
|
||||
}
|
||||
|
||||
return nil, errs.ErrNewPasswordEqualsOldInvalid
|
||||
@@ -147,7 +135,7 @@ func (a *ForgetPasswordsApi) UserResetPasswordHandler(c *core.WebContext) (any,
|
||||
_, _, err = a.users.UpdateUser(c, userNew, false)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[forget_passwords.UserResetPasswordHandler] failed to update user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
log.ErrorfWithRequestId(c, "[forget_passwords.UserResetPasswordHandler] failed to update user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
@@ -155,9 +143,9 @@ func (a *ForgetPasswordsApi) UserResetPasswordHandler(c *core.WebContext) (any,
|
||||
err = a.tokens.DeleteTokensBeforeTime(c, uid, now)
|
||||
|
||||
if err == nil {
|
||||
log.Infof(c, "[forget_passwords.UserResetPasswordHandler] revoke old tokens before unix time \"%d\" for user \"uid:%d\"", now, user.Uid)
|
||||
log.InfofWithRequestId(c, "[forget_passwords.UserResetPasswordHandler] revoke old tokens before unix time \"%d\" for user \"uid:%d\"", now, user.Uid)
|
||||
} else {
|
||||
log.Warnf(c, "[forget_passwords.UserResetPasswordHandler] failed to revoke old tokens for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
log.WarnfWithRequestId(c, "[forget_passwords.UserResetPasswordHandler] failed to revoke old tokens for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
}
|
||||
|
||||
return true, nil
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ var (
|
||||
)
|
||||
|
||||
// HealthStatusHandler returns the health status of current service
|
||||
func (a *HealthsApi) HealthStatusHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
func (a *HealthsApi) HealthStatusHandler(c *core.Context) (any, *errs.Error) {
|
||||
result := make(map[string]string)
|
||||
|
||||
result["version"] = settings.Version
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
@@ -9,115 +10,56 @@ import (
|
||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||
)
|
||||
|
||||
const openStreetMapTileImageUrlFormat = "https://tile.openstreetmap.org/{z}/{x}/{y}.png" // https://tile.openstreetmap.org/{z}/{x}/{y}.png
|
||||
const openStreetMapHumanitarianStyleTileImageUrlFormat = "https://a.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png" // https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png
|
||||
const openTopoMapTileImageUrlFormat = "https://tile.opentopomap.org/{z}/{x}/{y}.png" // https://tile.opentopomap.org/{z}/{x}/{y}.png
|
||||
const opnvKarteMapTileImageUrlFormat = "https://tileserver.memomaps.de/tilegen/{z}/{x}/{y}.png" // https://tileserver.memomaps.de/tilegen/{z}/{x}/{y}.png
|
||||
const cyclOSMMapTileImageUrlFormat = "https://a.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png" // https://{s}.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png
|
||||
const cartoDBMapTileImageUrlFormat = "https://a.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{scale}.png" // https://{s}.basemaps.cartocdn.com/{style}/{z}/{x}/{y}{scale}.png
|
||||
const tomtomMapTileImageUrlFormat = "https://api.tomtom.com/map/1/tile/basic/main/{z}/{x}/{y}.png" // https://api.tomtom.com/map/{versionNumber}/tile/{layer}/{style}/{z}/{x}/{y}.png?key={key}&language={language}
|
||||
const tianDiTuMapTileImageUrlFormat = "https://t0.tianditu.gov.cn/vec_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}" // https://t{s}.tianditu.gov.cn/vec_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk={key}
|
||||
const tianDiTuMapAnnotationUrlFormat = "https://t0.tianditu.gov.cn/cva_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cva&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}" // https://t{s}.tianditu.gov.cn/cva_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cva&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk={key}
|
||||
const openStreetMapTileImageUrlFormat = "https://tile.openstreetmap.org/%s/%s/%s" // https://tile.openstreetmap.org/{z}/{x}/{y}.png
|
||||
const openStreetMapHumanitarianStyleTileImageUrlFormat = "https://a.tile.openstreetmap.fr/hot/%s/%s/%s" // https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png
|
||||
const openTopoMapTileImageUrlFormat = "https://tile.opentopomap.org/%s/%s/%s" // https://tile.opentopomap.org/{z}/{x}/{y}.png
|
||||
const opnvKarteMapTileImageUrlFormat = "https://tileserver.memomaps.de/tilegen/%s/%s/%s" // https://tileserver.memomaps.de/tilegen/{z}/{x}/{y}.png
|
||||
const cyclOSMMapTileImageUrlFormat = "https://a.tile-cyclosm.openstreetmap.fr/cyclosm/%s/%s/%s" // https://{s}.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png
|
||||
const tomtomMapTileImageUrlFormat = "https://api.tomtom.com/map/1/tile/basic/main/%s/%s/%s" // https://api.tomtom.com/map/{versionNumber}/tile/{layer}/{style}/{z}/{x}/{y}.png?key={key}&language={language}
|
||||
|
||||
// MapImageProxy represents map image proxy
|
||||
type MapImageProxy struct {
|
||||
ApiUsingConfig
|
||||
}
|
||||
|
||||
// Initialize a map image proxy singleton instance
|
||||
var (
|
||||
MapImages = &MapImageProxy{
|
||||
ApiUsingConfig: ApiUsingConfig{
|
||||
container: settings.Container,
|
||||
},
|
||||
}
|
||||
MapImages = &MapImageProxy{}
|
||||
)
|
||||
|
||||
// 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) {
|
||||
if mapProvider == settings.OpenStreetMapProvider {
|
||||
return openStreetMapTileImageUrlFormat, nil
|
||||
} else if mapProvider == settings.OpenStreetMapHumanitarianStyleProvider {
|
||||
return openStreetMapHumanitarianStyleTileImageUrlFormat, nil
|
||||
} else if mapProvider == settings.OpenTopoMapProvider {
|
||||
return openTopoMapTileImageUrlFormat, nil
|
||||
} else if mapProvider == settings.OPNVKarteMapProvider {
|
||||
return opnvKarteMapTileImageUrlFormat, nil
|
||||
} else if mapProvider == settings.CyclOSMMapProvider {
|
||||
return cyclOSMMapTileImageUrlFormat, nil
|
||||
} else if mapProvider == settings.CartoDBMapProvider {
|
||||
return cartoDBMapTileImageUrlFormat, nil
|
||||
} else if mapProvider == settings.TomTomMapProvider {
|
||||
targetUrl := tomtomMapTileImageUrlFormat + "?key=" + p.CurrentConfig().TomTomMapAPIKey
|
||||
language := c.Query("language")
|
||||
|
||||
if language != "" {
|
||||
targetUrl = targetUrl + "&language=" + language
|
||||
}
|
||||
|
||||
return targetUrl, nil
|
||||
} else if mapProvider == settings.TianDiTuProvider {
|
||||
return tianDiTuMapTileImageUrlFormat + "&tk=" + p.CurrentConfig().TianDiTuAPIKey, nil
|
||||
} else if mapProvider == settings.CustomProvider {
|
||||
return p.CurrentConfig().CustomMapTileServerTileLayerUrl, nil
|
||||
}
|
||||
|
||||
return "", errs.ErrParameterInvalid
|
||||
})
|
||||
}
|
||||
|
||||
// MapAnnotationImageProxyHandler returns map annotation image
|
||||
func (p *MapImageProxy) MapAnnotationImageProxyHandler(c *core.WebContext) (*httputil.ReverseProxy, *errs.Error) {
|
||||
return p.mapImageProxyHandler(c, func(c *core.WebContext, mapProvider string) (string, *errs.Error) {
|
||||
if mapProvider == settings.TianDiTuProvider {
|
||||
return tianDiTuMapAnnotationUrlFormat + "&tk=" + p.CurrentConfig().TianDiTuAPIKey, nil
|
||||
} else if mapProvider == settings.CustomProvider {
|
||||
return p.CurrentConfig().CustomMapTileServerAnnotationLayerUrl, nil
|
||||
}
|
||||
|
||||
return "", errs.ErrParameterInvalid
|
||||
})
|
||||
}
|
||||
|
||||
func (p *MapImageProxy) mapImageProxyHandler(c *core.WebContext, fn func(c *core.WebContext, mapProvider string) (string, *errs.Error)) (*httputil.ReverseProxy, *errs.Error) {
|
||||
func (p *MapImageProxy) MapTileImageProxyHandler(c *core.Context) (*httputil.ReverseProxy, *errs.Error) {
|
||||
mapProvider := strings.Replace(c.Query("provider"), "-", "_", -1)
|
||||
targetUrl := ""
|
||||
|
||||
if mapProvider != p.CurrentConfig().MapProvider {
|
||||
return nil, errs.ErrMapProviderNotCurrent
|
||||
if mapProvider == settings.OpenStreetMapProvider {
|
||||
targetUrl = openStreetMapTileImageUrlFormat
|
||||
} else if mapProvider == settings.OpenStreetMapHumanitarianStyleProvider {
|
||||
targetUrl = openStreetMapHumanitarianStyleTileImageUrlFormat
|
||||
} else if mapProvider == settings.OpenTopoMapProvider {
|
||||
targetUrl = openTopoMapTileImageUrlFormat
|
||||
} else if mapProvider == settings.OPNVKarteMapProvider {
|
||||
targetUrl = opnvKarteMapTileImageUrlFormat
|
||||
} else if mapProvider == settings.CyclOSMMapProvider {
|
||||
targetUrl = cyclOSMMapTileImageUrlFormat
|
||||
} else if mapProvider == settings.TomTomMapProvider {
|
||||
targetUrl = tomtomMapTileImageUrlFormat + "?key=" + settings.Container.Current.TomTomMapAPIKey
|
||||
language := c.Query("language")
|
||||
|
||||
if language != "" {
|
||||
targetUrl = targetUrl + "&language=" + language
|
||||
}
|
||||
} else {
|
||||
return nil, errs.ErrParameterInvalid
|
||||
}
|
||||
|
||||
zoomLevel := c.Param("zoomLevel")
|
||||
coordinateX := c.Param("coordinateX")
|
||||
fileName := c.Param("fileName")
|
||||
fileNameParts := strings.Split(fileName, ".")
|
||||
coordinateY := fileNameParts[0]
|
||||
scale := c.Query("scale")
|
||||
|
||||
if len(fileNameParts) != 2 || fileNameParts[len(fileNameParts)-1] != "png" {
|
||||
return nil, errs.ErrImageExtensionNotSupported
|
||||
}
|
||||
|
||||
var err *errs.Error
|
||||
targetUrl, err = fn(c, mapProvider)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
transport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
utils.SetProxyUrl(transport, p.CurrentConfig().MapProxy)
|
||||
|
||||
director := func(req *http.Request) {
|
||||
imageRawUrl := targetUrl
|
||||
imageRawUrl = strings.Replace(imageRawUrl, "{z}", zoomLevel, -1)
|
||||
imageRawUrl = strings.Replace(imageRawUrl, "{x}", coordinateX, -1)
|
||||
imageRawUrl = strings.Replace(imageRawUrl, "{y}", coordinateY, -1)
|
||||
imageRawUrl = strings.Replace(imageRawUrl, "{scale}", scale, -1)
|
||||
zoomLevel := c.Param("zoomLevel")
|
||||
coordinateX := c.Param("coordinateX")
|
||||
fileName := c.Param("fileName")
|
||||
|
||||
imageRawUrl := fmt.Sprintf(targetUrl, zoomLevel, coordinateX, fileName)
|
||||
imageUrl, _ := url.Parse(imageRawUrl)
|
||||
|
||||
req.URL = imageUrl
|
||||
@@ -125,8 +67,5 @@ func (p *MapImageProxy) mapImageProxyHandler(c *core.WebContext, fn func(c *core
|
||||
req.Host = imageUrl.Host
|
||||
}
|
||||
|
||||
return &httputil.ReverseProxy{
|
||||
Transport: transport,
|
||||
Director: director,
|
||||
}, nil
|
||||
return &httputil.ReverseProxy{Director: director}, nil
|
||||
}
|
||||
|
||||
+5
-10
@@ -19,31 +19,26 @@ const (
|
||||
|
||||
// QrCodesApi represents qrcode generator api
|
||||
type QrCodesApi struct {
|
||||
ApiUsingConfig
|
||||
}
|
||||
|
||||
// Initialize a qrcode generator api singleton instance
|
||||
var (
|
||||
QrCodes = &QrCodesApi{
|
||||
ApiUsingConfig: ApiUsingConfig{
|
||||
container: settings.Container,
|
||||
},
|
||||
}
|
||||
QrCodes = &QrCodesApi{}
|
||||
)
|
||||
|
||||
// MobileUrlQrCodeHandler returns a mobile url qr code image
|
||||
func (a *QrCodesApi) MobileUrlQrCodeHandler(c *core.WebContext) ([]byte, string, *errs.Error) {
|
||||
fullUrl := a.CurrentConfig().RootUrl + "mobile"
|
||||
func (a *QrCodesApi) MobileUrlQrCodeHandler(c *core.Context) ([]byte, string, *errs.Error) {
|
||||
fullUrl := settings.Container.Current.RootUrl + "mobile"
|
||||
data, err := a.generateUrlQrCode(c, fullUrl)
|
||||
|
||||
if err != nil {
|
||||
return nil, "", errs.ErrOperationFailed
|
||||
}
|
||||
|
||||
return data, "image/png", nil
|
||||
return data, "", nil
|
||||
}
|
||||
|
||||
func (a *QrCodesApi) generateUrlQrCode(c *core.WebContext, url string) ([]byte, *errs.Error) {
|
||||
func (a *QrCodesApi) generateUrlQrCode(c *core.Context, url string) ([]byte, *errs.Error) {
|
||||
qrCodeImg, _ := qr.Encode(url, qr.M, qr.Auto)
|
||||
qrCodeImg, _ = barcode.Scale(qrCodeImg, qrCodeDefaultWidth, qrCodeDefaultHeight)
|
||||
imgData := &bytes.Buffer{}
|
||||
|
||||
@@ -1,208 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||
)
|
||||
|
||||
const ezbookkeepingServerSettingsGlobalVariableName = "EZBOOKKEEPING_SERVER_SETTINGS"
|
||||
const ezbookkeepingServerSettingsGlobalVariableFullName = "window." + ezbookkeepingServerSettingsGlobalVariableName
|
||||
const ezbookkeepingServerSettingsJavascriptFileHeader = ezbookkeepingServerSettingsGlobalVariableFullName +
|
||||
"=" + ezbookkeepingServerSettingsGlobalVariableFullName + "||{};\n"
|
||||
|
||||
// ServerSettingsApi represents server settings api
|
||||
type ServerSettingsApi struct {
|
||||
ApiUsingConfig
|
||||
}
|
||||
|
||||
// Initialize a server settings api singleton instance
|
||||
var (
|
||||
ServerSettings = &ServerSettingsApi{
|
||||
ApiUsingConfig: ApiUsingConfig{
|
||||
container: settings.Container,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// ServerSettingsJavascriptHandler returns the javascript contains server settings
|
||||
func (a *ServerSettingsApi) ServerSettingsJavascriptHandler(c *core.WebContext) ([]byte, string, *errs.Error) {
|
||||
config := a.CurrentConfig()
|
||||
builder := &strings.Builder{}
|
||||
builder.WriteString(ezbookkeepingServerSettingsJavascriptFileHeader)
|
||||
|
||||
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)
|
||||
|
||||
if config.LoginPageTips.Enabled {
|
||||
a.appendMultiLanguageTipSetting(builder, "lpt", config.LoginPageTips)
|
||||
}
|
||||
|
||||
a.appendStringSetting(builder, "m", config.MapProvider)
|
||||
|
||||
if config.EnableMapDataFetchProxy &&
|
||||
(config.MapProvider == settings.OpenStreetMapProvider ||
|
||||
config.MapProvider == settings.OpenStreetMapHumanitarianStyleProvider ||
|
||||
config.MapProvider == settings.OpenTopoMapProvider ||
|
||||
config.MapProvider == settings.OPNVKarteMapProvider ||
|
||||
config.MapProvider == settings.CyclOSMMapProvider ||
|
||||
config.MapProvider == settings.CartoDBMapProvider ||
|
||||
config.MapProvider == settings.TomTomMapProvider ||
|
||||
config.MapProvider == settings.TianDiTuProvider ||
|
||||
config.MapProvider == settings.CustomProvider) {
|
||||
a.appendBooleanSetting(builder, "mp", config.EnableMapDataFetchProxy)
|
||||
}
|
||||
|
||||
if config.MapProvider == settings.CustomProvider {
|
||||
a.appendStringSetting(builder, "cmzl", fmt.Sprintf("%d-%d-%d", config.CustomMapTileServerMinZoomLevel, config.CustomMapTileServerMaxZoomLevel, config.CustomMapTileServerDefaultZoomLevel))
|
||||
|
||||
if !config.EnableMapDataFetchProxy {
|
||||
a.appendStringSetting(builder, "cmsu", config.CustomMapTileServerTileLayerUrl)
|
||||
|
||||
if config.CustomMapTileServerAnnotationLayerUrl != "" {
|
||||
a.appendStringSetting(builder, "cmau", config.CustomMapTileServerAnnotationLayerUrl)
|
||||
}
|
||||
} else {
|
||||
if config.CustomMapTileServerAnnotationLayerUrl != "" {
|
||||
a.appendBooleanSetting(builder, "cmap", config.EnableMapDataFetchProxy)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if config.MapProvider == settings.TomTomMapProvider && config.TomTomMapAPIKey != "" && !config.EnableMapDataFetchProxy {
|
||||
a.appendStringSetting(builder, "tmak", config.TomTomMapAPIKey)
|
||||
}
|
||||
|
||||
if config.MapProvider == settings.TianDiTuProvider && config.TianDiTuAPIKey != "" && !config.EnableMapDataFetchProxy {
|
||||
a.appendStringSetting(builder, "tdak", config.TianDiTuAPIKey)
|
||||
}
|
||||
|
||||
if config.MapProvider == settings.GoogleMapProvider && config.GoogleMapAPIKey != "" {
|
||||
a.appendStringSetting(builder, "gmak", config.GoogleMapAPIKey)
|
||||
}
|
||||
|
||||
if config.MapProvider == settings.BaiduMapProvider && config.BaiduMapAK != "" {
|
||||
a.appendStringSetting(builder, "bmak", config.BaiduMapAK)
|
||||
}
|
||||
|
||||
if config.MapProvider == settings.AmapProvider && config.AmapApplicationKey != "" {
|
||||
a.appendStringSetting(builder, "amak", config.AmapApplicationKey)
|
||||
}
|
||||
|
||||
if config.MapProvider == settings.AmapProvider && config.AmapSecurityVerificationMethod != "" {
|
||||
a.appendStringSetting(builder, "amsv", config.AmapSecurityVerificationMethod)
|
||||
|
||||
if config.AmapSecurityVerificationMethod == settings.AmapSecurityVerificationExternalProxyMethod {
|
||||
a.appendStringSetting(builder, "amep", config.AmapApiExternalProxyUrl)
|
||||
}
|
||||
|
||||
if config.AmapSecurityVerificationMethod == settings.AmapSecurityVerificationPlainTextMethod {
|
||||
a.appendStringSetting(builder, "amas", config.AmapApplicationSecret)
|
||||
}
|
||||
}
|
||||
|
||||
if config.ExchangeRatesRequestTimeoutExceedDefaultValue {
|
||||
a.appendIntegerSetting(builder, "errt", int(config.ExchangeRatesRequestTimeout))
|
||||
}
|
||||
|
||||
return []byte(builder.String()), "", nil
|
||||
}
|
||||
|
||||
func (a *ServerSettingsApi) appendStringSetting(builder *strings.Builder, key string, value string) {
|
||||
builder.WriteString(ezbookkeepingServerSettingsGlobalVariableFullName)
|
||||
builder.WriteString("[")
|
||||
a.appendEncodedString(builder, key)
|
||||
builder.WriteString("]=")
|
||||
|
||||
a.appendEncodedString(builder, value)
|
||||
|
||||
builder.WriteString(";\n")
|
||||
}
|
||||
|
||||
func (a *ServerSettingsApi) appendMultiLanguageTipSetting(builder *strings.Builder, key string, value settings.TipConfig) {
|
||||
builder.WriteString(ezbookkeepingServerSettingsGlobalVariableFullName)
|
||||
builder.WriteString("[")
|
||||
a.appendEncodedString(builder, key)
|
||||
builder.WriteString("]={\n")
|
||||
|
||||
builder.WriteString("'default'")
|
||||
builder.WriteRune(':')
|
||||
a.appendEncodedString(builder, value.DefaultContent)
|
||||
|
||||
for languageTag, content := range value.MultiLanguageContent {
|
||||
builder.WriteString(",\n")
|
||||
a.appendEncodedString(builder, languageTag)
|
||||
builder.WriteRune(':')
|
||||
a.appendEncodedString(builder, content)
|
||||
}
|
||||
|
||||
builder.WriteString("\n};\n")
|
||||
}
|
||||
|
||||
func (a *ServerSettingsApi) appendBooleanSetting(builder *strings.Builder, key string, value bool) {
|
||||
builder.WriteString(ezbookkeepingServerSettingsGlobalVariableFullName)
|
||||
builder.WriteString("[")
|
||||
a.appendEncodedString(builder, key)
|
||||
builder.WriteString("]=")
|
||||
|
||||
if value {
|
||||
builder.WriteRune('1')
|
||||
} else {
|
||||
builder.WriteRune('0')
|
||||
}
|
||||
|
||||
builder.WriteString(";\n")
|
||||
}
|
||||
|
||||
func (a *ServerSettingsApi) appendIntegerSetting(builder *strings.Builder, key string, value int) {
|
||||
builder.WriteString(ezbookkeepingServerSettingsGlobalVariableFullName)
|
||||
builder.WriteString("[")
|
||||
a.appendEncodedString(builder, key)
|
||||
builder.WriteString("]=")
|
||||
builder.WriteString(utils.IntToString(value))
|
||||
builder.WriteString(";\n")
|
||||
}
|
||||
|
||||
func (a *ServerSettingsApi) appendEncodedString(builder *strings.Builder, content string) {
|
||||
builder.WriteRune('\'')
|
||||
runes := []rune(content)
|
||||
|
||||
for i := 0; i < len(runes); i++ {
|
||||
switch runes[i] {
|
||||
case '\\':
|
||||
builder.WriteRune('\\')
|
||||
builder.WriteRune('\\')
|
||||
case '\'':
|
||||
builder.WriteRune('\\')
|
||||
builder.WriteRune('\'')
|
||||
case '\n':
|
||||
builder.WriteRune('\\')
|
||||
builder.WriteRune('n')
|
||||
case '\r':
|
||||
builder.WriteRune('\\')
|
||||
builder.WriteRune('r')
|
||||
case '\t':
|
||||
builder.WriteRune('\\')
|
||||
builder.WriteRune('t')
|
||||
case '\f':
|
||||
builder.WriteRune('\\')
|
||||
builder.WriteRune('f')
|
||||
case '\b':
|
||||
builder.WriteRune('\\')
|
||||
builder.WriteRune('b')
|
||||
default:
|
||||
builder.WriteRune(runes[i])
|
||||
}
|
||||
}
|
||||
|
||||
builder.WriteRune('\'')
|
||||
}
|
||||
+26
-109
@@ -2,22 +2,17 @@ package api
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/mayswind/ezbookkeeping/pkg/avatars"
|
||||
"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"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||
)
|
||||
|
||||
// TokensApi represents token api
|
||||
type TokensApi struct {
|
||||
ApiUsingConfig
|
||||
ApiWithUserInfo
|
||||
tokens *services.TokenService
|
||||
users *services.UserService
|
||||
}
|
||||
@@ -25,29 +20,18 @@ type TokensApi struct {
|
||||
// Initialize a token api singleton instance
|
||||
var (
|
||||
Tokens = &TokensApi{
|
||||
ApiUsingConfig: ApiUsingConfig{
|
||||
container: settings.Container,
|
||||
},
|
||||
ApiWithUserInfo: ApiWithUserInfo{
|
||||
ApiUsingConfig: ApiUsingConfig{
|
||||
container: settings.Container,
|
||||
},
|
||||
ApiUsingAvatarProvider: ApiUsingAvatarProvider{
|
||||
container: avatars.Container,
|
||||
},
|
||||
},
|
||||
tokens: services.Tokens,
|
||||
users: services.Users,
|
||||
}
|
||||
)
|
||||
|
||||
// TokenListHandler returns available token list of current user
|
||||
func (a *TokensApi) TokenListHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
func (a *TokensApi) TokenListHandler(c *core.Context) (any, *errs.Error) {
|
||||
uid := c.GetCurrentUid()
|
||||
tokens, err := a.tokens.GetAllUnexpiredNormalTokensByUid(c, uid)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[tokens.TokenListHandler] failed to get all tokens for user \"uid:%d\", because %s", uid, err.Error())
|
||||
log.ErrorfWithRequestId(c, "[tokens.TokenListHandler] failed to get all tokens for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
@@ -60,7 +44,8 @@ func (a *TokensApi) TokenListHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
TokenId: a.tokens.GenerateTokenId(token),
|
||||
TokenType: token.TokenType,
|
||||
UserAgent: token.UserAgent,
|
||||
LastSeen: token.LastSeenUnixTime,
|
||||
CreatedAt: token.CreatedUnixTime,
|
||||
ExpiredAt: token.ExpiredUnixTime,
|
||||
}
|
||||
|
||||
if token.Uid == claims.Uid && utils.Int64ToString(token.UserTokenId) == claims.UserTokenId && token.CreatedUnixTime == claims.IssuedAt {
|
||||
@@ -76,7 +61,7 @@ func (a *TokensApi) TokenListHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
}
|
||||
|
||||
// TokenRevokeCurrentHandler revokes current token of current user
|
||||
func (a *TokensApi) TokenRevokeCurrentHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
func (a *TokensApi) TokenRevokeCurrentHandler(c *core.Context) (any, *errs.Error) {
|
||||
_, claims, err := a.tokens.ParseTokenByHeader(c)
|
||||
|
||||
if err != nil {
|
||||
@@ -86,7 +71,7 @@ func (a *TokensApi) TokenRevokeCurrentHandler(c *core.WebContext) (any, *errs.Er
|
||||
userTokenId, err := utils.StringToInt64(claims.UserTokenId)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[tokens.TokenRevokeCurrentHandler] parse user token id failed, because %s", err.Error())
|
||||
log.WarnfWithRequestId(c, "[tokens.TokenRevokeCurrentHandler] parse user token id failed, because %s", err.Error())
|
||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||
}
|
||||
|
||||
@@ -100,21 +85,21 @@ func (a *TokensApi) TokenRevokeCurrentHandler(c *core.WebContext) (any, *errs.Er
|
||||
err = a.tokens.DeleteToken(c, tokenRecord)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[token.TokenRevokeCurrentHandler] failed to revoke token \"id:%s\" for user \"uid:%d\", because %s", tokenId, claims.Uid, err.Error())
|
||||
log.ErrorfWithRequestId(c, "[token.TokenRevokeCurrentHandler] failed to revoke token \"id:%s\" for user \"uid:%d\", because %s", tokenId, claims.Uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
log.Infof(c, "[token.TokenRevokeCurrentHandler] user \"uid:%d\" has revoked token \"id:%s\"", claims.Uid, tokenId)
|
||||
log.InfofWithRequestId(c, "[token.TokenRevokeCurrentHandler] user \"uid:%d\" has revoked token \"id:%s\"", claims.Uid, tokenId)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// TokenRevokeHandler revokes specific token of current user
|
||||
func (a *TokensApi) TokenRevokeHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
func (a *TokensApi) TokenRevokeHandler(c *core.Context) (any, *errs.Error) {
|
||||
var tokenRevokeReq models.TokenRevokeRequest
|
||||
err := c.ShouldBindJSON(&tokenRevokeReq)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[tokens.TokenRevokeHandler] parse request failed, because %s", err.Error())
|
||||
log.WarnfWithRequestId(c, "[tokens.TokenRevokeHandler] parse request failed, because %s", err.Error())
|
||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||
}
|
||||
|
||||
@@ -122,7 +107,7 @@ func (a *TokensApi) TokenRevokeHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
|
||||
if err != nil {
|
||||
if !errs.IsCustomError(err) {
|
||||
log.Errorf(c, "[token.TokenRevokeHandler] failed to parse token \"id:%s\", because %s", tokenRevokeReq.TokenId, err.Error())
|
||||
log.ErrorfWithRequestId(c, "[token.TokenRevokeHandler] failed to parse token \"id:%s\", because %s", tokenRevokeReq.TokenId, err.Error())
|
||||
}
|
||||
|
||||
return nil, errs.Or(err, errs.ErrInvalidTokenId)
|
||||
@@ -131,44 +116,28 @@ func (a *TokensApi) TokenRevokeHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
uid := c.GetCurrentUid()
|
||||
|
||||
if tokenRecord.Uid != uid {
|
||||
log.Warnf(c, "[token.TokenRevokeHandler] token \"id:%s\" is not owned by user \"uid:%d\"", tokenRevokeReq.TokenId, uid)
|
||||
log.WarnfWithRequestId(c, "[token.TokenRevokeHandler] token \"id:%s\" is not owned by user \"uid:%d\"", tokenRevokeReq.TokenId, uid)
|
||||
return nil, errs.ErrInvalidTokenId
|
||||
}
|
||||
|
||||
if utils.Int64ToString(tokenRecord.UserTokenId) != c.GetTokenClaims().UserTokenId || tokenRecord.CreatedUnixTime != c.GetTokenClaims().IssuedAt {
|
||||
user, err := a.users.GetUserById(c, uid)
|
||||
|
||||
if err != nil {
|
||||
if !errs.IsCustomError(err) {
|
||||
log.Errorf(c, "[token.TokenRevokeHandler] failed to get user, because %s", err.Error())
|
||||
}
|
||||
|
||||
return nil, errs.ErrUserNotFound
|
||||
}
|
||||
|
||||
if user.FeatureRestriction.Contains(core.USER_FEATURE_RESTRICTION_TYPE_REVOKE_OTHER_SESSION) {
|
||||
return nil, errs.ErrNotPermittedToPerformThisAction
|
||||
}
|
||||
}
|
||||
|
||||
err = a.tokens.DeleteToken(c, tokenRecord)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[token.TokenRevokeHandler] failed to revoke token \"id:%s\" for user \"uid:%d\", because %s", tokenRevokeReq.TokenId, uid, err.Error())
|
||||
log.ErrorfWithRequestId(c, "[token.TokenRevokeHandler] failed to revoke token \"id:%s\" for user \"uid:%d\", because %s", tokenRevokeReq.TokenId, uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
log.Infof(c, "[token.TokenRevokeHandler] user \"uid:%d\" has revoked token \"id:%s\"", uid, tokenRevokeReq.TokenId)
|
||||
log.InfofWithRequestId(c, "[token.TokenRevokeHandler] user \"uid:%d\" has revoked token \"id:%s\"", uid, tokenRevokeReq.TokenId)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// TokenRevokeAllHandler revokes all tokens of current user except current token
|
||||
func (a *TokensApi) TokenRevokeAllHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
func (a *TokensApi) TokenRevokeAllHandler(c *core.Context) (any, *errs.Error) {
|
||||
uid := c.GetCurrentUid()
|
||||
tokens, err := a.tokens.GetAllTokensByUid(c, uid)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[tokens.TokenRevokeAllHandler] failed to get all tokens for user \"uid:%d\", because %s", uid, err.Error())
|
||||
log.ErrorfWithRequestId(c, "[tokens.TokenRevokeAllHandler] failed to get all tokens for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
@@ -186,86 +155,35 @@ func (a *TokensApi) TokenRevokeAllHandler(c *core.WebContext) (any, *errs.Error)
|
||||
|
||||
tokens = append(tokens[:currentTokenIndex], tokens[currentTokenIndex+1:]...)
|
||||
|
||||
if len(tokens) < 1 {
|
||||
return nil, errs.ErrTokenRecordNotFound
|
||||
}
|
||||
|
||||
user, err := a.users.GetUserById(c, uid)
|
||||
|
||||
if err != nil {
|
||||
if !errs.IsCustomError(err) {
|
||||
log.Errorf(c, "[token.TokenRevokeAllHandler] failed to get user, because %s", err.Error())
|
||||
}
|
||||
|
||||
return nil, errs.ErrUserNotFound
|
||||
}
|
||||
|
||||
if user.FeatureRestriction.Contains(core.USER_FEATURE_RESTRICTION_TYPE_REVOKE_OTHER_SESSION) {
|
||||
return nil, errs.ErrNotPermittedToPerformThisAction
|
||||
}
|
||||
|
||||
err = a.tokens.DeleteTokens(c, uid, tokens)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[token.TokenRevokeAllHandler] failed to revoke all tokens for user \"uid:%d\", because %s", uid, err.Error())
|
||||
log.ErrorfWithRequestId(c, "[token.TokenRevokeAllHandler] failed to revoke all tokens for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
log.Infof(c, "[token.TokenRevokeAllHandler] user \"uid:%d\" has revoked all tokens", uid)
|
||||
log.InfofWithRequestId(c, "[token.TokenRevokeAllHandler] user \"uid:%d\" has revoked all tokens", uid)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// TokenRefreshHandler refresh current token of current user
|
||||
func (a *TokensApi) TokenRefreshHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
func (a *TokensApi) TokenRefreshHandler(c *core.Context) (any, *errs.Error) {
|
||||
uid := c.GetCurrentUid()
|
||||
user, err := a.users.GetUserById(c, uid)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[token.TokenRefreshHandler] failed to get user \"uid:%d\" info, because %s", uid, err.Error())
|
||||
log.WarnfWithRequestId(c, "[token.TokenRefreshHandler] failed to get user \"uid:%d\" info, because %s", uid, err.Error())
|
||||
return nil, errs.ErrUserNotFound
|
||||
}
|
||||
|
||||
now := time.Now().Unix()
|
||||
oldTokenClaims := c.GetTokenClaims()
|
||||
|
||||
if now-oldTokenClaims.IssuedAt < int64(a.CurrentConfig().TokenMinRefreshInterval) {
|
||||
log.Infof(c, "[token.TokenRefreshHandler] token of user \"uid:%d\" does not need to be refreshed", uid)
|
||||
|
||||
userTokenId, err := utils.StringToInt64(oldTokenClaims.UserTokenId)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[tokens.TokenRefreshHandler] parse user token id failed, because %s", err.Error())
|
||||
} else {
|
||||
tokenRecord := &models.TokenRecord{
|
||||
Uid: oldTokenClaims.Uid,
|
||||
UserTokenId: userTokenId,
|
||||
CreatedUnixTime: oldTokenClaims.IssuedAt,
|
||||
}
|
||||
|
||||
tokenId := a.tokens.GenerateTokenId(tokenRecord)
|
||||
|
||||
err = a.tokens.UpdateTokenLastSeen(c, tokenRecord)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[token.TokenRefreshHandler] failed to update last seen of token \"id:%s\" for user \"uid:%d\", because %s", tokenId, uid, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
refreshResp := &models.TokenRefreshResponse{
|
||||
User: a.GetUserBasicInfo(user),
|
||||
NotificationContent: a.GetAfterOpenNotificationContent(user.Language, c.GetClientLocale()),
|
||||
}
|
||||
|
||||
return refreshResp, nil
|
||||
}
|
||||
|
||||
token, claims, err := a.tokens.CreateToken(c, user)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[token.TokenRefreshHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
log.ErrorfWithRequestId(c, "[token.TokenRefreshHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrTokenGenerating)
|
||||
}
|
||||
|
||||
oldTokenClaims := c.GetTokenClaims()
|
||||
oldUserTokenId, _ := utils.StringToInt64(oldTokenClaims.UserTokenId)
|
||||
oldTokenRecord := &models.TokenRecord{
|
||||
Uid: uid,
|
||||
@@ -276,13 +194,12 @@ func (a *TokensApi) TokenRefreshHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
c.SetTextualToken(token)
|
||||
c.SetTokenClaims(claims)
|
||||
|
||||
log.Infof(c, "[token.TokenRefreshHandler] user \"uid:%d\" token refreshed, new token will be expired at %d", user.Uid, claims.ExpiresAt)
|
||||
log.InfofWithRequestId(c, "[token.TokenRefreshHandler] user \"uid:%d\" token refreshed, new token will be expired at %d", user.Uid, claims.ExpiresAt)
|
||||
|
||||
refreshResp := &models.TokenRefreshResponse{
|
||||
NewToken: token,
|
||||
OldTokenId: a.tokens.GenerateTokenId(oldTokenRecord),
|
||||
User: a.GetUserBasicInfo(user),
|
||||
NotificationContent: a.GetAfterOpenNotificationContent(user.Language, c.GetClientLocale()),
|
||||
NewToken: token,
|
||||
OldTokenId: a.tokens.GenerateTokenId(oldTokenRecord),
|
||||
User: user.ToUserBasicInfo(),
|
||||
}
|
||||
|
||||
return refreshResp, nil
|
||||
|
||||
@@ -6,45 +6,31 @@ import (
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
|
||||
"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"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/services"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||
)
|
||||
|
||||
// TransactionCategoriesApi represents transaction category api
|
||||
type TransactionCategoriesApi struct {
|
||||
ApiUsingConfig
|
||||
ApiUsingDuplicateChecker
|
||||
categories *services.TransactionCategoryService
|
||||
}
|
||||
|
||||
// Initialize a transaction category api singleton instance
|
||||
var (
|
||||
TransactionCategories = &TransactionCategoriesApi{
|
||||
ApiUsingConfig: ApiUsingConfig{
|
||||
container: settings.Container,
|
||||
},
|
||||
ApiUsingDuplicateChecker: ApiUsingDuplicateChecker{
|
||||
ApiUsingConfig: ApiUsingConfig{
|
||||
container: settings.Container,
|
||||
},
|
||||
container: duplicatechecker.Container,
|
||||
},
|
||||
categories: services.TransactionCategories,
|
||||
}
|
||||
)
|
||||
|
||||
// CategoryListHandler returns transaction category list of current user
|
||||
func (a *TransactionCategoriesApi) CategoryListHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
func (a *TransactionCategoriesApi) CategoryListHandler(c *core.Context) (any, *errs.Error) {
|
||||
var categoryListReq models.TransactionCategoryListRequest
|
||||
err := c.ShouldBindQuery(&categoryListReq)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[transaction_categories.CategoryListHandler] parse request failed, because %s", err.Error())
|
||||
log.WarnfWithRequestId(c, "[transaction_categories.CategoryListHandler] parse request failed, because %s", err.Error())
|
||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||
}
|
||||
|
||||
@@ -52,7 +38,7 @@ func (a *TransactionCategoriesApi) CategoryListHandler(c *core.WebContext) (any,
|
||||
categories, err := a.categories.GetAllCategoriesByUid(c, uid, categoryListReq.Type, categoryListReq.ParentId)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transaction_categories.CategoryListHandler] failed to get categories for user \"uid:%d\", because %s", uid, err.Error())
|
||||
log.ErrorfWithRequestId(c, "[transaction_categories.CategoryListHandler] failed to get categories for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
@@ -60,12 +46,12 @@ func (a *TransactionCategoriesApi) CategoryListHandler(c *core.WebContext) (any,
|
||||
}
|
||||
|
||||
// CategoryGetHandler returns one specific transaction category of current user
|
||||
func (a *TransactionCategoriesApi) CategoryGetHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
func (a *TransactionCategoriesApi) CategoryGetHandler(c *core.Context) (any, *errs.Error) {
|
||||
var categoryGetReq models.TransactionCategoryGetRequest
|
||||
err := c.ShouldBindQuery(&categoryGetReq)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[transaction_categories.CategoryGetHandler] parse request failed, because %s", err.Error())
|
||||
log.WarnfWithRequestId(c, "[transaction_categories.CategoryGetHandler] parse request failed, because %s", err.Error())
|
||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||
}
|
||||
|
||||
@@ -73,7 +59,7 @@ func (a *TransactionCategoriesApi) CategoryGetHandler(c *core.WebContext) (any,
|
||||
category, err := a.categories.GetCategoryByCategoryId(c, uid, categoryGetReq.Id)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transaction_categories.CategoryGetHandler] failed to get category \"id:%d\" for user \"uid:%d\", because %s", categoryGetReq.Id, uid, err.Error())
|
||||
log.ErrorfWithRequestId(c, "[transaction_categories.CategoryGetHandler] failed to get category \"id:%d\" for user \"uid:%d\", because %s", categoryGetReq.Id, uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
@@ -83,17 +69,17 @@ func (a *TransactionCategoriesApi) CategoryGetHandler(c *core.WebContext) (any,
|
||||
}
|
||||
|
||||
// CategoryCreateHandler saves a new transaction category by request parameters for current user
|
||||
func (a *TransactionCategoriesApi) CategoryCreateHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
func (a *TransactionCategoriesApi) CategoryCreateHandler(c *core.Context) (any, *errs.Error) {
|
||||
var categoryCreateReq models.TransactionCategoryCreateRequest
|
||||
err := c.ShouldBindJSON(&categoryCreateReq)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[transaction_categories.CategoryCreateHandler] parse request failed, because %s", err.Error())
|
||||
log.WarnfWithRequestId(c, "[transaction_categories.CategoryCreateHandler] parse request failed, because %s", err.Error())
|
||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||
}
|
||||
|
||||
if categoryCreateReq.Type < models.CATEGORY_TYPE_INCOME || categoryCreateReq.Type > models.CATEGORY_TYPE_TRANSFER {
|
||||
log.Warnf(c, "[transaction_categories.CategoryCreateHandler] category type invalid, type is %d", categoryCreateReq.Type)
|
||||
log.WarnfWithRequestId(c, "[transaction_categories.CategoryCreateHandler] category type invalid, type is %d", categoryCreateReq.Type)
|
||||
return nil, errs.ErrTransactionCategoryTypeInvalid
|
||||
}
|
||||
|
||||
@@ -103,17 +89,17 @@ func (a *TransactionCategoriesApi) CategoryCreateHandler(c *core.WebContext) (an
|
||||
parentCategory, err := a.categories.GetCategoryByCategoryId(c, uid, categoryCreateReq.ParentId)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transaction_categories.CategoryCreateHandler] failed to get parent category \"id:%d\" for user \"uid:%d\", because %s", categoryCreateReq.ParentId, uid, err.Error())
|
||||
log.ErrorfWithRequestId(c, "[transaction_categories.CategoryCreateHandler] failed to get parent category \"id:%d\" for user \"uid:%d\", because %s", categoryCreateReq.ParentId, uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
if parentCategory == nil {
|
||||
log.Warnf(c, "[transaction_categories.CategoryCreateHandler] parent category \"id:%d\" does not exist for user \"uid:%d\"", categoryCreateReq.ParentId, uid)
|
||||
log.WarnfWithRequestId(c, "[transaction_categories.CategoryCreateHandler] parent category \"id:%d\" does not exist for user \"uid:%d\"", categoryCreateReq.ParentId, uid)
|
||||
return nil, errs.ErrParentTransactionCategoryNotFound
|
||||
}
|
||||
|
||||
if parentCategory.ParentCategoryId > 0 {
|
||||
log.Warnf(c, "[transaction_categories.CategoryCreateHandler] parent category \"id:%d\" has another parent category \"id:%d\" for user \"uid:%d\"", parentCategory.CategoryId, parentCategory.ParentCategoryId, uid)
|
||||
log.WarnfWithRequestId(c, "[transaction_categories.CategoryCreateHandler] parent category \"id:%d\" has another parent category \"id:%d\" for user \"uid:%d\"", parentCategory.CategoryId, parentCategory.ParentCategoryId, uid)
|
||||
return nil, errs.ErrCannotAddToSecondaryTransactionCategory
|
||||
}
|
||||
}
|
||||
@@ -127,56 +113,33 @@ func (a *TransactionCategoriesApi) CategoryCreateHandler(c *core.WebContext) (an
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transaction_categories.CategoryCreateHandler] failed to get max display order for user \"uid:%d\", because %s", uid, err.Error())
|
||||
log.ErrorfWithRequestId(c, "[transaction_categories.CategoryCreateHandler] failed to get max display order for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
category := a.createNewCategoryModel(uid, &categoryCreateReq, maxOrderId+1)
|
||||
|
||||
if a.CurrentConfig().EnableDuplicateSubmissionsCheck && categoryCreateReq.ClientSessionId != "" {
|
||||
found, remark := a.GetSubmissionRemark(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_CATEGORY, uid, categoryCreateReq.ClientSessionId)
|
||||
|
||||
if found {
|
||||
log.Infof(c, "[transaction_categories.CategoryCreateHandler] another category \"id:%s\" has been created for user \"uid:%d\"", remark, uid)
|
||||
categoryId, err := utils.StringToInt64(remark)
|
||||
|
||||
if err == nil {
|
||||
category, err = a.categories.GetCategoryByCategoryId(c, uid, categoryId)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transaction_categories.CategoryCreateHandler] failed to get existed category \"id:%d\" for user \"uid:%d\", because %s", categoryId, uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
categoryResp := category.ToTransactionCategoryInfoResponse()
|
||||
|
||||
return categoryResp, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = a.categories.CreateCategory(c, category)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transaction_categories.CategoryCreateHandler] failed to create category \"id:%d\" for user \"uid:%d\", because %s", category.CategoryId, uid, err.Error())
|
||||
log.ErrorfWithRequestId(c, "[transaction_categories.CategoryCreateHandler] failed to create category \"id:%d\" for user \"uid:%d\", because %s", category.CategoryId, uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
log.Infof(c, "[transaction_categories.CategoryCreateHandler] user \"uid:%d\" has created a new category \"id:%d\" successfully", uid, category.CategoryId)
|
||||
log.InfofWithRequestId(c, "[transaction_categories.CategoryCreateHandler] user \"uid:%d\" has created a new category \"id:%d\" successfully", uid, category.CategoryId)
|
||||
|
||||
a.SetSubmissionRemark(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_CATEGORY, uid, categoryCreateReq.ClientSessionId, utils.Int64ToString(category.CategoryId))
|
||||
categoryResp := category.ToTransactionCategoryInfoResponse()
|
||||
|
||||
return categoryResp, nil
|
||||
}
|
||||
|
||||
// CategoryCreateBatchHandler saves some new transaction category by request parameters for current user
|
||||
func (a *TransactionCategoriesApi) CategoryCreateBatchHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
func (a *TransactionCategoriesApi) CategoryCreateBatchHandler(c *core.Context) (any, *errs.Error) {
|
||||
var categoryCreateBatchReq models.TransactionCategoryCreateBatchRequest
|
||||
err := c.ShouldBindBodyWith(&categoryCreateBatchReq, binding.JSON)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[transaction_categories.CategoryCreateBatchHandler] parse request failed, because %s", err.Error())
|
||||
log.WarnfWithRequestId(c, "[transaction_categories.CategoryCreateBatchHandler] parse request failed, because %s", err.Error())
|
||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||
}
|
||||
|
||||
@@ -192,12 +155,12 @@ func (a *TransactionCategoriesApi) CategoryCreateBatchHandler(c *core.WebContext
|
||||
}
|
||||
|
||||
// CategoryModifyHandler saves an existed transaction category by request parameters for current user
|
||||
func (a *TransactionCategoriesApi) CategoryModifyHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
func (a *TransactionCategoriesApi) CategoryModifyHandler(c *core.Context) (any, *errs.Error) {
|
||||
var categoryModifyReq models.TransactionCategoryModifyRequest
|
||||
err := c.ShouldBindJSON(&categoryModifyReq)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[transaction_categories.CategoryModifyHandler] parse request failed, because %s", err.Error())
|
||||
log.WarnfWithRequestId(c, "[transaction_categories.CategoryModifyHandler] parse request failed, because %s", err.Error())
|
||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||
}
|
||||
|
||||
@@ -205,23 +168,21 @@ func (a *TransactionCategoriesApi) CategoryModifyHandler(c *core.WebContext) (an
|
||||
category, err := a.categories.GetCategoryByCategoryId(c, uid, categoryModifyReq.Id)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transaction_categories.CategoryModifyHandler] failed to get category \"id:%d\" for user \"uid:%d\", because %s", categoryModifyReq.Id, uid, err.Error())
|
||||
log.ErrorfWithRequestId(c, "[transaction_categories.CategoryModifyHandler] failed to get category \"id:%d\" for user \"uid:%d\", because %s", categoryModifyReq.Id, uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
newCategory := &models.TransactionCategory{
|
||||
CategoryId: category.CategoryId,
|
||||
Uid: uid,
|
||||
ParentCategoryId: categoryModifyReq.ParentId,
|
||||
Name: categoryModifyReq.Name,
|
||||
Icon: categoryModifyReq.Icon,
|
||||
Color: categoryModifyReq.Color,
|
||||
Comment: categoryModifyReq.Comment,
|
||||
Hidden: categoryModifyReq.Hidden,
|
||||
CategoryId: category.CategoryId,
|
||||
Uid: uid,
|
||||
Name: categoryModifyReq.Name,
|
||||
Icon: categoryModifyReq.Icon,
|
||||
Color: categoryModifyReq.Color,
|
||||
Comment: categoryModifyReq.Comment,
|
||||
Hidden: categoryModifyReq.Hidden,
|
||||
}
|
||||
|
||||
if newCategory.ParentCategoryId == category.ParentCategoryId &&
|
||||
newCategory.Name == category.Name &&
|
||||
if newCategory.Name == category.Name &&
|
||||
newCategory.Icon == category.Icon &&
|
||||
newCategory.Color == category.Color &&
|
||||
newCategory.Comment == category.Comment &&
|
||||
@@ -229,48 +190,17 @@ func (a *TransactionCategoriesApi) CategoryModifyHandler(c *core.WebContext) (an
|
||||
return nil, errs.ErrNothingWillBeUpdated
|
||||
}
|
||||
|
||||
if category.ParentCategoryId == 0 && newCategory.ParentCategoryId != 0 {
|
||||
return nil, errs.Or(err, errs.ErrNotAllowChangePrimaryTransactionCategoryToSecondary)
|
||||
}
|
||||
|
||||
if category.ParentCategoryId != 0 && newCategory.ParentCategoryId == 0 {
|
||||
return nil, errs.Or(err, errs.ErrNotAllowChangeSecondaryTransactionCategoryToPrimary)
|
||||
}
|
||||
|
||||
if newCategory.ParentCategoryId != category.ParentCategoryId {
|
||||
fromPrimaryCategory, err := a.categories.GetCategoryByCategoryId(c, uid, category.ParentCategoryId)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transaction_categories.CategoryModifyHandler] failed to get old primary category \"id:%d\" of category \"id:%d\" for user \"uid:%d\", because %s", category.ParentCategoryId, categoryModifyReq.Id, uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
toPrimaryCategory, err := a.categories.GetCategoryByCategoryId(c, uid, newCategory.ParentCategoryId)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transaction_categories.CategoryModifyHandler] failed to get new primary category \"id:%d\" of category \"id:%d\" for user \"uid:%d\", because %s", newCategory.ParentCategoryId, categoryModifyReq.Id, uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
if fromPrimaryCategory.Type != toPrimaryCategory.Type {
|
||||
return nil, errs.Or(err, errs.ErrNotAllowChangePrimaryTransactionType)
|
||||
}
|
||||
|
||||
if toPrimaryCategory.ParentCategoryId != 0 {
|
||||
return nil, errs.Or(err, errs.ErrNotAllowUseSecondaryTransactionAsPrimaryCategory)
|
||||
}
|
||||
}
|
||||
|
||||
err = a.categories.ModifyCategory(c, newCategory)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transaction_categories.CategoryModifyHandler] failed to update category \"id:%d\" for user \"uid:%d\", because %s", categoryModifyReq.Id, uid, err.Error())
|
||||
log.ErrorfWithRequestId(c, "[transaction_categories.CategoryModifyHandler] failed to update category \"id:%d\" for user \"uid:%d\", because %s", categoryModifyReq.Id, uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
log.Infof(c, "[transaction_categories.CategoryModifyHandler] user \"uid:%d\" has updated category \"id:%d\" successfully", uid, categoryModifyReq.Id)
|
||||
log.InfofWithRequestId(c, "[transaction_categories.CategoryModifyHandler] user \"uid:%d\" has updated category \"id:%d\" successfully", uid, categoryModifyReq.Id)
|
||||
|
||||
newCategory.Type = category.Type
|
||||
newCategory.ParentCategoryId = category.ParentCategoryId
|
||||
newCategory.DisplayOrder = category.DisplayOrder
|
||||
categoryResp := newCategory.ToTransactionCategoryInfoResponse()
|
||||
|
||||
@@ -278,12 +208,12 @@ func (a *TransactionCategoriesApi) CategoryModifyHandler(c *core.WebContext) (an
|
||||
}
|
||||
|
||||
// CategoryHideHandler hides an existed transaction category by request parameters for current user
|
||||
func (a *TransactionCategoriesApi) CategoryHideHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
func (a *TransactionCategoriesApi) CategoryHideHandler(c *core.Context) (any, *errs.Error) {
|
||||
var categoryHideReq models.TransactionCategoryHideRequest
|
||||
err := c.ShouldBindJSON(&categoryHideReq)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[transaction_categories.CategoryHideHandler] parse request failed, because %s", err.Error())
|
||||
log.WarnfWithRequestId(c, "[transaction_categories.CategoryHideHandler] parse request failed, because %s", err.Error())
|
||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||
}
|
||||
|
||||
@@ -291,21 +221,21 @@ func (a *TransactionCategoriesApi) CategoryHideHandler(c *core.WebContext) (any,
|
||||
err = a.categories.HideCategory(c, uid, []int64{categoryHideReq.Id}, categoryHideReq.Hidden)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transaction_categories.CategoryHideHandler] failed to hide category \"id:%d\" for user \"uid:%d\", because %s", categoryHideReq.Id, uid, err.Error())
|
||||
log.ErrorfWithRequestId(c, "[transaction_categories.CategoryHideHandler] failed to hide category \"id:%d\" for user \"uid:%d\", because %s", categoryHideReq.Id, uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
log.Infof(c, "[transaction_categories.CategoryHideHandler] user \"uid:%d\" has hidden category \"id:%d\"", uid, categoryHideReq.Id)
|
||||
log.InfofWithRequestId(c, "[transaction_categories.CategoryHideHandler] user \"uid:%d\" has hidden category \"id:%d\"", uid, categoryHideReq.Id)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// CategoryMoveHandler moves display order of existed transaction categories by request parameters for current user
|
||||
func (a *TransactionCategoriesApi) CategoryMoveHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
func (a *TransactionCategoriesApi) CategoryMoveHandler(c *core.Context) (any, *errs.Error) {
|
||||
var categoryMoveReq models.TransactionCategoryMoveRequest
|
||||
err := c.ShouldBindJSON(&categoryMoveReq)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[transaction_categories.CategoryMoveHandler] parse request failed, because %s", err.Error())
|
||||
log.WarnfWithRequestId(c, "[transaction_categories.CategoryMoveHandler] parse request failed, because %s", err.Error())
|
||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||
}
|
||||
|
||||
@@ -326,21 +256,21 @@ func (a *TransactionCategoriesApi) CategoryMoveHandler(c *core.WebContext) (any,
|
||||
err = a.categories.ModifyCategoryDisplayOrders(c, uid, categories)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transaction_categories.CategoryMoveHandler] failed to move categories for user \"uid:%d\", because %s", uid, err.Error())
|
||||
log.ErrorfWithRequestId(c, "[transaction_categories.CategoryMoveHandler] failed to move categories for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
log.Infof(c, "[transaction_categories.CategoryMoveHandler] user \"uid:%d\" has moved categories", uid)
|
||||
log.InfofWithRequestId(c, "[transaction_categories.CategoryMoveHandler] user \"uid:%d\" has moved categories", uid)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// CategoryDeleteHandler deletes an existed transaction category by request parameters for current user
|
||||
func (a *TransactionCategoriesApi) CategoryDeleteHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
func (a *TransactionCategoriesApi) CategoryDeleteHandler(c *core.Context) (any, *errs.Error) {
|
||||
var categoryDeleteReq models.TransactionCategoryDeleteRequest
|
||||
err := c.ShouldBindJSON(&categoryDeleteReq)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[transaction_categories.CategoryDeleteHandler] parse request failed, because %s", err.Error())
|
||||
log.WarnfWithRequestId(c, "[transaction_categories.CategoryDeleteHandler] parse request failed, because %s", err.Error())
|
||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||
}
|
||||
|
||||
@@ -348,15 +278,15 @@ func (a *TransactionCategoriesApi) CategoryDeleteHandler(c *core.WebContext) (an
|
||||
err = a.categories.DeleteCategory(c, uid, categoryDeleteReq.Id)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transaction_categories.CategoryDeleteHandler] failed to delete category \"id:%d\" for user \"uid:%d\", because %s", categoryDeleteReq.Id, uid, err.Error())
|
||||
log.ErrorfWithRequestId(c, "[transaction_categories.CategoryDeleteHandler] failed to delete category \"id:%d\" for user \"uid:%d\", because %s", categoryDeleteReq.Id, uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
log.Infof(c, "[transaction_categories.CategoryDeleteHandler] user \"uid:%d\" has deleted category \"id:%d\"", uid, categoryDeleteReq.Id)
|
||||
log.InfofWithRequestId(c, "[transaction_categories.CategoryDeleteHandler] user \"uid:%d\" has deleted category \"id:%d\"", uid, categoryDeleteReq.Id)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (a *TransactionCategoriesApi) createBatchCategories(c *core.WebContext, uid int64, categoryCreateBatchReq *models.TransactionCategoryCreateBatchRequest) ([]*models.TransactionCategory, error) {
|
||||
func (a *TransactionCategoriesApi) createBatchCategories(c *core.Context, uid int64, categoryCreateBatchReq *models.TransactionCategoryCreateBatchRequest) ([]*models.TransactionCategory, error) {
|
||||
var err error
|
||||
categoryTypeMaxOrderMap := make(map[models.TransactionCategoryType]int32)
|
||||
categoriesMap := make(map[*models.TransactionCategory][]*models.TransactionCategory)
|
||||
@@ -371,7 +301,7 @@ func (a *TransactionCategoriesApi) createBatchCategories(c *core.WebContext, uid
|
||||
maxOrderId, err = a.categories.GetMaxDisplayOrder(c, uid, categoryCreateReq.Type)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transaction_categories.CategoryCreateBatchHandler] failed to get max display order for user \"uid:%d\", because %s", uid, err.Error())
|
||||
log.ErrorfWithRequestId(c, "[transaction_categories.CategoryCreateBatchHandler] failed to get max display order for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
}
|
||||
@@ -399,11 +329,11 @@ func (a *TransactionCategoriesApi) createBatchCategories(c *core.WebContext, uid
|
||||
categories, err := a.categories.CreateCategories(c, uid, categoriesMap)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transaction_categories.createBatchCategories] failed to create categories for user \"uid:%d\", because %s", uid, err.Error())
|
||||
log.ErrorfWithRequestId(c, "[transaction_categories.createBatchCategories] failed to create categories for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
log.Infof(c, "[transaction_categories.createBatchCategories] user \"uid:%d\" has created categories successfully", uid)
|
||||
log.InfofWithRequestId(c, "[transaction_categories.createBatchCategories] user \"uid:%d\" has created categories successfully", uid)
|
||||
|
||||
return categories, nil
|
||||
}
|
||||
|
||||
@@ -1,183 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"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"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/services"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||
)
|
||||
|
||||
// TransactionPicturesApi represents transaction pictures api
|
||||
type TransactionPicturesApi struct {
|
||||
ApiUsingConfig
|
||||
ApiUsingDuplicateChecker
|
||||
users *services.UserService
|
||||
pictures *services.TransactionPictureService
|
||||
}
|
||||
|
||||
// Initialize a transaction api singleton instance
|
||||
var (
|
||||
TransactionPictures = &TransactionPicturesApi{
|
||||
ApiUsingConfig: ApiUsingConfig{
|
||||
container: settings.Container,
|
||||
},
|
||||
ApiUsingDuplicateChecker: ApiUsingDuplicateChecker{
|
||||
ApiUsingConfig: ApiUsingConfig{
|
||||
container: settings.Container,
|
||||
},
|
||||
container: duplicatechecker.Container,
|
||||
},
|
||||
users: services.Users,
|
||||
pictures: services.TransactionPictures,
|
||||
}
|
||||
)
|
||||
|
||||
// TransactionPictureUploadHandler saves transaction picture by request parameters for current user
|
||||
func (a *TransactionPicturesApi) TransactionPictureUploadHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
uid := c.GetCurrentUid()
|
||||
form, err := c.MultipartForm()
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transaction_pictures.TransactionPictureUploadHandler] failed to get multi-part form data for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.ErrParameterInvalid
|
||||
}
|
||||
|
||||
pictureFiles := form.File["picture"]
|
||||
|
||||
if len(pictureFiles) < 1 {
|
||||
log.Warnf(c, "[transaction_pictures.TransactionPictureUploadHandler] there is no transaction picture in request for user \"uid:%d\"", uid)
|
||||
return nil, errs.ErrNoTransactionPicture
|
||||
}
|
||||
|
||||
if pictureFiles[0].Size < 1 {
|
||||
log.Warnf(c, "[transaction_pictures.TransactionPictureUploadHandler] the size of transaction picture in request is zero for user \"uid:%d\"", uid)
|
||||
return nil, errs.ErrTransactionPictureIsEmpty
|
||||
}
|
||||
|
||||
if pictureFiles[0].Size > int64(a.CurrentConfig().MaxTransactionPictureFileSize) {
|
||||
log.Warnf(c, "[transaction_pictures.TransactionPictureUploadHandler] the upload file size \"%d\" exceeds the maximum size \"%d\" of transaction picture for user \"uid:%d\"", pictureFiles[0].Size, a.CurrentConfig().MaxTransactionPictureFileSize, uid)
|
||||
return nil, errs.ErrExceedMaxTransactionPictureFileSize
|
||||
}
|
||||
|
||||
fileExtension := utils.GetFileNameExtension(pictureFiles[0].Filename)
|
||||
|
||||
if utils.GetImageContentType(fileExtension) == "" {
|
||||
log.Warnf(c, "[transaction_pictures.TransactionPictureUploadHandler] the file extension \"%s\" of transaction picture in request is not supported for user \"uid:%d\"", fileExtension, uid)
|
||||
return nil, errs.ErrImageTypeNotSupported
|
||||
}
|
||||
|
||||
pictureFile, err := pictureFiles[0].Open()
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transaction_pictures.TransactionPictureUploadHandler] failed to get transaction picture file from request for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.ErrOperationFailed
|
||||
}
|
||||
|
||||
pictureInfo := a.createNewPictureInfoModel(uid, fileExtension, c.ClientIP())
|
||||
|
||||
clientSessionIds := form.Value["clientSessionId"]
|
||||
clientSessionId := ""
|
||||
|
||||
if len(clientSessionIds) > 0 {
|
||||
clientSessionId = clientSessionIds[0]
|
||||
}
|
||||
|
||||
if a.CurrentConfig().EnableDuplicateSubmissionsCheck && clientSessionId != "" {
|
||||
found, remark := a.GetSubmissionRemark(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_PICTURE, uid, clientSessionId)
|
||||
|
||||
if found {
|
||||
log.Infof(c, "[transaction_pictures.TransactionPictureUploadHandler] another transaction picture \"id:%s\" has been uploaded for user \"uid:%d\"", remark, uid)
|
||||
pictureId, err := utils.StringToInt64(remark)
|
||||
|
||||
if err == nil {
|
||||
pictureInfo, err = a.pictures.GetPictureInfoByPictureId(c, uid, pictureId)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transaction_pictures.TransactionPictureUploadHandler] failed to get existed transaction picture \"id:%d\" for user \"uid:%d\", because %s", pictureId, uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
pictureInfoResp := a.GetTransactionPictureInfoResponse(pictureInfo)
|
||||
|
||||
return pictureInfoResp, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = a.pictures.UploadPicture(c, pictureInfo, pictureFile)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transaction_pictures.TransactionPictureUploadHandler] failed to update transaction picture for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
a.SetSubmissionRemark(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_PICTURE, uid, clientSessionId, utils.Int64ToString(pictureInfo.PictureId))
|
||||
pictureInfoResp := a.GetTransactionPictureInfoResponse(pictureInfo)
|
||||
|
||||
return pictureInfoResp, nil
|
||||
}
|
||||
|
||||
// TransactionPictureGetHandler returns transaction picture data for current user
|
||||
func (a *TransactionPicturesApi) TransactionPictureGetHandler(c *core.WebContext) ([]byte, string, *errs.Error) {
|
||||
fileName := c.Param("fileName")
|
||||
fileExtension := utils.GetFileNameExtension(fileName)
|
||||
contentType := utils.GetImageContentType(fileExtension)
|
||||
|
||||
if contentType == "" {
|
||||
return nil, "", errs.ErrImageTypeNotSupported
|
||||
}
|
||||
|
||||
fileBaseName := utils.GetFileNameWithoutExtension(fileName)
|
||||
pictureId, err := utils.StringToInt64(fileBaseName)
|
||||
|
||||
if err != nil {
|
||||
return nil, "", errs.ErrTransactionPictureIdInvalid
|
||||
}
|
||||
|
||||
uid := c.GetCurrentUid()
|
||||
pictureData, err := a.pictures.GetPictureByPictureId(c, uid, pictureId, fileExtension)
|
||||
|
||||
if err != nil {
|
||||
if !errs.IsCustomError(err) {
|
||||
log.Errorf(c, "[transaction_pictures.TransactionPictureUploadHandler] failed to get transaction picture, because %s", err.Error())
|
||||
}
|
||||
|
||||
return nil, "", errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
return pictureData, contentType, nil
|
||||
}
|
||||
|
||||
// TransactionPictureRemoveUnusedHandler removes unused transaction picture by request parameters for current user
|
||||
func (a *TransactionPicturesApi) TransactionPictureRemoveUnusedHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
var pictureDeleteReq models.TransactionPictureUnusedDeleteRequest
|
||||
err := c.ShouldBindJSON(&pictureDeleteReq)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[transaction_pictures.TransactionPictureRemoveUnusedHandler] parse request failed, because %s", err.Error())
|
||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||
}
|
||||
|
||||
uid := c.GetCurrentUid()
|
||||
err = a.pictures.RemoveUnusedTransactionPicture(c, uid, pictureDeleteReq.Id)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transaction_pictures.TransactionPictureRemoveUnusedHandler] failed to remove unused transaction picture for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (a *TransactionPicturesApi) createNewPictureInfoModel(uid int64, fileExtension string, clientIp string) *models.TransactionPictureInfo {
|
||||
return &models.TransactionPictureInfo{
|
||||
Uid: uid,
|
||||
TransactionId: models.TransactionPictureNewPictureTransactionId,
|
||||
PictureExtension: fileExtension,
|
||||
CreatedIp: clientIp,
|
||||
}
|
||||
}
|
||||
+28
-28
@@ -23,12 +23,12 @@ var (
|
||||
)
|
||||
|
||||
// TagListHandler returns transaction tag list of current user
|
||||
func (a *TransactionTagsApi) TagListHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
func (a *TransactionTagsApi) TagListHandler(c *core.Context) (any, *errs.Error) {
|
||||
uid := c.GetCurrentUid()
|
||||
tags, err := a.tags.GetAllTagsByUid(c, uid)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transaction_tags.TagListHandler] failed to get tags for user \"uid:%d\", because %s", uid, err.Error())
|
||||
log.ErrorfWithRequestId(c, "[transaction_tags.TagListHandler] failed to get tags for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
@@ -44,12 +44,12 @@ func (a *TransactionTagsApi) TagListHandler(c *core.WebContext) (any, *errs.Erro
|
||||
}
|
||||
|
||||
// TagGetHandler returns one specific transaction tag of current user
|
||||
func (a *TransactionTagsApi) TagGetHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
func (a *TransactionTagsApi) TagGetHandler(c *core.Context) (any, *errs.Error) {
|
||||
var tagGetReq models.TransactionTagGetRequest
|
||||
err := c.ShouldBindQuery(&tagGetReq)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[transaction_tags.TagGetHandler] parse request failed, because %s", err.Error())
|
||||
log.WarnfWithRequestId(c, "[transaction_tags.TagGetHandler] parse request failed, because %s", err.Error())
|
||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ func (a *TransactionTagsApi) TagGetHandler(c *core.WebContext) (any, *errs.Error
|
||||
tag, err := a.tags.GetTagByTagId(c, uid, tagGetReq.Id)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transaction_tags.TagGetHandler] failed to get tag \"id:%d\" for user \"uid:%d\", because %s", tagGetReq.Id, uid, err.Error())
|
||||
log.ErrorfWithRequestId(c, "[transaction_tags.TagGetHandler] failed to get tag \"id:%d\" for user \"uid:%d\", because %s", tagGetReq.Id, uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
@@ -67,12 +67,12 @@ func (a *TransactionTagsApi) TagGetHandler(c *core.WebContext) (any, *errs.Error
|
||||
}
|
||||
|
||||
// TagCreateHandler saves a new transaction tag by request parameters for current user
|
||||
func (a *TransactionTagsApi) TagCreateHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
func (a *TransactionTagsApi) TagCreateHandler(c *core.Context) (any, *errs.Error) {
|
||||
var tagCreateReq models.TransactionTagCreateRequest
|
||||
err := c.ShouldBindJSON(&tagCreateReq)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[transaction_tags.TagCreateHandler] parse request failed, because %s", err.Error())
|
||||
log.WarnfWithRequestId(c, "[transaction_tags.TagCreateHandler] parse request failed, because %s", err.Error())
|
||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ func (a *TransactionTagsApi) TagCreateHandler(c *core.WebContext) (any, *errs.Er
|
||||
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())
|
||||
log.ErrorfWithRequestId(c, "[transaction_tags.TagCreateHandler] failed to get max display order for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
@@ -90,11 +90,11 @@ func (a *TransactionTagsApi) TagCreateHandler(c *core.WebContext) (any, *errs.Er
|
||||
err = a.tags.CreateTag(c, tag)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transaction_tags.TagCreateHandler] failed to create tag \"id:%d\" for user \"uid:%d\", because %s", tag.TagId, uid, err.Error())
|
||||
log.ErrorfWithRequestId(c, "[transaction_tags.TagCreateHandler] failed to create tag \"id:%d\" for user \"uid:%d\", because %s", tag.TagId, uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
log.Infof(c, "[transaction_tags.TagCreateHandler] user \"uid:%d\" has created a new tag \"id:%d\" successfully", uid, tag.TagId)
|
||||
log.InfofWithRequestId(c, "[transaction_tags.TagCreateHandler] user \"uid:%d\" has created a new tag \"id:%d\" successfully", uid, tag.TagId)
|
||||
|
||||
tagResp := tag.ToTransactionTagInfoResponse()
|
||||
|
||||
@@ -102,12 +102,12 @@ func (a *TransactionTagsApi) TagCreateHandler(c *core.WebContext) (any, *errs.Er
|
||||
}
|
||||
|
||||
// TagModifyHandler saves an existed transaction tag by request parameters for current user
|
||||
func (a *TransactionTagsApi) TagModifyHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
func (a *TransactionTagsApi) TagModifyHandler(c *core.Context) (any, *errs.Error) {
|
||||
var tagModifyReq models.TransactionTagModifyRequest
|
||||
err := c.ShouldBindJSON(&tagModifyReq)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[transaction_tags.TagModifyHandler] parse request failed, because %s", err.Error())
|
||||
log.WarnfWithRequestId(c, "[transaction_tags.TagModifyHandler] parse request failed, because %s", err.Error())
|
||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||
}
|
||||
|
||||
@@ -115,7 +115,7 @@ func (a *TransactionTagsApi) TagModifyHandler(c *core.WebContext) (any, *errs.Er
|
||||
tag, err := a.tags.GetTagByTagId(c, uid, tagModifyReq.Id)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transaction_tags.TagModifyHandler] failed to get tag \"id:%d\" for user \"uid:%d\", because %s", tagModifyReq.Id, uid, err.Error())
|
||||
log.ErrorfWithRequestId(c, "[transaction_tags.TagModifyHandler] failed to get tag \"id:%d\" for user \"uid:%d\", because %s", tagModifyReq.Id, uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
@@ -132,11 +132,11 @@ func (a *TransactionTagsApi) TagModifyHandler(c *core.WebContext) (any, *errs.Er
|
||||
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())
|
||||
log.ErrorfWithRequestId(c, "[transaction_tags.TagModifyHandler] failed to update tag \"id:%d\" for user \"uid:%d\", because %s", tagModifyReq.Id, uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
log.Infof(c, "[transaction_tags.TagModifyHandler] user \"uid:%d\" has updated tag \"id:%d\" successfully", uid, tagModifyReq.Id)
|
||||
log.InfofWithRequestId(c, "[transaction_tags.TagModifyHandler] user \"uid:%d\" has updated tag \"id:%d\" successfully", uid, tagModifyReq.Id)
|
||||
|
||||
tag.Name = newTag.Name
|
||||
tagResp := tag.ToTransactionTagInfoResponse()
|
||||
@@ -144,13 +144,13 @@ func (a *TransactionTagsApi) TagModifyHandler(c *core.WebContext) (any, *errs.Er
|
||||
return tagResp, nil
|
||||
}
|
||||
|
||||
// TagHideHandler hides a transaction tag by request parameters for current user
|
||||
func (a *TransactionTagsApi) TagHideHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
// TagHideHandler hides an transaction tag by request parameters for current user
|
||||
func (a *TransactionTagsApi) TagHideHandler(c *core.Context) (any, *errs.Error) {
|
||||
var tagHideReq models.TransactionTagHideRequest
|
||||
err := c.ShouldBindJSON(&tagHideReq)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[transaction_tags.TagHideHandler] parse request failed, because %s", err.Error())
|
||||
log.WarnfWithRequestId(c, "[transaction_tags.CategoryHideHandler] parse request failed, because %s", err.Error())
|
||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||
}
|
||||
|
||||
@@ -158,21 +158,21 @@ func (a *TransactionTagsApi) TagHideHandler(c *core.WebContext) (any, *errs.Erro
|
||||
err = a.tags.HideTag(c, uid, []int64{tagHideReq.Id}, tagHideReq.Hidden)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transaction_tags.TagHideHandler] failed to hide tag \"id:%d\" for user \"uid:%d\", because %s", tagHideReq.Id, uid, err.Error())
|
||||
log.ErrorfWithRequestId(c, "[transaction_tags.CategoryHideHandler] failed to hide tag \"id:%d\" for user \"uid:%d\", because %s", tagHideReq.Id, uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
log.Infof(c, "[transaction_tags.TagHideHandler] user \"uid:%d\" has hidden tag \"id:%d\"", uid, tagHideReq.Id)
|
||||
log.InfofWithRequestId(c, "[transaction_tags.CategoryHideHandler] user \"uid:%d\" has hidden category \"id:%d\"", uid, tagHideReq.Id)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// TagMoveHandler moves display order of existed transaction tags by request parameters for current user
|
||||
func (a *TransactionTagsApi) TagMoveHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
func (a *TransactionTagsApi) TagMoveHandler(c *core.Context) (any, *errs.Error) {
|
||||
var tagMoveReq models.TransactionTagMoveRequest
|
||||
err := c.ShouldBindJSON(&tagMoveReq)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[transaction_tags.TagMoveHandler] parse request failed, because %s", err.Error())
|
||||
log.WarnfWithRequestId(c, "[transaction_tags.CategoryMoveHandler] parse request failed, because %s", err.Error())
|
||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||
}
|
||||
|
||||
@@ -193,21 +193,21 @@ func (a *TransactionTagsApi) TagMoveHandler(c *core.WebContext) (any, *errs.Erro
|
||||
err = a.tags.ModifyTagDisplayOrders(c, uid, tags)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transaction_tags.TagMoveHandler] failed to move tags for user \"uid:%d\", because %s", uid, err.Error())
|
||||
log.ErrorfWithRequestId(c, "[transaction_tags.CategoryMoveHandler] failed to move tags for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
log.Infof(c, "[transaction_tags.TagMoveHandler] user \"uid:%d\" has moved tags", uid)
|
||||
log.InfofWithRequestId(c, "[transaction_tags.CategoryMoveHandler] user \"uid:%d\" has moved categories", uid)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// TagDeleteHandler deletes an existed transaction tag by request parameters for current user
|
||||
func (a *TransactionTagsApi) TagDeleteHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
func (a *TransactionTagsApi) TagDeleteHandler(c *core.Context) (any, *errs.Error) {
|
||||
var tagDeleteReq models.TransactionTagDeleteRequest
|
||||
err := c.ShouldBindJSON(&tagDeleteReq)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[transaction_tags.TagDeleteHandler] parse request failed, because %s", err.Error())
|
||||
log.WarnfWithRequestId(c, "[transaction_tags.TagDeleteHandler] parse request failed, because %s", err.Error())
|
||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||
}
|
||||
|
||||
@@ -215,11 +215,11 @@ func (a *TransactionTagsApi) TagDeleteHandler(c *core.WebContext) (any, *errs.Er
|
||||
err = a.tags.DeleteTag(c, uid, tagDeleteReq.Id)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transaction_tags.TagDeleteHandler] failed to delete tag \"id:%d\" for user \"uid:%d\", because %s", tagDeleteReq.Id, uid, err.Error())
|
||||
log.ErrorfWithRequestId(c, "[transaction_tags.TagDeleteHandler] failed to delete tag \"id:%d\" for user \"uid:%d\", because %s", tagDeleteReq.Id, uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
log.Infof(c, "[transaction_tags.TagDeleteHandler] user \"uid:%d\" has deleted tag \"id:%d\"", uid, tagDeleteReq.Id)
|
||||
log.InfofWithRequestId(c, "[transaction_tags.TagDeleteHandler] user \"uid:%d\" has deleted tag \"id:%d\"", uid, tagDeleteReq.Id)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,558 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
"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"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/services"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||
)
|
||||
|
||||
const maximumTagsCountOfTemplate = 10
|
||||
|
||||
// TransactionTemplatesApi represents transaction template api
|
||||
type TransactionTemplatesApi struct {
|
||||
ApiUsingConfig
|
||||
ApiUsingDuplicateChecker
|
||||
templates *services.TransactionTemplateService
|
||||
}
|
||||
|
||||
// Initialize a transaction template api singleton instance
|
||||
var (
|
||||
TransactionTemplates = &TransactionTemplatesApi{
|
||||
ApiUsingConfig: ApiUsingConfig{
|
||||
container: settings.Container,
|
||||
},
|
||||
ApiUsingDuplicateChecker: ApiUsingDuplicateChecker{
|
||||
ApiUsingConfig: ApiUsingConfig{
|
||||
container: settings.Container,
|
||||
},
|
||||
container: duplicatechecker.Container,
|
||||
},
|
||||
templates: services.TransactionTemplates,
|
||||
}
|
||||
)
|
||||
|
||||
// TemplateListHandler returns transaction template list of current user
|
||||
func (a *TransactionTemplatesApi) TemplateListHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
var templateListReq models.TransactionTemplateListRequest
|
||||
err := c.ShouldBindQuery(&templateListReq)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[transaction_templates.TemplateListHandler] parse request failed, because %s", err.Error())
|
||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||
}
|
||||
|
||||
if templateListReq.TemplateType < models.TRANSACTION_TEMPLATE_TYPE_NORMAL || templateListReq.TemplateType > models.TRANSACTION_TEMPLATE_TYPE_SCHEDULE {
|
||||
log.Warnf(c, "[transaction_templates.TemplateListHandler] template type invalid, type is %d", templateListReq.TemplateType)
|
||||
return nil, errs.ErrTransactionTemplateTypeInvalid
|
||||
}
|
||||
|
||||
if templateListReq.TemplateType == models.TRANSACTION_TEMPLATE_TYPE_SCHEDULE && !a.CurrentConfig().EnableScheduledTransaction {
|
||||
return nil, errs.ErrScheduledTransactionNotEnabled
|
||||
}
|
||||
|
||||
uid := c.GetCurrentUid()
|
||||
templates, err := a.templates.GetAllTemplatesByUid(c, uid, templateListReq.TemplateType)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transaction_templates.TemplateListHandler] failed to get templates for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
templateResps := make(models.TransactionTemplateInfoResponseSlice, len(templates))
|
||||
serverUtcOffset := utils.GetServerTimezoneOffsetMinutes()
|
||||
|
||||
for i := 0; i < len(templates); i++ {
|
||||
templateResps[i] = templates[i].ToTransactionTemplateInfoResponse(serverUtcOffset)
|
||||
}
|
||||
|
||||
sort.Sort(templateResps)
|
||||
|
||||
return templateResps, nil
|
||||
}
|
||||
|
||||
// TemplateGetHandler returns one specific transaction template of current user
|
||||
func (a *TransactionTemplatesApi) TemplateGetHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
var templateGetReq models.TransactionTemplateGetRequest
|
||||
err := c.ShouldBindQuery(&templateGetReq)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[transaction_templates.TemplateGetHandler] parse request failed, because %s", err.Error())
|
||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||
}
|
||||
|
||||
uid := c.GetCurrentUid()
|
||||
template, err := a.templates.GetTemplateByTemplateId(c, uid, templateGetReq.Id)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transaction_templates.TemplateGetHandler] failed to get template \"id:%d\" for user \"uid:%d\", because %s", templateGetReq.Id, uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
if template.TemplateType == models.TRANSACTION_TEMPLATE_TYPE_SCHEDULE && !a.CurrentConfig().EnableScheduledTransaction {
|
||||
return nil, errs.ErrScheduledTransactionNotEnabled
|
||||
}
|
||||
|
||||
serverUtcOffset := utils.GetServerTimezoneOffsetMinutes()
|
||||
templateResp := template.ToTransactionTemplateInfoResponse(serverUtcOffset)
|
||||
|
||||
return templateResp, nil
|
||||
}
|
||||
|
||||
// TemplateCreateHandler saves a new transaction template by request parameters for current user
|
||||
func (a *TransactionTemplatesApi) TemplateCreateHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
var templateCreateReq models.TransactionTemplateCreateRequest
|
||||
err := c.ShouldBindJSON(&templateCreateReq)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[transaction_templates.TemplateCreateHandler] parse request failed, because %s", err.Error())
|
||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||
}
|
||||
|
||||
if templateCreateReq.TemplateType < models.TRANSACTION_TEMPLATE_TYPE_NORMAL || templateCreateReq.TemplateType > models.TRANSACTION_TEMPLATE_TYPE_SCHEDULE {
|
||||
log.Warnf(c, "[transaction_templates.TemplateCreateHandler] template type invalid, type is %d", templateCreateReq.TemplateType)
|
||||
return nil, errs.ErrTransactionTemplateTypeInvalid
|
||||
}
|
||||
|
||||
if templateCreateReq.TemplateType == models.TRANSACTION_TEMPLATE_TYPE_SCHEDULE && !a.CurrentConfig().EnableScheduledTransaction {
|
||||
return nil, errs.ErrScheduledTransactionNotEnabled
|
||||
}
|
||||
|
||||
if templateCreateReq.Type <= models.TRANSACTION_TYPE_MODIFY_BALANCE || templateCreateReq.Type > models.TRANSACTION_TYPE_TRANSFER {
|
||||
log.Warnf(c, "[transaction_templates.TemplateCreateHandler] transaction type invalid, type is %d", templateCreateReq.Type)
|
||||
return nil, errs.ErrTransactionTypeInvalid
|
||||
}
|
||||
|
||||
if templateCreateReq.TemplateType == models.TRANSACTION_TEMPLATE_TYPE_SCHEDULE {
|
||||
if templateCreateReq.ScheduledFrequencyType == nil ||
|
||||
templateCreateReq.ScheduledFrequency == nil ||
|
||||
templateCreateReq.ScheduledTimezoneUtcOffset == nil {
|
||||
return nil, errs.ErrScheduledTransactionFrequencyInvalid
|
||||
}
|
||||
|
||||
if *templateCreateReq.ScheduledFrequencyType == models.TRANSACTION_SCHEDULE_FREQUENCY_TYPE_DISABLED && *templateCreateReq.ScheduledFrequency != "" {
|
||||
return nil, errs.ErrScheduledTransactionFrequencyInvalid
|
||||
} else if *templateCreateReq.ScheduledFrequencyType != models.TRANSACTION_SCHEDULE_FREQUENCY_TYPE_DISABLED && *templateCreateReq.ScheduledFrequency == "" {
|
||||
return nil, errs.ErrScheduledTransactionFrequencyInvalid
|
||||
}
|
||||
}
|
||||
|
||||
if len(templateCreateReq.TagIds) > maximumTagsCountOfTemplate {
|
||||
return nil, errs.ErrTransactionTemplateHasTooManyTags
|
||||
}
|
||||
|
||||
uid := c.GetCurrentUid()
|
||||
|
||||
maxOrderId, err := a.templates.GetMaxDisplayOrder(c, uid, templateCreateReq.TemplateType)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transaction_templates.TemplateCreateHandler] failed to get max display order for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
serverUtcOffset := utils.GetServerTimezoneOffsetMinutes()
|
||||
template, err := a.createNewTemplateModel(uid, &templateCreateReq, maxOrderId+1)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transaction_templates.TemplateCreateHandler] failed to create new template for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
if a.CurrentConfig().EnableDuplicateSubmissionsCheck && templateCreateReq.ClientSessionId != "" {
|
||||
found, remark := a.GetSubmissionRemark(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_TEMPLATE, uid, templateCreateReq.ClientSessionId)
|
||||
|
||||
if found {
|
||||
log.Infof(c, "[transaction_templates.TemplateCreateHandler] another template \"id:%s\" has been created for user \"uid:%d\"", remark, uid)
|
||||
templateId, err := utils.StringToInt64(remark)
|
||||
|
||||
if err == nil {
|
||||
template, err = a.templates.GetTemplateByTemplateId(c, uid, templateId)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transaction_templates.TemplateCreateHandler] failed to get existed template \"id:%d\" for user \"uid:%d\", because %s", templateId, uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
templateResp := template.ToTransactionTemplateInfoResponse(serverUtcOffset)
|
||||
|
||||
return templateResp, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = a.templates.CreateTemplate(c, template)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transaction_templates.TemplateCreateHandler] failed to create template \"id:%d\" for user \"uid:%d\", because %s", template.TemplateId, uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
log.Infof(c, "[transaction_templates.TemplateCreateHandler] user \"uid:%d\" has created a new template \"id:%d\" successfully", uid, template.TemplateId)
|
||||
|
||||
a.SetSubmissionRemark(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_TEMPLATE, uid, templateCreateReq.ClientSessionId, utils.Int64ToString(template.TemplateId))
|
||||
templateResp := template.ToTransactionTemplateInfoResponse(serverUtcOffset)
|
||||
|
||||
return templateResp, nil
|
||||
}
|
||||
|
||||
// TemplateModifyHandler saves an existed transaction template by request parameters for current user
|
||||
func (a *TransactionTemplatesApi) TemplateModifyHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
var templateModifyReq models.TransactionTemplateModifyRequest
|
||||
err := c.ShouldBindJSON(&templateModifyReq)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[transaction_templates.TemplateModifyHandler] parse request failed, because %s", err.Error())
|
||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||
}
|
||||
|
||||
if templateModifyReq.Type <= models.TRANSACTION_TYPE_MODIFY_BALANCE || templateModifyReq.Type > models.TRANSACTION_TYPE_TRANSFER {
|
||||
log.Warnf(c, "[transaction_templates.TemplateModifyHandler] transaction type invalid, type is %d", templateModifyReq.Type)
|
||||
return nil, errs.ErrTransactionTypeInvalid
|
||||
}
|
||||
|
||||
uid := c.GetCurrentUid()
|
||||
template, err := a.templates.GetTemplateByTemplateId(c, uid, templateModifyReq.Id)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transaction_templates.TemplateModifyHandler] failed to get template \"id:%d\" for user \"uid:%d\", because %s", templateModifyReq.Id, uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
if template.TemplateType == models.TRANSACTION_TEMPLATE_TYPE_SCHEDULE && !a.CurrentConfig().EnableScheduledTransaction {
|
||||
return nil, errs.ErrScheduledTransactionNotEnabled
|
||||
}
|
||||
|
||||
if template.TemplateType == models.TRANSACTION_TEMPLATE_TYPE_SCHEDULE {
|
||||
if templateModifyReq.ScheduledFrequencyType == nil ||
|
||||
templateModifyReq.ScheduledFrequency == nil ||
|
||||
templateModifyReq.ScheduledTimezoneUtcOffset == nil {
|
||||
return nil, errs.ErrScheduledTransactionFrequencyInvalid
|
||||
}
|
||||
|
||||
if *templateModifyReq.ScheduledFrequencyType == models.TRANSACTION_SCHEDULE_FREQUENCY_TYPE_DISABLED && *templateModifyReq.ScheduledFrequency != "" {
|
||||
return nil, errs.ErrScheduledTransactionFrequencyInvalid
|
||||
} else if *templateModifyReq.ScheduledFrequencyType != models.TRANSACTION_SCHEDULE_FREQUENCY_TYPE_DISABLED && *templateModifyReq.ScheduledFrequency == "" {
|
||||
return nil, errs.ErrScheduledTransactionFrequencyInvalid
|
||||
}
|
||||
}
|
||||
|
||||
if len(templateModifyReq.TagIds) > maximumTagsCountOfTemplate {
|
||||
return nil, errs.ErrTransactionTemplateHasTooManyTags
|
||||
}
|
||||
|
||||
newTemplate := &models.TransactionTemplate{
|
||||
TemplateId: template.TemplateId,
|
||||
Uid: uid,
|
||||
Name: templateModifyReq.Name,
|
||||
Type: templateModifyReq.Type,
|
||||
CategoryId: templateModifyReq.CategoryId,
|
||||
AccountId: templateModifyReq.SourceAccountId,
|
||||
TagIds: strings.Join(templateModifyReq.TagIds, ","),
|
||||
Amount: templateModifyReq.SourceAmount,
|
||||
RelatedAccountId: templateModifyReq.DestinationAccountId,
|
||||
RelatedAccountAmount: templateModifyReq.DestinationAmount,
|
||||
HideAmount: templateModifyReq.HideAmount,
|
||||
Comment: templateModifyReq.Comment,
|
||||
}
|
||||
|
||||
if template.TemplateType == models.TRANSACTION_TEMPLATE_TYPE_SCHEDULE {
|
||||
newTemplate.ScheduledFrequencyType = *templateModifyReq.ScheduledFrequencyType
|
||||
newTemplate.ScheduledFrequency = a.getOrderedFrequencyValues(*templateModifyReq.ScheduledFrequency)
|
||||
newTemplate.ScheduledAt = a.getUTCScheduledAt(*templateModifyReq.ScheduledTimezoneUtcOffset)
|
||||
newTemplate.ScheduledTimezoneUtcOffset = *templateModifyReq.ScheduledTimezoneUtcOffset
|
||||
|
||||
if templateModifyReq.ScheduledStartDate != nil {
|
||||
startTime, err := utils.ParseFromLongDateFirstTime(*templateModifyReq.ScheduledStartDate, *templateModifyReq.ScheduledTimezoneUtcOffset)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transaction_templates.TemplateModifyHandler] failed to parse scheduled start date for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
startUnixTime := startTime.Unix()
|
||||
newTemplate.ScheduledStartTime = &startUnixTime
|
||||
}
|
||||
|
||||
if templateModifyReq.ScheduledEndDate != nil {
|
||||
endTime, err := utils.ParseFromLongDateLastTime(*templateModifyReq.ScheduledEndDate, *templateModifyReq.ScheduledTimezoneUtcOffset)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transaction_templates.TemplateModifyHandler] failed to parse scheduled end date for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
endUnixTime := endTime.Unix()
|
||||
newTemplate.ScheduledEndTime = &endUnixTime
|
||||
}
|
||||
|
||||
if newTemplate.ScheduledStartTime != nil && newTemplate.ScheduledEndTime != nil && *newTemplate.ScheduledStartTime > *newTemplate.ScheduledEndTime {
|
||||
return nil, errs.ErrScheduledTransactionTemplateStartDataLaterThanEndDate
|
||||
}
|
||||
}
|
||||
|
||||
if newTemplate.Name == template.Name &&
|
||||
newTemplate.Type == template.Type &&
|
||||
newTemplate.CategoryId == template.CategoryId &&
|
||||
newTemplate.AccountId == template.AccountId &&
|
||||
newTemplate.TagIds == template.TagIds &&
|
||||
newTemplate.Amount == template.Amount &&
|
||||
newTemplate.RelatedAccountId == template.RelatedAccountId &&
|
||||
newTemplate.RelatedAccountAmount == template.RelatedAccountAmount &&
|
||||
newTemplate.HideAmount == template.HideAmount &&
|
||||
newTemplate.Comment == template.Comment {
|
||||
if template.TemplateType == models.TRANSACTION_TEMPLATE_TYPE_NORMAL {
|
||||
return nil, errs.ErrNothingWillBeUpdated
|
||||
} else if template.TemplateType == models.TRANSACTION_TEMPLATE_TYPE_SCHEDULE {
|
||||
if newTemplate.ScheduledFrequencyType == template.ScheduledFrequencyType &&
|
||||
newTemplate.ScheduledFrequency == template.ScheduledFrequency &&
|
||||
newTemplate.ScheduledStartTime == template.ScheduledStartTime &&
|
||||
newTemplate.ScheduledEndTime == template.ScheduledEndTime &&
|
||||
newTemplate.ScheduledAt == template.ScheduledAt &&
|
||||
newTemplate.ScheduledTimezoneUtcOffset == template.ScheduledTimezoneUtcOffset {
|
||||
return nil, errs.ErrNothingWillBeUpdated
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = a.templates.ModifyTemplate(c, newTemplate)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transaction_templates.TemplateModifyHandler] failed to update template \"id:%d\" for user \"uid:%d\", because %s", templateModifyReq.Id, uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
log.Infof(c, "[transaction_templates.TemplateModifyHandler] user \"uid:%d\" has updated template \"id:%d\" successfully", uid, templateModifyReq.Id)
|
||||
|
||||
serverUtcOffset := utils.GetServerTimezoneOffsetMinutes()
|
||||
newTemplate.TemplateType = template.TemplateType
|
||||
newTemplate.DisplayOrder = template.DisplayOrder
|
||||
newTemplate.Hidden = template.Hidden
|
||||
templateResp := newTemplate.ToTransactionTemplateInfoResponse(serverUtcOffset)
|
||||
|
||||
return templateResp, nil
|
||||
}
|
||||
|
||||
// TemplateHideHandler hides a transaction template by request parameters for current user
|
||||
func (a *TransactionTemplatesApi) TemplateHideHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
var templateHideReq models.TransactionTemplateHideRequest
|
||||
err := c.ShouldBindJSON(&templateHideReq)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[transaction_templates.TemplateHideHandler] parse request failed, because %s", err.Error())
|
||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||
}
|
||||
|
||||
uid := c.GetCurrentUid()
|
||||
|
||||
template, err := a.templates.GetTemplateByTemplateId(c, uid, templateHideReq.Id)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transaction_templates.TemplateHideHandler] failed to get template \"id:%d\" for user \"uid:%d\", because %s", templateHideReq.Id, uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
if template.TemplateType == models.TRANSACTION_TEMPLATE_TYPE_SCHEDULE && !a.CurrentConfig().EnableScheduledTransaction {
|
||||
return nil, errs.ErrScheduledTransactionNotEnabled
|
||||
}
|
||||
|
||||
err = a.templates.HideTemplate(c, uid, []int64{templateHideReq.Id}, templateHideReq.Hidden)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transaction_templates.TemplateHideHandler] failed to hide template \"id:%d\" for user \"uid:%d\", because %s", templateHideReq.Id, uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
log.Infof(c, "[transaction_templates.TemplateHideHandler] user \"uid:%d\" has hidden template \"id:%d\"", uid, templateHideReq.Id)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// TemplateMoveHandler moves display order of existed transaction templates by request parameters for current user
|
||||
func (a *TransactionTemplatesApi) TemplateMoveHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
var templateMoveReq models.TransactionTemplateMoveRequest
|
||||
err := c.ShouldBindJSON(&templateMoveReq)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[transaction_templates.TemplateMoveHandler] parse request failed, because %s", err.Error())
|
||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||
}
|
||||
|
||||
uid := c.GetCurrentUid()
|
||||
|
||||
if len(templateMoveReq.NewDisplayOrders) > 0 {
|
||||
template, err := a.templates.GetTemplateByTemplateId(c, uid, templateMoveReq.NewDisplayOrders[0].Id)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transaction_templates.TemplateMoveHandler] failed to get template \"id:%d\" for user \"uid:%d\", because %s", templateMoveReq.NewDisplayOrders[0].Id, uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
if template.TemplateType == models.TRANSACTION_TEMPLATE_TYPE_SCHEDULE && !a.CurrentConfig().EnableScheduledTransaction {
|
||||
return nil, errs.ErrScheduledTransactionNotEnabled
|
||||
}
|
||||
}
|
||||
|
||||
templates := make([]*models.TransactionTemplate, len(templateMoveReq.NewDisplayOrders))
|
||||
|
||||
for i := 0; i < len(templateMoveReq.NewDisplayOrders); i++ {
|
||||
newDisplayOrder := templateMoveReq.NewDisplayOrders[i]
|
||||
template := &models.TransactionTemplate{
|
||||
Uid: uid,
|
||||
TemplateId: newDisplayOrder.Id,
|
||||
DisplayOrder: newDisplayOrder.DisplayOrder,
|
||||
}
|
||||
|
||||
templates[i] = template
|
||||
}
|
||||
|
||||
err = a.templates.ModifyTemplateDisplayOrders(c, uid, templates)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transaction_templates.TemplateMoveHandler] failed to move templates for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
log.Infof(c, "[transaction_templates.TemplateMoveHandler] user \"uid:%d\" has moved templates", uid)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// TemplateDeleteHandler deletes an existed transaction template by request parameters for current user
|
||||
func (a *TransactionTemplatesApi) TemplateDeleteHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
var templateDeleteReq models.TransactionTemplateDeleteRequest
|
||||
err := c.ShouldBindJSON(&templateDeleteReq)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[transaction_templates.TemplateDeleteHandler] parse request failed, because %s", err.Error())
|
||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||
}
|
||||
|
||||
uid := c.GetCurrentUid()
|
||||
|
||||
template, err := a.templates.GetTemplateByTemplateId(c, uid, templateDeleteReq.Id)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transaction_templates.TemplateDeleteHandler] failed to get template \"id:%d\" for user \"uid:%d\", because %s", templateDeleteReq.Id, uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
if template.TemplateType == models.TRANSACTION_TEMPLATE_TYPE_SCHEDULE && !a.CurrentConfig().EnableScheduledTransaction {
|
||||
return nil, errs.ErrScheduledTransactionNotEnabled
|
||||
}
|
||||
|
||||
err = a.templates.DeleteTemplate(c, uid, templateDeleteReq.Id)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transaction_templates.TemplateDeleteHandler] failed to delete template \"id:%d\" for user \"uid:%d\", because %s", templateDeleteReq.Id, uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
log.Infof(c, "[transaction_templates.TemplateDeleteHandler] user \"uid:%d\" has deleted template \"id:%d\"", uid, templateDeleteReq.Id)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (a *TransactionTemplatesApi) createNewTemplateModel(uid int64, templateCreateReq *models.TransactionTemplateCreateRequest, order int32) (*models.TransactionTemplate, error) {
|
||||
template := &models.TransactionTemplate{
|
||||
Uid: uid,
|
||||
TemplateType: templateCreateReq.TemplateType,
|
||||
Name: templateCreateReq.Name,
|
||||
Type: templateCreateReq.Type,
|
||||
CategoryId: templateCreateReq.CategoryId,
|
||||
AccountId: templateCreateReq.SourceAccountId,
|
||||
TagIds: strings.Join(templateCreateReq.TagIds, ","),
|
||||
Amount: templateCreateReq.SourceAmount,
|
||||
RelatedAccountId: templateCreateReq.DestinationAccountId,
|
||||
RelatedAccountAmount: templateCreateReq.DestinationAmount,
|
||||
HideAmount: templateCreateReq.HideAmount,
|
||||
Comment: templateCreateReq.Comment,
|
||||
DisplayOrder: order,
|
||||
}
|
||||
|
||||
if templateCreateReq.TemplateType == models.TRANSACTION_TEMPLATE_TYPE_SCHEDULE {
|
||||
template.ScheduledFrequencyType = *templateCreateReq.ScheduledFrequencyType
|
||||
template.ScheduledFrequency = a.getOrderedFrequencyValues(*templateCreateReq.ScheduledFrequency)
|
||||
template.ScheduledAt = a.getUTCScheduledAt(*templateCreateReq.ScheduledTimezoneUtcOffset)
|
||||
template.ScheduledTimezoneUtcOffset = *templateCreateReq.ScheduledTimezoneUtcOffset
|
||||
|
||||
if templateCreateReq.ScheduledStartDate != nil {
|
||||
startTime, err := utils.ParseFromLongDateFirstTime(*templateCreateReq.ScheduledStartDate, *templateCreateReq.ScheduledTimezoneUtcOffset)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
startUnixTime := startTime.Unix()
|
||||
template.ScheduledStartTime = &startUnixTime
|
||||
}
|
||||
|
||||
if templateCreateReq.ScheduledEndDate != nil {
|
||||
endTime, err := utils.ParseFromLongDateLastTime(*templateCreateReq.ScheduledEndDate, *templateCreateReq.ScheduledTimezoneUtcOffset)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
endUnixTime := endTime.Unix()
|
||||
template.ScheduledEndTime = &endUnixTime
|
||||
}
|
||||
|
||||
if template.ScheduledStartTime != nil && template.ScheduledEndTime != nil && *template.ScheduledStartTime > *template.ScheduledEndTime {
|
||||
return nil, errs.ErrScheduledTransactionTemplateStartDataLaterThanEndDate
|
||||
}
|
||||
}
|
||||
|
||||
return template, nil
|
||||
}
|
||||
|
||||
func (a *TransactionTemplatesApi) getUTCScheduledAt(scheduledTimezoneUtcOffset int16) int16 {
|
||||
templateTimeZone := time.FixedZone("Template Timezone", int(scheduledTimezoneUtcOffset)*60)
|
||||
transactionTime := time.Date(2020, 1, 1, 0, 0, 0, 0, templateTimeZone)
|
||||
transactionTimeInUTC := transactionTime.In(time.UTC)
|
||||
|
||||
minutesElapsedOfDayInUtc := transactionTimeInUTC.Hour()*60 + transactionTimeInUTC.Minute()
|
||||
|
||||
return int16(minutesElapsedOfDayInUtc)
|
||||
}
|
||||
|
||||
func (a *TransactionTemplatesApi) getOrderedFrequencyValues(frequencyValue string) string {
|
||||
if frequencyValue == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
items := strings.Split(frequencyValue, ",")
|
||||
values := make([]int, 0, len(items))
|
||||
valueExistMap := make(map[int]bool)
|
||||
|
||||
for i := 0; i < len(items); i++ {
|
||||
value, err := utils.StringToInt(items[i])
|
||||
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, exists := valueExistMap[value]; !exists {
|
||||
values = append(values, value)
|
||||
valueExistMap[value] = true
|
||||
}
|
||||
}
|
||||
|
||||
sort.Ints(values)
|
||||
|
||||
var sortedFrequencyValueBuilder strings.Builder
|
||||
|
||||
for i := 0; i < len(values); i++ {
|
||||
if sortedFrequencyValueBuilder.Len() > 0 {
|
||||
sortedFrequencyValueBuilder.WriteRune(',')
|
||||
}
|
||||
|
||||
sortedFrequencyValueBuilder.WriteString(utils.IntToString(values[i]))
|
||||
}
|
||||
|
||||
return sortedFrequencyValueBuilder.String()
|
||||
}
|
||||
+235
-890
File diff suppressed because it is too large
Load Diff
@@ -32,7 +32,7 @@ var (
|
||||
)
|
||||
|
||||
// TwoFactorStatusHandler returns 2fa status of current user
|
||||
func (a *TwoFactorAuthorizationsApi) TwoFactorStatusHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
func (a *TwoFactorAuthorizationsApi) TwoFactorStatusHandler(c *core.Context) (any, *errs.Error) {
|
||||
uid := c.GetCurrentUid()
|
||||
twoFactorSetting, err := a.twoFactorAuthorizations.GetUserTwoFactorSettingByUid(c, uid)
|
||||
|
||||
@@ -45,7 +45,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorStatusHandler(c *core.WebContext)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[twofactor_authorizations.TwoFactorStatusHandler] failed to get two-factor setting, because %s", err.Error())
|
||||
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorStatusHandler] failed to get two factor setting, because %s", err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
@@ -58,12 +58,12 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorStatusHandler(c *core.WebContext)
|
||||
}
|
||||
|
||||
// TwoFactorEnableRequestHandler returns a new 2fa secret and qr code for current user to set 2fa and verify passcode next
|
||||
func (a *TwoFactorAuthorizationsApi) TwoFactorEnableRequestHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
func (a *TwoFactorAuthorizationsApi) TwoFactorEnableRequestHandler(c *core.Context) (any, *errs.Error) {
|
||||
uid := c.GetCurrentUid()
|
||||
enabled, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(c, uid)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[twofactor_authorizations.TwoFactorEnableRequestHandler] failed to check two-factor setting, because %s", err.Error())
|
||||
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableRequestHandler] failed to check two factor setting, because %s", err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
@@ -75,27 +75,23 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorEnableRequestHandler(c *core.WebCo
|
||||
|
||||
if err != nil {
|
||||
if !errs.IsCustomError(err) {
|
||||
log.Errorf(c, "[twofactor_authorizations.TwoFactorEnableRequestHandler] failed to get user, because %s", err.Error())
|
||||
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableRequestHandler] failed to get user, because %s", err.Error())
|
||||
}
|
||||
|
||||
return nil, errs.ErrUserNotFound
|
||||
}
|
||||
|
||||
if user.FeatureRestriction.Contains(core.USER_FEATURE_RESTRICTION_TYPE_ENABLE_2FA) {
|
||||
return nil, errs.ErrNotPermittedToPerformThisAction
|
||||
}
|
||||
|
||||
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())
|
||||
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableRequestHandler] failed to generate two factor secret, because %s", err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
img, err := key.Image(240, 240)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[twofactor_authorizations.TwoFactorEnableRequestHandler] failed to generate two-factor qrcode, because %s", err.Error())
|
||||
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableRequestHandler] failed to generate two factor qrcode, because %s", err.Error())
|
||||
return nil, errs.ErrOperationFailed
|
||||
}
|
||||
|
||||
@@ -114,12 +110,12 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorEnableRequestHandler(c *core.WebCo
|
||||
}
|
||||
|
||||
// TwoFactorEnableConfirmHandler enables 2fa for current user
|
||||
func (a *TwoFactorAuthorizationsApi) TwoFactorEnableConfirmHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
func (a *TwoFactorAuthorizationsApi) TwoFactorEnableConfirmHandler(c *core.Context) (any, *errs.Error) {
|
||||
var confirmReq models.TwoFactorEnableConfirmRequest
|
||||
err := c.ShouldBindJSON(&confirmReq)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] parse request failed, because %s", err.Error())
|
||||
log.WarnfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] parse request failed, because %s", err.Error())
|
||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||
}
|
||||
|
||||
@@ -127,7 +123,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorEnableConfirmHandler(c *core.WebCo
|
||||
exists, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(c, uid)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to check two-factor setting, because %s", err.Error())
|
||||
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to check two factor setting, because %s", err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
@@ -139,62 +135,58 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorEnableConfirmHandler(c *core.WebCo
|
||||
|
||||
if err != nil {
|
||||
if !errs.IsCustomError(err) {
|
||||
log.Errorf(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to get user, because %s", err.Error())
|
||||
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to get user, because %s", err.Error())
|
||||
}
|
||||
|
||||
return nil, errs.ErrUserNotFound
|
||||
}
|
||||
|
||||
if user.FeatureRestriction.Contains(core.USER_FEATURE_RESTRICTION_TYPE_ENABLE_2FA) {
|
||||
return nil, errs.ErrNotPermittedToPerformThisAction
|
||||
}
|
||||
|
||||
twoFactorSetting := &models.TwoFactor{
|
||||
Uid: uid,
|
||||
Secret: confirmReq.Secret,
|
||||
}
|
||||
|
||||
if !totp.Validate(confirmReq.Passcode, confirmReq.Secret) {
|
||||
log.Warnf(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] passcode is invalid")
|
||||
log.WarnfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] passcode is invalid")
|
||||
return nil, errs.ErrPasscodeInvalid
|
||||
}
|
||||
|
||||
recoveryCodes, err := a.twoFactorAuthorizations.GenerateTwoFactorRecoveryCodes()
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to generate two-factor recovery codes for user \"uid:%d\", because %s", uid, err.Error())
|
||||
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to generate two factor recovery codes for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
err = a.twoFactorAuthorizations.CreateTwoFactorRecoveryCodes(c, uid, recoveryCodes, user.Salt)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to create two-factor recovery codes for user \"uid:%d\", because %s", uid, err.Error())
|
||||
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to create two factor recovery codes for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
err = a.twoFactorAuthorizations.CreateTwoFactorSetting(c, twoFactorSetting)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to create two-factor setting for user \"uid:%d\", because %s", uid, err.Error())
|
||||
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to create two factor setting for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
log.Infof(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] user \"uid:%d\" has enabled two-factor authorization", uid)
|
||||
log.InfofWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] user \"uid:%d\" has enabled two factor authorization", uid)
|
||||
|
||||
now := time.Now().Unix()
|
||||
err = a.tokens.DeleteTokensBeforeTime(c, uid, now)
|
||||
|
||||
if err == nil {
|
||||
log.Infof(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] revoke old tokens before unix time \"%d\" for user \"uid:%d\"", now, user.Uid)
|
||||
log.InfofWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] revoke old tokens before unix time \"%d\" for user \"uid:%d\"", now, user.Uid)
|
||||
} else {
|
||||
log.Warnf(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to revoke old tokens for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
log.WarnfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to revoke old tokens for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
}
|
||||
|
||||
token, claims, err := a.tokens.CreateToken(c, user)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
log.WarnfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
|
||||
confirmResp := &models.TwoFactorEnableConfirmResponse{
|
||||
RecoveryCodes: recoveryCodes,
|
||||
@@ -206,7 +198,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorEnableConfirmHandler(c *core.WebCo
|
||||
c.SetTextualToken(token)
|
||||
c.SetTokenClaims(claims)
|
||||
|
||||
log.Infof(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] user \"uid:%d\" token refreshed, new token will be expired at %d", user.Uid, claims.ExpiresAt)
|
||||
log.InfofWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] user \"uid:%d\" token refreshed, new token will be expired at %d", user.Uid, claims.ExpiresAt)
|
||||
|
||||
confirmResp := &models.TwoFactorEnableConfirmResponse{
|
||||
Token: token,
|
||||
@@ -217,12 +209,12 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorEnableConfirmHandler(c *core.WebCo
|
||||
}
|
||||
|
||||
// TwoFactorDisableHandler disables 2fa for current user
|
||||
func (a *TwoFactorAuthorizationsApi) TwoFactorDisableHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
func (a *TwoFactorAuthorizationsApi) TwoFactorDisableHandler(c *core.Context) (any, *errs.Error) {
|
||||
var disableReq models.TwoFactorDisableRequest
|
||||
err := c.ShouldBindJSON(&disableReq)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[twofactor_authorizations.TwoFactorDisableHandler] parse request failed, because %s", err.Error())
|
||||
log.WarnfWithRequestId(c, "[twofactor_authorizations.TwoFactorDisableHandler] parse request failed, because %s", err.Error())
|
||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||
}
|
||||
|
||||
@@ -231,16 +223,12 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorDisableHandler(c *core.WebContext)
|
||||
|
||||
if err != nil {
|
||||
if !errs.IsCustomError(err) {
|
||||
log.Warnf(c, "[twofactor_authorizations.TwoFactorDisableHandler] failed to get user for user \"uid:%d\", because %s", uid, err.Error())
|
||||
log.WarnfWithRequestId(c, "[twofactor_authorizations.TwoFactorDisableHandler] failed to get user for user \"uid:%d\", because %s", uid, err.Error())
|
||||
}
|
||||
|
||||
return nil, errs.ErrUserNotFound
|
||||
}
|
||||
|
||||
if user.FeatureRestriction.Contains(core.USER_FEATURE_RESTRICTION_TYPE_DISABLE_2FA) {
|
||||
return nil, errs.ErrNotPermittedToPerformThisAction
|
||||
}
|
||||
|
||||
if !a.users.IsPasswordEqualsUserPassword(disableReq.Password, user) {
|
||||
return nil, errs.ErrUserPasswordWrong
|
||||
}
|
||||
@@ -248,7 +236,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorDisableHandler(c *core.WebContext)
|
||||
enableTwoFactor, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(c, uid)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[twofactor_authorizations.TwoFactorDisableHandler] failed to check two-factor setting, because %s", err.Error())
|
||||
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorDisableHandler] failed to check two factor setting, because %s", err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
@@ -259,29 +247,29 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorDisableHandler(c *core.WebContext)
|
||||
err = a.twoFactorAuthorizations.DeleteTwoFactorRecoveryCodes(c, uid)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[twofactor_authorizations.TwoFactorDisableHandler] failed to delete two-factor recovery codes for user \"uid:%d\"", uid)
|
||||
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorDisableHandler] failed to delete two factor recovery codes for user \"uid:%d\"", uid)
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
err = a.twoFactorAuthorizations.DeleteTwoFactorSetting(c, uid)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[twofactor_authorizations.TwoFactorDisableHandler] failed to delete two-factor setting for user \"uid:%d\"", uid)
|
||||
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorDisableHandler] failed to delete two factor setting for user \"uid:%d\"", uid)
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
log.Infof(c, "[twofactor_authorizations.TwoFactorDisableHandler] user \"uid:%d\" has disabled two-factor authorization", uid)
|
||||
log.InfofWithRequestId(c, "[twofactor_authorizations.TwoFactorDisableHandler] user \"uid:%d\" has disabled two factor authorization", uid)
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// TwoFactorRecoveryCodeRegenerateHandler returns new 2fa recovery codes and revokes old recovery codes for current user
|
||||
func (a *TwoFactorAuthorizationsApi) TwoFactorRecoveryCodeRegenerateHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
func (a *TwoFactorAuthorizationsApi) TwoFactorRecoveryCodeRegenerateHandler(c *core.Context) (any, *errs.Error) {
|
||||
var regenerateReq models.TwoFactorRegenerateRecoveryCodeRequest
|
||||
err := c.ShouldBindJSON(®enerateReq)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[twofactor_authorizations.TwoFactorRecoveryCodeRegenerateHandler] parse request failed, because %s", err.Error())
|
||||
log.WarnfWithRequestId(c, "[twofactor_authorizations.TwoFactorRecoveryCodeRegenerateHandler] parse request failed, because %s", err.Error())
|
||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||
}
|
||||
|
||||
@@ -290,7 +278,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorRecoveryCodeRegenerateHandler(c *c
|
||||
|
||||
if err != nil {
|
||||
if !errs.IsCustomError(err) {
|
||||
log.Warnf(c, "[twofactor_authorizations.TwoFactorRecoveryCodeRegenerateHandler] failed to get user for user \"uid:%d\", because %s", uid, err.Error())
|
||||
log.WarnfWithRequestId(c, "[twofactor_authorizations.TwoFactorRecoveryCodeRegenerateHandler] failed to get user for user \"uid:%d\", because %s", uid, err.Error())
|
||||
}
|
||||
|
||||
return nil, errs.ErrUserNotFound
|
||||
@@ -303,7 +291,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorRecoveryCodeRegenerateHandler(c *c
|
||||
enableTwoFactor, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(c, uid)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[twofactor_authorizations.TwoFactorRecoveryCodeRegenerateHandler] failed to check two-factor setting, because %s", err.Error())
|
||||
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorRecoveryCodeRegenerateHandler] failed to check two factor setting, because %s", err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
@@ -314,14 +302,14 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorRecoveryCodeRegenerateHandler(c *c
|
||||
recoveryCodes, err := a.twoFactorAuthorizations.GenerateTwoFactorRecoveryCodes()
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[twofactor_authorizations.TwoFactorRecoveryCodeRegenerateHandler] failed to generate two-factor recovery codes for user \"uid:%d\", because %s", uid, err.Error())
|
||||
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorRecoveryCodeRegenerateHandler] failed to generate two factor recovery codes for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
err = a.twoFactorAuthorizations.CreateTwoFactorRecoveryCodes(c, uid, recoveryCodes, user.Salt)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[twofactor_authorizations.TwoFactorRecoveryCodeRegenerateHandler] failed to create two-factor recovery codes for user \"uid:%d\", because %s", uid, err.Error())
|
||||
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorRecoveryCodeRegenerateHandler] failed to create two factor recovery codes for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
@@ -329,7 +317,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorRecoveryCodeRegenerateHandler(c *c
|
||||
RecoveryCodes: recoveryCodes,
|
||||
}
|
||||
|
||||
log.Infof(c, "[twofactor_authorizations.TwoFactorRecoveryCodeRegenerateHandler] user \"uid:%d\" has regenerated two-factor recovery codes", uid)
|
||||
log.InfofWithRequestId(c, "[twofactor_authorizations.TwoFactorRecoveryCodeRegenerateHandler] user \"uid:%d\" has regenerated two factor recovery codes", uid)
|
||||
|
||||
return recoveryCodesResp, nil
|
||||
}
|
||||
|
||||
+65
-334
@@ -6,22 +6,17 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
|
||||
"github.com/mayswind/ezbookkeeping/pkg/avatars"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||
"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"
|
||||
)
|
||||
|
||||
// UsersApi represents user api
|
||||
type UsersApi struct {
|
||||
ApiUsingConfig
|
||||
ApiWithUserInfo
|
||||
users *services.UserService
|
||||
tokens *services.TokenService
|
||||
accounts *services.AccountService
|
||||
@@ -30,17 +25,6 @@ type UsersApi struct {
|
||||
// Initialize a user api singleton instance
|
||||
var (
|
||||
Users = &UsersApi{
|
||||
ApiUsingConfig: ApiUsingConfig{
|
||||
container: settings.Container,
|
||||
},
|
||||
ApiWithUserInfo: ApiWithUserInfo{
|
||||
ApiUsingConfig: ApiUsingConfig{
|
||||
container: settings.Container,
|
||||
},
|
||||
ApiUsingAvatarProvider: ApiUsingAvatarProvider{
|
||||
container: avatars.Container,
|
||||
},
|
||||
},
|
||||
users: services.Users,
|
||||
tokens: services.Tokens,
|
||||
accounts: services.Accounts,
|
||||
@@ -48,8 +32,8 @@ var (
|
||||
)
|
||||
|
||||
// UserRegisterHandler saves a new user by request parameters
|
||||
func (a *UsersApi) UserRegisterHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
if !a.CurrentConfig().EnableUserRegister {
|
||||
func (a *UsersApi) UserRegisterHandler(c *core.Context) (any, *errs.Error) {
|
||||
if !settings.Container.Current.EnableUserRegister {
|
||||
return nil, errs.ErrUserRegistrationNotAllowed
|
||||
}
|
||||
|
||||
@@ -57,12 +41,12 @@ func (a *UsersApi) UserRegisterHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
err := c.ShouldBindBodyWith(&userRegisterReq, binding.JSON)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[users.UserRegisterHandler] parse request failed, because %s", err.Error())
|
||||
log.WarnfWithRequestId(c, "[users.UserRegisterHandler] parse request failed, because %s", err.Error())
|
||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||
}
|
||||
|
||||
if userRegisterReq.DefaultCurrency == validators.ParentAccountCurrencyPlaceholder {
|
||||
log.Warnf(c, "[users.UserRegisterHandler] user default currency is invalid")
|
||||
log.WarnfWithRequestId(c, "[users.UserRegisterHandler] user default currency is invalid")
|
||||
return nil, errs.ErrUserDefaultCurrencyIsInvalid
|
||||
}
|
||||
|
||||
@@ -79,17 +63,16 @@ func (a *UsersApi) UserRegisterHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
DefaultCurrency: userRegisterReq.DefaultCurrency,
|
||||
FirstDayOfWeek: userRegisterReq.FirstDayOfWeek,
|
||||
TransactionEditScope: models.TRANSACTION_EDIT_SCOPE_ALL,
|
||||
FeatureRestriction: a.CurrentConfig().DefaultFeatureRestrictions,
|
||||
}
|
||||
|
||||
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())
|
||||
log.ErrorfWithRequestId(c, "[users.UserRegisterHandler] failed to create user \"%s\", because %s", user.Username, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
log.Infof(c, "[users.UserRegisterHandler] user \"%s\" has registered successfully, uid is %d", user.Username, user.Uid)
|
||||
log.InfofWithRequestId(c, "[users.UserRegisterHandler] user \"%s\" has registered successfully, uid is %d", user.Username, user.Uid)
|
||||
|
||||
presetCategoriesSaved := false
|
||||
|
||||
@@ -103,38 +86,37 @@ func (a *UsersApi) UserRegisterHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
|
||||
authResp := &models.RegisterResponse{
|
||||
AuthResponse: models.AuthResponse{
|
||||
Need2FA: false,
|
||||
User: a.GetUserBasicInfo(user),
|
||||
NotificationContent: a.GetAfterRegisterNotificationContent(user.Language, c.GetClientLocale()),
|
||||
Need2FA: false,
|
||||
User: user.ToUserBasicInfo(),
|
||||
},
|
||||
NeedVerifyEmail: a.CurrentConfig().EnableUserVerifyEmail && a.CurrentConfig().EnableUserForceVerifyEmail,
|
||||
NeedVerifyEmail: settings.Container.Current.EnableUserVerifyEmail && settings.Container.Current.EnableUserForceVerifyEmail,
|
||||
PresetCategoriesSaved: presetCategoriesSaved,
|
||||
}
|
||||
|
||||
if a.CurrentConfig().EnableUserVerifyEmail && a.CurrentConfig().EnableSMTP {
|
||||
if settings.Container.Current.EnableUserVerifyEmail && settings.Container.Current.EnableSMTP {
|
||||
token, _, err := a.tokens.CreateEmailVerifyToken(c, user)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[users.UserRegisterHandler] failed to create email verify token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
log.ErrorfWithRequestId(c, "[users.UserRegisterHandler] failed to create email verify token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
} else {
|
||||
go func() {
|
||||
err = a.users.SendVerifyEmail(user, token, c.GetClientLocale())
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[users.UserRegisterHandler] cannot send verify email to \"%s\", because %s", user.Email, err.Error())
|
||||
log.WarnfWithRequestId(c, "[users.UserRegisterHandler] cannot send verify email to \"%s\", because %s", user.Email, err.Error())
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
if a.CurrentConfig().EnableUserForceVerifyEmail {
|
||||
if settings.Container.Current.EnableUserForceVerifyEmail {
|
||||
return authResp, nil
|
||||
}
|
||||
|
||||
token, claims, err := a.tokens.CreateToken(c, user)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[users.UserRegisterHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
log.WarnfWithRequestId(c, "[users.UserRegisterHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
return authResp, nil
|
||||
}
|
||||
|
||||
@@ -142,13 +124,13 @@ func (a *UsersApi) UserRegisterHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
c.SetTextualToken(token)
|
||||
c.SetTokenClaims(claims)
|
||||
|
||||
log.Infof(c, "[users.UserRegisterHandler] user \"uid:%d\" has logined, token will be expired at %d", user.Uid, claims.ExpiresAt)
|
||||
log.InfofWithRequestId(c, "[users.UserRegisterHandler] user \"uid:%d\" has logined, token will be expired at %d", user.Uid, claims.ExpiresAt)
|
||||
|
||||
return authResp, nil
|
||||
}
|
||||
|
||||
// UserEmailVerifyHandler sets user email address verified
|
||||
func (a *UsersApi) UserEmailVerifyHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
func (a *UsersApi) UserEmailVerifyHandler(c *core.Context) (any, *errs.Error) {
|
||||
var userVerifyEmailReq models.UserVerifyEmailRequest
|
||||
err := c.ShouldBindJSON(&userVerifyEmailReq)
|
||||
|
||||
@@ -157,35 +139,35 @@ func (a *UsersApi) UserEmailVerifyHandler(c *core.WebContext) (any, *errs.Error)
|
||||
|
||||
if err != nil {
|
||||
if !errs.IsCustomError(err) {
|
||||
log.Errorf(c, "[users.UserEmailVerifyHandler] failed to get user, because %s", err.Error())
|
||||
log.ErrorfWithRequestId(c, "[users.UserEmailVerifyHandler] failed to get user, because %s", err.Error())
|
||||
}
|
||||
|
||||
return nil, errs.ErrUserNotFound
|
||||
}
|
||||
|
||||
if user.Disabled {
|
||||
log.Warnf(c, "[users.UserEmailVerifyHandler] user \"uid:%d\" is disabled", user.Uid)
|
||||
log.WarnfWithRequestId(c, "[users.UserEmailVerifyHandler] user \"uid:%d\" is disabled", user.Uid)
|
||||
return nil, errs.ErrUserIsDisabled
|
||||
}
|
||||
|
||||
if user.EmailVerified {
|
||||
log.Warnf(c, "[users.UserEmailVerifyHandler] user \"uid:%d\" email has been verified", user.Uid)
|
||||
log.WarnfWithRequestId(c, "[users.UserEmailVerifyHandler] user \"uid:%d\" email has been verified", user.Uid)
|
||||
return nil, errs.ErrEmailIsVerified
|
||||
}
|
||||
|
||||
err = a.users.SetUserEmailVerified(c, user.Username)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[users.UserEmailVerifyHandler] failed to update user \"uid:%d\" email address verified, because %s", user.Uid, err.Error())
|
||||
log.ErrorfWithRequestId(c, "[users.UserEmailVerifyHandler] failed to update user \"uid:%d\" email address verified, because %s", user.Uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
err = a.tokens.DeleteTokensByType(c, uid, core.USER_TOKEN_TYPE_EMAIL_VERIFY)
|
||||
|
||||
if err == nil {
|
||||
log.Infof(c, "[users.UserEmailVerifyHandler] revoke old email verify tokens for user \"uid:%d\"", user.Uid)
|
||||
log.InfofWithRequestId(c, "[users.UserEmailVerifyHandler] revoke old email verify tokens for user \"uid:%d\"", user.Uid)
|
||||
} else {
|
||||
log.Warnf(c, "[users.UserEmailVerifyHandler] failed to revoke old email verify tokens for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
log.WarnfWithRequestId(c, "[users.UserEmailVerifyHandler] failed to revoke old email verify tokens for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
}
|
||||
|
||||
resp := &models.UserVerifyEmailResponse{}
|
||||
@@ -194,47 +176,45 @@ func (a *UsersApi) UserEmailVerifyHandler(c *core.WebContext) (any, *errs.Error)
|
||||
token, claims, err := a.tokens.CreateToken(c, user)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[users.UserEmailVerifyHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
log.WarnfWithRequestId(c, "[users.UserEmailVerifyHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
resp.NewToken = token
|
||||
resp.User = a.GetUserBasicInfo(user)
|
||||
resp.NotificationContent = a.GetAfterLoginNotificationContent(user.Language, c.GetClientLocale())
|
||||
|
||||
resp.User = user.ToUserBasicInfo()
|
||||
c.SetTextualToken(token)
|
||||
c.SetTokenClaims(claims)
|
||||
|
||||
log.Infof(c, "[users.UserEmailVerifyHandler] user \"uid:%d\" token created, new token will be expired at %d", user.Uid, claims.ExpiresAt)
|
||||
log.InfofWithRequestId(c, "[users.UserEmailVerifyHandler] user \"uid:%d\" token created, new token will be expired at %d", user.Uid, claims.ExpiresAt)
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// UserProfileHandler returns user profile of current user
|
||||
func (a *UsersApi) UserProfileHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
func (a *UsersApi) UserProfileHandler(c *core.Context) (any, *errs.Error) {
|
||||
uid := c.GetCurrentUid()
|
||||
user, err := a.users.GetUserById(c, uid)
|
||||
|
||||
if err != nil {
|
||||
if !errs.IsCustomError(err) {
|
||||
log.Errorf(c, "[users.UserRegisterHandler] failed to get user, because %s", err.Error())
|
||||
log.ErrorfWithRequestId(c, "[users.UserRegisterHandler] failed to get user, because %s", err.Error())
|
||||
}
|
||||
|
||||
return nil, errs.ErrUserNotFound
|
||||
}
|
||||
|
||||
userResp := a.getUserProfileResponse(user)
|
||||
userResp := user.ToUserProfileResponse()
|
||||
return userResp, nil
|
||||
}
|
||||
|
||||
// UserUpdateProfileHandler saves user profile by request parameters for current user
|
||||
func (a *UsersApi) UserUpdateProfileHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
func (a *UsersApi) UserUpdateProfileHandler(c *core.Context) (any, *errs.Error) {
|
||||
var userUpdateReq models.UserProfileUpdateRequest
|
||||
err := c.ShouldBindJSON(&userUpdateReq)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[users.UserUpdateProfileHandler] parse request failed, because %s", err.Error())
|
||||
log.WarnfWithRequestId(c, "[users.UserUpdateProfileHandler] parse request failed, because %s", err.Error())
|
||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||
}
|
||||
|
||||
@@ -243,7 +223,7 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.WebContext) (any, *errs.Erro
|
||||
|
||||
if err != nil {
|
||||
if !errs.IsCustomError(err) {
|
||||
log.Errorf(c, "[users.UserUpdateProfileHandler] failed to get user, because %s", err.Error())
|
||||
log.ErrorfWithRequestId(c, "[users.UserUpdateProfileHandler] failed to get user, because %s", err.Error())
|
||||
}
|
||||
|
||||
return nil, errs.ErrUserNotFound
|
||||
@@ -252,7 +232,6 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.WebContext) (any, *errs.Erro
|
||||
userUpdateReq.Email = strings.TrimSpace(userUpdateReq.Email)
|
||||
userUpdateReq.Nickname = strings.TrimSpace(userUpdateReq.Nickname)
|
||||
|
||||
modifyProfileBasicInfo := false
|
||||
anythingUpdate := false
|
||||
userNew := &models.User{
|
||||
Uid: user.Uid,
|
||||
@@ -260,20 +239,12 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.WebContext) (any, *errs.Erro
|
||||
}
|
||||
|
||||
if userUpdateReq.Email != "" && userUpdateReq.Email != user.Email {
|
||||
if user.FeatureRestriction.Contains(core.USER_FEATURE_RESTRICTION_TYPE_UPDATE_EMAIL) {
|
||||
return nil, errs.ErrNotPermittedToPerformThisAction
|
||||
}
|
||||
|
||||
user.Email = userUpdateReq.Email
|
||||
userNew.Email = userUpdateReq.Email
|
||||
anythingUpdate = true
|
||||
}
|
||||
|
||||
if userUpdateReq.Password != "" {
|
||||
if user.FeatureRestriction.Contains(core.USER_FEATURE_RESTRICTION_TYPE_UPDATE_PASSWORD) {
|
||||
return nil, errs.ErrNotPermittedToPerformThisAction
|
||||
}
|
||||
|
||||
if !a.users.IsPasswordEqualsUserPassword(userUpdateReq.OldPassword, user) {
|
||||
return nil, errs.ErrUserPasswordWrong
|
||||
}
|
||||
@@ -287,37 +258,24 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.WebContext) (any, *errs.Erro
|
||||
if userUpdateReq.Nickname != "" && userUpdateReq.Nickname != user.Nickname {
|
||||
user.Nickname = userUpdateReq.Nickname
|
||||
userNew.Nickname = userUpdateReq.Nickname
|
||||
modifyProfileBasicInfo = true
|
||||
anythingUpdate = true
|
||||
}
|
||||
|
||||
if userUpdateReq.DefaultAccountId > 0 && userUpdateReq.DefaultAccountId != user.DefaultAccountId {
|
||||
accountMap, err := a.accounts.GetAccountsByAccountIds(c, uid, []int64{userUpdateReq.DefaultAccountId})
|
||||
accounts, err := a.accounts.GetAccountsByAccountIds(c, uid, []int64{userUpdateReq.DefaultAccountId})
|
||||
|
||||
if err != nil || len(accountMap) < 1 {
|
||||
if err != nil || len(accounts) < 1 {
|
||||
return nil, errs.Or(err, errs.ErrUserDefaultAccountIsInvalid)
|
||||
}
|
||||
|
||||
if _, exists := accountMap[userUpdateReq.DefaultAccountId]; !exists {
|
||||
log.Warnf(c, "[users.UserUpdateProfileHandler] account \"id:%d\" does not exist for user \"uid:%d\"", userUpdateReq.DefaultAccountId, uid)
|
||||
return nil, errs.ErrUserDefaultAccountIsInvalid
|
||||
}
|
||||
|
||||
if accountMap[userUpdateReq.DefaultAccountId].Hidden {
|
||||
log.Warnf(c, "[users.UserUpdateProfileHandler] account \"id:%d\" is hidden of user \"uid:%d\"", userUpdateReq.DefaultAccountId, uid)
|
||||
return nil, errs.ErrUserDefaultAccountIsHidden
|
||||
}
|
||||
|
||||
user.DefaultAccountId = userUpdateReq.DefaultAccountId
|
||||
userNew.DefaultAccountId = userUpdateReq.DefaultAccountId
|
||||
modifyProfileBasicInfo = true
|
||||
anythingUpdate = true
|
||||
}
|
||||
|
||||
if userUpdateReq.TransactionEditScope != nil && *userUpdateReq.TransactionEditScope != user.TransactionEditScope {
|
||||
user.TransactionEditScope = *userUpdateReq.TransactionEditScope
|
||||
userNew.TransactionEditScope = *userUpdateReq.TransactionEditScope
|
||||
modifyProfileBasicInfo = true
|
||||
anythingUpdate = true
|
||||
} else {
|
||||
userNew.TransactionEditScope = models.TRANSACTION_EDIT_SCOPE_INVALID
|
||||
@@ -329,145 +287,53 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.WebContext) (any, *errs.Erro
|
||||
user.Language = userUpdateReq.Language
|
||||
userNew.Language = userUpdateReq.Language
|
||||
modifyUserLanguage = true
|
||||
modifyProfileBasicInfo = true
|
||||
anythingUpdate = true
|
||||
}
|
||||
|
||||
if userUpdateReq.DefaultCurrency != "" && userUpdateReq.DefaultCurrency != user.DefaultCurrency {
|
||||
user.DefaultCurrency = userUpdateReq.DefaultCurrency
|
||||
userNew.DefaultCurrency = userUpdateReq.DefaultCurrency
|
||||
modifyProfileBasicInfo = true
|
||||
anythingUpdate = true
|
||||
}
|
||||
|
||||
if userUpdateReq.FirstDayOfWeek != nil && *userUpdateReq.FirstDayOfWeek != user.FirstDayOfWeek {
|
||||
user.FirstDayOfWeek = *userUpdateReq.FirstDayOfWeek
|
||||
userNew.FirstDayOfWeek = *userUpdateReq.FirstDayOfWeek
|
||||
modifyProfileBasicInfo = true
|
||||
anythingUpdate = true
|
||||
} else {
|
||||
userNew.FirstDayOfWeek = core.WEEKDAY_INVALID
|
||||
userNew.FirstDayOfWeek = models.WEEKDAY_INVALID
|
||||
}
|
||||
|
||||
if userUpdateReq.LongDateFormat != nil && *userUpdateReq.LongDateFormat != user.LongDateFormat {
|
||||
user.LongDateFormat = *userUpdateReq.LongDateFormat
|
||||
userNew.LongDateFormat = *userUpdateReq.LongDateFormat
|
||||
modifyProfileBasicInfo = true
|
||||
anythingUpdate = true
|
||||
} else {
|
||||
userNew.LongDateFormat = core.LONG_DATE_FORMAT_INVALID
|
||||
userNew.LongDateFormat = models.LONG_DATE_FORMAT_INVALID
|
||||
}
|
||||
|
||||
if userUpdateReq.ShortDateFormat != nil && *userUpdateReq.ShortDateFormat != user.ShortDateFormat {
|
||||
user.ShortDateFormat = *userUpdateReq.ShortDateFormat
|
||||
userNew.ShortDateFormat = *userUpdateReq.ShortDateFormat
|
||||
modifyProfileBasicInfo = true
|
||||
anythingUpdate = true
|
||||
} else {
|
||||
userNew.ShortDateFormat = core.SHORT_DATE_FORMAT_INVALID
|
||||
userNew.ShortDateFormat = models.SHORT_DATE_FORMAT_INVALID
|
||||
}
|
||||
|
||||
if userUpdateReq.LongTimeFormat != nil && *userUpdateReq.LongTimeFormat != user.LongTimeFormat {
|
||||
user.LongTimeFormat = *userUpdateReq.LongTimeFormat
|
||||
userNew.LongTimeFormat = *userUpdateReq.LongTimeFormat
|
||||
modifyProfileBasicInfo = true
|
||||
anythingUpdate = true
|
||||
} else {
|
||||
userNew.LongTimeFormat = core.LONG_TIME_FORMAT_INVALID
|
||||
userNew.LongTimeFormat = models.LONG_TIME_FORMAT_INVALID
|
||||
}
|
||||
|
||||
if userUpdateReq.ShortTimeFormat != nil && *userUpdateReq.ShortTimeFormat != user.ShortTimeFormat {
|
||||
user.ShortTimeFormat = *userUpdateReq.ShortTimeFormat
|
||||
userNew.ShortTimeFormat = *userUpdateReq.ShortTimeFormat
|
||||
modifyProfileBasicInfo = true
|
||||
anythingUpdate = true
|
||||
} else {
|
||||
userNew.ShortTimeFormat = core.SHORT_TIME_FORMAT_INVALID
|
||||
}
|
||||
|
||||
if userUpdateReq.DecimalSeparator != nil && *userUpdateReq.DecimalSeparator != user.DecimalSeparator {
|
||||
user.DecimalSeparator = *userUpdateReq.DecimalSeparator
|
||||
userNew.DecimalSeparator = *userUpdateReq.DecimalSeparator
|
||||
modifyProfileBasicInfo = true
|
||||
anythingUpdate = true
|
||||
} else {
|
||||
userNew.DecimalSeparator = core.DECIMAL_SEPARATOR_INVALID
|
||||
}
|
||||
|
||||
if userUpdateReq.DigitGroupingSymbol != nil && *userUpdateReq.DigitGroupingSymbol != user.DigitGroupingSymbol {
|
||||
user.DigitGroupingSymbol = *userUpdateReq.DigitGroupingSymbol
|
||||
userNew.DigitGroupingSymbol = *userUpdateReq.DigitGroupingSymbol
|
||||
modifyProfileBasicInfo = true
|
||||
anythingUpdate = true
|
||||
} else {
|
||||
userNew.DigitGroupingSymbol = core.DIGIT_GROUPING_SYMBOL_INVALID
|
||||
}
|
||||
|
||||
if userUpdateReq.DigitGrouping != nil && *userUpdateReq.DigitGrouping != user.DigitGrouping {
|
||||
user.DigitGrouping = *userUpdateReq.DigitGrouping
|
||||
userNew.DigitGrouping = *userUpdateReq.DigitGrouping
|
||||
modifyProfileBasicInfo = true
|
||||
anythingUpdate = true
|
||||
} else {
|
||||
userNew.DigitGrouping = core.DIGIT_GROUPING_TYPE_INVALID
|
||||
}
|
||||
|
||||
if userUpdateReq.CurrencyDisplayType != nil && *userUpdateReq.CurrencyDisplayType != user.CurrencyDisplayType {
|
||||
user.CurrencyDisplayType = *userUpdateReq.CurrencyDisplayType
|
||||
userNew.CurrencyDisplayType = *userUpdateReq.CurrencyDisplayType
|
||||
modifyProfileBasicInfo = true
|
||||
anythingUpdate = true
|
||||
} else {
|
||||
userNew.CurrencyDisplayType = core.CURRENCY_DISPLAY_TYPE_INVALID
|
||||
}
|
||||
|
||||
if userUpdateReq.ExpenseAmountColor != nil && *userUpdateReq.ExpenseAmountColor != user.ExpenseAmountColor {
|
||||
user.ExpenseAmountColor = *userUpdateReq.ExpenseAmountColor
|
||||
userNew.ExpenseAmountColor = *userUpdateReq.ExpenseAmountColor
|
||||
modifyProfileBasicInfo = true
|
||||
anythingUpdate = true
|
||||
} else {
|
||||
userNew.ExpenseAmountColor = models.AMOUNT_COLOR_TYPE_INVALID
|
||||
}
|
||||
|
||||
if userUpdateReq.IncomeAmountColor != nil && *userUpdateReq.IncomeAmountColor != user.IncomeAmountColor {
|
||||
user.IncomeAmountColor = *userUpdateReq.IncomeAmountColor
|
||||
userNew.IncomeAmountColor = *userUpdateReq.IncomeAmountColor
|
||||
modifyProfileBasicInfo = true
|
||||
anythingUpdate = true
|
||||
} else {
|
||||
userNew.IncomeAmountColor = models.AMOUNT_COLOR_TYPE_INVALID
|
||||
}
|
||||
|
||||
if modifyProfileBasicInfo && user.FeatureRestriction.Contains(core.USER_FEATURE_RESTRICTION_TYPE_UPDATE_PROFILE_BASIC_INFO) {
|
||||
return nil, errs.ErrNotPermittedToPerformThisAction
|
||||
}
|
||||
|
||||
if modifyUserLanguage || userNew.DecimalSeparator != core.DECIMAL_SEPARATOR_INVALID || userNew.DigitGroupingSymbol != core.DIGIT_GROUPING_SYMBOL_INVALID {
|
||||
decimalSeparator := userNew.DecimalSeparator
|
||||
digitGroupingSymbol := userNew.DigitGroupingSymbol
|
||||
|
||||
if userNew.DecimalSeparator == core.DECIMAL_SEPARATOR_INVALID {
|
||||
decimalSeparator = user.DecimalSeparator
|
||||
}
|
||||
|
||||
if userNew.DigitGroupingSymbol == core.DIGIT_GROUPING_SYMBOL_INVALID {
|
||||
digitGroupingSymbol = user.DigitGroupingSymbol
|
||||
}
|
||||
|
||||
locale := user.Language
|
||||
|
||||
if modifyUserLanguage {
|
||||
locale = userNew.Language
|
||||
}
|
||||
|
||||
if locale == "" {
|
||||
locale = c.GetClientLocale()
|
||||
}
|
||||
|
||||
if locales.IsDecimalSeparatorEqualsDigitGroupingSymbol(decimalSeparator, digitGroupingSymbol, locale) {
|
||||
return nil, errs.ErrDecimalSeparatorAndDigitGroupingSymbolCannotBeEqual
|
||||
}
|
||||
userNew.ShortTimeFormat = models.SHORT_TIME_FORMAT_INVALID
|
||||
}
|
||||
|
||||
if !anythingUpdate {
|
||||
@@ -477,7 +343,7 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.WebContext) (any, *errs.Erro
|
||||
keyProfileUpdated, emailSetToUnverified, err := a.users.UpdateUser(c, userNew, modifyUserLanguage)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[users.UserUpdateProfileHandler] failed to update user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
log.ErrorfWithRequestId(c, "[users.UserUpdateProfileHandler] failed to update user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
@@ -485,28 +351,28 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.WebContext) (any, *errs.Erro
|
||||
user.EmailVerified = false
|
||||
}
|
||||
|
||||
log.Infof(c, "[users.UserUpdateProfileHandler] user \"uid:%d\" has updated successfully", user.Uid)
|
||||
log.InfofWithRequestId(c, "[users.UserUpdateProfileHandler] user \"uid:%d\" has updated successfully", user.Uid)
|
||||
|
||||
resp := &models.UserProfileUpdateResponse{
|
||||
User: a.GetUserBasicInfo(user),
|
||||
User: user.ToUserBasicInfo(),
|
||||
}
|
||||
|
||||
if emailSetToUnverified && a.CurrentConfig().EnableUserVerifyEmail && a.CurrentConfig().EnableSMTP {
|
||||
if emailSetToUnverified && settings.Container.Current.EnableUserVerifyEmail && settings.Container.Current.EnableSMTP {
|
||||
err = a.tokens.DeleteTokensByType(c, uid, core.USER_TOKEN_TYPE_EMAIL_VERIFY)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[users.UserUpdateProfileHandler] failed to revoke old email verify tokens for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
log.ErrorfWithRequestId(c, "[users.UserUpdateProfileHandler] failed to revoke old email verify tokens for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
} else {
|
||||
token, _, err := a.tokens.CreateEmailVerifyToken(c, user)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[users.UserUpdateProfileHandler] failed to create email verify token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
log.ErrorfWithRequestId(c, "[users.UserUpdateProfileHandler] failed to create email verify token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
} else {
|
||||
go func() {
|
||||
err = a.users.SendVerifyEmail(user, token, c.GetClientLocale())
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[users.UserUpdateProfileHandler] cannot send verify email to \"%s\", because %s", user.Email, err.Error())
|
||||
log.WarnfWithRequestId(c, "[users.UserUpdateProfileHandler] cannot send verify email to \"%s\", because %s", user.Email, err.Error())
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -518,15 +384,15 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.WebContext) (any, *errs.Erro
|
||||
err = a.tokens.DeleteTokensBeforeTime(c, uid, now)
|
||||
|
||||
if err == nil {
|
||||
log.Infof(c, "[users.UserUpdateProfileHandler] revoke old tokens before unix time \"%d\" for user \"uid:%d\"", now, user.Uid)
|
||||
log.InfofWithRequestId(c, "[users.UserUpdateProfileHandler] revoke old tokens before unix time \"%d\" for user \"uid:%d\"", now, user.Uid)
|
||||
} else {
|
||||
log.Warnf(c, "[users.UserUpdateProfileHandler] failed to revoke old tokens for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
log.WarnfWithRequestId(c, "[users.UserUpdateProfileHandler] failed to revoke old tokens for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
}
|
||||
|
||||
token, claims, err := a.tokens.CreateToken(c, user)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[users.UserUpdateProfileHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
log.WarnfWithRequestId(c, "[users.UserUpdateProfileHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
@@ -534,7 +400,7 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.WebContext) (any, *errs.Erro
|
||||
c.SetTextualToken(token)
|
||||
c.SetTokenClaims(claims)
|
||||
|
||||
log.Infof(c, "[users.UserUpdateProfileHandler] user \"uid:%d\" token refreshed, new token will be expired at %d", user.Uid, claims.ExpiresAt)
|
||||
log.InfofWithRequestId(c, "[users.UserUpdateProfileHandler] user \"uid:%d\" token refreshed, new token will be expired at %d", user.Uid, claims.ExpiresAt)
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
@@ -542,109 +408,9 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.WebContext) (any, *errs.Erro
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// UserUpdateAvatarHandler saves user avatar by request parameters for current user
|
||||
func (a *UsersApi) UserUpdateAvatarHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
uid := c.GetCurrentUid()
|
||||
user, err := a.users.GetUserById(c, uid)
|
||||
|
||||
if err != nil {
|
||||
if !errs.IsCustomError(err) {
|
||||
log.Errorf(c, "[users.UserUpdateAvatarHandler] failed to get user, because %s", err.Error())
|
||||
}
|
||||
|
||||
return nil, errs.ErrUserNotFound
|
||||
}
|
||||
|
||||
if user.FeatureRestriction.Contains(core.USER_FEATURE_RESTRICTION_TYPE_UPDATE_AVATAR) {
|
||||
return nil, errs.ErrNotPermittedToPerformThisAction
|
||||
}
|
||||
|
||||
form, err := c.MultipartForm()
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[users.UserUpdateAvatarHandler] failed to get multi-part form data for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
return nil, errs.ErrParameterInvalid
|
||||
}
|
||||
|
||||
avatarFiles := form.File["avatar"]
|
||||
|
||||
if len(avatarFiles) < 1 {
|
||||
log.Warnf(c, "[users.UserUpdateAvatarHandler] there is no user avatar in request for user \"uid:%d\"", user.Uid)
|
||||
return nil, errs.ErrNoUserAvatar
|
||||
}
|
||||
|
||||
if avatarFiles[0].Size < 1 {
|
||||
log.Warnf(c, "[users.UserUpdateAvatarHandler] the size of user avatar in request is zero for user \"uid:%d\"", user.Uid)
|
||||
return nil, errs.ErrUserAvatarIsEmpty
|
||||
}
|
||||
|
||||
if avatarFiles[0].Size > int64(a.CurrentConfig().MaxAvatarFileSize) {
|
||||
log.Warnf(c, "[users.UserUpdateAvatarHandler] the upload file size \"%d\" exceeds the maximum size \"%d\" of user avatar for user \"uid:%d\"", avatarFiles[0].Size, a.CurrentConfig().MaxAvatarFileSize, uid)
|
||||
return nil, errs.ErrExceedMaxUserAvatarFileSize
|
||||
}
|
||||
|
||||
fileExtension := utils.GetFileNameExtension(avatarFiles[0].Filename)
|
||||
|
||||
if utils.GetImageContentType(fileExtension) == "" {
|
||||
log.Warnf(c, "[users.UserUpdateAvatarHandler] the file extension \"%s\" of user avatar in request is not supported for user \"uid:%d\"", fileExtension, user.Uid)
|
||||
return nil, errs.ErrImageTypeNotSupported
|
||||
}
|
||||
|
||||
avatarFile, err := avatarFiles[0].Open()
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[users.UserUpdateAvatarHandler] failed to get avatar file from request for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
return nil, errs.ErrOperationFailed
|
||||
}
|
||||
|
||||
err = a.users.UpdateUserAvatar(c, user.Uid, avatarFile, fileExtension, user.CustomAvatarType)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[users.UserUpdateAvatarHandler] failed to update avatar for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
user.CustomAvatarType = fileExtension
|
||||
userResp := a.getUserProfileResponse(user)
|
||||
return userResp, nil
|
||||
}
|
||||
|
||||
// UserRemoveAvatarHandler removes user avatar by request parameters for current user
|
||||
func (a *UsersApi) UserRemoveAvatarHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
uid := c.GetCurrentUid()
|
||||
user, err := a.users.GetUserById(c, uid)
|
||||
|
||||
if err != nil {
|
||||
if !errs.IsCustomError(err) {
|
||||
log.Errorf(c, "[users.UserRemoveAvatarHandler] failed to get user, because %s", err.Error())
|
||||
}
|
||||
|
||||
return nil, errs.ErrUserNotFound
|
||||
}
|
||||
|
||||
if user.FeatureRestriction.Contains(core.USER_FEATURE_RESTRICTION_TYPE_UPDATE_AVATAR) {
|
||||
return nil, errs.ErrNotPermittedToPerformThisAction
|
||||
}
|
||||
|
||||
if user.CustomAvatarType == "" {
|
||||
return nil, errs.ErrNothingWillBeUpdated
|
||||
}
|
||||
|
||||
err = a.users.RemoveUserAvatar(c, user.Uid, user.CustomAvatarType)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[users.UserRemoveAvatarHandler] failed to remove avatar for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
user.CustomAvatarType = ""
|
||||
userResp := a.getUserProfileResponse(user)
|
||||
return userResp, nil
|
||||
}
|
||||
|
||||
// UserSendVerifyEmailByUnloginUserHandler sends unlogin user verify email
|
||||
func (a *UsersApi) UserSendVerifyEmailByUnloginUserHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
if !a.CurrentConfig().EnableUserVerifyEmail {
|
||||
func (a *UsersApi) UserSendVerifyEmailByUnloginUserHandler(c *core.Context) (any, *errs.Error) {
|
||||
if !settings.Container.Current.EnableUserVerifyEmail {
|
||||
return nil, errs.ErrEmailValidationNotAllowed
|
||||
}
|
||||
|
||||
@@ -655,35 +421,35 @@ func (a *UsersApi) UserSendVerifyEmailByUnloginUserHandler(c *core.WebContext) (
|
||||
|
||||
if err != nil {
|
||||
if !errs.IsCustomError(err) {
|
||||
log.Errorf(c, "[users.UserSendVerifyEmailByUnloginUserHandler] failed to get user, because %s", err.Error())
|
||||
log.ErrorfWithRequestId(c, "[users.UserSendVerifyEmailByUnloginUserHandler] failed to get user, because %s", err.Error())
|
||||
}
|
||||
|
||||
return nil, errs.ErrUserNotFound
|
||||
}
|
||||
|
||||
if !a.users.IsPasswordEqualsUserPassword(userResendVerifyEmailReq.Password, user) {
|
||||
log.Warnf(c, "[users.UserSendVerifyEmailByUnloginUserHandler] request password not equals to the user password")
|
||||
log.WarnfWithRequestId(c, "[users.UserSendVerifyEmailByUnloginUserHandler] request password not equals to the user password")
|
||||
return nil, errs.ErrUserPasswordWrong
|
||||
}
|
||||
|
||||
if user.Disabled {
|
||||
log.Warnf(c, "[users.UserSendVerifyEmailByUnloginUserHandler] user \"uid:%d\" is disabled", user.Uid)
|
||||
log.WarnfWithRequestId(c, "[users.UserSendVerifyEmailByUnloginUserHandler] user \"uid:%d\" is disabled", user.Uid)
|
||||
return nil, errs.ErrUserIsDisabled
|
||||
}
|
||||
|
||||
if user.EmailVerified {
|
||||
log.Warnf(c, "[users.UserSendVerifyEmailByUnloginUserHandler] user \"uid:%d\" email has been verified", user.Uid)
|
||||
log.WarnfWithRequestId(c, "[users.UserSendVerifyEmailByUnloginUserHandler] user \"uid:%d\" email has been verified", user.Uid)
|
||||
return nil, errs.ErrEmailIsVerified
|
||||
}
|
||||
|
||||
if !a.CurrentConfig().EnableSMTP {
|
||||
if !settings.Container.Current.EnableSMTP {
|
||||
return nil, errs.ErrSMTPServerNotEnabled
|
||||
}
|
||||
|
||||
token, _, err := a.tokens.CreateEmailVerifyToken(c, user)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[users.UserSendVerifyEmailByUnloginUserHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
log.ErrorfWithRequestId(c, "[users.UserSendVerifyEmailByUnloginUserHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
return nil, errs.ErrTokenGenerating
|
||||
}
|
||||
|
||||
@@ -691,7 +457,7 @@ func (a *UsersApi) UserSendVerifyEmailByUnloginUserHandler(c *core.WebContext) (
|
||||
err = a.users.SendVerifyEmail(user, token, c.GetClientLocale())
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[users.UserSendVerifyEmailByUnloginUserHandler] cannot send email to \"%s\", because %s", user.Email, err.Error())
|
||||
log.WarnfWithRequestId(c, "[users.UserSendVerifyEmailByUnloginUserHandler] cannot send email to \"%s\", because %s", user.Email, err.Error())
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -699,8 +465,8 @@ func (a *UsersApi) UserSendVerifyEmailByUnloginUserHandler(c *core.WebContext) (
|
||||
}
|
||||
|
||||
// UserSendVerifyEmailByLoginedUserHandler sends logined user verify email
|
||||
func (a *UsersApi) UserSendVerifyEmailByLoginedUserHandler(c *core.WebContext) (any, *errs.Error) {
|
||||
if !a.CurrentConfig().EnableUserVerifyEmail {
|
||||
func (a *UsersApi) UserSendVerifyEmailByLoginedUserHandler(c *core.Context) (any, *errs.Error) {
|
||||
if !settings.Container.Current.EnableUserVerifyEmail {
|
||||
return nil, errs.ErrEmailValidationNotAllowed
|
||||
}
|
||||
|
||||
@@ -709,25 +475,25 @@ func (a *UsersApi) UserSendVerifyEmailByLoginedUserHandler(c *core.WebContext) (
|
||||
|
||||
if err != nil {
|
||||
if !errs.IsCustomError(err) {
|
||||
log.Errorf(c, "[users.UserSendVerifyEmailByLoginedUserHandler] failed to get user, because %s", err.Error())
|
||||
log.ErrorfWithRequestId(c, "[users.UserSendVerifyEmailByLoginedUserHandler] failed to get user, because %s", err.Error())
|
||||
}
|
||||
|
||||
return nil, errs.ErrUserNotFound
|
||||
}
|
||||
|
||||
if user.EmailVerified {
|
||||
log.Warnf(c, "[users.UserSendVerifyEmailByLoginedUserHandler] user \"uid:%d\" email has been verified", user.Uid)
|
||||
log.WarnfWithRequestId(c, "[users.UserSendVerifyEmailByLoginedUserHandler] user \"uid:%d\" email has been verified", user.Uid)
|
||||
return nil, errs.ErrEmailIsVerified
|
||||
}
|
||||
|
||||
if !a.CurrentConfig().EnableSMTP {
|
||||
if !settings.Container.Current.EnableSMTP {
|
||||
return nil, errs.ErrSMTPServerNotEnabled
|
||||
}
|
||||
|
||||
token, _, err := a.tokens.CreateEmailVerifyToken(c, user)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[users.UserSendVerifyEmailByLoginedUserHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
log.ErrorfWithRequestId(c, "[users.UserSendVerifyEmailByLoginedUserHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
return nil, errs.ErrTokenGenerating
|
||||
}
|
||||
|
||||
@@ -735,44 +501,9 @@ func (a *UsersApi) UserSendVerifyEmailByLoginedUserHandler(c *core.WebContext) (
|
||||
err = a.users.SendVerifyEmail(user, token, c.GetClientLocale())
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[users.UserSendVerifyEmailByLoginedUserHandler] cannot send email to \"%s\", because %s", user.Email, err.Error())
|
||||
log.WarnfWithRequestId(c, "[users.UserSendVerifyEmailByLoginedUserHandler] cannot send email to \"%s\", because %s", user.Email, err.Error())
|
||||
}
|
||||
}()
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// UserGetAvatarHandler returns user avatar data for current user
|
||||
func (a *UsersApi) UserGetAvatarHandler(c *core.WebContext) ([]byte, string, *errs.Error) {
|
||||
fileName := c.Param("fileName")
|
||||
fileExtension := utils.GetFileNameExtension(fileName)
|
||||
contentType := utils.GetImageContentType(fileExtension)
|
||||
|
||||
if contentType == "" {
|
||||
return nil, "", errs.ErrImageTypeNotSupported
|
||||
}
|
||||
|
||||
uid := c.GetCurrentUid()
|
||||
fileBaseName := utils.GetFileNameWithoutExtension(fileName)
|
||||
|
||||
if utils.Int64ToString(uid) != fileBaseName {
|
||||
log.Warnf(c, "[users.UserGetAvatarHandler] cannot get other user avatar \"uid:%s\" for user \"uid:%d\"", fileBaseName, uid)
|
||||
return nil, "", errs.ErrUserIdInvalid
|
||||
}
|
||||
|
||||
avatarData, err := a.users.GetUserAvatar(c, uid, fileExtension)
|
||||
|
||||
if err != nil {
|
||||
if !errs.IsCustomError(err) {
|
||||
log.Errorf(c, "[users.UserGetAvatarHandler] failed to get user avatar, because %s", err.Error())
|
||||
}
|
||||
|
||||
return nil, "", errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
return avatarData, contentType, nil
|
||||
}
|
||||
|
||||
func (a *UsersApi) getUserProfileResponse(user *models.User) *models.UserProfileResponse {
|
||||
return user.ToUserProfileResponse(a.GetUserBasicInfo(user))
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
package avatars
|
||||
|
||||
import "github.com/mayswind/ezbookkeeping/pkg/models"
|
||||
|
||||
// AvatarProvider is user avatar provider interface
|
||||
type AvatarProvider interface {
|
||||
GetAvatarUrl(user *models.User) string
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
package avatars
|
||||
|
||||
import (
|
||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||
)
|
||||
|
||||
// AvatarProviderContainer contains the current user avatar provider
|
||||
type AvatarProviderContainer struct {
|
||||
Current AvatarProvider
|
||||
}
|
||||
|
||||
// Initialize a user avatar provider container singleton instance
|
||||
var (
|
||||
Container = &AvatarProviderContainer{}
|
||||
)
|
||||
|
||||
// InitializeAvatarProvider initializes the current user avatar provider according to the config
|
||||
func InitializeAvatarProvider(config *settings.Config) error {
|
||||
if config.AvatarProvider == core.USER_AVATAR_PROVIDER_INTERNAL {
|
||||
Container.Current = NewInternalStorageAvatarProvider(config)
|
||||
return nil
|
||||
} else if config.AvatarProvider == core.USER_AVATAR_PROVIDER_GRAVATAR {
|
||||
Container.Current = NewGravatarAvatarProvider()
|
||||
return nil
|
||||
} else if config.AvatarProvider == "" {
|
||||
Container.Current = NewNullAvatarProvider()
|
||||
return nil
|
||||
}
|
||||
|
||||
return errs.ErrInvalidAvatarProvider
|
||||
}
|
||||
|
||||
// GetAvatarUrl returns the avatar url by the current user avatar provider
|
||||
func (p *AvatarProviderContainer) GetAvatarUrl(user *models.User) string {
|
||||
return p.Current.GetAvatarUrl(user)
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package avatars
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||
)
|
||||
|
||||
// Reference: https://en.gravatar.com/site/implement/hash/
|
||||
const gravatarUrlFormat = "https://www.gravatar.com/avatar/%s"
|
||||
|
||||
// GravatarAvatarProvider represents the gravatar avatar provider
|
||||
type GravatarAvatarProvider struct {
|
||||
}
|
||||
|
||||
// NewGravatarAvatarProvider returns a new gravatar avatar provider
|
||||
func NewGravatarAvatarProvider() *GravatarAvatarProvider {
|
||||
return &GravatarAvatarProvider{}
|
||||
}
|
||||
|
||||
// GetAvatarUrl returns the gravatar url
|
||||
func (p *GravatarAvatarProvider) GetAvatarUrl(user *models.User) string {
|
||||
email := user.Email
|
||||
email = strings.TrimSpace(email)
|
||||
email = strings.ToLower(email)
|
||||
emailMd5 := utils.MD5EncodeToString([]byte(email))
|
||||
|
||||
return fmt.Sprintf(gravatarUrlFormat, emailMd5)
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package avatars
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||
)
|
||||
|
||||
func TestGravatarAvatarProvider_GetGravatarUrl(t *testing.T) {
|
||||
avatarProvider := NewGravatarAvatarProvider()
|
||||
|
||||
expectedValue := "https://www.gravatar.com/avatar/0bc83cb571cd1c50ba6f3e8a78ef1346"
|
||||
actualValue := avatarProvider.GetAvatarUrl(&models.User{
|
||||
Email: "MyEmailAddress@example.com",
|
||||
})
|
||||
|
||||
assert.Equal(t, expectedValue, actualValue)
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package avatars
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||
)
|
||||
|
||||
const internalAvatarUrlFormat = "%savatar/%d.%s"
|
||||
|
||||
// InternalStorageAvatarProvider represents the internal storage avatar provider
|
||||
type InternalStorageAvatarProvider struct {
|
||||
webRootUrl string
|
||||
}
|
||||
|
||||
// NewInternalStorageAvatarProvider returns a new internal storage avatar provider
|
||||
func NewInternalStorageAvatarProvider(config *settings.Config) *InternalStorageAvatarProvider {
|
||||
return &InternalStorageAvatarProvider{
|
||||
webRootUrl: config.RootUrl,
|
||||
}
|
||||
}
|
||||
|
||||
// GetAvatarUrl returns the built-in avatar url
|
||||
func (p *InternalStorageAvatarProvider) GetAvatarUrl(user *models.User) string {
|
||||
if user.CustomAvatarType == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
return fmt.Sprintf(internalAvatarUrlFormat, p.webRootUrl, user.Uid, user.CustomAvatarType)
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
package avatars
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||
)
|
||||
|
||||
func TestInternalStorageAvatarProvider_GetAvatarUrl(t *testing.T) {
|
||||
avatarProvider := NewInternalStorageAvatarProvider(&settings.Config{
|
||||
RootUrl: "https://foo.bar/",
|
||||
})
|
||||
|
||||
expectedValue := "https://foo.bar/avatar/1234567890.jpg"
|
||||
actualValue := avatarProvider.GetAvatarUrl(&models.User{
|
||||
Uid: 1234567890,
|
||||
CustomAvatarType: "jpg",
|
||||
})
|
||||
|
||||
assert.Equal(t, expectedValue, actualValue)
|
||||
}
|
||||
|
||||
func TestInternalStorageAvatarProvider_GetAvatarUrl_EmptyCustomAvatarType(t *testing.T) {
|
||||
avatarProvider := NewInternalStorageAvatarProvider(&settings.Config{
|
||||
RootUrl: "https://foo.bar/",
|
||||
})
|
||||
|
||||
expectedValue := ""
|
||||
actualValue := avatarProvider.GetAvatarUrl(&models.User{
|
||||
Uid: 1234567890,
|
||||
CustomAvatarType: "",
|
||||
})
|
||||
|
||||
assert.Equal(t, expectedValue, actualValue)
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package avatars
|
||||
|
||||
import (
|
||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||
)
|
||||
|
||||
// NullAvatarProvider represents the null avatar provider
|
||||
type NullAvatarProvider struct {
|
||||
}
|
||||
|
||||
// NewNullAvatarProvider returns a new null avatar provider
|
||||
func NewNullAvatarProvider() *NullAvatarProvider {
|
||||
return &NullAvatarProvider{}
|
||||
}
|
||||
|
||||
// GetAvatarUrl returns an empty url
|
||||
func (p *NullAvatarProvider) GetAvatarUrl(user *models.User) string {
|
||||
return ""
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package avatars
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||
)
|
||||
|
||||
func TestNullAvatarProvider_GetGravatarUrl(t *testing.T) {
|
||||
avatarProvider := NewNullAvatarProvider()
|
||||
|
||||
expectedValue := ""
|
||||
actualValue := avatarProvider.GetAvatarUrl(&models.User{
|
||||
Email: "MyEmailAddress@example.com",
|
||||
})
|
||||
|
||||
assert.Equal(t, expectedValue, actualValue)
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package cli
|
||||
|
||||
import "github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||
|
||||
// CliUsingConfig represents a cli that need to use config
|
||||
type CliUsingConfig struct {
|
||||
container *settings.ConfigContainer
|
||||
}
|
||||
|
||||
// CurrentConfig returns the current config
|
||||
func (l *CliUsingConfig) CurrentConfig() *settings.Config {
|
||||
return l.container.Current
|
||||
}
|
||||
+163
-447
File diff suppressed because it is too large
Load Diff
@@ -1,27 +0,0 @@
|
||||
package alipay
|
||||
|
||||
// alipayAppTransactionDataCsvFileImporter defines the structure of alipay app csv importer for transaction data
|
||||
type alipayAppTransactionDataCsvFileImporter struct {
|
||||
alipayTransactionDataCsvFileImporter
|
||||
}
|
||||
|
||||
// Initialize a alipay app transaction data csv file importer singleton instance
|
||||
var (
|
||||
AlipayAppTransactionDataCsvFileImporter = &alipayAppTransactionDataCsvFileImporter{
|
||||
alipayTransactionDataCsvFileImporter{
|
||||
fileHeaderLine: "------------------------------------------------------------------------------------",
|
||||
dataHeaderStartContent: "支付宝(中国)网络技术有限公司 电子客户回单",
|
||||
originalColumnNames: alipayTransactionColumnNames{
|
||||
timeColumnName: "交易时间",
|
||||
categoryColumnName: "交易分类",
|
||||
targetNameColumnName: "交易对方",
|
||||
productNameColumnName: "商品说明",
|
||||
amountColumnName: "金额",
|
||||
typeColumnName: "收/支",
|
||||
relatedAccountColumnName: "收/付款方式",
|
||||
statusColumnName: "交易状态",
|
||||
descriptionColumnName: "备注",
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -1,161 +0,0 @@
|
||||
package alipay
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/csv"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/text/encoding/simplifiedchinese"
|
||||
"golang.org/x/text/transform"
|
||||
|
||||
csvdatatable "github.com/mayswind/ezbookkeeping/pkg/converters/csv"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
|
||||
"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/utils"
|
||||
)
|
||||
|
||||
var alipayTransactionSupportedColumns = map[datatable.TransactionDataTableColumn]bool{
|
||||
datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME: true,
|
||||
datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE: true,
|
||||
datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY: true,
|
||||
datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME: true,
|
||||
datatable.TRANSACTION_DATA_TABLE_AMOUNT: true,
|
||||
datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME: true,
|
||||
datatable.TRANSACTION_DATA_TABLE_DESCRIPTION: true,
|
||||
}
|
||||
|
||||
var alipayTransactionTypeNameMapping = map[models.TransactionType]string{
|
||||
models.TRANSACTION_TYPE_INCOME: "收入",
|
||||
models.TRANSACTION_TYPE_EXPENSE: "支出",
|
||||
models.TRANSACTION_TYPE_TRANSFER: "不计收支",
|
||||
}
|
||||
|
||||
// alipayTransactionColumnNames defines the structure of alipay transaction plain text header names
|
||||
type alipayTransactionColumnNames struct {
|
||||
timeColumnName string
|
||||
categoryColumnName string
|
||||
targetNameColumnName string
|
||||
productNameColumnName string
|
||||
amountColumnName string
|
||||
typeColumnName string
|
||||
relatedAccountColumnName string
|
||||
statusColumnName string
|
||||
descriptionColumnName string
|
||||
}
|
||||
|
||||
// alipayTransactionDataCsvFileImporter defines the structure of alipay csv importer for transaction data
|
||||
type alipayTransactionDataCsvFileImporter struct {
|
||||
fileHeaderLine 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, defaultTimezoneOffset int16, 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.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionTag, error) {
|
||||
enc := simplifiedchinese.GB18030
|
||||
reader := transform.NewReader(bytes.NewReader(data), enc.NewDecoder())
|
||||
|
||||
dataTable, err := c.createNewAlipayImportedDataTable(ctx, reader, c.fileHeaderLine, c.dataHeaderStartContent, c.dataBottomEndLineRune)
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
commonDataTable := datatable.CreateNewImportedCommonDataTable(dataTable)
|
||||
|
||||
if !commonDataTable.HasColumn(c.originalColumnNames.timeColumnName) ||
|
||||
!commonDataTable.HasColumn(c.originalColumnNames.amountColumnName) ||
|
||||
!commonDataTable.HasColumn(c.originalColumnNames.typeColumnName) ||
|
||||
!commonDataTable.HasColumn(c.originalColumnNames.statusColumnName) {
|
||||
log.Errorf(ctx, "[alipay_transaction_data_csv_file_importer.ParseImportedData] cannot parse alipay csv data, because missing essential columns in header row")
|
||||
return nil, nil, nil, nil, nil, nil, errs.ErrMissingRequiredFieldInHeaderRow
|
||||
}
|
||||
|
||||
transactionRowParser := createAlipayTransactionDataRowParser(c.originalColumnNames)
|
||||
transactionDataTable := datatable.CreateNewCommonTransactionDataTable(commonDataTable, alipayTransactionSupportedColumns, transactionRowParser)
|
||||
dataTableImporter := datatable.CreateNewSimpleImporter(alipayTransactionTypeNameMapping)
|
||||
|
||||
return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezoneOffset, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
|
||||
}
|
||||
|
||||
func (c *alipayTransactionDataCsvFileImporter) createNewAlipayImportedDataTable(ctx core.Context, reader io.Reader, fileHeaderLine string, dataHeaderStartContent string, dataBottomEndLineRune rune) (datatable.ImportedDataTable, error) {
|
||||
csvReader := csv.NewReader(reader)
|
||||
csvReader.FieldsPerRecord = -1
|
||||
|
||||
allOriginalLines := make([][]string, 0)
|
||||
hasFileHeader := false
|
||||
foundContentBeforeDataHeaderLine := false
|
||||
|
||||
for {
|
||||
items, err := csvReader.Read()
|
||||
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(ctx, "[alipay_transaction_csv_data_table.createNewAlipayImportedDataTable] cannot parse alipay csv data, because %s", err.Error())
|
||||
return nil, errs.ErrInvalidCSVFile
|
||||
}
|
||||
|
||||
if !hasFileHeader {
|
||||
if len(items) <= 0 {
|
||||
continue
|
||||
} else if strings.Index(items[0], fileHeaderLine) == 0 {
|
||||
hasFileHeader = true
|
||||
continue
|
||||
} else {
|
||||
log.Warnf(ctx, "[alipay_transaction_csv_data_table.createNewAlipayImportedDataTable] read unexpected line before read file header, line content is %s", strings.Join(items, ","))
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if !foundContentBeforeDataHeaderLine {
|
||||
if len(items) <= 0 {
|
||||
continue
|
||||
} else if strings.Index(items[0], dataHeaderStartContent) >= 0 {
|
||||
foundContentBeforeDataHeaderLine = true
|
||||
continue
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if foundContentBeforeDataHeaderLine {
|
||||
if len(items) <= 0 {
|
||||
continue
|
||||
} else if len(items) == 1 && dataBottomEndLineRune > 0 && utils.ContainsOnlyOneRune(items[0], dataBottomEndLineRune) {
|
||||
break
|
||||
}
|
||||
|
||||
for i := 0; i < len(items); i++ {
|
||||
items[i] = strings.Trim(items[i], " ")
|
||||
}
|
||||
|
||||
if len(allOriginalLines) > 0 && len(items) < len(allOriginalLines[0]) {
|
||||
log.Errorf(ctx, "[alipay_transaction_csv_data_table.createNewAlipayImportedDataTable] cannot parse row \"index:%d\", because may missing some columns (column count %d in data row is less than header column count %d)", len(allOriginalLines), len(items), len(allOriginalLines[0]))
|
||||
return nil, errs.ErrFewerFieldsInDataRowThanInHeaderRow
|
||||
}
|
||||
|
||||
allOriginalLines = append(allOriginalLines, items)
|
||||
}
|
||||
}
|
||||
|
||||
if !hasFileHeader || !foundContentBeforeDataHeaderLine {
|
||||
return nil, errs.ErrInvalidFileHeader
|
||||
}
|
||||
|
||||
if len(allOriginalLines) < 2 {
|
||||
log.Errorf(ctx, "[alipay_transaction_csv_data_table.createNewAlipayImportedDataTable] cannot parse import data, because data table row count is less 1")
|
||||
return nil, errs.ErrNotFoundTransactionDataInFile
|
||||
}
|
||||
|
||||
dataTable := csvdatatable.CreateNewCustomCsvImportedDataTable(allOriginalLines)
|
||||
|
||||
return dataTable, nil
|
||||
}
|
||||
@@ -1,614 +0,0 @@
|
||||
package alipay
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"golang.org/x/text/encoding/simplifiedchinese"
|
||||
|
||||
"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 TestAlipayCsvFileImporterParseImportedData_MinimumValidData(t *testing.T) {
|
||||
converter := AlipayWebTransactionDataCsvFileImporter
|
||||
context := core.NewNullContext()
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1234567890,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
data, err := simplifiedchinese.GB18030.NewEncoder().String("支付宝交易记录明细查询\n" +
|
||||
"账号:[xxx@xxx.xxx]\n" +
|
||||
"起始日期:[2024-01-01 00:00:00] 终止日期:[2024-09-01 23:59:59]\n" +
|
||||
"---------------------------------交易记录明细列表------------------------------------\n" +
|
||||
"交易创建时间 ,商品名称 ,金额(元),收/支 ,交易状态 ,\n" +
|
||||
"2024-09-01 01:23:45 ,xxxx ,0.12 ,收入 ,交易成功 ,\n" +
|
||||
"2024-09-01 12:34:56 ,xxxx ,123.45 ,支出 ,交易成功 ,\n" +
|
||||
"2024-09-01 23:59:59 ,充值-普通充值 ,0.05 ,不计收支 ,交易成功 ,\n" +
|
||||
"2024-09-02 23:59:59 ,提现-普通提现 ,0.03 ,不计收支 ,交易成功 ,\n" +
|
||||
"------------------------------------------------------------------------------------\n")
|
||||
assert.Nil(t, err)
|
||||
|
||||
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))
|
||||
assert.Equal(t, 2, len(allNewAccounts))
|
||||
assert.Equal(t, 1, len(allNewSubExpenseCategories))
|
||||
assert.Equal(t, 1, len(allNewSubIncomeCategories))
|
||||
assert.Equal(t, 1, 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, "2024-09-01 01:23:45", utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime), time.UTC))
|
||||
assert.Equal(t, int64(12), allNewTransactions[0].Amount)
|
||||
assert.Equal(t, "Alipay", allNewTransactions[0].OriginalSourceAccountName)
|
||||
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, "2024-09-01 12:34:56", utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(allNewTransactions[1].TransactionTime), time.UTC))
|
||||
assert.Equal(t, int64(12345), allNewTransactions[1].Amount)
|
||||
assert.Equal(t, "", allNewTransactions[1].OriginalSourceAccountName)
|
||||
assert.Equal(t, "", allNewTransactions[1].OriginalCategoryName)
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewTransactions[2].Uid)
|
||||
assert.Equal(t, models.TRANSACTION_DB_TYPE_TRANSFER_OUT, allNewTransactions[2].Type)
|
||||
assert.Equal(t, "2024-09-01 23:59:59", utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(allNewTransactions[2].TransactionTime), time.UTC))
|
||||
assert.Equal(t, int64(5), allNewTransactions[2].Amount)
|
||||
assert.Equal(t, "", allNewTransactions[2].OriginalSourceAccountName)
|
||||
assert.Equal(t, "Alipay", allNewTransactions[2].OriginalDestinationAccountName)
|
||||
assert.Equal(t, "", allNewTransactions[2].OriginalCategoryName)
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewTransactions[3].Uid)
|
||||
assert.Equal(t, models.TRANSACTION_DB_TYPE_TRANSFER_OUT, allNewTransactions[3].Type)
|
||||
assert.Equal(t, "2024-09-02 23:59:59", utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(allNewTransactions[3].TransactionTime), time.UTC))
|
||||
assert.Equal(t, int64(3), allNewTransactions[3].Amount)
|
||||
assert.Equal(t, "Alipay", allNewTransactions[3].OriginalSourceAccountName)
|
||||
assert.Equal(t, "", allNewTransactions[3].OriginalDestinationAccountName)
|
||||
assert.Equal(t, "", allNewTransactions[3].OriginalCategoryName)
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewAccounts[0].Uid)
|
||||
assert.Equal(t, "Alipay", allNewAccounts[0].Name)
|
||||
assert.Equal(t, "CNY", allNewAccounts[0].Currency)
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewAccounts[1].Uid)
|
||||
assert.Equal(t, "", allNewAccounts[1].Name)
|
||||
assert.Equal(t, "CNY", 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)
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewSubTransferCategories[0].Uid)
|
||||
assert.Equal(t, "", allNewSubTransferCategories[0].Name)
|
||||
}
|
||||
|
||||
func TestAlipayCsvFileImporterParseImportedData_ParseRefundTransaction(t *testing.T) {
|
||||
converter := AlipayWebTransactionDataCsvFileImporter
|
||||
context := core.NewNullContext()
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1234567890,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
data1, err := simplifiedchinese.GB18030.NewEncoder().String("支付宝交易记录明细查询\n" +
|
||||
"账号:[xxx@xxx.xxx]\n" +
|
||||
"起始日期:[2024-01-01 00:00:00] 终止日期:[2024-09-01 23:59:59]\n" +
|
||||
"---------------------------------交易记录明细列表------------------------------------\n" +
|
||||
"交易创建时间 ,金额(元),收/支 ,交易状态 ,\n" +
|
||||
"2024-09-01 01:23:45 ,0.12 ,不计收支 ,退款成功 ,\n" +
|
||||
"------------------------------------------------------------------------------------\n")
|
||||
assert.Nil(t, err)
|
||||
|
||||
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)
|
||||
assert.Equal(t, models.TRANSACTION_DB_TYPE_EXPENSE, allNewTransactions[0].Type)
|
||||
assert.Equal(t, "2024-09-01 01:23:45", utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime), time.UTC))
|
||||
assert.Equal(t, int64(-12), allNewTransactions[0].Amount)
|
||||
assert.Equal(t, "", allNewTransactions[0].OriginalSourceAccountName)
|
||||
assert.Equal(t, "", allNewTransactions[0].OriginalCategoryName)
|
||||
|
||||
data2, err := simplifiedchinese.GB18030.NewEncoder().String("支付宝交易记录明细查询\n" +
|
||||
"账号:[xxx@xxx.xxx]\n" +
|
||||
"起始日期:[2024-01-01 00:00:00] 终止日期:[2024-09-01 23:59:59]\n" +
|
||||
"---------------------------------交易记录明细列表------------------------------------\n" +
|
||||
"交易创建时间 ,金额(元),收/支 ,交易状态 ,\n" +
|
||||
"2024-09-01 01:23:45 ,0.12 ,收入 ,退税成功 ,\n" +
|
||||
"------------------------------------------------------------------------------------\n")
|
||||
assert.Nil(t, err)
|
||||
|
||||
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)
|
||||
assert.Equal(t, models.TRANSACTION_DB_TYPE_EXPENSE, allNewTransactions[0].Type)
|
||||
assert.Equal(t, "2024-09-01 01:23:45", utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime), time.UTC))
|
||||
assert.Equal(t, int64(-12), allNewTransactions[0].Amount)
|
||||
assert.Equal(t, "", allNewTransactions[0].OriginalSourceAccountName)
|
||||
assert.Equal(t, "", allNewTransactions[0].OriginalCategoryName)
|
||||
}
|
||||
|
||||
func TestAlipayCsvFileImporterParseImportedData_ParseInvalidTime(t *testing.T) {
|
||||
converter := AlipayWebTransactionDataCsvFileImporter
|
||||
context := core.NewNullContext()
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1234567890,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
data1, err := simplifiedchinese.GB18030.NewEncoder().String("支付宝交易记录明细查询\n" +
|
||||
"账号:[xxx@xxx.xxx]\n" +
|
||||
"起始日期:[2024-01-01 00:00:00] 终止日期:[2024-09-01 23:59:59]\n" +
|
||||
"---------------------------------交易记录明细列表------------------------------------\n" +
|
||||
"交易创建时间 ,金额(元),收/支 ,交易状态 ,\n" +
|
||||
"2024-09-01T12:34:56 ,0.12 ,收入 ,交易成功 ,\n" +
|
||||
"------------------------------------------------------------------------------------\n")
|
||||
assert.Nil(t, err)
|
||||
|
||||
_, _, _, _, _, _, 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" +
|
||||
"账号:[xxx@xxx.xxx]\n" +
|
||||
"起始日期:[2024-01-01 00:00:00] 终止日期:[2024-09-01 23:59:59]\n" +
|
||||
"---------------------------------交易记录明细列表------------------------------------\n" +
|
||||
"交易创建时间 ,金额(元),收/支 ,交易状态 ,\n" +
|
||||
"09/01/2024 12:34:56 ,0.12 ,收入 ,交易成功 ,\n" +
|
||||
"------------------------------------------------------------------------------------\n")
|
||||
assert.Nil(t, err)
|
||||
|
||||
_, _, _, _, _, _, 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) {
|
||||
converter := AlipayWebTransactionDataCsvFileImporter
|
||||
context := core.NewNullContext()
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1234567890,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
data, err := simplifiedchinese.GB18030.NewEncoder().String("支付宝交易记录明细查询\n" +
|
||||
"账号:[xxx@xxx.xxx]\n" +
|
||||
"起始日期:[2024-01-01 00:00:00] 终止日期:[2024-09-01 23:59:59]\n" +
|
||||
"---------------------------------交易记录明细列表------------------------------------\n" +
|
||||
"交易创建时间 ,金额(元),收/支 ,交易状态 ,\n" +
|
||||
"2024-09-01 12:34:56 ,0.12 , ,交易成功 ,\n" +
|
||||
"------------------------------------------------------------------------------------\n")
|
||||
assert.Nil(t, err)
|
||||
|
||||
_, _, _, _, _, _, 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) {
|
||||
converter := AlipayWebTransactionDataCsvFileImporter
|
||||
context := core.NewNullContext()
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1234567890,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
// income to alipay wallet
|
||||
data1, err := simplifiedchinese.GB18030.NewEncoder().String("支付宝交易记录明细查询\n" +
|
||||
"账号:[xxx@xxx.xxx]\n" +
|
||||
"起始日期:[2024-01-01 00:00:00] 终止日期:[2024-09-01 23:59:59]\n" +
|
||||
"---------------------------------交易记录明细列表------------------------------------\n" +
|
||||
"交易创建时间 ,交易对方 ,金额(元),收/支 ,交易状态 ,\n" +
|
||||
"2024-09-01 12:34:56 ,test ,0.12 ,收入 ,交易成功 ,\n" +
|
||||
"------------------------------------------------------------------------------------\n")
|
||||
assert.Nil(t, err)
|
||||
|
||||
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(data1), 0, nil, nil, nil, nil, nil)
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, 1, len(allNewTransactions))
|
||||
assert.Equal(t, "Alipay", allNewTransactions[0].OriginalSourceAccountName)
|
||||
|
||||
// refund to other account
|
||||
data2, err := simplifiedchinese.GB18030.NewEncoder().String("支付宝交易记录明细查询\n" +
|
||||
"账号:[xxx@xxx.xxx]\n" +
|
||||
"起始日期:[2024-01-01 00:00:00] 终止日期:[2024-09-01 23:59:59]\n" +
|
||||
"---------------------------------交易记录明细列表------------------------------------\n" +
|
||||
"交易创建时间 ,交易对方 ,金额(元),收/支 ,交易状态 ,\n" +
|
||||
"2024-09-01 12:34:56 ,test ,0.12 ,不计收支 ,退款成功 ,\n" +
|
||||
"------------------------------------------------------------------------------------\n")
|
||||
assert.Nil(t, err)
|
||||
|
||||
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data2), 0, nil, nil, nil, nil, nil)
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, 1, len(allNewTransactions))
|
||||
assert.Equal(t, "", allNewTransactions[0].OriginalSourceAccountName)
|
||||
|
||||
// transfer to alipay wallet
|
||||
data3, err := simplifiedchinese.GB18030.NewEncoder().String("支付宝交易记录明细查询\n" +
|
||||
"账号:[xxx@xxx.xxx]\n" +
|
||||
"起始日期:[2024-01-01 00:00:00] 终止日期:[2024-09-01 23:59:59]\n" +
|
||||
"---------------------------------交易记录明细列表------------------------------------\n" +
|
||||
"交易创建时间 ,交易对方 ,商品名称 ,金额(元),收/支 ,交易状态 ,\n" +
|
||||
"2024-09-01 12:34:56 ,test ,充值-普通充值 ,0.12 ,不计收支 ,交易成功 ,\n" +
|
||||
"------------------------------------------------------------------------------------\n")
|
||||
assert.Nil(t, err)
|
||||
|
||||
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data3), 0, nil, nil, nil, nil, nil)
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, 1, len(allNewTransactions))
|
||||
assert.Equal(t, "", allNewTransactions[0].OriginalSourceAccountName)
|
||||
assert.Equal(t, "Alipay", allNewTransactions[0].OriginalDestinationAccountName)
|
||||
|
||||
// transfer from alipay wallet
|
||||
data4, err := simplifiedchinese.GB18030.NewEncoder().String("支付宝交易记录明细查询\n" +
|
||||
"账号:[xxx@xxx.xxx]\n" +
|
||||
"起始日期:[2024-01-01 00:00:00] 终止日期:[2024-09-01 23:59:59]\n" +
|
||||
"---------------------------------交易记录明细列表------------------------------------\n" +
|
||||
"交易创建时间 ,交易对方 ,商品名称 ,金额(元),收/支 ,交易状态 ,\n" +
|
||||
"2024-09-01 12:34:56 ,test ,提现-实时提现 ,0.12 ,不计收支 ,交易成功 ,\n" +
|
||||
"------------------------------------------------------------------------------------\n")
|
||||
assert.Nil(t, err)
|
||||
|
||||
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data4), 0, nil, nil, nil, nil, nil)
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, 1, len(allNewTransactions))
|
||||
assert.Equal(t, "Alipay", allNewTransactions[0].OriginalSourceAccountName)
|
||||
assert.Equal(t, "test", allNewTransactions[0].OriginalDestinationAccountName)
|
||||
|
||||
// transfer in
|
||||
data5, err := simplifiedchinese.GB18030.NewEncoder().String("支付宝交易记录明细查询\n" +
|
||||
"账号:[xxx@xxx.xxx]\n" +
|
||||
"起始日期:[2024-01-01 00:00:00] 终止日期:[2024-09-01 23:59:59]\n" +
|
||||
"---------------------------------交易记录明细列表------------------------------------\n" +
|
||||
"交易创建时间 ,交易对方 ,商品名称 ,金额(元),收/支 ,交易状态 ,\n" +
|
||||
"2024-09-01 12:34:56 ,test ,xx-转入 ,0.12 ,不计收支 ,交易成功 ,\n" +
|
||||
"------------------------------------------------------------------------------------\n")
|
||||
assert.Nil(t, err)
|
||||
|
||||
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data5), 0, nil, nil, nil, nil, nil)
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, 1, len(allNewTransactions))
|
||||
assert.Equal(t, "", allNewTransactions[0].OriginalSourceAccountName)
|
||||
assert.Equal(t, "test", allNewTransactions[0].OriginalDestinationAccountName)
|
||||
|
||||
// transfer out
|
||||
data6, err := simplifiedchinese.GB18030.NewEncoder().String("支付宝交易记录明细查询\n" +
|
||||
"账号:[xxx@xxx.xxx]\n" +
|
||||
"起始日期:[2024-01-01 00:00:00] 终止日期:[2024-09-01 23:59:59]\n" +
|
||||
"---------------------------------交易记录明细列表------------------------------------\n" +
|
||||
"交易创建时间 ,交易对方 ,商品名称 ,金额(元),收/支 ,交易状态 ,\n" +
|
||||
"2024-09-01 12:34:56 ,test ,xx-转出 ,0.12 ,不计收支 ,交易成功 ,\n" +
|
||||
"------------------------------------------------------------------------------------\n")
|
||||
assert.Nil(t, err)
|
||||
|
||||
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data6), 0, nil, nil, nil, nil, nil)
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, 1, len(allNewTransactions))
|
||||
assert.Equal(t, "", allNewTransactions[0].OriginalSourceAccountName)
|
||||
assert.Equal(t, "test", allNewTransactions[0].OriginalDestinationAccountName)
|
||||
|
||||
// repayment
|
||||
data7, err := simplifiedchinese.GB18030.NewEncoder().String("支付宝交易记录明细查询\n" +
|
||||
"账号:[xxx@xxx.xxx]\n" +
|
||||
"起始日期:[2024-01-01 00:00:00] 终止日期:[2024-09-01 23:59:59]\n" +
|
||||
"---------------------------------交易记录明细列表------------------------------------\n" +
|
||||
"交易创建时间 ,交易对方 ,商品名称 ,金额(元),收/支 ,交易状态 ,\n" +
|
||||
"2024-09-01 12:34:56 ,test ,xx还款 ,0.12 ,不计收支 ,交易成功 ,\n" +
|
||||
"------------------------------------------------------------------------------------\n")
|
||||
assert.Nil(t, err)
|
||||
|
||||
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data7), 0, nil, nil, nil, nil, nil)
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, 1, len(allNewTransactions))
|
||||
assert.Equal(t, "", allNewTransactions[0].OriginalSourceAccountName)
|
||||
assert.Equal(t, "test", allNewTransactions[0].OriginalDestinationAccountName)
|
||||
}
|
||||
|
||||
func TestAlipayCsvFileImporterParseImportedData_ParseCategory(t *testing.T) {
|
||||
converter := AlipayAppTransactionDataCsvFileImporter
|
||||
context := core.NewNullContext()
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1234567890,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
data1, err := simplifiedchinese.GB18030.NewEncoder().String("------------------------------------------------------------------------------------\n" +
|
||||
"导出信息:\n" +
|
||||
"姓名:xxx\n" +
|
||||
"支付宝账户:xxx@xxx.xxx\n" +
|
||||
"起始时间:[2024-01-01 00:00:00] 终止时间:[2024-09-01 23:59:59]\n" +
|
||||
"导出交易类型:[全部]\n" +
|
||||
"------------------------支付宝(中国)网络技术有限公司 电子客户回单------------------------\n" +
|
||||
"交易时间,交易分类,商品说明,收/支,金额,交易状态,\n" +
|
||||
"2024-09-01 01:23:45,Test Category,xxxx,收入,0.12,交易成功,\n" +
|
||||
"2024-09-01 12:34:56,Test Category2,xxxx,支出,123.45,交易成功,\n" +
|
||||
"2024-09-01 23:59:59,Test Category3,充值-普通充值,不计收支,0.05,交易成功,\n")
|
||||
assert.Nil(t, err)
|
||||
|
||||
allNewTransactions, _, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, _, err := converter.ParseImportedData(context, user, []byte(data1), 0, nil, nil, nil, nil, nil)
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, 3, len(allNewTransactions))
|
||||
assert.Equal(t, 1, len(allNewSubExpenseCategories))
|
||||
assert.Equal(t, 1, len(allNewSubIncomeCategories))
|
||||
assert.Equal(t, 1, len(allNewSubTransferCategories))
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewSubExpenseCategories[0].Uid)
|
||||
assert.Equal(t, "Test Category2", allNewSubExpenseCategories[0].Name)
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewSubIncomeCategories[0].Uid)
|
||||
assert.Equal(t, "Test Category", allNewSubIncomeCategories[0].Name)
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewSubTransferCategories[0].Uid)
|
||||
assert.Equal(t, "Test Category3", allNewSubTransferCategories[0].Name)
|
||||
}
|
||||
|
||||
func TestAlipayCsvFileImporterParseImportedData_ParseRelatedAccount(t *testing.T) {
|
||||
converter := AlipayAppTransactionDataCsvFileImporter
|
||||
context := core.NewNullContext()
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1234567890,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
data1, err := simplifiedchinese.GB18030.NewEncoder().String("------------------------------------------------------------------------------------\n" +
|
||||
"导出信息:\n" +
|
||||
"姓名:xxx\n" +
|
||||
"支付宝账户:xxx@xxx.xxx\n" +
|
||||
"起始时间:[2024-01-01 00:00:00] 终止时间:[2024-09-01 23:59:59]\n" +
|
||||
"导出交易类型:[全部]\n" +
|
||||
"------------------------支付宝(中国)网络技术有限公司 电子客户回单------------------------\n" +
|
||||
"交易时间,商品说明,收/支,金额,收/付款方式,交易状态,\n" +
|
||||
"2024-09-01 03:45:07,余额宝-单次转入,不计收支,0.01,Test Account,交易成功,\n" +
|
||||
"2024-09-01 05:07:29,信用卡还款,不计收支,0.02,Test Account2,交易成功,\n")
|
||||
assert.Nil(t, err)
|
||||
|
||||
allNewTransactions, allNewAccounts, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(data1), 0, nil, nil, nil, nil, nil)
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, 2, len(allNewTransactions))
|
||||
assert.Equal(t, 3, len(allNewAccounts))
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewTransactions[0].Uid)
|
||||
assert.Equal(t, int64(1), allNewTransactions[0].Amount)
|
||||
assert.Equal(t, "Test Account", allNewTransactions[0].OriginalSourceAccountName)
|
||||
assert.Equal(t, "", allNewTransactions[0].OriginalDestinationAccountName)
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewTransactions[1].Uid)
|
||||
assert.Equal(t, int64(2), allNewTransactions[1].Amount)
|
||||
assert.Equal(t, "Test Account2", allNewTransactions[1].OriginalSourceAccountName)
|
||||
assert.Equal(t, "", allNewTransactions[1].OriginalDestinationAccountName)
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewAccounts[0].Uid)
|
||||
assert.Equal(t, "Test Account", allNewAccounts[0].Name)
|
||||
assert.Equal(t, "CNY", allNewAccounts[0].Currency)
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewAccounts[1].Uid)
|
||||
assert.Equal(t, "", allNewAccounts[1].Name)
|
||||
assert.Equal(t, "CNY", allNewAccounts[1].Currency)
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewAccounts[2].Uid)
|
||||
assert.Equal(t, "Test Account2", allNewAccounts[2].Name)
|
||||
assert.Equal(t, "CNY", allNewAccounts[2].Currency)
|
||||
}
|
||||
|
||||
func TestAlipayCsvFileImporterParseImportedData_ParseDescription(t *testing.T) {
|
||||
converter := AlipayWebTransactionDataCsvFileImporter
|
||||
context := core.NewNullContext()
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1234567890,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
data1, err := simplifiedchinese.GB18030.NewEncoder().String("支付宝交易记录明细查询\n" +
|
||||
"账号:[xxx@xxx.xxx]\n" +
|
||||
"起始日期:[2024-01-01 00:00:00] 终止日期:[2024-09-01 23:59:59]\n" +
|
||||
"---------------------------------交易记录明细列表------------------------------------\n" +
|
||||
"交易创建时间 ,商品名称 ,金额(元),收/支 ,交易状态 ,备注 ,\n" +
|
||||
"2024-09-01 12:34:56 ,test ,0.12 ,收入 ,交易成功 ,test2 ,\n" +
|
||||
"------------------------------------------------------------------------------------\n")
|
||||
assert.Nil(t, err)
|
||||
|
||||
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(data1), 0, nil, nil, nil, nil, nil)
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, 1, len(allNewTransactions))
|
||||
assert.Equal(t, "test2", allNewTransactions[0].Comment)
|
||||
|
||||
data2, err := simplifiedchinese.GB18030.NewEncoder().String("支付宝交易记录明细查询\n" +
|
||||
"账号:[xxx@xxx.xxx]\n" +
|
||||
"起始日期:[2024-01-01 00:00:00] 终止日期:[2024-09-01 23:59:59]\n" +
|
||||
"---------------------------------交易记录明细列表------------------------------------\n" +
|
||||
"交易创建时间 ,商品名称 ,金额(元),收/支 ,交易状态 ,备注 ,\n" +
|
||||
"2024-09-01 12:34:56 ,test ,0.12 ,收入 ,交易成功 , ,\n" +
|
||||
"------------------------------------------------------------------------------------\n")
|
||||
assert.Nil(t, err)
|
||||
|
||||
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data2), 0, nil, nil, nil, nil, nil)
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, 1, len(allNewTransactions))
|
||||
assert.Equal(t, "test", allNewTransactions[0].Comment)
|
||||
}
|
||||
|
||||
func TestAlipayCsvFileImporterParseImportedData_SkipClosedIncomeOrTransferTransaction(t *testing.T) {
|
||||
converter := AlipayWebTransactionDataCsvFileImporter
|
||||
context := core.NewNullContext()
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1234567890,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
data, err := simplifiedchinese.GB18030.NewEncoder().String("支付宝交易记录明细查询\n" +
|
||||
"账号:[xxx@xxx.xxx]\n" +
|
||||
"起始日期:[2024-01-01 00:00:00] 终止日期:[2024-09-01 23:59:59]\n" +
|
||||
"---------------------------------交易记录明细列表------------------------------------\n" +
|
||||
"交易创建时间 ,商品名称 ,金额(元),收/支 ,交易状态 ,\n" +
|
||||
"2024-09-01 01:23:45 ,xxxx ,0.12 ,收入 ,交易关闭 ,\n" +
|
||||
"2024-09-01 23:59:59 ,充值-普通充值 ,0.05 ,不计收支 ,交易关闭 ,\n" +
|
||||
"------------------------------------------------------------------------------------\n")
|
||||
assert.Nil(t, err)
|
||||
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrNotFoundTransactionDataInFile.Message)
|
||||
}
|
||||
|
||||
func TestAlipayCsvFileImporterParseImportedData_SkipUnknownProductTransferTransaction(t *testing.T) {
|
||||
converter := AlipayWebTransactionDataCsvFileImporter
|
||||
context := core.NewNullContext()
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1234567890,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
data, err := simplifiedchinese.GB18030.NewEncoder().String("支付宝交易记录明细查询\n" +
|
||||
"账号:[xxx@xxx.xxx]\n" +
|
||||
"起始日期:[2024-01-01 00:00:00] 终止日期:[2024-09-01 23:59:59]\n" +
|
||||
"---------------------------------交易记录明细列表------------------------------------\n" +
|
||||
"交易创建时间 ,商品名称 ,金额(元),收/支 ,交易状态 ,\n" +
|
||||
"2024-09-01 23:59:59 ,xxxx ,0.05 ,不计收支 ,交易成功 ,\n" +
|
||||
"------------------------------------------------------------------------------------\n")
|
||||
assert.Nil(t, err)
|
||||
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrNotFoundTransactionDataInFile.Message)
|
||||
}
|
||||
|
||||
func TestAlipayCsvFileImporterParseImportedData_SkipUnknownStatusTransaction(t *testing.T) {
|
||||
converter := AlipayWebTransactionDataCsvFileImporter
|
||||
context := core.NewNullContext()
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1234567890,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
data, err := simplifiedchinese.GB18030.NewEncoder().String("支付宝交易记录明细查询\n" +
|
||||
"账号:[xxx@xxx.xxx]\n" +
|
||||
"起始日期:[2024-01-01 00:00:00] 终止日期:[2024-09-01 23:59:59]\n" +
|
||||
"---------------------------------交易记录明细列表------------------------------------\n" +
|
||||
"交易创建时间 ,商品名称 ,金额(元),收/支 ,交易状态 ,\n" +
|
||||
"2024-09-01 01:23:45 ,xxxx ,0.12 ,收入 ,xxxx ,\n" +
|
||||
"------------------------------------------------------------------------------------\n")
|
||||
assert.Nil(t, err)
|
||||
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrNotFoundTransactionDataInFile.Message)
|
||||
}
|
||||
|
||||
func TestAlipayCsvFileImporterParseImportedData_MissingFileHeader(t *testing.T) {
|
||||
converter := AlipayWebTransactionDataCsvFileImporter
|
||||
context := core.NewNullContext()
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
data, err := simplifiedchinese.GB18030.NewEncoder().String(
|
||||
"交易创建时间 ,金额(元),收/支 ,交易状态 ,\n" +
|
||||
"2024-09-01 12:34:56 ,0.12 ,收入 ,交易成功 ,\n" +
|
||||
"------------------------------------------------------------------------------------\n")
|
||||
assert.Nil(t, err)
|
||||
|
||||
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrInvalidFileHeader.Message)
|
||||
|
||||
_, _, _, _, _, _, 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) {
|
||||
converter := AlipayWebTransactionDataCsvFileImporter
|
||||
context := core.NewNullContext()
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
// Missing Time Column
|
||||
data1, err := simplifiedchinese.GB18030.NewEncoder().String("支付宝交易记录明细查询\n" +
|
||||
"账号:[xxx@xxx.xxx]\n" +
|
||||
"起始日期:[2024-01-01 00:00:00] 终止日期:[2024-09-01 23:59:59]\n" +
|
||||
"---------------------------------交易记录明细列表------------------------------------\n" +
|
||||
"金额(元),收/支 ,交易状态 ,\n" +
|
||||
"0.12 ,收入 ,交易成功 ,\n" +
|
||||
"------------------------------------------------------------------------------------\n")
|
||||
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data1), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
|
||||
|
||||
// Missing Amount Column
|
||||
data2, err := simplifiedchinese.GB18030.NewEncoder().String("支付宝交易记录明细查询\n" +
|
||||
"账号:[xxx@xxx.xxx]\n" +
|
||||
"起始日期:[2024-01-01 00:00:00] 终止日期:[2024-09-01 23:59:59]\n" +
|
||||
"---------------------------------交易记录明细列表------------------------------------\n" +
|
||||
"交易创建时间 ,收/支 ,交易状态 ,\n" +
|
||||
"2024-09-01 12:34:56 ,收入 ,交易成功 ,\n" +
|
||||
"------------------------------------------------------------------------------------\n")
|
||||
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data2), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
|
||||
|
||||
// Missing Status Column
|
||||
data3, err := simplifiedchinese.GB18030.NewEncoder().String("支付宝交易记录明细查询\n" +
|
||||
"账号:[xxx@xxx.xxx]\n" +
|
||||
"起始日期:[2024-01-01 00:00:00] 终止日期:[2024-09-01 23:59:59]\n" +
|
||||
"---------------------------------交易记录明细列表------------------------------------\n" +
|
||||
"交易创建时间 ,金额(元),收/支 ,\n" +
|
||||
"2024-09-01 12:34:56 ,0.12 ,收入 ,\n" +
|
||||
"------------------------------------------------------------------------------------\n")
|
||||
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data3), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
|
||||
|
||||
// Missing Type Column
|
||||
data4, err := simplifiedchinese.GB18030.NewEncoder().String("支付宝交易记录明细查询\n" +
|
||||
"账号:[xxx@xxx.xxx]\n" +
|
||||
"起始日期:[2024-01-01 00:00:00] 终止日期:[2024-09-01 23:59:59]\n" +
|
||||
"---------------------------------交易记录明细列表------------------------------------\n" +
|
||||
"交易创建时间 ,金额(元),交易状态 ,\n" +
|
||||
"2024-09-01 12:34:56 ,0.12 ,交易成功 ,\n" +
|
||||
"------------------------------------------------------------------------------------\n")
|
||||
_, _, _, _, _, _, 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) {
|
||||
converter := AlipayWebTransactionDataCsvFileImporter
|
||||
context := core.NewNullContext()
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
data1, err := simplifiedchinese.GB18030.NewEncoder().String("支付宝交易记录明细查询\n" +
|
||||
"账号:[xxx@xxx.xxx]\n" +
|
||||
"起始日期:[2024-01-01 00:00:00] 终止日期:[2024-09-01 23:59:59]\n" +
|
||||
"---------------------------------交易记录明细列表------------------------------------\n" +
|
||||
"交易创建时间 ,金额(元),收/支 ,交易状态 ,\n" +
|
||||
"------------------------------------------------------------------------------------\n")
|
||||
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data1), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrNotFoundTransactionDataInFile.Message)
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
package alipay
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/locales"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/log"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||
)
|
||||
|
||||
const alipayTransactionDataStatusSuccessName = "交易成功"
|
||||
const alipayTransactionDataStatusPaymentSuccessName = "支付成功"
|
||||
const alipayTransactionDataStatusRepaymentSuccessName = "还款成功"
|
||||
const alipayTransactionDataStatusClosedName = "交易关闭"
|
||||
const alipayTransactionDataStatusRefundSuccessName = "退款成功"
|
||||
const alipayTransactionDataStatusTaxRefundSuccessName = "退税成功"
|
||||
|
||||
const alipayTransactionDataProductNameTransferToAlipayPrefix = "充值-"
|
||||
const alipayTransactionDataProductNameTransferFromAlipayPrefix = "提现-"
|
||||
const alipayTransactionDataProductNameTransferInText = "转入"
|
||||
const alipayTransactionDataProductNameTransferOutText = "转出"
|
||||
const alipayTransactionDataProductNameRepaymentText = "还款"
|
||||
|
||||
// alipayTransactionDataRowParser defines the structure of alipay transaction data row parser
|
||||
type alipayTransactionDataRowParser struct {
|
||||
columns alipayTransactionColumnNames
|
||||
}
|
||||
|
||||
// Parse returns the converted transaction data row
|
||||
func (p *alipayTransactionDataRowParser) Parse(ctx core.Context, user *models.User, dataTable *datatable.CommonTransactionDataTable, dataRow datatable.CommonDataRow, rowId string) (rowData map[datatable.TransactionDataTableColumn]string, rowDataValid bool, err error) {
|
||||
if dataRow.GetData(p.columns.typeColumnName) != alipayTransactionTypeNameMapping[models.TRANSACTION_TYPE_INCOME] &&
|
||||
dataRow.GetData(p.columns.typeColumnName) != alipayTransactionTypeNameMapping[models.TRANSACTION_TYPE_EXPENSE] &&
|
||||
dataRow.GetData(p.columns.typeColumnName) != alipayTransactionTypeNameMapping[models.TRANSACTION_TYPE_TRANSFER] {
|
||||
log.Warnf(ctx, "[alipay_transaction_data_row_parser.Parse] skip parsing transaction in row \"%s\", because type is \"%s\"", rowId, dataRow.GetData(p.columns.typeColumnName))
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
if dataRow.GetData(p.columns.statusColumnName) != alipayTransactionDataStatusSuccessName &&
|
||||
dataRow.GetData(p.columns.statusColumnName) != alipayTransactionDataStatusPaymentSuccessName &&
|
||||
dataRow.GetData(p.columns.statusColumnName) != alipayTransactionDataStatusRepaymentSuccessName &&
|
||||
dataRow.GetData(p.columns.statusColumnName) != alipayTransactionDataStatusClosedName &&
|
||||
dataRow.GetData(p.columns.statusColumnName) != alipayTransactionDataStatusRefundSuccessName &&
|
||||
dataRow.GetData(p.columns.statusColumnName) != alipayTransactionDataStatusTaxRefundSuccessName {
|
||||
log.Warnf(ctx, "[alipay_transaction_data_row_parser.Parse] skip parsing transaction in row \"%s\", because status is \"%s\"", rowId, dataRow.GetData(p.columns.statusColumnName))
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
data := make(map[datatable.TransactionDataTableColumn]string, len(alipayTransactionSupportedColumns))
|
||||
|
||||
if dataTable.HasOriginalColumn(p.columns.timeColumnName) {
|
||||
data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME] = dataRow.GetData(p.columns.timeColumnName)
|
||||
}
|
||||
|
||||
if dataTable.HasOriginalColumn(p.columns.categoryColumnName) {
|
||||
data[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY] = dataRow.GetData(p.columns.categoryColumnName)
|
||||
} else {
|
||||
data[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY] = ""
|
||||
}
|
||||
|
||||
if dataTable.HasOriginalColumn(p.columns.amountColumnName) {
|
||||
data[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = dataRow.GetData(p.columns.amountColumnName)
|
||||
}
|
||||
|
||||
if dataTable.HasOriginalColumn(p.columns.descriptionColumnName) && dataRow.GetData(p.columns.descriptionColumnName) != "" {
|
||||
data[datatable.TRANSACTION_DATA_TABLE_DESCRIPTION] = dataRow.GetData(p.columns.descriptionColumnName)
|
||||
} else if dataTable.HasOriginalColumn(p.columns.productNameColumnName) && dataRow.GetData(p.columns.productNameColumnName) != "" {
|
||||
data[datatable.TRANSACTION_DATA_TABLE_DESCRIPTION] = dataRow.GetData(p.columns.productNameColumnName)
|
||||
} else {
|
||||
data[datatable.TRANSACTION_DATA_TABLE_DESCRIPTION] = ""
|
||||
}
|
||||
|
||||
relatedAccountName := ""
|
||||
|
||||
if dataTable.HasOriginalColumn(p.columns.relatedAccountColumnName) {
|
||||
relatedAccountName = dataRow.GetData(p.columns.relatedAccountColumnName)
|
||||
}
|
||||
|
||||
statusName := ""
|
||||
|
||||
if dataTable.HasOriginalColumn(p.columns.statusColumnName) {
|
||||
statusName = dataRow.GetData(p.columns.statusColumnName)
|
||||
}
|
||||
|
||||
locale := user.Language
|
||||
|
||||
if locale == "" {
|
||||
locale = ctx.GetClientLocale()
|
||||
}
|
||||
|
||||
localeTextItems := locales.GetLocaleTextItems(locale)
|
||||
|
||||
if dataTable.HasOriginalColumn(p.columns.typeColumnName) {
|
||||
data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = dataRow.GetData(p.columns.typeColumnName)
|
||||
|
||||
if dataRow.GetData(p.columns.typeColumnName) == alipayTransactionTypeNameMapping[models.TRANSACTION_TYPE_INCOME] {
|
||||
if statusName == alipayTransactionDataStatusClosedName {
|
||||
log.Warnf(ctx, "[alipay_transaction_data_row_parser.Parse] skip parsing transaction in row \"%s\", because income transaction is closed", rowId)
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
if statusName == alipayTransactionDataStatusSuccessName {
|
||||
data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = localeTextItems.DataConverterTextItems.Alipay
|
||||
data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = ""
|
||||
} else {
|
||||
data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = ""
|
||||
data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = ""
|
||||
}
|
||||
} else if dataRow.GetData(p.columns.typeColumnName) == alipayTransactionTypeNameMapping[models.TRANSACTION_TYPE_TRANSFER] {
|
||||
if statusName == alipayTransactionDataStatusClosedName {
|
||||
log.Warnf(ctx, "[alipay_transaction_data_row_parser.Parse] skip parsing transaction in row \"%s\", because non-income/expense transaction is closed", rowId)
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
targetName := ""
|
||||
productName := ""
|
||||
|
||||
if dataTable.HasOriginalColumn(p.columns.targetNameColumnName) {
|
||||
targetName = dataRow.GetData(p.columns.targetNameColumnName)
|
||||
}
|
||||
|
||||
if dataTable.HasOriginalColumn(p.columns.productNameColumnName) {
|
||||
productName = dataRow.GetData(p.columns.productNameColumnName)
|
||||
}
|
||||
|
||||
if statusName == alipayTransactionDataStatusRefundSuccessName {
|
||||
data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = alipayTransactionTypeNameMapping[models.TRANSACTION_TYPE_INCOME]
|
||||
data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = relatedAccountName
|
||||
data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = ""
|
||||
} else {
|
||||
if strings.Index(productName, alipayTransactionDataProductNameTransferToAlipayPrefix) == 0 { // transfer to alipay wallet
|
||||
data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = ""
|
||||
data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = localeTextItems.DataConverterTextItems.Alipay
|
||||
} else if strings.Index(productName, alipayTransactionDataProductNameTransferFromAlipayPrefix) == 0 { // transfer from alipay wallet
|
||||
data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = localeTextItems.DataConverterTextItems.Alipay
|
||||
data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = targetName
|
||||
} else if strings.Index(productName, alipayTransactionDataProductNameTransferInText) >= 0 { // transfer in
|
||||
data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = relatedAccountName
|
||||
data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = targetName
|
||||
} else if strings.Index(productName, alipayTransactionDataProductNameTransferOutText) >= 0 { // transfer out
|
||||
data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = relatedAccountName
|
||||
data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = targetName
|
||||
} else if strings.Index(productName, alipayTransactionDataProductNameRepaymentText) >= 0 { // repayment
|
||||
data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = relatedAccountName
|
||||
data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = targetName
|
||||
} else {
|
||||
log.Warnf(ctx, "[alipay_transaction_data_row_parser.Parse] skip parsing transaction in row \"%s\", because product name (\"%s\") is unknown", rowId, productName)
|
||||
return nil, false, nil
|
||||
}
|
||||
}
|
||||
} else {
|
||||
data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = relatedAccountName
|
||||
data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = ""
|
||||
}
|
||||
}
|
||||
|
||||
if data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] == alipayTransactionTypeNameMapping[models.TRANSACTION_TYPE_INCOME] && statusName != "" {
|
||||
if statusName == alipayTransactionDataStatusRefundSuccessName || statusName == alipayTransactionDataStatusTaxRefundSuccessName {
|
||||
amount, err := utils.ParseAmount(data[datatable.TRANSACTION_DATA_TABLE_AMOUNT])
|
||||
|
||||
if err == nil {
|
||||
data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = alipayTransactionTypeNameMapping[models.TRANSACTION_TYPE_EXPENSE]
|
||||
data[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = utils.FormatAmount(-amount)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return data, true, nil
|
||||
}
|
||||
|
||||
// createAlipayTransactionDataRowParser returns alipay transaction data row parser
|
||||
func createAlipayTransactionDataRowParser(originalColumnNames alipayTransactionColumnNames) datatable.CommonTransactionDataRowParser {
|
||||
return &alipayTransactionDataRowParser{
|
||||
columns: originalColumnNames,
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package alipay
|
||||
|
||||
// alipayWebTransactionDataCsvFileImporter defines the structure of alipay (web) csv importer for transaction data
|
||||
type alipayWebTransactionDataCsvFileImporter struct {
|
||||
alipayTransactionDataCsvFileImporter
|
||||
}
|
||||
|
||||
// Initialize a alipay (web) transaction data csv file importer singleton instance
|
||||
var (
|
||||
AlipayWebTransactionDataCsvFileImporter = &alipayWebTransactionDataCsvFileImporter{
|
||||
alipayTransactionDataCsvFileImporter{
|
||||
fileHeaderLine: "支付宝交易记录明细查询",
|
||||
dataHeaderStartContent: "交易记录明细列表",
|
||||
dataBottomEndLineRune: '-',
|
||||
originalColumnNames: alipayTransactionColumnNames{
|
||||
timeColumnName: "交易创建时间",
|
||||
categoryColumnName: "",
|
||||
targetNameColumnName: "交易对方",
|
||||
productNameColumnName: "商品名称",
|
||||
amountColumnName: "金额(元)",
|
||||
typeColumnName: "收/支",
|
||||
relatedAccountColumnName: "",
|
||||
statusColumnName: "交易状态",
|
||||
descriptionColumnName: "备注",
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -1,24 +0,0 @@
|
||||
package base
|
||||
|
||||
import (
|
||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||
)
|
||||
|
||||
// TransactionDataExporter defines the structure of transaction data exporter
|
||||
type TransactionDataExporter interface {
|
||||
// ToExportedContent returns the exported data
|
||||
ToExportedContent(ctx core.Context, uid int64, transactions []*models.Transaction, accountMap map[int64]*models.Account, categoryMap map[int64]*models.TransactionCategory, tagMap map[int64]*models.TransactionTag, allTagIndexes map[int64][]int64) ([]byte, error)
|
||||
}
|
||||
|
||||
// TransactionDataImporter defines the structure of transaction data importer
|
||||
type TransactionDataImporter interface {
|
||||
// ParseImportedData returns the imported data
|
||||
ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezoneOffset int16, accountMap map[string]*models.Account, expenseCategoryMap map[string]*models.TransactionCategory, incomeCategoryMap map[string]*models.TransactionCategory, transferCategoryMap 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
|
||||
type TransactionDataConverter interface {
|
||||
TransactionDataExporter
|
||||
TransactionDataImporter
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
package csv
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/log"
|
||||
)
|
||||
|
||||
// CsvFileImportedDataTable defines the structure of csv data table
|
||||
type CsvFileImportedDataTable struct {
|
||||
allLines [][]string
|
||||
}
|
||||
|
||||
// CsvFileImportedDataRow defines the structure of csv data table row
|
||||
type CsvFileImportedDataRow struct {
|
||||
dataTable *CsvFileImportedDataTable
|
||||
allItems []string
|
||||
}
|
||||
|
||||
// CsvFileImportedDataRowIterator defines the structure of csv data table row iterator
|
||||
type CsvFileImportedDataRowIterator struct {
|
||||
dataTable *CsvFileImportedDataTable
|
||||
currentIndex int
|
||||
}
|
||||
|
||||
// DataRowCount returns the total count of data row
|
||||
func (t *CsvFileImportedDataTable) DataRowCount() int {
|
||||
if len(t.allLines) < 1 {
|
||||
return 0
|
||||
}
|
||||
|
||||
return len(t.allLines) - 1
|
||||
}
|
||||
|
||||
// HeaderColumnNames returns the header column name list
|
||||
func (t *CsvFileImportedDataTable) HeaderColumnNames() []string {
|
||||
if len(t.allLines) < 1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return t.allLines[0]
|
||||
}
|
||||
|
||||
// DataRowIterator returns the iterator of data row
|
||||
func (t *CsvFileImportedDataTable) DataRowIterator() datatable.ImportedDataRowIterator {
|
||||
return &CsvFileImportedDataRowIterator{
|
||||
dataTable: t,
|
||||
currentIndex: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// ColumnCount returns the total count of column in this data row
|
||||
func (r *CsvFileImportedDataRow) ColumnCount() int {
|
||||
return len(r.allItems)
|
||||
}
|
||||
|
||||
// GetData returns the data in the specified column index
|
||||
func (r *CsvFileImportedDataRow) GetData(columnIndex int) string {
|
||||
if columnIndex >= len(r.allItems) {
|
||||
return ""
|
||||
}
|
||||
|
||||
return r.allItems[columnIndex]
|
||||
}
|
||||
|
||||
// HasNext returns whether the iterator does not reach the end
|
||||
func (t *CsvFileImportedDataRowIterator) HasNext() bool {
|
||||
return t.currentIndex+1 < len(t.dataTable.allLines)
|
||||
}
|
||||
|
||||
// CurrentRowId returns current index
|
||||
func (t *CsvFileImportedDataRowIterator) CurrentRowId() string {
|
||||
return fmt.Sprintf("line#%d", t.currentIndex)
|
||||
}
|
||||
|
||||
// Next returns the next imported data row
|
||||
func (t *CsvFileImportedDataRowIterator) Next() datatable.ImportedDataRow {
|
||||
if t.currentIndex+1 >= len(t.dataTable.allLines) {
|
||||
return nil
|
||||
}
|
||||
|
||||
t.currentIndex++
|
||||
|
||||
rowItems := t.dataTable.allLines[t.currentIndex]
|
||||
|
||||
return &CsvFileImportedDataRow{
|
||||
dataTable: t.dataTable,
|
||||
allItems: rowItems,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateNewCsvImportedDataTable returns comma separated values data table by io readers
|
||||
func CreateNewCsvImportedDataTable(ctx core.Context, reader io.Reader) (*CsvFileImportedDataTable, error) {
|
||||
return createNewCsvFileDataTable(ctx, reader, ',')
|
||||
}
|
||||
|
||||
// CreateNewCustomCsvImportedDataTable returns character separated values data table by io readers
|
||||
func CreateNewCustomCsvImportedDataTable(allLines [][]string) *CsvFileImportedDataTable {
|
||||
return &CsvFileImportedDataTable{
|
||||
allLines: allLines,
|
||||
}
|
||||
}
|
||||
|
||||
func createNewCsvFileDataTable(ctx core.Context, reader io.Reader, separator rune) (*CsvFileImportedDataTable, error) {
|
||||
csvReader := csv.NewReader(reader)
|
||||
csvReader.Comma = separator
|
||||
csvReader.FieldsPerRecord = -1
|
||||
|
||||
allLines := make([][]string, 0)
|
||||
|
||||
for {
|
||||
items, err := csvReader.Read()
|
||||
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(ctx, "[csv_file_imported_data_table.createNewCsvFileDataTable] cannot parse csv data, because %s", err.Error())
|
||||
return nil, errs.ErrInvalidCSVFile
|
||||
}
|
||||
|
||||
if len(items) == 1 && items[0] == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
allLines = append(allLines, items)
|
||||
}
|
||||
|
||||
return &CsvFileImportedDataTable{
|
||||
allLines: allLines,
|
||||
}, nil
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
package csv
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||
)
|
||||
|
||||
func TestCsvFileImportedDataTableDataRowCount(t *testing.T) {
|
||||
datatable := CreateNewCustomCsvImportedDataTable([][]string{
|
||||
{"A1", "B1", "C1"},
|
||||
{"A2", "B2", "C2"},
|
||||
{"A3", "B3", "C3"},
|
||||
})
|
||||
|
||||
assert.Equal(t, 2, datatable.DataRowCount())
|
||||
}
|
||||
|
||||
func TestCsvFileImportedDataTableDataRowCount_OnlyHeaderLine(t *testing.T) {
|
||||
datatable := CreateNewCustomCsvImportedDataTable([][]string{
|
||||
{"A1", "B1", "C1"},
|
||||
})
|
||||
|
||||
assert.Equal(t, 0, datatable.DataRowCount())
|
||||
}
|
||||
|
||||
func TestCsvFileImportedDataTableDataRowCount_EmptyContent(t *testing.T) {
|
||||
datatable := CreateNewCustomCsvImportedDataTable([][]string{})
|
||||
|
||||
assert.Equal(t, 0, datatable.DataRowCount())
|
||||
}
|
||||
|
||||
func TestCsvFileImportedDataTableHeaderColumnNames(t *testing.T) {
|
||||
datatable := CreateNewCustomCsvImportedDataTable([][]string{
|
||||
{"A1", "B1", "C1"},
|
||||
{"A2", "B2", "C2"},
|
||||
{"A3", "B3", "C3"},
|
||||
})
|
||||
|
||||
assert.EqualValues(t, []string{"A1", "B1", "C1"}, datatable.HeaderColumnNames())
|
||||
}
|
||||
|
||||
func TestCsvFileImportedDataTableHeaderColumnNames_EmptyContent(t *testing.T) {
|
||||
datatable := CreateNewCustomCsvImportedDataTable([][]string{})
|
||||
|
||||
assert.Nil(t, datatable.HeaderColumnNames())
|
||||
}
|
||||
|
||||
func TestCsvFileImportedDataRowIterator(t *testing.T) {
|
||||
datatable := CreateNewCustomCsvImportedDataTable([][]string{
|
||||
{"A1", "B1", "C1"},
|
||||
{"A2", "B2", "C2"},
|
||||
{"A3", "B3", "C3"},
|
||||
})
|
||||
|
||||
iterator := datatable.DataRowIterator()
|
||||
assert.True(t, iterator.HasNext())
|
||||
|
||||
// data row 1
|
||||
assert.NotNil(t, iterator.Next())
|
||||
assert.True(t, iterator.HasNext())
|
||||
|
||||
// data row 2
|
||||
assert.NotNil(t, iterator.Next())
|
||||
assert.False(t, iterator.HasNext())
|
||||
|
||||
// not existed data row 3
|
||||
assert.Nil(t, iterator.Next())
|
||||
assert.False(t, iterator.HasNext())
|
||||
|
||||
// not existed data row 4
|
||||
assert.Nil(t, iterator.Next())
|
||||
assert.False(t, iterator.HasNext())
|
||||
}
|
||||
|
||||
func TestCsvFileImportedDataRowColumnCount(t *testing.T) {
|
||||
datatable := CreateNewCustomCsvImportedDataTable([][]string{
|
||||
{"A1", "B1", "C1"},
|
||||
{"A2", "B2", "C2"},
|
||||
{"A3", "B3", "C3"},
|
||||
})
|
||||
|
||||
iterator := datatable.DataRowIterator()
|
||||
|
||||
row1 := iterator.Next()
|
||||
assert.EqualValues(t, 3, row1.ColumnCount())
|
||||
|
||||
row2 := iterator.Next()
|
||||
assert.EqualValues(t, 3, row2.ColumnCount())
|
||||
}
|
||||
|
||||
func TestCsvFileImportedDataRowGetData(t *testing.T) {
|
||||
datatable := CreateNewCustomCsvImportedDataTable([][]string{
|
||||
{"A1", "B1", "C1"},
|
||||
{"A2", "B2", "C2"},
|
||||
{"A3", "B3", "C3"},
|
||||
})
|
||||
|
||||
iterator := datatable.DataRowIterator()
|
||||
|
||||
row1 := iterator.Next()
|
||||
assert.Equal(t, "A2", row1.GetData(0))
|
||||
assert.Equal(t, "B2", row1.GetData(1))
|
||||
assert.Equal(t, "C2", row1.GetData(2))
|
||||
|
||||
row2 := iterator.Next()
|
||||
assert.Equal(t, "A3", row2.GetData(0))
|
||||
assert.Equal(t, "B3", row2.GetData(1))
|
||||
assert.Equal(t, "C3", row2.GetData(2))
|
||||
}
|
||||
|
||||
func TestCsvFileImportedDataRowGetData_GetNotExistedColumnData(t *testing.T) {
|
||||
datatable := CreateNewCustomCsvImportedDataTable([][]string{
|
||||
{"A1", "B1", "C1"},
|
||||
{"A2", "B2", "C2"},
|
||||
{"A3", "B3", "C3"},
|
||||
})
|
||||
|
||||
iterator := datatable.DataRowIterator()
|
||||
|
||||
row1 := iterator.Next()
|
||||
assert.Equal(t, "", row1.GetData(3))
|
||||
}
|
||||
|
||||
func TestCreateNewCsvImportedDataTable(t *testing.T) {
|
||||
context := core.NewNullContext()
|
||||
reader := bytes.NewReader([]byte("A1,B1,C1\n" +
|
||||
"A2,B2,C2\n" +
|
||||
"A3,B3,C3\n"))
|
||||
datatable, err := CreateNewCsvImportedDataTable(context, reader)
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, 2, datatable.DataRowCount())
|
||||
|
||||
iterator := datatable.DataRowIterator()
|
||||
assert.True(t, iterator.HasNext())
|
||||
|
||||
row1 := iterator.Next()
|
||||
assert.EqualValues(t, 3, row1.ColumnCount())
|
||||
assert.Equal(t, "A2", row1.GetData(0))
|
||||
assert.Equal(t, "B2", row1.GetData(1))
|
||||
assert.Equal(t, "C2", row1.GetData(2))
|
||||
assert.True(t, iterator.HasNext())
|
||||
|
||||
row2 := iterator.Next()
|
||||
assert.EqualValues(t, 3, row2.ColumnCount())
|
||||
assert.Equal(t, "A3", row2.GetData(0))
|
||||
assert.Equal(t, "B3", row2.GetData(1))
|
||||
assert.Equal(t, "C3", row2.GetData(2))
|
||||
assert.False(t, iterator.HasNext())
|
||||
}
|
||||
|
||||
func TestCreateNewCsvImportedDataTable_SkipBlankLine(t *testing.T) {
|
||||
context := core.NewNullContext()
|
||||
reader := bytes.NewReader([]byte("\n" +
|
||||
"A1,B1,C1\n" +
|
||||
"A2,B2,C2\n" +
|
||||
"\n" +
|
||||
"A3,B3,C3\n"))
|
||||
datatable, err := CreateNewCsvImportedDataTable(context, reader)
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, 2, datatable.DataRowCount())
|
||||
|
||||
iterator := datatable.DataRowIterator()
|
||||
assert.True(t, iterator.HasNext())
|
||||
|
||||
row1 := iterator.Next()
|
||||
assert.EqualValues(t, 3, row1.ColumnCount())
|
||||
assert.Equal(t, "A2", row1.GetData(0))
|
||||
assert.Equal(t, "B2", row1.GetData(1))
|
||||
assert.Equal(t, "C2", row1.GetData(2))
|
||||
assert.True(t, iterator.HasNext())
|
||||
|
||||
row2 := iterator.Next()
|
||||
assert.EqualValues(t, 3, row2.ColumnCount())
|
||||
assert.Equal(t, "A3", row2.GetData(0))
|
||||
assert.Equal(t, "B3", row2.GetData(1))
|
||||
assert.Equal(t, "C3", row2.GetData(2))
|
||||
assert.False(t, iterator.HasNext())
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package converters
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||
)
|
||||
|
||||
// DataConverter defines the structure of data exporter
|
||||
type DataConverter interface {
|
||||
// ToExportedContent returns the exported data
|
||||
ToExportedContent(uid int64, timezone *time.Location, transactions []*models.Transaction, accountMap map[int64]*models.Account, categoryMap map[int64]*models.TransactionCategory, tagMap map[int64]*models.TransactionTag, allTagIndexs map[int64][]int64) ([]byte, error)
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
package datatable
|
||||
|
||||
// CommonDataTable defines the structure of common data table
|
||||
type CommonDataTable interface {
|
||||
// HeaderColumnCount returns the total count of column in header row
|
||||
HeaderColumnCount() int
|
||||
|
||||
// HasColumn returns whether the common data table has specified column name
|
||||
HasColumn(columnName string) bool
|
||||
|
||||
// DataRowCount returns the total count of common data row
|
||||
DataRowCount() int
|
||||
|
||||
// DataRowIterator returns the iterator of common data row
|
||||
DataRowIterator() CommonDataRowIterator
|
||||
}
|
||||
|
||||
// CommonDataRow defines the structure of common data row
|
||||
type CommonDataRow interface {
|
||||
// ColumnCount returns the total count of column in this data row
|
||||
ColumnCount() int
|
||||
|
||||
// HasData returns whether the common data row has specified column data
|
||||
HasData(columnName string) bool
|
||||
|
||||
// GetData returns the data in the specified column name
|
||||
GetData(columnName string) string
|
||||
}
|
||||
|
||||
// CommonDataRowIterator defines the structure of common data row iterator
|
||||
type CommonDataRowIterator interface {
|
||||
// HasNext returns whether the iterator does not reach the end
|
||||
HasNext() bool
|
||||
|
||||
// CurrentRowId returns current row id
|
||||
CurrentRowId() string
|
||||
|
||||
// Next returns the next common data row
|
||||
Next() CommonDataRow
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
package datatable
|
||||
|
||||
import (
|
||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/log"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||
)
|
||||
|
||||
// CommonTransactionDataTable defines the structure of common transaction data table
|
||||
type CommonTransactionDataTable struct {
|
||||
innerDataTable CommonDataTable
|
||||
supportedDataColumns map[TransactionDataTableColumn]bool
|
||||
rowParser CommonTransactionDataRowParser
|
||||
}
|
||||
|
||||
// CommonTransactionDataRow defines the structure of common transaction data row
|
||||
type CommonTransactionDataRow struct {
|
||||
transactionDataTable *CommonTransactionDataTable
|
||||
rowData map[TransactionDataTableColumn]string
|
||||
rowDataValid bool
|
||||
}
|
||||
|
||||
// CommonTransactionDataRowIterator defines the structure of common transaction data row iterator
|
||||
type CommonTransactionDataRowIterator struct {
|
||||
transactionDataTable *CommonTransactionDataTable
|
||||
innerIterator CommonDataRowIterator
|
||||
}
|
||||
|
||||
// CommonTransactionDataRowParser defines the structure of common transaction data row parser
|
||||
type CommonTransactionDataRowParser interface {
|
||||
// Parse returns the converted transaction data row
|
||||
Parse(ctx core.Context, user *models.User, dataTable *CommonTransactionDataTable, dataRow CommonDataRow, rowId string) (rowData map[TransactionDataTableColumn]string, rowDataValid bool, err error)
|
||||
}
|
||||
|
||||
// HasColumn returns whether the data table has specified column
|
||||
func (t *CommonTransactionDataTable) HasColumn(column TransactionDataTableColumn) bool {
|
||||
_, exists := t.supportedDataColumns[column]
|
||||
return exists
|
||||
}
|
||||
|
||||
// HasOriginalColumn returns whether the original data table has specified column name
|
||||
func (t *CommonTransactionDataTable) HasOriginalColumn(columnName string) bool {
|
||||
return columnName != "" && t.innerDataTable.HasColumn(columnName)
|
||||
}
|
||||
|
||||
// TransactionRowCount returns the total count of transaction data row
|
||||
func (t *CommonTransactionDataTable) TransactionRowCount() int {
|
||||
return t.innerDataTable.DataRowCount()
|
||||
}
|
||||
|
||||
// TransactionRowIterator returns the iterator of transaction data row
|
||||
func (t *CommonTransactionDataTable) TransactionRowIterator() TransactionDataRowIterator {
|
||||
return &CommonTransactionDataRowIterator{
|
||||
transactionDataTable: t,
|
||||
innerIterator: t.innerDataTable.DataRowIterator(),
|
||||
}
|
||||
}
|
||||
|
||||
// IsValid returns whether this row is valid data for importing
|
||||
func (r *CommonTransactionDataRow) IsValid() bool {
|
||||
return r.rowDataValid
|
||||
}
|
||||
|
||||
// GetData returns the data in the specified column type
|
||||
func (r *CommonTransactionDataRow) GetData(column TransactionDataTableColumn) string {
|
||||
if !r.rowDataValid {
|
||||
return ""
|
||||
}
|
||||
|
||||
_, exists := r.transactionDataTable.supportedDataColumns[column]
|
||||
|
||||
if !exists {
|
||||
return ""
|
||||
}
|
||||
|
||||
return r.rowData[column]
|
||||
}
|
||||
|
||||
// HasNext returns whether the iterator does not reach the end
|
||||
func (t *CommonTransactionDataRowIterator) HasNext() bool {
|
||||
return t.innerIterator.HasNext()
|
||||
}
|
||||
|
||||
// Next returns the next transaction data row
|
||||
func (t *CommonTransactionDataRowIterator) Next(ctx core.Context, user *models.User) (daraRow TransactionDataRow, err error) {
|
||||
commonRow := t.innerIterator.Next()
|
||||
|
||||
if commonRow == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
rowId := t.innerIterator.CurrentRowId()
|
||||
rowData, rowDataValid, err := t.transactionDataTable.rowParser.Parse(ctx, user, t.transactionDataTable, commonRow, rowId)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(ctx, "[common_transaction_data_table.Next] cannot parse data row, because %s", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &CommonTransactionDataRow{
|
||||
transactionDataTable: t.transactionDataTable,
|
||||
rowData: rowData,
|
||||
rowDataValid: rowDataValid,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CreateNewCommonTransactionDataTable returns transaction data table from Common data table
|
||||
func CreateNewCommonTransactionDataTable(dataTable CommonDataTable, supportedDataColumns map[TransactionDataTableColumn]bool, rowParser CommonTransactionDataRowParser) *CommonTransactionDataTable {
|
||||
return &CommonTransactionDataTable{
|
||||
innerDataTable: dataTable,
|
||||
supportedDataColumns: supportedDataColumns,
|
||||
rowParser: rowParser,
|
||||
}
|
||||
}
|
||||
@@ -1,578 +0,0 @@
|
||||
package datatable
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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/utils"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/validators"
|
||||
)
|
||||
|
||||
// DataTableTransactionDataExporter defines the structure of plain text data table exporter for transaction data
|
||||
type DataTableTransactionDataExporter struct {
|
||||
transactionTypeMapping map[models.TransactionType]string
|
||||
geoLocationSeparator string
|
||||
transactionTagSeparator string
|
||||
}
|
||||
|
||||
// DataTableTransactionDataImporter defines the structure of plain text data table importer for transaction data
|
||||
type DataTableTransactionDataImporter struct {
|
||||
transactionTypeMapping map[models.TransactionType]string
|
||||
geoLocationSeparator string
|
||||
transactionTagSeparator string
|
||||
}
|
||||
|
||||
// CreateNewExporter returns a new data table transaction data exporter according to the specified arguments
|
||||
func CreateNewExporter(transactionTypeMapping map[models.TransactionType]string, geoLocationSeparator string, transactionTagSeparator string) *DataTableTransactionDataExporter {
|
||||
return &DataTableTransactionDataExporter{
|
||||
transactionTypeMapping: transactionTypeMapping,
|
||||
geoLocationSeparator: geoLocationSeparator,
|
||||
transactionTagSeparator: transactionTagSeparator,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateNewImporter returns a new data table transaction data importer according to the specified arguments
|
||||
func CreateNewImporter(transactionTypeMapping map[models.TransactionType]string, geoLocationSeparator string, transactionTagSeparator string) *DataTableTransactionDataImporter {
|
||||
return &DataTableTransactionDataImporter{
|
||||
transactionTypeMapping: transactionTypeMapping,
|
||||
geoLocationSeparator: geoLocationSeparator,
|
||||
transactionTagSeparator: transactionTagSeparator,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateNewSimpleImporter returns a new data table transaction data importer according to the specified arguments
|
||||
func CreateNewSimpleImporter(transactionTypeMapping map[models.TransactionType]string) *DataTableTransactionDataImporter {
|
||||
return &DataTableTransactionDataImporter{
|
||||
transactionTypeMapping: transactionTypeMapping,
|
||||
}
|
||||
}
|
||||
|
||||
// BuildExportedContent writes the exported transaction data to the data table builder
|
||||
func (c *DataTableTransactionDataExporter) BuildExportedContent(ctx core.Context, dataTableBuilder TransactionDataTableBuilder, uid int64, transactions []*models.Transaction, accountMap map[int64]*models.Account, categoryMap map[int64]*models.TransactionCategory, tagMap map[int64]*models.TransactionTag, allTagIndexes map[int64][]int64) error {
|
||||
for i := 0; i < len(transactions); i++ {
|
||||
transaction := transactions[i]
|
||||
|
||||
if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN {
|
||||
continue
|
||||
}
|
||||
|
||||
dataRowMap := make(map[TransactionDataTableColumn]string, 15)
|
||||
transactionTimeZone := time.FixedZone("Transaction Timezone", int(transaction.TimezoneUtcOffset)*60)
|
||||
|
||||
dataRowMap[TRANSACTION_DATA_TABLE_TRANSACTION_TIME] = utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime), transactionTimeZone)
|
||||
dataRowMap[TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE] = utils.FormatTimezoneOffset(transactionTimeZone)
|
||||
dataRowMap[TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = dataTableBuilder.ReplaceDelimiters(c.getDisplayTransactionTypeName(transaction.Type))
|
||||
dataRowMap[TRANSACTION_DATA_TABLE_CATEGORY] = c.getExportedTransactionCategoryName(dataTableBuilder, transaction.CategoryId, categoryMap)
|
||||
dataRowMap[TRANSACTION_DATA_TABLE_SUB_CATEGORY] = c.getExportedTransactionSubCategoryName(dataTableBuilder, transaction.CategoryId, categoryMap)
|
||||
dataRowMap[TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = c.getExportedAccountName(dataTableBuilder, transaction.AccountId, accountMap)
|
||||
dataRowMap[TRANSACTION_DATA_TABLE_ACCOUNT_CURRENCY] = c.getAccountCurrency(dataTableBuilder, transaction.AccountId, accountMap)
|
||||
dataRowMap[TRANSACTION_DATA_TABLE_AMOUNT] = utils.FormatAmount(transaction.Amount)
|
||||
|
||||
if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT {
|
||||
dataRowMap[TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = c.getExportedAccountName(dataTableBuilder, transaction.RelatedAccountId, accountMap)
|
||||
dataRowMap[TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_CURRENCY] = c.getAccountCurrency(dataTableBuilder, transaction.RelatedAccountId, accountMap)
|
||||
dataRowMap[TRANSACTION_DATA_TABLE_RELATED_AMOUNT] = utils.FormatAmount(transaction.RelatedAccountAmount)
|
||||
}
|
||||
|
||||
dataRowMap[TRANSACTION_DATA_TABLE_GEOGRAPHIC_LOCATION] = c.getExportedGeographicLocation(transaction)
|
||||
dataRowMap[TRANSACTION_DATA_TABLE_TAGS] = c.getExportedTags(dataTableBuilder, transaction.TransactionId, allTagIndexes, tagMap)
|
||||
dataRowMap[TRANSACTION_DATA_TABLE_DESCRIPTION] = dataTableBuilder.ReplaceDelimiters(transaction.Comment)
|
||||
|
||||
dataTableBuilder.AppendTransaction(dataRowMap)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *DataTableTransactionDataExporter) getDisplayTransactionTypeName(transactionDbType models.TransactionDbType) string {
|
||||
transactionType, err := transactionDbType.ToTransactionType()
|
||||
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
transactionTypeName, exists := c.transactionTypeMapping[transactionType]
|
||||
|
||||
if !exists {
|
||||
return ""
|
||||
}
|
||||
|
||||
return transactionTypeName
|
||||
}
|
||||
|
||||
func (c *DataTableTransactionDataExporter) getExportedTransactionCategoryName(dataTableBuilder TransactionDataTableBuilder, categoryId int64, categoryMap map[int64]*models.TransactionCategory) string {
|
||||
category, exists := categoryMap[categoryId]
|
||||
|
||||
if !exists {
|
||||
return ""
|
||||
}
|
||||
|
||||
if category.ParentCategoryId == 0 {
|
||||
return dataTableBuilder.ReplaceDelimiters(category.Name)
|
||||
}
|
||||
|
||||
parentCategory, exists := categoryMap[category.ParentCategoryId]
|
||||
|
||||
if !exists {
|
||||
return ""
|
||||
}
|
||||
|
||||
return dataTableBuilder.ReplaceDelimiters(parentCategory.Name)
|
||||
}
|
||||
|
||||
func (c *DataTableTransactionDataExporter) getExportedTransactionSubCategoryName(dataTableBuilder TransactionDataTableBuilder, categoryId int64, categoryMap map[int64]*models.TransactionCategory) string {
|
||||
category, exists := categoryMap[categoryId]
|
||||
|
||||
if exists {
|
||||
return dataTableBuilder.ReplaceDelimiters(category.Name)
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (c *DataTableTransactionDataExporter) getExportedAccountName(dataTableBuilder TransactionDataTableBuilder, accountId int64, accountMap map[int64]*models.Account) string {
|
||||
account, exists := accountMap[accountId]
|
||||
|
||||
if exists {
|
||||
return dataTableBuilder.ReplaceDelimiters(account.Name)
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (c *DataTableTransactionDataExporter) getAccountCurrency(dataTableBuilder TransactionDataTableBuilder, accountId int64, accountMap map[int64]*models.Account) string {
|
||||
account, exists := accountMap[accountId]
|
||||
|
||||
if exists {
|
||||
return dataTableBuilder.ReplaceDelimiters(account.Currency)
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (c *DataTableTransactionDataExporter) getExportedGeographicLocation(transaction *models.Transaction) string {
|
||||
if transaction.GeoLongitude != 0 || transaction.GeoLatitude != 0 {
|
||||
return fmt.Sprintf("%f%s%f", transaction.GeoLongitude, c.geoLocationSeparator, transaction.GeoLatitude)
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *DataTableTransactionDataExporter) getExportedTags(dataTableBuilder TransactionDataTableBuilder, transactionId int64, allTagIndexes map[int64][]int64, tagMap map[int64]*models.TransactionTag) string {
|
||||
tagIndexes, exists := allTagIndexes[transactionId]
|
||||
|
||||
if !exists {
|
||||
return ""
|
||||
}
|
||||
|
||||
var ret strings.Builder
|
||||
|
||||
for i := 0; i < len(tagIndexes); i++ {
|
||||
tagIndex := tagIndexes[i]
|
||||
tag, exists := tagMap[tagIndex]
|
||||
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
if ret.Len() > 0 {
|
||||
ret.WriteString(c.transactionTagSeparator)
|
||||
}
|
||||
|
||||
ret.WriteString(strings.Replace(tag.Name, c.transactionTagSeparator, " ", -1))
|
||||
}
|
||||
|
||||
return dataTableBuilder.ReplaceDelimiters(ret.String())
|
||||
}
|
||||
|
||||
// ParseImportedData returns the imported transaction data
|
||||
func (c *DataTableTransactionDataImporter) ParseImportedData(ctx core.Context, user *models.User, dataTable TransactionDataTable, defaultTimezoneOffset int16, 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.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionTag, error) {
|
||||
if dataTable.TransactionRowCount() < 1 {
|
||||
log.Errorf(ctx, "[data_table_transaction_data_converter.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
|
||||
}
|
||||
|
||||
nameDbTypeMap, err := c.buildTransactionTypeNameDbTypeMap()
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
if !dataTable.HasColumn(TRANSACTION_DATA_TABLE_TRANSACTION_TIME) ||
|
||||
!dataTable.HasColumn(TRANSACTION_DATA_TABLE_TRANSACTION_TYPE) ||
|
||||
!dataTable.HasColumn(TRANSACTION_DATA_TABLE_SUB_CATEGORY) ||
|
||||
!dataTable.HasColumn(TRANSACTION_DATA_TABLE_ACCOUNT_NAME) ||
|
||||
!dataTable.HasColumn(TRANSACTION_DATA_TABLE_AMOUNT) ||
|
||||
!dataTable.HasColumn(TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME) {
|
||||
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] cannot parse import data for user \"uid:%d\", because missing essential columns in header row", user.Uid)
|
||||
return nil, nil, nil, nil, nil, nil, errs.ErrMissingRequiredFieldInHeaderRow
|
||||
}
|
||||
|
||||
if accountMap == nil {
|
||||
accountMap = make(map[string]*models.Account)
|
||||
}
|
||||
|
||||
if expenseCategoryMap == nil {
|
||||
expenseCategoryMap = make(map[string]*models.TransactionCategory)
|
||||
}
|
||||
|
||||
if incomeCategoryMap == nil {
|
||||
incomeCategoryMap = make(map[string]*models.TransactionCategory)
|
||||
}
|
||||
|
||||
if transferCategoryMap == nil {
|
||||
transferCategoryMap = make(map[string]*models.TransactionCategory)
|
||||
}
|
||||
|
||||
if tagMap == nil {
|
||||
tagMap = make(map[string]*models.TransactionTag)
|
||||
}
|
||||
|
||||
allNewTransactions := make(models.ImportedTransactionSlice, 0, dataTable.TransactionRowCount())
|
||||
allNewAccounts := make([]*models.Account, 0)
|
||||
allNewSubExpenseCategories := make([]*models.TransactionCategory, 0)
|
||||
allNewSubIncomeCategories := make([]*models.TransactionCategory, 0)
|
||||
allNewSubTransferCategories := make([]*models.TransactionCategory, 0)
|
||||
allNewTags := make([]*models.TransactionTag, 0)
|
||||
|
||||
dataRowIterator := dataTable.TransactionRowIterator()
|
||||
dataRowIndex := 0
|
||||
|
||||
for dataRowIterator.HasNext() {
|
||||
dataRowIndex++
|
||||
dataRow, err := dataRowIterator.Next(ctx, user)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] cannot parse data row \"index:%d\" for user \"uid:%d\", because %s", dataRowIndex, user.Uid, err.Error())
|
||||
return nil, nil, nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
if !dataRow.IsValid() {
|
||||
continue
|
||||
}
|
||||
|
||||
timezoneOffset := defaultTimezoneOffset
|
||||
|
||||
if dataTable.HasColumn(TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE) {
|
||||
transactionTimezone, err := utils.ParseFromTimezoneOffset(dataRow.GetData(TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE))
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] cannot parse time zone \"%s\" in data row \"index:%d\" for user \"uid:%d\", because %s", dataRow.GetData(TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE), dataRowIndex, user.Uid, err.Error())
|
||||
return nil, nil, nil, nil, nil, nil, errs.ErrTransactionTimeZoneInvalid
|
||||
}
|
||||
|
||||
timezoneOffset = utils.GetTimezoneOffsetMinutes(transactionTimezone)
|
||||
}
|
||||
|
||||
transactionTime, err := utils.ParseFromLongDateTime(dataRow.GetData(TRANSACTION_DATA_TABLE_TRANSACTION_TIME), timezoneOffset)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] cannot parse time \"%s\" in data row \"index:%d\" for user \"uid:%d\", because %s", dataRow.GetData(TRANSACTION_DATA_TABLE_TRANSACTION_TIME), dataRowIndex, user.Uid, err.Error())
|
||||
return nil, nil, nil, nil, nil, nil, errs.ErrTransactionTimeInvalid
|
||||
}
|
||||
|
||||
transactionDbType, err := c.getTransactionDbType(nameDbTypeMap, dataRow.GetData(TRANSACTION_DATA_TABLE_TRANSACTION_TYPE))
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] cannot parse transaction type \"%s\" in data row \"index:%d\" for user \"uid:%d\", because %s", dataRow.GetData(TRANSACTION_DATA_TABLE_TRANSACTION_TYPE), dataRowIndex, user.Uid, err.Error())
|
||||
return nil, nil, nil, nil, nil, nil, errs.Or(err, errs.ErrTransactionTypeInvalid)
|
||||
}
|
||||
|
||||
categoryId := int64(0)
|
||||
subCategoryName := ""
|
||||
|
||||
if transactionDbType != models.TRANSACTION_DB_TYPE_MODIFY_BALANCE {
|
||||
transactionCategoryType, err := c.getTransactionCategoryType(transactionDbType)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] cannot parse transaction category type in data row \"index:%d\" for user \"uid:%d\", because %s", dataRowIndex, user.Uid, err.Error())
|
||||
return nil, nil, nil, nil, nil, nil, errs.Or(err, errs.ErrTransactionTypeInvalid)
|
||||
}
|
||||
|
||||
subCategoryName = dataRow.GetData(TRANSACTION_DATA_TABLE_SUB_CATEGORY)
|
||||
|
||||
if transactionDbType == models.TRANSACTION_DB_TYPE_EXPENSE {
|
||||
subCategory, exists := expenseCategoryMap[subCategoryName]
|
||||
|
||||
if !exists {
|
||||
subCategory = c.createNewTransactionCategoryModel(user.Uid, subCategoryName, transactionCategoryType)
|
||||
allNewSubExpenseCategories = append(allNewSubExpenseCategories, subCategory)
|
||||
expenseCategoryMap[subCategoryName] = subCategory
|
||||
}
|
||||
|
||||
categoryId = subCategory.CategoryId
|
||||
} else if transactionDbType == models.TRANSACTION_DB_TYPE_INCOME {
|
||||
subCategory, exists := incomeCategoryMap[subCategoryName]
|
||||
|
||||
if !exists {
|
||||
subCategory = c.createNewTransactionCategoryModel(user.Uid, subCategoryName, transactionCategoryType)
|
||||
allNewSubIncomeCategories = append(allNewSubIncomeCategories, subCategory)
|
||||
incomeCategoryMap[subCategoryName] = subCategory
|
||||
}
|
||||
|
||||
categoryId = subCategory.CategoryId
|
||||
} else if transactionDbType == models.TRANSACTION_DB_TYPE_TRANSFER_OUT {
|
||||
subCategory, exists := transferCategoryMap[subCategoryName]
|
||||
|
||||
if !exists {
|
||||
subCategory = c.createNewTransactionCategoryModel(user.Uid, subCategoryName, transactionCategoryType)
|
||||
allNewSubTransferCategories = append(allNewSubTransferCategories, subCategory)
|
||||
transferCategoryMap[subCategoryName] = subCategory
|
||||
}
|
||||
|
||||
categoryId = subCategory.CategoryId
|
||||
}
|
||||
}
|
||||
|
||||
accountName := dataRow.GetData(TRANSACTION_DATA_TABLE_ACCOUNT_NAME)
|
||||
accountCurrency := user.DefaultCurrency
|
||||
|
||||
if dataTable.HasColumn(TRANSACTION_DATA_TABLE_ACCOUNT_CURRENCY) {
|
||||
accountCurrency = dataRow.GetData(TRANSACTION_DATA_TABLE_ACCOUNT_CURRENCY)
|
||||
|
||||
if _, ok := validators.AllCurrencyNames[accountCurrency]; !ok {
|
||||
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] account currency \"%s\" is not supported in data row \"index:%d\" for user \"uid:%d\"", accountCurrency, dataRowIndex, user.Uid)
|
||||
return nil, nil, nil, nil, nil, nil, errs.ErrAccountCurrencyInvalid
|
||||
}
|
||||
}
|
||||
|
||||
account, exists := accountMap[accountName]
|
||||
|
||||
if !exists {
|
||||
account = c.createNewAccountModel(user.Uid, accountName, accountCurrency)
|
||||
allNewAccounts = append(allNewAccounts, account)
|
||||
accountMap[accountName] = account
|
||||
}
|
||||
|
||||
if dataTable.HasColumn(TRANSACTION_DATA_TABLE_ACCOUNT_CURRENCY) {
|
||||
if account.Name != "" && account.Currency != accountCurrency {
|
||||
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] currency \"%s\" in data row \"index:%d\" not equals currency \"%s\" of the account for user \"uid:%d\"", accountCurrency, dataRowIndex, account.Currency, user.Uid)
|
||||
return nil, nil, nil, nil, nil, nil, errs.ErrAccountCurrencyInvalid
|
||||
}
|
||||
} else if exists {
|
||||
accountCurrency = account.Currency
|
||||
}
|
||||
|
||||
amount, err := utils.ParseAmount(dataRow.GetData(TRANSACTION_DATA_TABLE_AMOUNT))
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] cannot parse acmount \"%s\" in data row \"index:%d\" for user \"uid:%d\", because %s", dataRow.GetData(TRANSACTION_DATA_TABLE_AMOUNT), dataRowIndex, user.Uid, err.Error())
|
||||
return nil, nil, nil, nil, nil, nil, errs.ErrAmountInvalid
|
||||
}
|
||||
|
||||
relatedAccountId := int64(0)
|
||||
relatedAccountAmount := int64(0)
|
||||
account2Name := ""
|
||||
account2Currency := ""
|
||||
|
||||
if transactionDbType == models.TRANSACTION_DB_TYPE_TRANSFER_OUT {
|
||||
account2Name = dataRow.GetData(TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME)
|
||||
account2Currency = user.DefaultCurrency
|
||||
|
||||
if dataTable.HasColumn(TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_CURRENCY) {
|
||||
account2Currency = dataRow.GetData(TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_CURRENCY)
|
||||
|
||||
if _, ok := validators.AllCurrencyNames[account2Currency]; !ok {
|
||||
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] account2 currency \"%s\" is not supported in data row \"index:%d\" for user \"uid:%d\"", account2Currency, dataRowIndex, user.Uid)
|
||||
return nil, nil, nil, nil, nil, nil, errs.ErrAccountCurrencyInvalid
|
||||
}
|
||||
}
|
||||
|
||||
account2, exists := accountMap[account2Name]
|
||||
|
||||
if !exists {
|
||||
account2 = c.createNewAccountModel(user.Uid, account2Name, account2Currency)
|
||||
allNewAccounts = append(allNewAccounts, account2)
|
||||
accountMap[account2Name] = account2
|
||||
}
|
||||
|
||||
if dataTable.HasColumn(TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_CURRENCY) {
|
||||
if account2.Name != "" && account2.Currency != account2Currency {
|
||||
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] currency \"%s\" in data row \"index:%d\" not equals currency \"%s\" of the account2 for user \"uid:%d\"", account2Currency, dataRowIndex, account2.Currency, user.Uid)
|
||||
return nil, nil, nil, nil, nil, nil, errs.ErrAccountCurrencyInvalid
|
||||
}
|
||||
} else if exists {
|
||||
account2Currency = account2.Currency
|
||||
}
|
||||
|
||||
relatedAccountId = account2.AccountId
|
||||
|
||||
if dataTable.HasColumn(TRANSACTION_DATA_TABLE_RELATED_AMOUNT) {
|
||||
relatedAccountAmount, err = utils.ParseAmount(dataRow.GetData(TRANSACTION_DATA_TABLE_RELATED_AMOUNT))
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] cannot parse acmount2 \"%s\" in data row \"index:%d\" for user \"uid:%d\", because %s", dataRow.GetData(TRANSACTION_DATA_TABLE_RELATED_AMOUNT), dataRowIndex, user.Uid, err.Error())
|
||||
return nil, nil, nil, nil, nil, nil, errs.ErrAmountInvalid
|
||||
}
|
||||
} else if transactionDbType == models.TRANSACTION_DB_TYPE_TRANSFER_OUT {
|
||||
relatedAccountAmount = amount
|
||||
}
|
||||
}
|
||||
|
||||
geoLongitude := float64(0)
|
||||
geoLatitude := float64(0)
|
||||
|
||||
if dataTable.HasColumn(TRANSACTION_DATA_TABLE_GEOGRAPHIC_LOCATION) && c.geoLocationSeparator != "" {
|
||||
geoLocationItems := strings.Split(dataRow.GetData(TRANSACTION_DATA_TABLE_GEOGRAPHIC_LOCATION), c.geoLocationSeparator)
|
||||
|
||||
if len(geoLocationItems) == 2 {
|
||||
geoLongitude, err = utils.StringToFloat64(geoLocationItems[0])
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] cannot parse geographic location \"%s\" in data row \"index:%d\" for user \"uid:%d\", because %s", dataRow.GetData(TRANSACTION_DATA_TABLE_GEOGRAPHIC_LOCATION), dataRowIndex, user.Uid, err.Error())
|
||||
return nil, nil, nil, nil, nil, nil, errs.ErrGeographicLocationInvalid
|
||||
}
|
||||
|
||||
geoLatitude, err = utils.StringToFloat64(geoLocationItems[1])
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] cannot parse geographic location \"%s\" in data row \"index:%d\" for user \"uid:%d\", because %s", dataRow.GetData(TRANSACTION_DATA_TABLE_GEOGRAPHIC_LOCATION), dataRowIndex, user.Uid, err.Error())
|
||||
return nil, nil, nil, nil, nil, nil, errs.ErrGeographicLocationInvalid
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var tagIds []string
|
||||
var tagNames []string
|
||||
|
||||
if dataTable.HasColumn(TRANSACTION_DATA_TABLE_TAGS) {
|
||||
var tagNameItems []string
|
||||
|
||||
if c.transactionTagSeparator != "" {
|
||||
tagNameItems = strings.Split(dataRow.GetData(TRANSACTION_DATA_TABLE_TAGS), c.transactionTagSeparator)
|
||||
} else {
|
||||
tagNameItems = append(tagNameItems, dataRow.GetData(TRANSACTION_DATA_TABLE_TAGS))
|
||||
}
|
||||
|
||||
for i := 0; i < len(tagNameItems); i++ {
|
||||
tagName := tagNameItems[i]
|
||||
|
||||
if tagName == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
description := ""
|
||||
|
||||
if dataTable.HasColumn(TRANSACTION_DATA_TABLE_DESCRIPTION) {
|
||||
description = dataRow.GetData(TRANSACTION_DATA_TABLE_DESCRIPTION)
|
||||
}
|
||||
|
||||
transaction := &models.ImportTransaction{
|
||||
Transaction: &models.Transaction{
|
||||
Uid: user.Uid,
|
||||
Type: transactionDbType,
|
||||
CategoryId: categoryId,
|
||||
TransactionTime: utils.GetMinTransactionTimeFromUnixTime(transactionTime.Unix()),
|
||||
TimezoneUtcOffset: timezoneOffset,
|
||||
AccountId: account.AccountId,
|
||||
Amount: amount,
|
||||
HideAmount: false,
|
||||
RelatedAccountId: relatedAccountId,
|
||||
RelatedAccountAmount: relatedAccountAmount,
|
||||
Comment: description,
|
||||
GeoLongitude: geoLongitude,
|
||||
GeoLatitude: geoLatitude,
|
||||
CreatedIp: "127.0.0.1",
|
||||
},
|
||||
TagIds: tagIds,
|
||||
OriginalCategoryName: subCategoryName,
|
||||
OriginalSourceAccountName: accountName,
|
||||
OriginalSourceAccountCurrency: accountCurrency,
|
||||
OriginalDestinationAccountName: account2Name,
|
||||
OriginalDestinationAccountCurrency: account2Currency,
|
||||
OriginalTagNames: tagNames,
|
||||
}
|
||||
|
||||
allNewTransactions = append(allNewTransactions, transaction)
|
||||
}
|
||||
|
||||
if len(allNewTransactions) < 1 {
|
||||
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] no transaction data parsed for \"uid:%d\"", user.Uid)
|
||||
return nil, nil, nil, nil, nil, nil, errs.ErrNotFoundTransactionDataInFile
|
||||
}
|
||||
|
||||
sort.Sort(allNewTransactions)
|
||||
|
||||
return allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, nil
|
||||
}
|
||||
|
||||
func (c *DataTableTransactionDataImporter) buildTransactionTypeNameDbTypeMap() (map[string]models.TransactionDbType, error) {
|
||||
if c.transactionTypeMapping == nil {
|
||||
return nil, errs.ErrTransactionTypeInvalid
|
||||
}
|
||||
|
||||
nameDbTypeMap := make(map[string]models.TransactionDbType, len(c.transactionTypeMapping))
|
||||
nameDbTypeMap[c.transactionTypeMapping[models.TRANSACTION_TYPE_MODIFY_BALANCE]] = models.TRANSACTION_DB_TYPE_MODIFY_BALANCE
|
||||
nameDbTypeMap[c.transactionTypeMapping[models.TRANSACTION_TYPE_INCOME]] = models.TRANSACTION_DB_TYPE_INCOME
|
||||
nameDbTypeMap[c.transactionTypeMapping[models.TRANSACTION_TYPE_EXPENSE]] = models.TRANSACTION_DB_TYPE_EXPENSE
|
||||
nameDbTypeMap[c.transactionTypeMapping[models.TRANSACTION_TYPE_TRANSFER]] = models.TRANSACTION_DB_TYPE_TRANSFER_OUT
|
||||
|
||||
return nameDbTypeMap, nil
|
||||
}
|
||||
|
||||
func (c *DataTableTransactionDataImporter) getTransactionDbType(nameDbTypeMap map[string]models.TransactionDbType, transactionTypeName string) (models.TransactionDbType, error) {
|
||||
transactionType, exists := nameDbTypeMap[transactionTypeName]
|
||||
|
||||
if !exists {
|
||||
return 0, errs.ErrTransactionTypeInvalid
|
||||
}
|
||||
|
||||
return transactionType, nil
|
||||
}
|
||||
|
||||
func (c *DataTableTransactionDataImporter) getTransactionCategoryType(transactionType models.TransactionDbType) (models.TransactionCategoryType, error) {
|
||||
if transactionType == models.TRANSACTION_DB_TYPE_INCOME {
|
||||
return models.CATEGORY_TYPE_INCOME, nil
|
||||
} else if transactionType == models.TRANSACTION_DB_TYPE_EXPENSE {
|
||||
return models.CATEGORY_TYPE_EXPENSE, nil
|
||||
} else if transactionType == models.TRANSACTION_DB_TYPE_TRANSFER_OUT {
|
||||
return models.CATEGORY_TYPE_TRANSFER, nil
|
||||
} else {
|
||||
return 0, errs.ErrTransactionTypeInvalid
|
||||
}
|
||||
}
|
||||
|
||||
func (c *DataTableTransactionDataImporter) createNewAccountModel(uid int64, accountName string, currency string) *models.Account {
|
||||
return &models.Account{
|
||||
Uid: uid,
|
||||
Name: accountName,
|
||||
Currency: currency,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *DataTableTransactionDataImporter) createNewTransactionCategoryModel(uid int64, categoryName string, transactionCategoryType models.TransactionCategoryType) *models.TransactionCategory {
|
||||
return &models.TransactionCategory{
|
||||
Uid: uid,
|
||||
Name: categoryName,
|
||||
Type: transactionCategoryType,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *DataTableTransactionDataImporter) createNewTransactionTagModel(uid int64, tagName string) *models.TransactionTag {
|
||||
return &models.TransactionTag{
|
||||
Uid: uid,
|
||||
Name: tagName,
|
||||
}
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
package datatable
|
||||
|
||||
// ImportedCommonDataTable defines the structure of imported common data table
|
||||
type ImportedCommonDataTable struct {
|
||||
innerDataTable ImportedDataTable
|
||||
dataColumnIndexes map[string]int
|
||||
}
|
||||
|
||||
// ImportedCommonDataRow defines the structure of imported common data row
|
||||
type ImportedCommonDataRow struct {
|
||||
rowData map[string]string
|
||||
}
|
||||
|
||||
// ImportedCommonDataRowIterator defines the structure of imported common data row iterator
|
||||
type ImportedCommonDataRowIterator struct {
|
||||
commonDataTable *ImportedCommonDataTable
|
||||
innerIterator ImportedDataRowIterator
|
||||
}
|
||||
|
||||
// HeaderColumnCount returns the total count of column in header row
|
||||
func (t *ImportedCommonDataTable) HeaderColumnCount() int {
|
||||
return len(t.innerDataTable.HeaderColumnNames())
|
||||
}
|
||||
|
||||
// HasColumn returns whether the data table has specified column name
|
||||
func (t *ImportedCommonDataTable) HasColumn(columnName string) bool {
|
||||
index, exists := t.dataColumnIndexes[columnName]
|
||||
return exists && index >= 0
|
||||
}
|
||||
|
||||
// DataRowCount returns the total count of common data row
|
||||
func (t *ImportedCommonDataTable) DataRowCount() int {
|
||||
return t.innerDataTable.DataRowCount()
|
||||
}
|
||||
|
||||
// DataRowIterator returns the iterator of common data row
|
||||
func (t *ImportedCommonDataTable) DataRowIterator() CommonDataRowIterator {
|
||||
return &ImportedCommonDataRowIterator{
|
||||
commonDataTable: t,
|
||||
innerIterator: t.innerDataTable.DataRowIterator(),
|
||||
}
|
||||
}
|
||||
|
||||
// HasData returns whether the common data row has specified column data
|
||||
func (r *ImportedCommonDataRow) HasData(columnName string) bool {
|
||||
_, exists := r.rowData[columnName]
|
||||
return exists
|
||||
}
|
||||
|
||||
// ColumnCount returns the total count of column in this data row
|
||||
func (r *ImportedCommonDataRow) ColumnCount() int {
|
||||
return len(r.rowData)
|
||||
}
|
||||
|
||||
// GetData returns the data in the specified column name
|
||||
func (r *ImportedCommonDataRow) GetData(columnName string) string {
|
||||
return r.rowData[columnName]
|
||||
}
|
||||
|
||||
// HasNext returns whether the iterator does not reach the end
|
||||
func (t *ImportedCommonDataRowIterator) HasNext() bool {
|
||||
return t.innerIterator.HasNext()
|
||||
}
|
||||
|
||||
// CurrentRowId returns current row id
|
||||
func (t *ImportedCommonDataRowIterator) CurrentRowId() string {
|
||||
return t.innerIterator.CurrentRowId()
|
||||
}
|
||||
|
||||
// Next returns the next common data row
|
||||
func (t *ImportedCommonDataRowIterator) Next() CommonDataRow {
|
||||
importedRow := t.innerIterator.Next()
|
||||
|
||||
if importedRow == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
rowData := make(map[string]string, len(t.commonDataTable.dataColumnIndexes))
|
||||
|
||||
for column, columnIndex := range t.commonDataTable.dataColumnIndexes {
|
||||
if columnIndex < 0 || columnIndex >= importedRow.ColumnCount() {
|
||||
continue
|
||||
}
|
||||
|
||||
value := importedRow.GetData(columnIndex)
|
||||
rowData[column] = value
|
||||
}
|
||||
|
||||
return &ImportedCommonDataRow{
|
||||
rowData: rowData,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateNewImportedCommonDataTable returns common data table from imported data table
|
||||
func CreateNewImportedCommonDataTable(dataTable ImportedDataTable) *ImportedCommonDataTable {
|
||||
headerLineItems := dataTable.HeaderColumnNames()
|
||||
dataColumnIndexes := make(map[string]int, len(headerLineItems))
|
||||
|
||||
for i := 0; i < len(headerLineItems); i++ {
|
||||
dataColumnIndexes[headerLineItems[i]] = i
|
||||
}
|
||||
|
||||
return &ImportedCommonDataTable{
|
||||
innerDataTable: dataTable,
|
||||
dataColumnIndexes: dataColumnIndexes,
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package datatable
|
||||
|
||||
// ImportedDataTable defines the structure of imported data table
|
||||
type ImportedDataTable interface {
|
||||
// DataRowCount returns the total count of data row
|
||||
DataRowCount() int
|
||||
|
||||
// HeaderColumnNames returns the header column name list
|
||||
HeaderColumnNames() []string
|
||||
|
||||
// DataRowIterator returns the iterator of data row
|
||||
DataRowIterator() ImportedDataRowIterator
|
||||
}
|
||||
|
||||
// ImportedDataRow defines the structure of imported data row
|
||||
type ImportedDataRow interface {
|
||||
// ColumnCount returns the total count of column in this data row
|
||||
ColumnCount() int
|
||||
|
||||
// GetData returns the data in the specified column index
|
||||
GetData(columnIndex int) string
|
||||
}
|
||||
|
||||
// ImportedDataRowIterator defines the structure of imported data row iterator
|
||||
type ImportedDataRowIterator interface {
|
||||
// HasNext returns whether the iterator does not reach the end
|
||||
HasNext() bool
|
||||
|
||||
// CurrentRowId returns current row id
|
||||
CurrentRowId() string
|
||||
|
||||
// Next returns the next imported data row
|
||||
Next() ImportedDataRow
|
||||
}
|
||||
@@ -1,188 +0,0 @@
|
||||
package datatable
|
||||
|
||||
import (
|
||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/log"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||
)
|
||||
|
||||
// ImportedTransactionDataTable defines the structure of imported transaction data table
|
||||
type ImportedTransactionDataTable struct {
|
||||
innerDataTable ImportedDataTable
|
||||
dataColumnMapping map[TransactionDataTableColumn]string
|
||||
dataColumnIndexes map[TransactionDataTableColumn]int
|
||||
rowParser TransactionDataRowParser
|
||||
addedColumns map[TransactionDataTableColumn]bool
|
||||
}
|
||||
|
||||
// ImportedTransactionDataRow defines the structure of imported transaction data row
|
||||
type ImportedTransactionDataRow struct {
|
||||
transactionDataTable *ImportedTransactionDataTable
|
||||
rowData map[TransactionDataTableColumn]string
|
||||
rowDataValid bool
|
||||
}
|
||||
|
||||
// ImportedTransactionDataRowIterator defines the structure of imported transaction data row iterator
|
||||
type ImportedTransactionDataRowIterator struct {
|
||||
transactionDataTable *ImportedTransactionDataTable
|
||||
innerIterator ImportedDataRowIterator
|
||||
}
|
||||
|
||||
// HasColumn returns whether the data table has specified column
|
||||
func (t *ImportedTransactionDataTable) HasColumn(column TransactionDataTableColumn) bool {
|
||||
index, exists := t.dataColumnIndexes[column]
|
||||
|
||||
if exists && index >= 0 {
|
||||
return exists
|
||||
}
|
||||
|
||||
if t.addedColumns != nil {
|
||||
_, exists = t.addedColumns[column]
|
||||
|
||||
if exists {
|
||||
return exists
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// TransactionRowCount returns the total count of transaction data row
|
||||
func (t *ImportedTransactionDataTable) TransactionRowCount() int {
|
||||
return t.innerDataTable.DataRowCount()
|
||||
}
|
||||
|
||||
// TransactionRowIterator returns the iterator of transaction data row
|
||||
func (t *ImportedTransactionDataTable) TransactionRowIterator() TransactionDataRowIterator {
|
||||
return &ImportedTransactionDataRowIterator{
|
||||
transactionDataTable: t,
|
||||
innerIterator: t.innerDataTable.DataRowIterator(),
|
||||
}
|
||||
}
|
||||
|
||||
// IsValid returns whether this row is valid data for importing
|
||||
func (r *ImportedTransactionDataRow) IsValid() bool {
|
||||
return r.rowDataValid
|
||||
}
|
||||
|
||||
// GetData returns the data in the specified column type
|
||||
func (r *ImportedTransactionDataRow) GetData(column TransactionDataTableColumn) string {
|
||||
if !r.rowDataValid {
|
||||
return ""
|
||||
}
|
||||
|
||||
_, exists := r.transactionDataTable.dataColumnIndexes[column]
|
||||
|
||||
if exists {
|
||||
return r.rowData[column]
|
||||
}
|
||||
|
||||
if r.transactionDataTable.addedColumns != nil {
|
||||
_, exists = r.transactionDataTable.addedColumns[column]
|
||||
|
||||
if exists {
|
||||
return r.rowData[column]
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// HasNext returns whether the iterator does not reach the end
|
||||
func (t *ImportedTransactionDataRowIterator) HasNext() bool {
|
||||
return t.innerIterator.HasNext()
|
||||
}
|
||||
|
||||
// Next returns the next transaction data row
|
||||
func (t *ImportedTransactionDataRowIterator) Next(ctx core.Context, user *models.User) (daraRow TransactionDataRow, err error) {
|
||||
importedRow := t.innerIterator.Next()
|
||||
|
||||
if importedRow == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if importedRow.ColumnCount() == 1 && importedRow.GetData(0) == "" {
|
||||
return &ImportedTransactionDataRow{
|
||||
transactionDataTable: t.transactionDataTable,
|
||||
rowData: nil,
|
||||
rowDataValid: false,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if importedRow.ColumnCount() < len(t.transactionDataTable.dataColumnIndexes) {
|
||||
log.Errorf(ctx, "[imported_transaction_data_table.Next] cannot parse data row, because may missing some columns (column count %d in data row is less than header column count %d)", importedRow.ColumnCount(), len(t.transactionDataTable.dataColumnIndexes))
|
||||
return nil, errs.ErrFewerFieldsInDataRowThanInHeaderRow
|
||||
}
|
||||
|
||||
rowData := make(map[TransactionDataTableColumn]string, len(t.transactionDataTable.dataColumnIndexes))
|
||||
rowDataValid := true
|
||||
|
||||
for column, columnIndex := range t.transactionDataTable.dataColumnIndexes {
|
||||
if columnIndex < 0 || columnIndex >= importedRow.ColumnCount() {
|
||||
continue
|
||||
}
|
||||
|
||||
value := importedRow.GetData(columnIndex)
|
||||
rowData[column] = value
|
||||
}
|
||||
|
||||
if t.transactionDataTable.rowParser != nil {
|
||||
rowData, rowDataValid, err = t.transactionDataTable.rowParser.Parse(rowData)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(ctx, "[imported_transaction_data_table.Next] cannot parse data row, because %s", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &ImportedTransactionDataRow{
|
||||
transactionDataTable: t.transactionDataTable,
|
||||
rowData: rowData,
|
||||
rowDataValid: rowDataValid,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CreateNewImportedTransactionDataTable returns transaction data table from imported data table
|
||||
func CreateNewImportedTransactionDataTable(dataTable ImportedDataTable, dataColumnMapping map[TransactionDataTableColumn]string) *ImportedTransactionDataTable {
|
||||
return CreateNewImportedTransactionDataTableWithRowParser(dataTable, dataColumnMapping, nil)
|
||||
}
|
||||
|
||||
// CreateNewImportedTransactionDataTableWithRowParser returns transaction data table from imported data table
|
||||
func CreateNewImportedTransactionDataTableWithRowParser(dataTable ImportedDataTable, dataColumnMapping map[TransactionDataTableColumn]string, rowParser TransactionDataRowParser) *ImportedTransactionDataTable {
|
||||
headerLineItems := dataTable.HeaderColumnNames()
|
||||
headerItemMap := make(map[string]int, len(headerLineItems))
|
||||
|
||||
for i := 0; i < len(headerLineItems); i++ {
|
||||
headerItemMap[headerLineItems[i]] = i
|
||||
}
|
||||
|
||||
dataColumnIndexes := make(map[TransactionDataTableColumn]int, len(headerLineItems))
|
||||
|
||||
for column, columnName := range dataColumnMapping {
|
||||
columnIndex, exists := headerItemMap[columnName]
|
||||
|
||||
if exists {
|
||||
dataColumnIndexes[column] = columnIndex
|
||||
}
|
||||
}
|
||||
|
||||
var addedColumns map[TransactionDataTableColumn]bool
|
||||
|
||||
if rowParser != nil {
|
||||
addedColumnsByParser := rowParser.GetAddedColumns()
|
||||
addedColumns = make(map[TransactionDataTableColumn]bool, len(addedColumnsByParser))
|
||||
|
||||
for i := 0; i < len(addedColumnsByParser); i++ {
|
||||
addedColumns[addedColumnsByParser[i]] = true
|
||||
}
|
||||
}
|
||||
|
||||
return &ImportedTransactionDataTable{
|
||||
innerDataTable: dataTable,
|
||||
dataColumnMapping: dataColumnMapping,
|
||||
dataColumnIndexes: dataColumnIndexes,
|
||||
rowParser: rowParser,
|
||||
addedColumns: addedColumns,
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
package datatable
|
||||
|
||||
import (
|
||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||
)
|
||||
|
||||
// TransactionDataTable defines the structure of transaction data table
|
||||
type TransactionDataTable interface {
|
||||
// HasColumn returns whether the transaction data table has specified column
|
||||
HasColumn(column TransactionDataTableColumn) bool
|
||||
|
||||
// TransactionRowCount returns the total count of transaction data row
|
||||
TransactionRowCount() int
|
||||
|
||||
// TransactionRowIterator returns the iterator of transaction data row
|
||||
TransactionRowIterator() TransactionDataRowIterator
|
||||
}
|
||||
|
||||
// TransactionDataRow defines the structure of transaction data row
|
||||
type TransactionDataRow interface {
|
||||
// IsValid returns whether this row is valid data for importing
|
||||
IsValid() bool
|
||||
|
||||
// GetData returns the data in the specified column type
|
||||
GetData(column TransactionDataTableColumn) string
|
||||
}
|
||||
|
||||
// TransactionDataRowIterator defines the structure of transaction data row iterator
|
||||
type TransactionDataRowIterator interface {
|
||||
// HasNext returns whether the iterator does not reach the end
|
||||
HasNext() bool
|
||||
|
||||
// Next returns the next transaction data row
|
||||
Next(ctx core.Context, user *models.User) (daraRow TransactionDataRow, err error)
|
||||
}
|
||||
|
||||
// TransactionDataRowParser defines the structure of transaction data row parser
|
||||
type TransactionDataRowParser interface {
|
||||
// GetAddedColumns returns the added columns after converting the data row
|
||||
GetAddedColumns() []TransactionDataTableColumn
|
||||
|
||||
// Parse returns the converted transaction data row
|
||||
Parse(data map[TransactionDataTableColumn]string) (rowData map[TransactionDataTableColumn]string, rowDataValid bool, err error)
|
||||
}
|
||||
|
||||
// TransactionDataTableBuilder defines the structure of data table builder
|
||||
type TransactionDataTableBuilder interface {
|
||||
// AppendTransaction appends the specified transaction to data builder
|
||||
AppendTransaction(data map[TransactionDataTableColumn]string)
|
||||
|
||||
// ReplaceDelimiters returns the text after removing the delimiters
|
||||
ReplaceDelimiters(text string) string
|
||||
}
|
||||
|
||||
// TransactionDataTableColumn represents the data column type of data table
|
||||
type TransactionDataTableColumn byte
|
||||
|
||||
// Transaction data table columns
|
||||
const (
|
||||
TRANSACTION_DATA_TABLE_TRANSACTION_TIME TransactionDataTableColumn = 1
|
||||
TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE TransactionDataTableColumn = 2
|
||||
TRANSACTION_DATA_TABLE_TRANSACTION_TYPE TransactionDataTableColumn = 3
|
||||
TRANSACTION_DATA_TABLE_CATEGORY TransactionDataTableColumn = 4
|
||||
TRANSACTION_DATA_TABLE_SUB_CATEGORY TransactionDataTableColumn = 5
|
||||
TRANSACTION_DATA_TABLE_ACCOUNT_NAME TransactionDataTableColumn = 6
|
||||
TRANSACTION_DATA_TABLE_ACCOUNT_CURRENCY TransactionDataTableColumn = 7
|
||||
TRANSACTION_DATA_TABLE_AMOUNT TransactionDataTableColumn = 8
|
||||
TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME TransactionDataTableColumn = 9
|
||||
TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_CURRENCY TransactionDataTableColumn = 10
|
||||
TRANSACTION_DATA_TABLE_RELATED_AMOUNT TransactionDataTableColumn = 11
|
||||
TRANSACTION_DATA_TABLE_GEOGRAPHIC_LOCATION TransactionDataTableColumn = 12
|
||||
TRANSACTION_DATA_TABLE_TAGS TransactionDataTableColumn = 13
|
||||
TRANSACTION_DATA_TABLE_DESCRIPTION TransactionDataTableColumn = 14
|
||||
)
|
||||
@@ -1,210 +0,0 @@
|
||||
package datatable
|
||||
|
||||
import (
|
||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/log"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||
)
|
||||
|
||||
// WritableTransactionDataTable defines the structure of writable transaction data table
|
||||
type WritableTransactionDataTable struct {
|
||||
allData []map[TransactionDataTableColumn]string
|
||||
supportedColumns map[TransactionDataTableColumn]bool
|
||||
rowParser TransactionDataRowParser
|
||||
addedColumns map[TransactionDataTableColumn]bool
|
||||
}
|
||||
|
||||
// WritableTransactionDataRow defines the structure of transaction data row of writable data table
|
||||
type WritableTransactionDataRow struct {
|
||||
dataTable *WritableTransactionDataTable
|
||||
rowData map[TransactionDataTableColumn]string
|
||||
rowDataValid bool
|
||||
}
|
||||
|
||||
// WritableTransactionDataRowIterator defines the structure of transaction data row iterator of writable data table
|
||||
type WritableTransactionDataRowIterator struct {
|
||||
dataTable *WritableTransactionDataTable
|
||||
nextIndex int
|
||||
}
|
||||
|
||||
// Add appends a new record to data table
|
||||
func (t *WritableTransactionDataTable) Add(data map[TransactionDataTableColumn]string) {
|
||||
finalData := make(map[TransactionDataTableColumn]string, len(data))
|
||||
|
||||
for column, value := range data {
|
||||
_, exists := t.supportedColumns[column]
|
||||
|
||||
if exists {
|
||||
finalData[column] = value
|
||||
}
|
||||
}
|
||||
|
||||
t.allData = append(t.allData, finalData)
|
||||
}
|
||||
|
||||
// Get returns the record in the specified index
|
||||
func (t *WritableTransactionDataTable) Get(index int) (*WritableTransactionDataRow, error) {
|
||||
if index >= len(t.allData) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
rowData := t.allData[index]
|
||||
rowDataValid := true
|
||||
|
||||
if t.rowParser != nil {
|
||||
var err error
|
||||
rowData, rowDataValid, err = t.rowParser.Parse(rowData)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &WritableTransactionDataRow{
|
||||
dataTable: t,
|
||||
rowData: rowData,
|
||||
rowDataValid: rowDataValid,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// HasColumn returns whether the data table has specified column
|
||||
func (t *WritableTransactionDataTable) HasColumn(column TransactionDataTableColumn) bool {
|
||||
_, exists := t.supportedColumns[column]
|
||||
|
||||
if exists {
|
||||
return exists
|
||||
}
|
||||
|
||||
if t.addedColumns != nil {
|
||||
_, exists = t.addedColumns[column]
|
||||
|
||||
if exists {
|
||||
return exists
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// TransactionRowCount returns the total count of transaction data row
|
||||
func (t *WritableTransactionDataTable) TransactionRowCount() int {
|
||||
return len(t.allData)
|
||||
}
|
||||
|
||||
// TransactionRowIterator returns the iterator of transaction data row
|
||||
func (t *WritableTransactionDataTable) TransactionRowIterator() TransactionDataRowIterator {
|
||||
return &WritableTransactionDataRowIterator{
|
||||
dataTable: t,
|
||||
nextIndex: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// ColumnCount returns the total count of column in this data row
|
||||
func (r *WritableTransactionDataRow) ColumnCount() int {
|
||||
if !r.rowDataValid {
|
||||
return 0
|
||||
}
|
||||
|
||||
columnCount := 0
|
||||
|
||||
for column := range r.rowData {
|
||||
if r.dataTable.supportedColumns[column] || r.dataTable.addedColumns[column] {
|
||||
columnCount++
|
||||
}
|
||||
}
|
||||
|
||||
return columnCount
|
||||
}
|
||||
|
||||
// IsValid returns whether this row is valid data for importing
|
||||
func (r *WritableTransactionDataRow) IsValid() bool {
|
||||
return r.rowDataValid
|
||||
}
|
||||
|
||||
// GetData returns the data in the specified column type
|
||||
func (r *WritableTransactionDataRow) GetData(column TransactionDataTableColumn) string {
|
||||
if !r.rowDataValid {
|
||||
return ""
|
||||
}
|
||||
|
||||
_, exists := r.dataTable.supportedColumns[column]
|
||||
|
||||
if exists {
|
||||
return r.rowData[column]
|
||||
}
|
||||
|
||||
if r.dataTable.addedColumns != nil {
|
||||
_, exists = r.dataTable.addedColumns[column]
|
||||
|
||||
if exists {
|
||||
return r.rowData[column]
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// HasNext returns whether the iterator does not reach the end
|
||||
func (t *WritableTransactionDataRowIterator) HasNext() bool {
|
||||
return t.nextIndex < len(t.dataTable.allData)
|
||||
}
|
||||
|
||||
// Next returns the next transaction data row
|
||||
func (t *WritableTransactionDataRowIterator) Next(ctx core.Context, user *models.User) (daraRow TransactionDataRow, err error) {
|
||||
if t.nextIndex >= len(t.dataTable.allData) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
rowData := t.dataTable.allData[t.nextIndex]
|
||||
rowDataValid := true
|
||||
|
||||
if t.dataTable.rowParser != nil {
|
||||
rowData, rowDataValid, err = t.dataTable.rowParser.Parse(rowData)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(ctx, "[writable_transaction_data_table.Next] cannot parse data row, because %s", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
t.nextIndex++
|
||||
|
||||
return &WritableTransactionDataRow{
|
||||
dataTable: t.dataTable,
|
||||
rowData: rowData,
|
||||
rowDataValid: rowDataValid,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CreateNewWritableTransactionDataTable returns a new writable transaction data table according to the specified columns
|
||||
func CreateNewWritableTransactionDataTable(columns []TransactionDataTableColumn) *WritableTransactionDataTable {
|
||||
return CreateNewWritableTransactionDataTableWithRowParser(columns, nil)
|
||||
}
|
||||
|
||||
// CreateNewWritableTransactionDataTableWithRowParser returns a new writable transaction data table according to the specified columns
|
||||
func CreateNewWritableTransactionDataTableWithRowParser(columns []TransactionDataTableColumn, rowParser TransactionDataRowParser) *WritableTransactionDataTable {
|
||||
supportedColumns := make(map[TransactionDataTableColumn]bool, len(columns))
|
||||
|
||||
for i := 0; i < len(columns); i++ {
|
||||
column := columns[i]
|
||||
supportedColumns[column] = true
|
||||
}
|
||||
|
||||
var addedColumns map[TransactionDataTableColumn]bool
|
||||
|
||||
if rowParser != nil {
|
||||
addedColumnsByParser := rowParser.GetAddedColumns()
|
||||
addedColumns = make(map[TransactionDataTableColumn]bool, len(addedColumnsByParser))
|
||||
|
||||
for i := 0; i < len(addedColumnsByParser); i++ {
|
||||
addedColumns[addedColumnsByParser[i]] = true
|
||||
}
|
||||
}
|
||||
|
||||
return &WritableTransactionDataTable{
|
||||
allData: make([]map[TransactionDataTableColumn]string, 0),
|
||||
supportedColumns: supportedColumns,
|
||||
rowParser: rowParser,
|
||||
addedColumns: addedColumns,
|
||||
}
|
||||
}
|
||||
@@ -1,380 +0,0 @@
|
||||
package datatable
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||
)
|
||||
|
||||
// testDataRowParser defines the structure of test transaction data row parser
|
||||
type testDataRowParser struct {
|
||||
}
|
||||
|
||||
// GetAddedColumns returns the added columns after converting the data row
|
||||
func (p *testDataRowParser) GetAddedColumns() []TransactionDataTableColumn {
|
||||
return []TransactionDataTableColumn{
|
||||
TRANSACTION_DATA_TABLE_DESCRIPTION,
|
||||
}
|
||||
}
|
||||
|
||||
// Parse returns the converted transaction data row
|
||||
func (p *testDataRowParser) Parse(data map[TransactionDataTableColumn]string) (rowData map[TransactionDataTableColumn]string, rowDataValid bool, err error) {
|
||||
rowData = make(map[TransactionDataTableColumn]string, len(data))
|
||||
|
||||
for column, value := range data {
|
||||
rowData[column] = value
|
||||
}
|
||||
|
||||
if _, exists := rowData[TRANSACTION_DATA_TABLE_SUB_CATEGORY]; exists {
|
||||
rowData[TRANSACTION_DATA_TABLE_SUB_CATEGORY] = "foo"
|
||||
} else {
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
rowData[TRANSACTION_DATA_TABLE_TAGS] = "test"
|
||||
rowData[TRANSACTION_DATA_TABLE_DESCRIPTION] = "bar"
|
||||
|
||||
return rowData, true, nil
|
||||
}
|
||||
|
||||
func TestWritableDataTableCreate(t *testing.T) {
|
||||
columns := make([]TransactionDataTableColumn, 5)
|
||||
columns[0] = TRANSACTION_DATA_TABLE_TRANSACTION_TIME
|
||||
columns[1] = TRANSACTION_DATA_TABLE_TRANSACTION_TYPE
|
||||
columns[2] = TRANSACTION_DATA_TABLE_SUB_CATEGORY
|
||||
columns[3] = TRANSACTION_DATA_TABLE_ACCOUNT_NAME
|
||||
columns[4] = TRANSACTION_DATA_TABLE_AMOUNT
|
||||
|
||||
writableDataTable := CreateNewWritableTransactionDataTable(columns)
|
||||
|
||||
assert.Equal(t, 0, writableDataTable.TransactionRowCount())
|
||||
assert.True(t, writableDataTable.HasColumn(TRANSACTION_DATA_TABLE_TRANSACTION_TIME))
|
||||
assert.True(t, writableDataTable.HasColumn(TRANSACTION_DATA_TABLE_TRANSACTION_TYPE))
|
||||
assert.True(t, writableDataTable.HasColumn(TRANSACTION_DATA_TABLE_SUB_CATEGORY))
|
||||
assert.True(t, writableDataTable.HasColumn(TRANSACTION_DATA_TABLE_ACCOUNT_NAME))
|
||||
assert.True(t, writableDataTable.HasColumn(TRANSACTION_DATA_TABLE_AMOUNT))
|
||||
assert.False(t, writableDataTable.HasColumn(TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE))
|
||||
assert.False(t, writableDataTable.HasColumn(TRANSACTION_DATA_TABLE_ACCOUNT_CURRENCY))
|
||||
}
|
||||
|
||||
func TestWritableDataTableAdd(t *testing.T) {
|
||||
columns := make([]TransactionDataTableColumn, 5)
|
||||
columns[0] = TRANSACTION_DATA_TABLE_TRANSACTION_TIME
|
||||
columns[1] = TRANSACTION_DATA_TABLE_TRANSACTION_TYPE
|
||||
columns[2] = TRANSACTION_DATA_TABLE_SUB_CATEGORY
|
||||
columns[3] = TRANSACTION_DATA_TABLE_ACCOUNT_NAME
|
||||
columns[4] = TRANSACTION_DATA_TABLE_AMOUNT
|
||||
|
||||
writableDataTable := CreateNewWritableTransactionDataTable(columns)
|
||||
|
||||
assert.Equal(t, 0, writableDataTable.TransactionRowCount())
|
||||
|
||||
expectedTransactionTime := "2024-09-01 01:23:45"
|
||||
expectedTransactionType := "Expense"
|
||||
expectedSubCategory := "Test Category"
|
||||
expectedAccountName := "Test Account"
|
||||
expectedAmount := "123.45"
|
||||
|
||||
writableDataTable.Add(map[TransactionDataTableColumn]string{
|
||||
TRANSACTION_DATA_TABLE_TRANSACTION_TIME: expectedTransactionTime,
|
||||
TRANSACTION_DATA_TABLE_TRANSACTION_TYPE: expectedTransactionType,
|
||||
TRANSACTION_DATA_TABLE_SUB_CATEGORY: expectedSubCategory,
|
||||
TRANSACTION_DATA_TABLE_ACCOUNT_NAME: expectedAccountName,
|
||||
TRANSACTION_DATA_TABLE_AMOUNT: expectedAmount,
|
||||
})
|
||||
assert.Equal(t, 1, writableDataTable.TransactionRowCount())
|
||||
|
||||
dataRow, err := writableDataTable.Get(0)
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.True(t, dataRow.IsValid())
|
||||
|
||||
actualTransactionTime := dataRow.GetData(TRANSACTION_DATA_TABLE_TRANSACTION_TIME)
|
||||
assert.Equal(t, expectedTransactionTime, actualTransactionTime)
|
||||
|
||||
actualTransactionType := dataRow.GetData(TRANSACTION_DATA_TABLE_TRANSACTION_TYPE)
|
||||
assert.Equal(t, expectedTransactionType, actualTransactionType)
|
||||
|
||||
actualSubCategory := dataRow.GetData(TRANSACTION_DATA_TABLE_SUB_CATEGORY)
|
||||
assert.Equal(t, expectedSubCategory, actualSubCategory)
|
||||
|
||||
actualAccountName := dataRow.GetData(TRANSACTION_DATA_TABLE_ACCOUNT_NAME)
|
||||
assert.Equal(t, expectedAccountName, actualAccountName)
|
||||
|
||||
actualAmount := dataRow.GetData(TRANSACTION_DATA_TABLE_AMOUNT)
|
||||
assert.Equal(t, expectedAmount, actualAmount)
|
||||
}
|
||||
|
||||
func TestWritableDataTableAdd_NotExistsColumn(t *testing.T) {
|
||||
columns := make([]TransactionDataTableColumn, 1)
|
||||
columns[0] = TRANSACTION_DATA_TABLE_TRANSACTION_TIME
|
||||
|
||||
writableDataTable := CreateNewWritableTransactionDataTable(columns)
|
||||
|
||||
expectedTransactionUnixTime := time.Now().Unix()
|
||||
expectedTextualTransactionTime := utils.FormatUnixTimeToLongDateTime(expectedTransactionUnixTime, time.Local)
|
||||
expectedTransactionType := "Expense"
|
||||
|
||||
writableDataTable.Add(map[TransactionDataTableColumn]string{
|
||||
TRANSACTION_DATA_TABLE_TRANSACTION_TIME: expectedTextualTransactionTime,
|
||||
TRANSACTION_DATA_TABLE_TRANSACTION_TYPE: expectedTransactionType,
|
||||
})
|
||||
assert.Equal(t, 1, writableDataTable.TransactionRowCount())
|
||||
|
||||
dataRow, err := writableDataTable.Get(0)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 1, dataRow.ColumnCount())
|
||||
}
|
||||
|
||||
func TestWritableDataTableGet_NotExistsRow(t *testing.T) {
|
||||
columns := make([]TransactionDataTableColumn, 1)
|
||||
columns[0] = TRANSACTION_DATA_TABLE_TRANSACTION_TIME
|
||||
|
||||
writableDataTable := CreateNewWritableTransactionDataTable(columns)
|
||||
assert.Equal(t, 0, writableDataTable.TransactionRowCount())
|
||||
|
||||
dataRow, err := writableDataTable.Get(0)
|
||||
assert.Nil(t, err)
|
||||
assert.Nil(t, dataRow)
|
||||
}
|
||||
|
||||
func TestWritableDataRowGetData_NotExistsColumn(t *testing.T) {
|
||||
columns := make([]TransactionDataTableColumn, 1)
|
||||
columns[0] = TRANSACTION_DATA_TABLE_TRANSACTION_TIME
|
||||
|
||||
writableDataTable := CreateNewWritableTransactionDataTable(columns)
|
||||
|
||||
expectedTransactionUnixTime := time.Now().Unix()
|
||||
expectedTextualTransactionTime := utils.FormatUnixTimeToLongDateTime(expectedTransactionUnixTime, time.Local)
|
||||
|
||||
writableDataTable.Add(map[TransactionDataTableColumn]string{
|
||||
TRANSACTION_DATA_TABLE_TRANSACTION_TIME: expectedTextualTransactionTime,
|
||||
})
|
||||
assert.Equal(t, 1, writableDataTable.TransactionRowCount())
|
||||
|
||||
dataRow, err := writableDataTable.Get(0)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 1, dataRow.ColumnCount())
|
||||
assert.Equal(t, "", dataRow.GetData(TRANSACTION_DATA_TABLE_TRANSACTION_TYPE))
|
||||
}
|
||||
|
||||
func TestWritableDataTableDataRowIterator(t *testing.T) {
|
||||
columns := make([]TransactionDataTableColumn, 5)
|
||||
columns[0] = TRANSACTION_DATA_TABLE_TRANSACTION_TIME
|
||||
columns[1] = TRANSACTION_DATA_TABLE_TRANSACTION_TYPE
|
||||
columns[2] = TRANSACTION_DATA_TABLE_SUB_CATEGORY
|
||||
columns[3] = TRANSACTION_DATA_TABLE_ACCOUNT_NAME
|
||||
columns[4] = TRANSACTION_DATA_TABLE_AMOUNT
|
||||
|
||||
writableDataTable := CreateNewWritableTransactionDataTable(columns)
|
||||
assert.Equal(t, 0, writableDataTable.TransactionRowCount())
|
||||
|
||||
expectedTransactionUnixTimes := make([]int64, 3)
|
||||
expectedTransactionTimes := make([]string, 3)
|
||||
expectedTransactionTypes := make([]string, 3)
|
||||
expectedSubCategories := make([]string, 3)
|
||||
expectedAccountNames := make([]string, 3)
|
||||
expectedAmounts := make([]string, 3)
|
||||
|
||||
expectedTransactionUnixTimes[0] = time.Now().Add(-5 * time.Hour).Unix()
|
||||
expectedTransactionTimes[0] = utils.FormatUnixTimeToLongDateTime(expectedTransactionUnixTimes[0], time.Local)
|
||||
expectedTransactionTypes[0] = "Balance Modification"
|
||||
expectedSubCategories[0] = ""
|
||||
expectedAccountNames[0] = "Test Account"
|
||||
expectedAmounts[0] = "123.45"
|
||||
writableDataTable.Add(map[TransactionDataTableColumn]string{
|
||||
TRANSACTION_DATA_TABLE_TRANSACTION_TIME: expectedTransactionTimes[0],
|
||||
TRANSACTION_DATA_TABLE_TRANSACTION_TYPE: expectedTransactionTypes[0],
|
||||
TRANSACTION_DATA_TABLE_SUB_CATEGORY: expectedSubCategories[0],
|
||||
TRANSACTION_DATA_TABLE_ACCOUNT_NAME: expectedAccountNames[0],
|
||||
TRANSACTION_DATA_TABLE_AMOUNT: expectedAmounts[0],
|
||||
})
|
||||
|
||||
expectedTransactionUnixTimes[1] = time.Now().Add(-45 * time.Minute).Unix()
|
||||
expectedTransactionTimes[1] = utils.FormatUnixTimeToLongDateTime(expectedTransactionUnixTimes[1], time.Local)
|
||||
expectedTransactionTypes[1] = "Expense"
|
||||
expectedSubCategories[1] = "Test Category2"
|
||||
expectedAccountNames[1] = "Test Account"
|
||||
expectedAmounts[1] = "-23.4"
|
||||
writableDataTable.Add(map[TransactionDataTableColumn]string{
|
||||
TRANSACTION_DATA_TABLE_TRANSACTION_TIME: expectedTransactionTimes[1],
|
||||
TRANSACTION_DATA_TABLE_TRANSACTION_TYPE: expectedTransactionTypes[1],
|
||||
TRANSACTION_DATA_TABLE_SUB_CATEGORY: expectedSubCategories[1],
|
||||
TRANSACTION_DATA_TABLE_ACCOUNT_NAME: expectedAccountNames[1],
|
||||
TRANSACTION_DATA_TABLE_AMOUNT: expectedAmounts[1],
|
||||
})
|
||||
|
||||
expectedTransactionUnixTimes[2] = time.Now().Unix()
|
||||
expectedTransactionTimes[2] = utils.FormatUnixTimeToLongDateTime(expectedTransactionUnixTimes[2], time.Local)
|
||||
expectedTransactionTypes[2] = "Income"
|
||||
expectedSubCategories[2] = "Test Category3"
|
||||
expectedAccountNames[2] = "Test Account2"
|
||||
expectedAmounts[2] = "123"
|
||||
writableDataTable.Add(map[TransactionDataTableColumn]string{
|
||||
TRANSACTION_DATA_TABLE_TRANSACTION_TIME: expectedTransactionTimes[2],
|
||||
TRANSACTION_DATA_TABLE_TRANSACTION_TYPE: expectedTransactionTypes[2],
|
||||
TRANSACTION_DATA_TABLE_SUB_CATEGORY: expectedSubCategories[2],
|
||||
TRANSACTION_DATA_TABLE_ACCOUNT_NAME: expectedAccountNames[2],
|
||||
TRANSACTION_DATA_TABLE_AMOUNT: expectedAmounts[2],
|
||||
})
|
||||
assert.Equal(t, 3, writableDataTable.TransactionRowCount())
|
||||
|
||||
index := 0
|
||||
iterator := writableDataTable.TransactionRowIterator()
|
||||
|
||||
for iterator.HasNext() {
|
||||
dataRow, err := iterator.Next(core.NewNullContext(), &models.User{})
|
||||
assert.Nil(t, err)
|
||||
|
||||
actualTransactionTime := dataRow.GetData(TRANSACTION_DATA_TABLE_TRANSACTION_TIME)
|
||||
assert.Equal(t, expectedTransactionTimes[index], actualTransactionTime)
|
||||
|
||||
actualTransactionType := dataRow.GetData(TRANSACTION_DATA_TABLE_TRANSACTION_TYPE)
|
||||
assert.Equal(t, expectedTransactionTypes[index], actualTransactionType)
|
||||
|
||||
actualSubCategory := dataRow.GetData(TRANSACTION_DATA_TABLE_SUB_CATEGORY)
|
||||
assert.Equal(t, expectedSubCategories[index], actualSubCategory)
|
||||
|
||||
actualAccountName := dataRow.GetData(TRANSACTION_DATA_TABLE_ACCOUNT_NAME)
|
||||
assert.Equal(t, expectedAccountNames[index], actualAccountName)
|
||||
|
||||
actualAmount := dataRow.GetData(TRANSACTION_DATA_TABLE_AMOUNT)
|
||||
assert.Equal(t, expectedAmounts[index], actualAmount)
|
||||
|
||||
index++
|
||||
}
|
||||
|
||||
assert.Equal(t, 3, index)
|
||||
}
|
||||
|
||||
func TestWritableDataTableWithRowParser(t *testing.T) {
|
||||
columns := make([]TransactionDataTableColumn, 5)
|
||||
columns[0] = TRANSACTION_DATA_TABLE_TRANSACTION_TIME
|
||||
columns[1] = TRANSACTION_DATA_TABLE_TRANSACTION_TYPE
|
||||
columns[2] = TRANSACTION_DATA_TABLE_SUB_CATEGORY
|
||||
columns[3] = TRANSACTION_DATA_TABLE_ACCOUNT_NAME
|
||||
columns[4] = TRANSACTION_DATA_TABLE_AMOUNT
|
||||
|
||||
writableDataTable := CreateNewWritableTransactionDataTableWithRowParser(columns, &testDataRowParser{})
|
||||
|
||||
assert.True(t, writableDataTable.HasColumn(TRANSACTION_DATA_TABLE_DESCRIPTION))
|
||||
assert.False(t, writableDataTable.HasColumn(TRANSACTION_DATA_TABLE_TAGS))
|
||||
assert.Equal(t, 0, writableDataTable.TransactionRowCount())
|
||||
|
||||
writableDataTable.Add(map[TransactionDataTableColumn]string{
|
||||
TRANSACTION_DATA_TABLE_TRANSACTION_TIME: "2024-09-01 01:23:45",
|
||||
TRANSACTION_DATA_TABLE_TRANSACTION_TYPE: "Expense",
|
||||
TRANSACTION_DATA_TABLE_SUB_CATEGORY: "Test Category",
|
||||
TRANSACTION_DATA_TABLE_ACCOUNT_NAME: "Test Account",
|
||||
TRANSACTION_DATA_TABLE_AMOUNT: "123.45",
|
||||
})
|
||||
assert.Equal(t, 1, writableDataTable.TransactionRowCount())
|
||||
|
||||
// first row
|
||||
dataRow, err := writableDataTable.Get(0)
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, dataRow.IsValid())
|
||||
assert.Equal(t, 6, dataRow.ColumnCount())
|
||||
|
||||
actualSubCategory := dataRow.GetData(TRANSACTION_DATA_TABLE_SUB_CATEGORY)
|
||||
assert.Equal(t, "foo", actualSubCategory)
|
||||
|
||||
actualTags := dataRow.GetData(TRANSACTION_DATA_TABLE_TAGS)
|
||||
assert.Equal(t, "", actualTags)
|
||||
|
||||
actualDescription := dataRow.GetData(TRANSACTION_DATA_TABLE_DESCRIPTION)
|
||||
assert.Equal(t, "bar", actualDescription)
|
||||
|
||||
writableDataTable.Add(map[TransactionDataTableColumn]string{
|
||||
TRANSACTION_DATA_TABLE_TRANSACTION_TIME: "2024-09-01 12:34:56",
|
||||
TRANSACTION_DATA_TABLE_TRANSACTION_TYPE: "Income",
|
||||
TRANSACTION_DATA_TABLE_ACCOUNT_NAME: "Test Account2",
|
||||
TRANSACTION_DATA_TABLE_AMOUNT: "0.12",
|
||||
})
|
||||
assert.Equal(t, 2, writableDataTable.TransactionRowCount())
|
||||
|
||||
// second row
|
||||
dataRow, err = writableDataTable.Get(1)
|
||||
assert.Nil(t, err)
|
||||
assert.False(t, dataRow.IsValid())
|
||||
assert.Equal(t, 0, dataRow.ColumnCount())
|
||||
|
||||
actualSubCategory = dataRow.GetData(TRANSACTION_DATA_TABLE_SUB_CATEGORY)
|
||||
assert.Equal(t, "", actualSubCategory)
|
||||
|
||||
actualTags = dataRow.GetData(TRANSACTION_DATA_TABLE_TAGS)
|
||||
assert.Equal(t, "", actualTags)
|
||||
|
||||
actualDescription = dataRow.GetData(TRANSACTION_DATA_TABLE_DESCRIPTION)
|
||||
assert.Equal(t, "", actualDescription)
|
||||
}
|
||||
|
||||
func TestWritableDataTableDataRowIteratorWithRowParser(t *testing.T) {
|
||||
columns := make([]TransactionDataTableColumn, 5)
|
||||
columns[0] = TRANSACTION_DATA_TABLE_TRANSACTION_TIME
|
||||
columns[1] = TRANSACTION_DATA_TABLE_TRANSACTION_TYPE
|
||||
columns[2] = TRANSACTION_DATA_TABLE_SUB_CATEGORY
|
||||
columns[3] = TRANSACTION_DATA_TABLE_ACCOUNT_NAME
|
||||
columns[4] = TRANSACTION_DATA_TABLE_AMOUNT
|
||||
|
||||
writableDataTable := CreateNewWritableTransactionDataTableWithRowParser(columns, &testDataRowParser{})
|
||||
|
||||
assert.True(t, writableDataTable.HasColumn(TRANSACTION_DATA_TABLE_DESCRIPTION))
|
||||
assert.False(t, writableDataTable.HasColumn(TRANSACTION_DATA_TABLE_TAGS))
|
||||
assert.Equal(t, 0, writableDataTable.TransactionRowCount())
|
||||
|
||||
writableDataTable.Add(map[TransactionDataTableColumn]string{
|
||||
TRANSACTION_DATA_TABLE_TRANSACTION_TIME: "2024-09-01 01:23:45",
|
||||
TRANSACTION_DATA_TABLE_TRANSACTION_TYPE: "Expense",
|
||||
TRANSACTION_DATA_TABLE_SUB_CATEGORY: "Test Category",
|
||||
TRANSACTION_DATA_TABLE_ACCOUNT_NAME: "Test Account",
|
||||
TRANSACTION_DATA_TABLE_AMOUNT: "123.45",
|
||||
})
|
||||
|
||||
writableDataTable.Add(map[TransactionDataTableColumn]string{
|
||||
TRANSACTION_DATA_TABLE_TRANSACTION_TIME: "2024-09-01 12:34:56",
|
||||
TRANSACTION_DATA_TABLE_TRANSACTION_TYPE: "Income",
|
||||
TRANSACTION_DATA_TABLE_ACCOUNT_NAME: "Test Account2",
|
||||
TRANSACTION_DATA_TABLE_AMOUNT: "0.12",
|
||||
})
|
||||
|
||||
iterator := writableDataTable.TransactionRowIterator()
|
||||
assert.True(t, iterator.HasNext())
|
||||
|
||||
// first row
|
||||
dataRow, err := iterator.Next(core.NewNullContext(), &models.User{})
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, dataRow.IsValid())
|
||||
|
||||
actualSubCategory := dataRow.GetData(TRANSACTION_DATA_TABLE_SUB_CATEGORY)
|
||||
assert.Equal(t, "foo", actualSubCategory)
|
||||
|
||||
actualTags := dataRow.GetData(TRANSACTION_DATA_TABLE_TAGS)
|
||||
assert.Equal(t, "", actualTags)
|
||||
|
||||
actualDescription := dataRow.GetData(TRANSACTION_DATA_TABLE_DESCRIPTION)
|
||||
assert.Equal(t, "bar", actualDescription)
|
||||
|
||||
assert.True(t, iterator.HasNext())
|
||||
|
||||
// second row
|
||||
dataRow, err = iterator.Next(core.NewNullContext(), &models.User{})
|
||||
assert.Nil(t, err)
|
||||
assert.False(t, dataRow.IsValid())
|
||||
|
||||
actualSubCategory = dataRow.GetData(TRANSACTION_DATA_TABLE_SUB_CATEGORY)
|
||||
assert.Equal(t, "", actualSubCategory)
|
||||
|
||||
actualTags = dataRow.GetData(TRANSACTION_DATA_TABLE_TAGS)
|
||||
assert.Equal(t, "", actualTags)
|
||||
|
||||
actualDescription = dataRow.GetData(TRANSACTION_DATA_TABLE_DESCRIPTION)
|
||||
assert.Equal(t, "", actualDescription)
|
||||
|
||||
assert.False(t, iterator.HasNext())
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package _default
|
||||
|
||||
// defaultTransactionDataCSVFileConverter defines the structure of ezbookkeeping default csv file converter
|
||||
type defaultTransactionDataCSVFileConverter struct {
|
||||
defaultTransactionDataPlainTextConverter
|
||||
}
|
||||
|
||||
// Initialize an ezbookkeeping default transaction data csv file converter singleton instance
|
||||
var (
|
||||
DefaultTransactionDataCSVFileConverter = &defaultTransactionDataCSVFileConverter{
|
||||
defaultTransactionDataPlainTextConverter{
|
||||
columnSeparator: ",",
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -1,105 +0,0 @@
|
||||
package _default
|
||||
|
||||
import (
|
||||
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||
)
|
||||
|
||||
// defaultTransactionDataPlainTextConverter defines the structure of ezbookkeeping default plain text converter for transaction data
|
||||
type defaultTransactionDataPlainTextConverter struct {
|
||||
columnSeparator string
|
||||
}
|
||||
|
||||
const ezbookkeepingLineSeparator = "\n"
|
||||
const ezbookkeepingGeoLocationSeparator = " "
|
||||
const ezbookkeepingTagSeparator = ";"
|
||||
|
||||
var ezbookkeepingDataColumnNameMapping = map[datatable.TransactionDataTableColumn]string{
|
||||
datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME: "Time",
|
||||
datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE: "Timezone",
|
||||
datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE: "Type",
|
||||
datatable.TRANSACTION_DATA_TABLE_CATEGORY: "Category",
|
||||
datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY: "Sub Category",
|
||||
datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME: "Account",
|
||||
datatable.TRANSACTION_DATA_TABLE_ACCOUNT_CURRENCY: "Account Currency",
|
||||
datatable.TRANSACTION_DATA_TABLE_AMOUNT: "Amount",
|
||||
datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME: "Account2",
|
||||
datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_CURRENCY: "Account2 Currency",
|
||||
datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT: "Account2 Amount",
|
||||
datatable.TRANSACTION_DATA_TABLE_GEOGRAPHIC_LOCATION: "Geographic Location",
|
||||
datatable.TRANSACTION_DATA_TABLE_TAGS: "Tags",
|
||||
datatable.TRANSACTION_DATA_TABLE_DESCRIPTION: "Description",
|
||||
}
|
||||
|
||||
var ezbookkeepingTransactionTypeNameMapping = map[models.TransactionType]string{
|
||||
models.TRANSACTION_TYPE_MODIFY_BALANCE: "Balance Modification",
|
||||
models.TRANSACTION_TYPE_INCOME: "Income",
|
||||
models.TRANSACTION_TYPE_EXPENSE: "Expense",
|
||||
models.TRANSACTION_TYPE_TRANSFER: "Transfer",
|
||||
}
|
||||
|
||||
var ezbookkeepingDataColumns = []datatable.TransactionDataTableColumn{
|
||||
datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME,
|
||||
datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE,
|
||||
datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE,
|
||||
datatable.TRANSACTION_DATA_TABLE_CATEGORY,
|
||||
datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY,
|
||||
datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME,
|
||||
datatable.TRANSACTION_DATA_TABLE_ACCOUNT_CURRENCY,
|
||||
datatable.TRANSACTION_DATA_TABLE_AMOUNT,
|
||||
datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME,
|
||||
datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_CURRENCY,
|
||||
datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT,
|
||||
datatable.TRANSACTION_DATA_TABLE_GEOGRAPHIC_LOCATION,
|
||||
datatable.TRANSACTION_DATA_TABLE_TAGS,
|
||||
datatable.TRANSACTION_DATA_TABLE_DESCRIPTION,
|
||||
}
|
||||
|
||||
// ToExportedContent returns the exported transaction plain text data
|
||||
func (c *defaultTransactionDataPlainTextConverter) ToExportedContent(ctx core.Context, uid int64, transactions []*models.Transaction, accountMap map[int64]*models.Account, categoryMap map[int64]*models.TransactionCategory, tagMap map[int64]*models.TransactionTag, allTagIndexes map[int64][]int64) ([]byte, error) {
|
||||
dataTableBuilder := createNewDefaultTransactionPlainTextDataTableBuilder(
|
||||
len(transactions),
|
||||
ezbookkeepingDataColumns,
|
||||
ezbookkeepingDataColumnNameMapping,
|
||||
c.columnSeparator,
|
||||
ezbookkeepingLineSeparator,
|
||||
)
|
||||
|
||||
dataTableExporter := datatable.CreateNewExporter(
|
||||
ezbookkeepingTransactionTypeNameMapping,
|
||||
ezbookkeepingGeoLocationSeparator,
|
||||
ezbookkeepingTagSeparator,
|
||||
)
|
||||
|
||||
err := dataTableExporter.BuildExportedContent(ctx, dataTableBuilder, uid, transactions, accountMap, categoryMap, tagMap, allTagIndexes)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return []byte(dataTableBuilder.String()), nil
|
||||
}
|
||||
|
||||
// ParseImportedData returns the imported data by parsing the transaction plain text data
|
||||
func (c *defaultTransactionDataPlainTextConverter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezoneOffset int16, accountMap map[string]*models.Account, expenseCategoryMap map[string]*models.TransactionCategory, incomeCategoryMap map[string]*models.TransactionCategory, transferCategoryMap 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,
|
||||
ezbookkeepingLineSeparator,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
transactionDataTable := datatable.CreateNewImportedTransactionDataTable(dataTable, ezbookkeepingDataColumnNameMapping)
|
||||
|
||||
dataTableImporter := datatable.CreateNewImporter(
|
||||
ezbookkeepingTransactionTypeNameMapping,
|
||||
ezbookkeepingGeoLocationSeparator,
|
||||
ezbookkeepingTagSeparator,
|
||||
)
|
||||
|
||||
return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezoneOffset, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
|
||||
}
|
||||
@@ -1,520 +0,0 @@
|
||||
package _default
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"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 TestDefaultTransactionDataCSVFileConverterToExportedContent(t *testing.T) {
|
||||
converter := DefaultTransactionDataCSVFileConverter
|
||||
context := core.NewNullContext()
|
||||
|
||||
transactions := make([]*models.Transaction, 3)
|
||||
transactions[0] = &models.Transaction{
|
||||
TransactionId: 1,
|
||||
TransactionTime: 1725165296000,
|
||||
Type: models.TRANSACTION_DB_TYPE_INCOME,
|
||||
TimezoneUtcOffset: 480,
|
||||
CategoryId: 2,
|
||||
AccountId: 1,
|
||||
Amount: 12345,
|
||||
GeoLongitude: 123.45,
|
||||
GeoLatitude: 45.67,
|
||||
Comment: "Hello,World",
|
||||
}
|
||||
transactions[1] = &models.Transaction{
|
||||
TransactionId: 2,
|
||||
TransactionTime: 1725194096000,
|
||||
Type: models.TRANSACTION_DB_TYPE_EXPENSE,
|
||||
TimezoneUtcOffset: 0,
|
||||
CategoryId: 4,
|
||||
AccountId: 1,
|
||||
Amount: -10,
|
||||
GeoLongitude: 0,
|
||||
GeoLatitude: 0,
|
||||
Comment: "Foo#Bar",
|
||||
}
|
||||
transactions[2] = &models.Transaction{
|
||||
TransactionId: 3,
|
||||
TransactionTime: 1725212096000,
|
||||
Type: models.TRANSACTION_DB_TYPE_TRANSFER_OUT,
|
||||
TimezoneUtcOffset: -300,
|
||||
CategoryId: 6,
|
||||
AccountId: 1,
|
||||
Amount: 12345,
|
||||
RelatedAccountId: 2,
|
||||
RelatedAccountAmount: 1735,
|
||||
Comment: "T\te\rs\nt\r\ntest",
|
||||
}
|
||||
|
||||
accountMap := make(map[int64]*models.Account, 2)
|
||||
accountMap[1] = &models.Account{
|
||||
AccountId: 1,
|
||||
Name: "Test Account",
|
||||
Currency: "CNY",
|
||||
}
|
||||
accountMap[2] = &models.Account{
|
||||
AccountId: 2,
|
||||
Name: "Test Account2",
|
||||
Currency: "USD",
|
||||
}
|
||||
|
||||
categoryMap := make(map[int64]*models.TransactionCategory, 6)
|
||||
categoryMap[1] = &models.TransactionCategory{
|
||||
CategoryId: 1,
|
||||
Type: models.CATEGORY_TYPE_INCOME,
|
||||
Name: "Test Category",
|
||||
}
|
||||
categoryMap[2] = &models.TransactionCategory{
|
||||
CategoryId: 2,
|
||||
Type: models.CATEGORY_TYPE_INCOME,
|
||||
ParentCategoryId: 1,
|
||||
Name: "Test Sub Category",
|
||||
}
|
||||
categoryMap[3] = &models.TransactionCategory{
|
||||
CategoryId: 3,
|
||||
Type: models.CATEGORY_TYPE_EXPENSE,
|
||||
Name: "Test Category2",
|
||||
}
|
||||
categoryMap[4] = &models.TransactionCategory{
|
||||
CategoryId: 4,
|
||||
Type: models.CATEGORY_TYPE_EXPENSE,
|
||||
ParentCategoryId: 3,
|
||||
Name: "Test Sub Category2",
|
||||
}
|
||||
categoryMap[5] = &models.TransactionCategory{
|
||||
CategoryId: 5,
|
||||
Type: models.CATEGORY_TYPE_TRANSFER,
|
||||
Name: "Test Category3",
|
||||
}
|
||||
categoryMap[6] = &models.TransactionCategory{
|
||||
CategoryId: 6,
|
||||
Type: models.CATEGORY_TYPE_TRANSFER,
|
||||
ParentCategoryId: 5,
|
||||
Name: "Test Sub Category3",
|
||||
}
|
||||
|
||||
tagMap := make(map[int64]*models.TransactionTag, 2)
|
||||
tagMap[1] = &models.TransactionTag{
|
||||
TagId: 1,
|
||||
Name: "Test,Tag",
|
||||
}
|
||||
tagMap[2] = &models.TransactionTag{
|
||||
TagId: 2,
|
||||
Name: "Test;Tag2",
|
||||
}
|
||||
|
||||
allTagIndexes := make(map[int64][]int64, 2)
|
||||
allTagIndexes[1] = []int64{1, 2}
|
||||
allTagIndexes[2] = []int64{3, 1, 4}
|
||||
allTagIndexes[3] = []int64{2, 3}
|
||||
|
||||
expectedContent := "Time,Timezone,Type,Category,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount,Geographic Location,Tags,Description\n" +
|
||||
"2024-09-01 12:34:56,+08:00,Income,Test Category,Test Sub Category,Test Account,CNY,123.45,,,,123.450000 45.670000,Test Tag;Test Tag2,Hello World\n" +
|
||||
"2024-09-01 12:34:56,+00:00,Expense,Test Category2,Test Sub Category2,Test Account,CNY,-0.10,,,,,Test Tag,Foo#Bar\n" +
|
||||
"2024-09-01 12:34:56,-05:00,Transfer,Test Category3,Test Sub Category3,Test Account,CNY,123.45,Test Account2,USD,17.35,,Test Tag2,T\te s t test\n"
|
||||
actualContent, err := converter.ToExportedContent(context, 123, transactions, accountMap, categoryMap, tagMap, allTagIndexes)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expectedContent, string(actualContent))
|
||||
}
|
||||
|
||||
func TestDefaultTransactionDataCSVFileConverterParseImportedData_MinimumValidData(t *testing.T) {
|
||||
converter := DefaultTransactionDataCSVFileConverter
|
||||
context := core.NewNullContext()
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1234567890,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
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"), 0, nil, nil, nil, nil, nil)
|
||||
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, 4, len(allNewTransactions))
|
||||
assert.Equal(t, 2, len(allNewAccounts))
|
||||
assert.Equal(t, 1, len(allNewSubExpenseCategories))
|
||||
assert.Equal(t, 1, len(allNewSubIncomeCategories))
|
||||
assert.Equal(t, 1, len(allNewSubTransferCategories))
|
||||
assert.Equal(t, 0, len(allNewTags))
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewTransactions[0].Uid)
|
||||
assert.Equal(t, models.TRANSACTION_DB_TYPE_MODIFY_BALANCE, allNewTransactions[0].Type)
|
||||
assert.Equal(t, int64(1725148800), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
|
||||
assert.Equal(t, int64(12345), allNewTransactions[0].Amount)
|
||||
assert.Equal(t, "Test Account", allNewTransactions[0].OriginalSourceAccountName)
|
||||
assert.Equal(t, "", allNewTransactions[0].OriginalCategoryName)
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewTransactions[1].Uid)
|
||||
assert.Equal(t, models.TRANSACTION_DB_TYPE_INCOME, allNewTransactions[1].Type)
|
||||
assert.Equal(t, int64(1725153825), utils.GetUnixTimeFromTransactionTime(allNewTransactions[1].TransactionTime))
|
||||
assert.Equal(t, int64(12), allNewTransactions[1].Amount)
|
||||
assert.Equal(t, "Test Account", allNewTransactions[1].OriginalSourceAccountName)
|
||||
assert.Equal(t, "Test Category", allNewTransactions[1].OriginalCategoryName)
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewTransactions[2].Uid)
|
||||
assert.Equal(t, models.TRANSACTION_DB_TYPE_EXPENSE, allNewTransactions[2].Type)
|
||||
assert.Equal(t, int64(1725194096), utils.GetUnixTimeFromTransactionTime(allNewTransactions[2].TransactionTime))
|
||||
assert.Equal(t, int64(100), allNewTransactions[2].Amount)
|
||||
assert.Equal(t, "Test Account", allNewTransactions[2].OriginalSourceAccountName)
|
||||
assert.Equal(t, "Test Category2", allNewTransactions[2].OriginalCategoryName)
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewTransactions[3].Uid)
|
||||
assert.Equal(t, models.TRANSACTION_DB_TYPE_TRANSFER_OUT, allNewTransactions[3].Type)
|
||||
assert.Equal(t, int64(1725235199), utils.GetUnixTimeFromTransactionTime(allNewTransactions[3].TransactionTime))
|
||||
assert.Equal(t, int64(5), allNewTransactions[3].Amount)
|
||||
assert.Equal(t, "Test Account", allNewTransactions[3].OriginalSourceAccountName)
|
||||
assert.Equal(t, "Test Account2", allNewTransactions[3].OriginalDestinationAccountName)
|
||||
assert.Equal(t, "Test Category3", allNewTransactions[3].OriginalCategoryName)
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewAccounts[0].Uid)
|
||||
assert.Equal(t, "Test Account", allNewAccounts[0].Name)
|
||||
assert.Equal(t, "CNY", allNewAccounts[0].Currency)
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewAccounts[1].Uid)
|
||||
assert.Equal(t, "Test Account2", allNewAccounts[1].Name)
|
||||
assert.Equal(t, "CNY", allNewAccounts[1].Currency)
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewSubExpenseCategories[0].Uid)
|
||||
assert.Equal(t, "Test Category2", allNewSubExpenseCategories[0].Name)
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewSubIncomeCategories[0].Uid)
|
||||
assert.Equal(t, "Test Category", allNewSubIncomeCategories[0].Name)
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewSubTransferCategories[0].Uid)
|
||||
assert.Equal(t, "Test Category3", allNewSubTransferCategories[0].Name)
|
||||
}
|
||||
|
||||
func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseInvalidTime(t *testing.T) {
|
||||
converter := DefaultTransactionDataCSVFileConverter
|
||||
context := core.NewNullContext()
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1234567890,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
|
||||
"2024-09-01T12:34:56,Expense,Test Category,Test Account,123.45,,"), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
|
||||
|
||||
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
|
||||
"09/01/2024 12:34:56,Expense,Test Category,Test Account,123.45,,"), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
|
||||
}
|
||||
|
||||
func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseInvalidType(t *testing.T) {
|
||||
converter := DefaultTransactionDataCSVFileConverter
|
||||
context := core.NewNullContext()
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1234567890,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
|
||||
"2024-09-01 12:34:56,Type,Test Category,Test Account,123.45,,"), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrTransactionTypeInvalid.Message)
|
||||
}
|
||||
|
||||
func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseValidTimezone(t *testing.T) {
|
||||
converter := DefaultTransactionDataCSVFileConverter
|
||||
context := core.NewNullContext()
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1234567890,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("Time,Timezone,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
|
||||
"2024-09-01 12:34:56,-10:00,Expense,Test Category,Test Account,123.45,,"), 0, nil, nil, nil, nil, nil)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 1, len(allNewTransactions))
|
||||
assert.Equal(t, int64(1725230096), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
|
||||
|
||||
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("Time,Timezone,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
|
||||
"2024-09-01 12:34:56,+00:00,Expense,Test Category,Test Account,123.45,,"), 0, nil, nil, nil, nil, nil)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 1, len(allNewTransactions))
|
||||
assert.Equal(t, int64(1725194096), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
|
||||
|
||||
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("Time,Timezone,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
|
||||
"2024-09-01 12:34:56,+12:45,Expense,Test Category,Test Account,123.45,,"), 0, nil, nil, nil, nil, nil)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 1, len(allNewTransactions))
|
||||
assert.Equal(t, int64(1725148196), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
|
||||
}
|
||||
|
||||
func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseInvalidTimezone(t *testing.T) {
|
||||
converter := DefaultTransactionDataCSVFileConverter
|
||||
context := core.NewNullContext()
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1234567890,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("Time,Timezone,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
|
||||
"2024-09-01 12:34:56,Asia/Shanghai,Expense,Test Category,Test Account,123.45,,"), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrTransactionTimeZoneInvalid.Message)
|
||||
}
|
||||
|
||||
func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseValidAccountCurrency(t *testing.T) {
|
||||
converter := DefaultTransactionDataCSVFileConverter
|
||||
context := core.NewNullContext()
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1234567890,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
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"), 0, nil, nil, nil, nil, nil)
|
||||
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, 2, len(allNewTransactions))
|
||||
assert.Equal(t, 2, len(allNewAccounts))
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewAccounts[0].Uid)
|
||||
assert.Equal(t, "Test Account", allNewAccounts[0].Name)
|
||||
assert.Equal(t, "USD", allNewAccounts[0].Currency)
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewAccounts[1].Uid)
|
||||
assert.Equal(t, "Test Account2", allNewAccounts[1].Name)
|
||||
assert.Equal(t, "EUR", allNewAccounts[1].Currency)
|
||||
}
|
||||
|
||||
func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseInvalidAccountCurrency(t *testing.T) {
|
||||
converter := DefaultTransactionDataCSVFileConverter
|
||||
context := core.NewNullContext()
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1234567890,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount\n"+
|
||||
"2024-09-01 01:23:45,Balance Modification,,Test Account,USD,123.45,,,\n"+
|
||||
"2024-09-01 12:34:56,Transfer,Test Category3,Test Account,CNY,1.23,Test Account2,EUR,1.10"), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message)
|
||||
|
||||
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount\n"+
|
||||
"2024-09-01 01:23:45,Balance Modification,,Test Account,USD,123.45,,,\n"+
|
||||
"2024-09-01 12:34:56,Transfer,Test Category3,Test Account2,CNY,1.23,Test Account,EUR,1.10"), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message)
|
||||
}
|
||||
|
||||
func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseNotSupportedCurrency(t *testing.T) {
|
||||
converter := DefaultTransactionDataCSVFileConverter
|
||||
context := core.NewNullContext()
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1234567890,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount\n"+
|
||||
"2024-09-01 01:23:45,Balance Modification,,Test Account,XXX,123.45,,,"), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message)
|
||||
|
||||
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount\n"+
|
||||
"2024-09-01 01:23:45,Transfer,Test Category,Test Account,USD,123.45,Test Account2,XXX,123.45"), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message)
|
||||
}
|
||||
|
||||
func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseInvalidAmount(t *testing.T) {
|
||||
converter := DefaultTransactionDataCSVFileConverter
|
||||
context := core.NewNullContext()
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1234567890,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
|
||||
"2024-09-01 12:34:56,Expense,Test Category,Test Account,123 45,,"), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
|
||||
|
||||
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
|
||||
"2024-09-01 12:34:56,Transfer,Test Category,Test Account,123.45,Test Account2,123 45"), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
|
||||
}
|
||||
|
||||
func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseNoAmount2(t *testing.T) {
|
||||
converter := DefaultTransactionDataCSVFileConverter
|
||||
context := core.NewNullContext()
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1234567890,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2\n"+
|
||||
"2024-09-01 12:34:56,Expense,Test Category,Test Account,123.45,"), 0, nil, nil, nil, nil, nil)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, int64(12345), allNewTransactions[0].Amount)
|
||||
assert.Equal(t, int64(0), allNewTransactions[0].RelatedAccountAmount)
|
||||
|
||||
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2\n"+
|
||||
"2024-09-01 12:34:56,Transfer,Test Category,Test Account,123.45,Test Account2"), 0, nil, nil, nil, nil, nil)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, int64(12345), allNewTransactions[0].Amount)
|
||||
assert.Equal(t, int64(12345), allNewTransactions[0].RelatedAccountAmount)
|
||||
}
|
||||
|
||||
func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseValidGeographicLocation(t *testing.T) {
|
||||
converter := DefaultTransactionDataCSVFileConverter
|
||||
context := core.NewNullContext()
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1234567890,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount,Geographic Location\n"+
|
||||
"2024-09-01 12:34:56,Expense,Test Category,Test Account,123.45,,,123.45 45.56"), 0, nil, nil, nil, nil, nil)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 1, len(allNewTransactions))
|
||||
assert.Equal(t, 123.45, allNewTransactions[0].GeoLongitude)
|
||||
assert.Equal(t, 45.56, allNewTransactions[0].GeoLatitude)
|
||||
}
|
||||
|
||||
func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseInvalidGeographicLocation(t *testing.T) {
|
||||
converter := DefaultTransactionDataCSVFileConverter
|
||||
context := core.NewNullContext()
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1234567890,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount,Geographic Location\n"+
|
||||
"2024-09-01 12:34:56,Expense,Test Category,Test Account,123.45,,,1"), 0, nil, nil, nil, nil, nil)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 1, len(allNewTransactions))
|
||||
assert.Equal(t, float64(0), allNewTransactions[0].GeoLongitude)
|
||||
assert.Equal(t, float64(0), allNewTransactions[0].GeoLatitude)
|
||||
|
||||
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount,Geographic Location\n"+
|
||||
"2024-09-01 12:34:56,Expense,Test Category,Test Account,123.45,,,a b"), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrGeographicLocationInvalid.Message)
|
||||
|
||||
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount,Geographic Location\n"+
|
||||
"2024-09-01 12:34:56,Expense,Test Category,Test Account,123.45,,,1 "), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrGeographicLocationInvalid.Message)
|
||||
}
|
||||
|
||||
func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseTag(t *testing.T) {
|
||||
converter := DefaultTransactionDataCSVFileConverter
|
||||
context := core.NewNullContext()
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1234567890,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
_, _, _, _, _, allNewTags, err := converter.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount,Tags\n"+
|
||||
"2024-09-01 00:00:00,Balance Modification,,Test Account,123.45,,,foo;;bar.;#test;hello\tworld;;"), 0, nil, nil, nil, nil, nil)
|
||||
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, 4, len(allNewTags))
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewTags[0].Uid)
|
||||
assert.Equal(t, "foo", allNewTags[0].Name)
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewTags[1].Uid)
|
||||
assert.Equal(t, "bar.", allNewTags[1].Name)
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewTags[2].Uid)
|
||||
assert.Equal(t, "#test", allNewTags[2].Name)
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewTags[3].Uid)
|
||||
assert.Equal(t, "hello\tworld", allNewTags[3].Name)
|
||||
}
|
||||
|
||||
func TestDefaultTransactionDataCSVFileConverterParseImportedData_ParseDescription(t *testing.T) {
|
||||
converter := DefaultTransactionDataCSVFileConverter
|
||||
context := core.NewNullContext()
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1234567890,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount,Description\n"+
|
||||
"2024-09-01 12:34:56,Expense,Test Category,Test Account,123.45,,,foo bar\t#test"), 0, nil, nil, nil, nil, nil)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 1, len(allNewTransactions))
|
||||
assert.Equal(t, "foo bar\t#test", allNewTransactions[0].Comment)
|
||||
}
|
||||
|
||||
func TestDefaultTransactionDataCSVFileConverterParseImportedData_MissingFileHeader(t *testing.T) {
|
||||
converter := DefaultTransactionDataCSVFileConverter
|
||||
context := core.NewNullContext()
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
_, _, _, _, _, _, 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) {
|
||||
converter := DefaultTransactionDataCSVFileConverter
|
||||
context := core.NewNullContext()
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
// Missing Time Column
|
||||
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("Timezone,Type,Category,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount,Geographic Location,Tags,Description\n"+
|
||||
"+08:00,Balance Modification,,Test Sub Category,Test Account,CNY,123.45,,,,,,"), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
|
||||
|
||||
// Missing Type Column
|
||||
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("Time,Category,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount,Geographic Location,Tags,Description\n"+
|
||||
"2024-09-01 00:00:00,+08:00,Test Category,Test Sub Category,Test Account,CNY,123.45,,,,,,"), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
|
||||
|
||||
// Missing Sub Category Column
|
||||
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("Time,Type,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount,Geographic Location,Tags,Description\n"+
|
||||
"2024-09-01 00:00:00,+08:00,Balance Modification,Test Account,CNY,123.45,,,,,,"), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
|
||||
|
||||
// Missing Account Name Column
|
||||
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("Time,Timezone,Type,Category,Sub Category,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount,Geographic Location,Tags,Description\n"+
|
||||
"2024-09-01 00:00:00,+08:00,Balance Modification,,Test Sub Category,CNY,123.45,,,,,,"), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
|
||||
|
||||
// Missing Amount Column
|
||||
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("Time,Timezone,Type,Category,Sub Category,Account,Account Currency,Account2,Account2 Currency,Account2 Amount,Geographic Location,Tags,Description\n"+
|
||||
"2024-09-01 00:00:00,+08:00,Balance Modification,,Test Sub Category,Test Account,CNY,,,,,,"), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
|
||||
|
||||
// Missing Account2 Name Column
|
||||
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("Time,Timezone,Type,Category,Sub Category,Account,Account Currency,Amount,Account2 Currency,Account2 Amount,Geographic Location,Tags,Description\n"+
|
||||
"2024-09-01 00:00:00,+08:00,Balance Modification,,Test Sub Category,Test Account,CNY,123.45,,,,,"), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package _default
|
||||
|
||||
// defaultTransactionDataTSVFileConverter defines the structure of ezbookkeeping default tsv file converter
|
||||
type defaultTransactionDataTSVFileConverter struct {
|
||||
defaultTransactionDataPlainTextConverter
|
||||
}
|
||||
|
||||
// Initialize an ezbookkeeping default transaction data tsv file converter singleton instance
|
||||
var (
|
||||
DefaultTransactionDataTSVFileConverter = &defaultTransactionDataTSVFileConverter{
|
||||
defaultTransactionDataPlainTextConverter{
|
||||
columnSeparator: "\t",
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -1,202 +0,0 @@
|
||||
package _default
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||
)
|
||||
|
||||
// defaultPlainTextDataTable defines the structure of ezbookkeeping default plain text data table
|
||||
type defaultPlainTextDataTable struct {
|
||||
columnSeparator string
|
||||
lineSeparator string
|
||||
allLines []string
|
||||
headerLineColumnNames []string
|
||||
}
|
||||
|
||||
// defaultPlainTextDataRow defines the structure of ezbookkeeping default plain text data row
|
||||
type defaultPlainTextDataRow struct {
|
||||
allItems []string
|
||||
}
|
||||
|
||||
// defaultPlainTextDataRowIterator defines the structure of ezbookkeeping default plain text data row iterator
|
||||
type defaultPlainTextDataRowIterator struct {
|
||||
dataTable *defaultPlainTextDataTable
|
||||
currentIndex int
|
||||
}
|
||||
|
||||
// defaultTransactionPlainTextDataTableBuilder defines the structure of ezbookkeeping default transaction plain text data table builder
|
||||
type defaultTransactionPlainTextDataTableBuilder struct {
|
||||
columnSeparator string
|
||||
lineSeparator string
|
||||
columns []datatable.TransactionDataTableColumn
|
||||
dataColumnNameMapping map[datatable.TransactionDataTableColumn]string
|
||||
dataLineFormat string
|
||||
builder *strings.Builder
|
||||
}
|
||||
|
||||
// DataRowCount returns the total count of data row
|
||||
func (t *defaultPlainTextDataTable) DataRowCount() int {
|
||||
if len(t.allLines) < 1 {
|
||||
return 0
|
||||
}
|
||||
|
||||
return len(t.allLines) - 1
|
||||
}
|
||||
|
||||
// HeaderColumnNames returns the header column name list
|
||||
func (t *defaultPlainTextDataTable) HeaderColumnNames() []string {
|
||||
return t.headerLineColumnNames
|
||||
}
|
||||
|
||||
// DataRowIterator returns the iterator of data row
|
||||
func (t *defaultPlainTextDataTable) DataRowIterator() datatable.ImportedDataRowIterator {
|
||||
return &defaultPlainTextDataRowIterator{
|
||||
dataTable: t,
|
||||
currentIndex: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// ColumnCount returns the total count of column in this data row
|
||||
func (r *defaultPlainTextDataRow) ColumnCount() int {
|
||||
return len(r.allItems)
|
||||
}
|
||||
|
||||
// GetData returns the data in the specified column index
|
||||
func (r *defaultPlainTextDataRow) GetData(columnIndex int) string {
|
||||
if columnIndex >= len(r.allItems) {
|
||||
return ""
|
||||
}
|
||||
|
||||
return r.allItems[columnIndex]
|
||||
}
|
||||
|
||||
// HasNext returns whether the iterator does not reach the end
|
||||
func (t *defaultPlainTextDataRowIterator) HasNext() bool {
|
||||
return t.currentIndex+1 < len(t.dataTable.allLines)
|
||||
}
|
||||
|
||||
// CurrentRowId returns current index
|
||||
func (t *defaultPlainTextDataRowIterator) CurrentRowId() string {
|
||||
return fmt.Sprintf("line#%d", t.currentIndex)
|
||||
}
|
||||
|
||||
// Next returns the next imported data row
|
||||
func (t *defaultPlainTextDataRowIterator) Next() datatable.ImportedDataRow {
|
||||
if t.currentIndex+1 >= len(t.dataTable.allLines) {
|
||||
return nil
|
||||
}
|
||||
|
||||
t.currentIndex++
|
||||
|
||||
rowContent := t.dataTable.allLines[t.currentIndex]
|
||||
rowItems := strings.Split(rowContent, t.dataTable.columnSeparator)
|
||||
|
||||
return &defaultPlainTextDataRow{
|
||||
allItems: rowItems,
|
||||
}
|
||||
}
|
||||
|
||||
// AppendTransaction appends the specified transaction to data builder
|
||||
func (b *defaultTransactionPlainTextDataTableBuilder) AppendTransaction(data map[datatable.TransactionDataTableColumn]string) {
|
||||
dataRowParams := make([]any, len(b.columns))
|
||||
|
||||
for i := 0; i < len(b.columns); i++ {
|
||||
dataRowParams[i] = data[b.columns[i]]
|
||||
}
|
||||
|
||||
b.builder.WriteString(fmt.Sprintf(b.dataLineFormat, dataRowParams...))
|
||||
}
|
||||
|
||||
// ReplaceDelimiters returns the text after removing the delimiters
|
||||
func (b *defaultTransactionPlainTextDataTableBuilder) ReplaceDelimiters(text string) string {
|
||||
text = strings.Replace(text, "\r\n", " ", -1)
|
||||
text = strings.Replace(text, "\r", " ", -1)
|
||||
text = strings.Replace(text, "\n", " ", -1)
|
||||
text = strings.Replace(text, b.columnSeparator, " ", -1)
|
||||
text = strings.Replace(text, b.lineSeparator, " ", -1)
|
||||
|
||||
return text
|
||||
}
|
||||
|
||||
// String returns the textual representation of this data
|
||||
func (b *defaultTransactionPlainTextDataTableBuilder) String() string {
|
||||
return b.builder.String()
|
||||
}
|
||||
|
||||
func (b *defaultTransactionPlainTextDataTableBuilder) generateHeaderLine() string {
|
||||
var ret strings.Builder
|
||||
|
||||
for i := 0; i < len(b.columns); i++ {
|
||||
if ret.Len() > 0 {
|
||||
ret.WriteString(b.columnSeparator)
|
||||
}
|
||||
|
||||
dataColumn := b.columns[i]
|
||||
columnName := b.dataColumnNameMapping[dataColumn]
|
||||
|
||||
ret.WriteString(columnName)
|
||||
}
|
||||
|
||||
ret.WriteString(b.lineSeparator)
|
||||
|
||||
return ret.String()
|
||||
}
|
||||
|
||||
func (b *defaultTransactionPlainTextDataTableBuilder) generateDataLineFormat() string {
|
||||
var ret strings.Builder
|
||||
|
||||
for i := 0; i < len(b.columns); i++ {
|
||||
if ret.Len() > 0 {
|
||||
ret.WriteString(b.columnSeparator)
|
||||
}
|
||||
|
||||
ret.WriteString("%s")
|
||||
}
|
||||
|
||||
ret.WriteString(b.lineSeparator)
|
||||
|
||||
return ret.String()
|
||||
}
|
||||
|
||||
func createNewDefaultPlainTextDataTable(content string, columnSeparator string, lineSeparator string) (*defaultPlainTextDataTable, error) {
|
||||
allLines := strings.Split(content, lineSeparator)
|
||||
|
||||
if len(allLines) < 2 {
|
||||
return nil, errs.ErrNotFoundTransactionDataInFile
|
||||
}
|
||||
|
||||
headerLine := allLines[0]
|
||||
headerLine = strings.ReplaceAll(headerLine, "\r", "")
|
||||
headerLineItems := strings.Split(headerLine, columnSeparator)
|
||||
|
||||
return &defaultPlainTextDataTable{
|
||||
columnSeparator: columnSeparator,
|
||||
lineSeparator: lineSeparator,
|
||||
allLines: allLines,
|
||||
headerLineColumnNames: headerLineItems,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func createNewDefaultTransactionPlainTextDataTableBuilder(transactionCount int, columns []datatable.TransactionDataTableColumn, dataColumnNameMapping map[datatable.TransactionDataTableColumn]string, columnSeparator string, lineSeparator string) *defaultTransactionPlainTextDataTableBuilder {
|
||||
var builder strings.Builder
|
||||
builder.Grow(transactionCount * 100)
|
||||
|
||||
dataTableBuilder := &defaultTransactionPlainTextDataTableBuilder{
|
||||
columnSeparator: columnSeparator,
|
||||
lineSeparator: lineSeparator,
|
||||
columns: columns,
|
||||
dataColumnNameMapping: dataColumnNameMapping,
|
||||
builder: &builder,
|
||||
}
|
||||
|
||||
headerLine := dataTableBuilder.generateHeaderLine()
|
||||
dataLineFormat := dataTableBuilder.generateDataLineFormat()
|
||||
|
||||
dataTableBuilder.builder.WriteString(headerLine)
|
||||
dataTableBuilder.dataLineFormat = dataLineFormat
|
||||
|
||||
return dataTableBuilder
|
||||
}
|
||||
@@ -1,227 +0,0 @@
|
||||
package dsv
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/csv"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/text/encoding"
|
||||
"golang.org/x/text/encoding/charmap"
|
||||
"golang.org/x/text/encoding/japanese"
|
||||
"golang.org/x/text/encoding/korean"
|
||||
"golang.org/x/text/encoding/simplifiedchinese"
|
||||
"golang.org/x/text/encoding/traditionalchinese"
|
||||
"golang.org/x/text/encoding/unicode"
|
||||
"golang.org/x/text/transform"
|
||||
|
||||
"github.com/mayswind/ezbookkeeping/pkg/converters/base"
|
||||
csvconverter "github.com/mayswind/ezbookkeeping/pkg/converters/csv"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
|
||||
"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/utils"
|
||||
)
|
||||
|
||||
var supportedFileTypeSeparators = map[string]rune{
|
||||
"custom_csv": ',',
|
||||
"custom_tsv": '\t',
|
||||
}
|
||||
|
||||
var supportedFileEncodings = map[string]encoding.Encoding{
|
||||
"utf-8": unicode.UTF8, // UTF-8
|
||||
"utf-8-bom": unicode.UTF8BOM, // UTF-8 with BOM
|
||||
"utf-16le": unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM), // UTF-16 Little Endian
|
||||
"utf-16be": unicode.UTF16(unicode.BigEndian, unicode.IgnoreBOM), // UTF-16 Big Endian
|
||||
"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{
|
||||
models.TRANSACTION_TYPE_MODIFY_BALANCE: utils.IntToString(int(models.TRANSACTION_TYPE_MODIFY_BALANCE)),
|
||||
models.TRANSACTION_TYPE_INCOME: utils.IntToString(int(models.TRANSACTION_TYPE_INCOME)),
|
||||
models.TRANSACTION_TYPE_EXPENSE: utils.IntToString(int(models.TRANSACTION_TYPE_EXPENSE)),
|
||||
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
|
||||
separator rune
|
||||
columnIndexMapping map[datatable.TransactionDataTableColumn]int
|
||||
transactionTypeNameMapping map[string]models.TransactionType
|
||||
hasHeaderLine bool
|
||||
timeFormat string
|
||||
timezoneFormat string
|
||||
geoLocationSeparator string
|
||||
transactionTagSeparator string
|
||||
}
|
||||
|
||||
// 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
|
||||
csvReader.FieldsPerRecord = -1
|
||||
|
||||
allLines := make([][]string, 0)
|
||||
|
||||
for {
|
||||
items, err := csvReader.Read()
|
||||
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(ctx, "[custom_transaction_data_dsv_file_importer.ParseDsvFileLines] cannot parse dsv data, because %s", err.Error())
|
||||
return nil, errs.ErrInvalidCSVFile
|
||||
}
|
||||
|
||||
if len(items) == 1 && items[0] == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
for index := range items {
|
||||
items[index] = strings.Trim(items[index], " ")
|
||||
}
|
||||
|
||||
allLines = append(allLines, items)
|
||||
}
|
||||
|
||||
return allLines, nil
|
||||
}
|
||||
|
||||
// ParseImportedData returns the imported data by parsing the custom transaction dsv data
|
||||
func (c *customTransactionDataDsvFileImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezoneOffset int16, accountMap map[string]*models.Account, expenseCategoryMap map[string]*models.TransactionCategory, incomeCategoryMap map[string]*models.TransactionCategory, transferCategoryMap 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
|
||||
}
|
||||
|
||||
if !c.hasHeaderLine {
|
||||
allLines = append([][]string{{}}, allLines...)
|
||||
}
|
||||
|
||||
dataTable := csvconverter.CreateNewCustomCsvImportedDataTable(allLines)
|
||||
transactionDataTable := CreateNewCustomPlainTextDataTable(dataTable, c.columnIndexMapping, c.transactionTypeNameMapping, c.timeFormat, c.timezoneFormat)
|
||||
dataTableImporter := datatable.CreateNewImporter(customTransactionTypeNameMapping, c.geoLocationSeparator, c.transactionTagSeparator)
|
||||
|
||||
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
|
||||
func IsDelimiterSeparatedValuesFileType(fileType string) bool {
|
||||
_, exists := supportedFileTypeSeparators[fileType]
|
||||
return exists
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
enc, exists := supportedFileEncodings[fileEncoding]
|
||||
|
||||
if !exists {
|
||||
return nil, errs.ErrImportFileEncodingNotSupported
|
||||
}
|
||||
|
||||
return &customTransactionDataDsvFileImporter{
|
||||
fileEncoding: enc,
|
||||
separator: separator,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CreateNewCustomTransactionDataDsvFileImporter returns a new custom dsv importer for transaction data
|
||||
func CreateNewCustomTransactionDataDsvFileImporter(fileType string, fileEncoding string, columnIndexMapping map[datatable.TransactionDataTableColumn]int, transactionTypeNameMapping map[string]models.TransactionType, hasHeaderLine bool, timeFormat string, timezoneFormat string, geoLocationSeparator string, transactionTagSeparator string) (base.TransactionDataImporter, error) {
|
||||
separator, exists := supportedFileTypeSeparators[fileType]
|
||||
|
||||
if !exists {
|
||||
return nil, errs.ErrImportFileTypeNotSupported
|
||||
}
|
||||
|
||||
enc, exists := supportedFileEncodings[fileEncoding]
|
||||
|
||||
if !exists {
|
||||
return nil, errs.ErrImportFileEncodingNotSupported
|
||||
}
|
||||
|
||||
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 &customTransactionDataDsvFileImporter{
|
||||
fileEncoding: enc,
|
||||
separator: separator,
|
||||
columnIndexMapping: columnIndexMapping,
|
||||
transactionTypeNameMapping: transactionTypeNameMapping,
|
||||
hasHeaderLine: hasHeaderLine,
|
||||
timeFormat: timeFormat,
|
||||
timezoneFormat: timezoneFormat,
|
||||
geoLocationSeparator: geoLocationSeparator,
|
||||
transactionTagSeparator: transactionTagSeparator,
|
||||
}, nil
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,275 +0,0 @@
|
||||
package dsv
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
|
||||
"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/utils"
|
||||
)
|
||||
|
||||
// customPlainTextDataTable defines the structure of custom plain text transaction data table
|
||||
type customPlainTextDataTable struct {
|
||||
innerDataTable datatable.ImportedDataTable
|
||||
columnIndexMapping map[datatable.TransactionDataTableColumn]int
|
||||
transactionTypeNameMapping map[string]models.TransactionType
|
||||
timeFormat string
|
||||
timezoneFormat string
|
||||
timeFormatIncludeTimezone bool
|
||||
}
|
||||
|
||||
// customPlainTextDataRow defines the structure of custom plain text transaction data row
|
||||
type customPlainTextDataRow struct {
|
||||
transactionDataTable *customPlainTextDataTable
|
||||
rowData map[datatable.TransactionDataTableColumn]string
|
||||
isValid bool
|
||||
}
|
||||
|
||||
// customPlainTextDataRowIterator defines the structure of custom plain text transaction data row iterator
|
||||
type customPlainTextDataRowIterator struct {
|
||||
transactionDataTable *customPlainTextDataTable
|
||||
innerIterator datatable.ImportedDataRowIterator
|
||||
}
|
||||
|
||||
// HasColumn returns whether the data table has specified column
|
||||
func (t *customPlainTextDataTable) HasColumn(column datatable.TransactionDataTableColumn) bool {
|
||||
// custom dsv file allows no sub category, account name and related account name column mapping, but data table converter needs these columns
|
||||
if column == datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY ||
|
||||
column == datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME ||
|
||||
column == datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME {
|
||||
return true
|
||||
}
|
||||
|
||||
// timezone column will be added when original time format contains timezone
|
||||
if t.timeFormatIncludeTimezone && column == datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE {
|
||||
return true
|
||||
}
|
||||
|
||||
_, exists := t.columnIndexMapping[column]
|
||||
return exists
|
||||
}
|
||||
|
||||
// TransactionRowCount returns the total count of transaction data row
|
||||
func (t *customPlainTextDataTable) TransactionRowCount() int {
|
||||
return t.innerDataTable.DataRowCount()
|
||||
}
|
||||
|
||||
// TransactionRowIterator returns the iterator of transaction data row
|
||||
func (t *customPlainTextDataTable) TransactionRowIterator() datatable.TransactionDataRowIterator {
|
||||
return &customPlainTextDataRowIterator{
|
||||
transactionDataTable: t,
|
||||
innerIterator: t.innerDataTable.DataRowIterator(),
|
||||
}
|
||||
}
|
||||
|
||||
// IsValid returns whether this row is valid data for importing
|
||||
func (r *customPlainTextDataRow) IsValid() bool {
|
||||
return r.isValid
|
||||
}
|
||||
|
||||
// GetData returns the data in the specified column type
|
||||
func (r *customPlainTextDataRow) GetData(column datatable.TransactionDataTableColumn) string {
|
||||
return r.rowData[column]
|
||||
}
|
||||
|
||||
// HasNext returns whether the iterator does not reach the end
|
||||
func (t *customPlainTextDataRowIterator) HasNext() bool {
|
||||
return t.innerIterator.HasNext()
|
||||
}
|
||||
|
||||
// Next returns the next transaction data row
|
||||
func (t *customPlainTextDataRowIterator) Next(ctx core.Context, user *models.User) (daraRow datatable.TransactionDataRow, err error) {
|
||||
importedRow := t.innerIterator.Next()
|
||||
|
||||
if importedRow == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
rowData, isValid, err := t.parseTransaction(ctx, user, importedRow)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(ctx, "[custom_transaction_plain_text_data_table.Next] cannot parsing transaction in row \"%s\", because %s", t.innerIterator.CurrentRowId(), err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &customPlainTextDataRow{
|
||||
transactionDataTable: t.transactionDataTable,
|
||||
rowData: rowData,
|
||||
isValid: isValid,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *customPlainTextDataRowIterator) parseTransaction(ctx core.Context, user *models.User, row datatable.ImportedDataRow) (map[datatable.TransactionDataTableColumn]string, bool, error) {
|
||||
rowData := make(map[datatable.TransactionDataTableColumn]string, len(t.transactionDataTable.columnIndexMapping))
|
||||
|
||||
for column, columnIndex := range t.transactionDataTable.columnIndexMapping {
|
||||
if columnIndex < 0 || columnIndex >= row.ColumnCount() {
|
||||
continue
|
||||
}
|
||||
|
||||
value := row.GetData(columnIndex)
|
||||
rowData[column] = value
|
||||
}
|
||||
|
||||
// parse transaction type
|
||||
if rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] != "" {
|
||||
transactionType, exists := t.transactionDataTable.transactionTypeNameMapping[rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE]]
|
||||
|
||||
if !exists {
|
||||
log.Warnf(ctx, "[custom_transaction_plain_text_data_table.parseTransaction] skip parsing this transaction, because transaction type \"%s\" mapping not defined", rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE])
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
mappedTransactionType, exists := customTransactionTypeNameMapping[transactionType]
|
||||
|
||||
if !exists {
|
||||
log.Errorf(ctx, "[custom_transaction_plain_text_data_table.parseTransaction] cannot parsing transaction type \"%s\", because type \"%d\" is invalid", rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE], transactionType)
|
||||
return nil, false, errs.ErrTransactionTypeInvalid
|
||||
}
|
||||
|
||||
rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = mappedTransactionType
|
||||
}
|
||||
|
||||
// parse date time
|
||||
if rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME] != "" {
|
||||
dateTime, err := time.Parse(t.transactionDataTable.timeFormat, rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME])
|
||||
|
||||
if err != nil {
|
||||
return nil, false, errs.ErrTransactionTimeInvalid
|
||||
}
|
||||
|
||||
rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME] = utils.FormatUnixTimeToLongDateTime(dateTime.Unix(), dateTime.Location())
|
||||
|
||||
if t.transactionDataTable.timeFormatIncludeTimezone {
|
||||
rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE] = utils.FormatTimezoneOffset(dateTime.Location())
|
||||
}
|
||||
}
|
||||
|
||||
// parse timezone
|
||||
if rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE] != "" {
|
||||
if t.transactionDataTable.timezoneFormat == "Z" || t.transactionDataTable.timezoneFormat == "" { // -HH:mm
|
||||
// Do Nothing
|
||||
} else if t.transactionDataTable.timezoneFormat == "ZZ" { // -HHmm
|
||||
timezone := rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE]
|
||||
|
||||
if len(timezone) != 5 {
|
||||
return nil, false, errs.ErrTransactionTimeZoneInvalid
|
||||
}
|
||||
|
||||
timezone = timezone[:3] + ":" + timezone[3:]
|
||||
rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE] = timezone
|
||||
} else {
|
||||
return nil, false, errs.ErrImportFileTransactionTimezoneFormatInvalid
|
||||
}
|
||||
}
|
||||
|
||||
// use primary category if sub category is empty
|
||||
if rowData[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY] == "" && rowData[datatable.TRANSACTION_DATA_TABLE_CATEGORY] != "" {
|
||||
rowData[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY] = rowData[datatable.TRANSACTION_DATA_TABLE_CATEGORY]
|
||||
}
|
||||
|
||||
// trim trailing zero in decimal
|
||||
if rowData[datatable.TRANSACTION_DATA_TABLE_AMOUNT] != "" {
|
||||
rowData[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = utils.TrimTrailingZerosInDecimal(rowData[datatable.TRANSACTION_DATA_TABLE_AMOUNT])
|
||||
amount, err := utils.ParseAmount(rowData[datatable.TRANSACTION_DATA_TABLE_AMOUNT])
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(ctx, "[custom_transaction_plain_text_data_table.parseTransaction] cannot parsing transaction amount \"%s\", because %s", rowData[datatable.TRANSACTION_DATA_TABLE_AMOUNT], err.Error())
|
||||
return nil, false, errs.ErrAmountInvalid
|
||||
}
|
||||
|
||||
rowData[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = utils.FormatAmount(amount)
|
||||
}
|
||||
|
||||
if rowData[datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT] != "" {
|
||||
rowData[datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT] = utils.TrimTrailingZerosInDecimal(rowData[datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT])
|
||||
amount, err := utils.ParseAmount(rowData[datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT])
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(ctx, "[custom_transaction_plain_text_data_table.parseTransaction] cannot parsing transaction related amount \"%s\", because %s", rowData[datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT], err.Error())
|
||||
return nil, false, errs.ErrAmountInvalid
|
||||
}
|
||||
|
||||
rowData[datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT] = utils.FormatAmount(amount)
|
||||
}
|
||||
|
||||
if _, exists := rowData[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY]; !exists {
|
||||
rowData[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY] = ""
|
||||
}
|
||||
|
||||
if _, exists := rowData[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME]; !exists {
|
||||
rowData[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = ""
|
||||
}
|
||||
|
||||
if _, exists := rowData[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME]; !exists {
|
||||
rowData[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = ""
|
||||
}
|
||||
|
||||
return rowData, true, nil
|
||||
}
|
||||
|
||||
// CreateNewCustomPlainTextDataTable returns transaction data table from imported data table
|
||||
func CreateNewCustomPlainTextDataTable(dataTable datatable.ImportedDataTable, columnIndexMapping map[datatable.TransactionDataTableColumn]int, transactionTypeNameMapping map[string]models.TransactionType, timeFormat string, timezoneFormat string) *customPlainTextDataTable {
|
||||
timeFormatIncludeTimezone := strings.Contains(timeFormat, "z") || strings.Contains(timeFormat, "Z")
|
||||
|
||||
return &customPlainTextDataTable{
|
||||
innerDataTable: dataTable,
|
||||
columnIndexMapping: columnIndexMapping,
|
||||
transactionTypeNameMapping: transactionTypeNameMapping,
|
||||
timeFormat: getDateTimeFormat(timeFormat),
|
||||
timezoneFormat: timezoneFormat,
|
||||
timeFormatIncludeTimezone: timeFormatIncludeTimezone,
|
||||
}
|
||||
}
|
||||
|
||||
func getDateTimeFormat(format string) string {
|
||||
// convert moment.js format to Go format
|
||||
|
||||
format = strings.ReplaceAll(format, "YYYY", "2006")
|
||||
format = strings.ReplaceAll(format, "YY", "06")
|
||||
|
||||
format = strings.ReplaceAll(format, "MMMM", "January")
|
||||
format = strings.ReplaceAll(format, "MMM", "Jan")
|
||||
format = strings.ReplaceAll(format, "MM", "01")
|
||||
format = strings.ReplaceAll(format, "M", "1")
|
||||
|
||||
format = strings.ReplaceAll(format, "DD", "02")
|
||||
format = strings.ReplaceAll(format, "D", "2")
|
||||
|
||||
format = strings.ReplaceAll(format, "dddd", "Monday")
|
||||
format = strings.ReplaceAll(format, "ddd", "Mon")
|
||||
|
||||
format = strings.ReplaceAll(format, "HH", "15")
|
||||
format = strings.ReplaceAll(format, "H", "15")
|
||||
|
||||
format = strings.ReplaceAll(format, "hh", "03")
|
||||
format = strings.ReplaceAll(format, "h", "3")
|
||||
|
||||
format = strings.ReplaceAll(format, "mm", "04")
|
||||
format = strings.ReplaceAll(format, "m", "4")
|
||||
|
||||
format = strings.ReplaceAll(format, "ss", "05")
|
||||
format = strings.ReplaceAll(format, "s", "5")
|
||||
|
||||
for i := 9; i >= 1; i-- {
|
||||
format = strings.ReplaceAll(format, "."+strings.Repeat("S", i), "."+strings.Repeat("9", i))
|
||||
}
|
||||
|
||||
format = strings.ReplaceAll(format, "A", "PM")
|
||||
format = strings.ReplaceAll(format, "a", "pm")
|
||||
|
||||
format = strings.ReplaceAll(format, "zz", "MST")
|
||||
format = strings.ReplaceAll(format, "z", "MST")
|
||||
|
||||
if strings.Contains(format, "ZZ") {
|
||||
format = strings.ReplaceAll(format, "ZZ", "Z0700")
|
||||
} else if strings.Contains(format, "Z") {
|
||||
format = strings.ReplaceAll(format, "Z", "Z07:00")
|
||||
}
|
||||
|
||||
return format
|
||||
}
|
||||
@@ -1,191 +0,0 @@
|
||||
package excel
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"github.com/extrame/xls"
|
||||
|
||||
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||
)
|
||||
|
||||
// ExcelFileImportedDataTable defines the structure of excel file data table
|
||||
type ExcelFileImportedDataTable struct {
|
||||
workbook *xls.WorkBook
|
||||
headerLineColumnNames []string
|
||||
}
|
||||
|
||||
// ExcelFileDataRow defines the structure of excel file data table row
|
||||
type ExcelFileDataRow struct {
|
||||
sheet *xls.WorkSheet
|
||||
rowIndex int
|
||||
}
|
||||
|
||||
// ExcelFileDataRowIterator defines the structure of excel file data table row iterator
|
||||
type ExcelFileDataRowIterator struct {
|
||||
dataTable *ExcelFileImportedDataTable
|
||||
currentSheetIndex int
|
||||
currentRowIndexInSheet uint16
|
||||
}
|
||||
|
||||
// DataRowCount returns the total count of data row
|
||||
func (t *ExcelFileImportedDataTable) DataRowCount() int {
|
||||
totalDataRowCount := 0
|
||||
|
||||
for i := 0; i < t.workbook.NumSheets(); i++ {
|
||||
sheet := t.workbook.GetSheet(i)
|
||||
|
||||
if sheet.MaxRow < 1 {
|
||||
continue
|
||||
}
|
||||
|
||||
totalDataRowCount += int(sheet.MaxRow)
|
||||
}
|
||||
|
||||
return totalDataRowCount
|
||||
}
|
||||
|
||||
// HeaderColumnNames returns the header column name list
|
||||
func (t *ExcelFileImportedDataTable) HeaderColumnNames() []string {
|
||||
return t.headerLineColumnNames
|
||||
}
|
||||
|
||||
// DataRowIterator returns the iterator of data row
|
||||
func (t *ExcelFileImportedDataTable) DataRowIterator() datatable.ImportedDataRowIterator {
|
||||
return &ExcelFileDataRowIterator{
|
||||
dataTable: t,
|
||||
currentSheetIndex: 0,
|
||||
currentRowIndexInSheet: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// ColumnCount returns the total count of column in this data row
|
||||
func (r *ExcelFileDataRow) ColumnCount() int {
|
||||
row := r.sheet.Row(r.rowIndex)
|
||||
return row.LastCol() + 1
|
||||
}
|
||||
|
||||
// GetData returns the data in the specified column index
|
||||
func (r *ExcelFileDataRow) GetData(columnIndex int) string {
|
||||
row := r.sheet.Row(r.rowIndex)
|
||||
return row.Col(columnIndex)
|
||||
}
|
||||
|
||||
// HasNext returns whether the iterator does not reach the end
|
||||
func (t *ExcelFileDataRowIterator) HasNext() bool {
|
||||
workbook := t.dataTable.workbook
|
||||
|
||||
if t.currentSheetIndex >= workbook.NumSheets() {
|
||||
return false
|
||||
}
|
||||
|
||||
currentSheet := workbook.GetSheet(t.currentSheetIndex)
|
||||
|
||||
if t.currentRowIndexInSheet+1 <= currentSheet.MaxRow {
|
||||
return true
|
||||
}
|
||||
|
||||
for i := t.currentSheetIndex + 1; i < workbook.NumSheets(); i++ {
|
||||
sheet := workbook.GetSheet(i)
|
||||
|
||||
if sheet.MaxRow < 1 {
|
||||
continue
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// CurrentRowId returns current index
|
||||
func (t *ExcelFileDataRowIterator) CurrentRowId() string {
|
||||
return fmt.Sprintf("table#%d-row#%d", t.currentSheetIndex, t.currentRowIndexInSheet)
|
||||
}
|
||||
|
||||
// Next returns the next imported data row
|
||||
func (t *ExcelFileDataRowIterator) Next() datatable.ImportedDataRow {
|
||||
workbook := t.dataTable.workbook
|
||||
currentRowIndexInTable := t.currentRowIndexInSheet
|
||||
|
||||
for i := t.currentSheetIndex; i < workbook.NumSheets(); i++ {
|
||||
sheet := workbook.GetSheet(i)
|
||||
|
||||
if currentRowIndexInTable+1 <= sheet.MaxRow {
|
||||
t.currentRowIndexInSheet++
|
||||
currentRowIndexInTable = t.currentRowIndexInSheet
|
||||
break
|
||||
}
|
||||
|
||||
t.currentSheetIndex++
|
||||
t.currentRowIndexInSheet = 0
|
||||
currentRowIndexInTable = 0
|
||||
}
|
||||
|
||||
if t.currentSheetIndex >= workbook.NumSheets() {
|
||||
return nil
|
||||
}
|
||||
|
||||
currentSheet := workbook.GetSheet(t.currentSheetIndex)
|
||||
|
||||
if t.currentRowIndexInSheet > currentSheet.MaxRow {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &ExcelFileDataRow{
|
||||
sheet: currentSheet,
|
||||
rowIndex: int(t.currentRowIndexInSheet),
|
||||
}
|
||||
}
|
||||
|
||||
// CreateNewExcelFileImportedDataTable returns excel xls data table by file binary data
|
||||
func CreateNewExcelFileImportedDataTable(data []byte) (*ExcelFileImportedDataTable, error) {
|
||||
reader := bytes.NewReader(data)
|
||||
workbook, err := xls.OpenReader(reader, "")
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var headerRowItems []string
|
||||
|
||||
for i := 0; i < workbook.NumSheets(); i++ {
|
||||
sheet := workbook.GetSheet(i)
|
||||
|
||||
if sheet.MaxRow < 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
row := sheet.Row(0)
|
||||
|
||||
if row == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if i == 0 {
|
||||
for j := 0; j <= row.LastCol(); j++ {
|
||||
headerItem := row.Col(j)
|
||||
|
||||
if headerItem == "" {
|
||||
break
|
||||
}
|
||||
|
||||
headerRowItems = append(headerRowItems, headerItem)
|
||||
}
|
||||
} else {
|
||||
for j := 0; j <= min(row.LastCol(), len(headerRowItems)-1); j++ {
|
||||
headerItem := row.Col(j)
|
||||
|
||||
if headerItem != headerRowItems[j] {
|
||||
return nil, errs.ErrFieldsInMultiTableAreDifferent
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &ExcelFileImportedDataTable{
|
||||
workbook: workbook,
|
||||
headerLineColumnNames: headerRowItems,
|
||||
}, nil
|
||||
}
|
||||
@@ -1,246 +0,0 @@
|
||||
package excel
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||
)
|
||||
|
||||
func TestExcelFileImportedDataTableDataRowCount(t *testing.T) {
|
||||
testdata, err := os.ReadFile("../../../testdata/simple_excel_file.xls")
|
||||
assert.Nil(t, err)
|
||||
|
||||
datatable, err := CreateNewExcelFileImportedDataTable(testdata)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 2, datatable.DataRowCount())
|
||||
}
|
||||
|
||||
func TestExcelFileImportedDataTableDataRowCount_MultipleSheets(t *testing.T) {
|
||||
testdata, err := os.ReadFile("../../../testdata/multiple_sheets_excel_file.xls")
|
||||
assert.Nil(t, err)
|
||||
|
||||
datatable, err := CreateNewExcelFileImportedDataTable(testdata)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 5, datatable.DataRowCount())
|
||||
}
|
||||
|
||||
func TestExcelFileImportedDataTableDataRowCount_OnlyHeaderLine(t *testing.T) {
|
||||
testdata, err := os.ReadFile("../../../testdata/only_one_row_excel_file.xls")
|
||||
assert.Nil(t, err)
|
||||
|
||||
datatable, err := CreateNewExcelFileImportedDataTable(testdata)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 0, datatable.DataRowCount())
|
||||
}
|
||||
|
||||
func TestExcelFileImportedDataTableDataRowCount_EmptyContent(t *testing.T) {
|
||||
testdata, err := os.ReadFile("../../../testdata/empty_excel_file.xls")
|
||||
assert.Nil(t, err)
|
||||
|
||||
datatable, err := CreateNewExcelFileImportedDataTable(testdata)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 0, datatable.DataRowCount())
|
||||
}
|
||||
|
||||
func TestExcelFileImportedDataTableHeaderColumnNames(t *testing.T) {
|
||||
testdata, err := os.ReadFile("../../../testdata/simple_excel_file.xls")
|
||||
assert.Nil(t, err)
|
||||
|
||||
datatable, err := CreateNewExcelFileImportedDataTable(testdata)
|
||||
assert.EqualValues(t, []string{"A1", "B1", "C1"}, datatable.HeaderColumnNames())
|
||||
}
|
||||
|
||||
func TestExcelFileImportedDataTableHeaderColumnNames_EmptyContent(t *testing.T) {
|
||||
testdata, err := os.ReadFile("../../../testdata/empty_excel_file.xls")
|
||||
assert.Nil(t, err)
|
||||
|
||||
datatable, err := CreateNewExcelFileImportedDataTable(testdata)
|
||||
assert.Nil(t, datatable.HeaderColumnNames())
|
||||
}
|
||||
|
||||
func TestExcelFileDataRowIterator(t *testing.T) {
|
||||
testdata, err := os.ReadFile("../../../testdata/simple_excel_file.xls")
|
||||
assert.Nil(t, err)
|
||||
|
||||
datatable, err := CreateNewExcelFileImportedDataTable(testdata)
|
||||
iterator := datatable.DataRowIterator()
|
||||
assert.True(t, iterator.HasNext())
|
||||
|
||||
// data row 1
|
||||
assert.NotNil(t, iterator.Next())
|
||||
assert.True(t, iterator.HasNext())
|
||||
|
||||
// data row 2
|
||||
assert.NotNil(t, iterator.Next())
|
||||
assert.False(t, iterator.HasNext())
|
||||
|
||||
// not existed data row 3
|
||||
assert.Nil(t, iterator.Next())
|
||||
assert.False(t, iterator.HasNext())
|
||||
|
||||
// not existed data row 4
|
||||
assert.Nil(t, iterator.Next())
|
||||
assert.False(t, iterator.HasNext())
|
||||
}
|
||||
|
||||
func TestExcelFileDataRowIterator_MultipleSheets(t *testing.T) {
|
||||
testdata, err := os.ReadFile("../../../testdata/multiple_sheets_excel_file.xls")
|
||||
assert.Nil(t, err)
|
||||
|
||||
datatable, err := CreateNewExcelFileImportedDataTable(testdata)
|
||||
iterator := datatable.DataRowIterator()
|
||||
assert.True(t, iterator.HasNext())
|
||||
|
||||
// sheet 1 data row 1
|
||||
assert.NotNil(t, iterator.Next())
|
||||
assert.True(t, iterator.HasNext())
|
||||
|
||||
// sheet 1 data row 2
|
||||
assert.NotNil(t, iterator.Next())
|
||||
assert.True(t, iterator.HasNext())
|
||||
|
||||
// sheet 3 data row 1
|
||||
assert.NotNil(t, iterator.Next())
|
||||
assert.True(t, iterator.HasNext())
|
||||
|
||||
// sheet 5 data row 1
|
||||
assert.NotNil(t, iterator.Next())
|
||||
assert.True(t, iterator.HasNext())
|
||||
|
||||
// sheet 5 data row 2
|
||||
assert.NotNil(t, iterator.Next())
|
||||
assert.False(t, iterator.HasNext())
|
||||
|
||||
// not existed data row
|
||||
assert.Nil(t, iterator.Next())
|
||||
assert.False(t, iterator.HasNext())
|
||||
|
||||
// not existed data row
|
||||
assert.Nil(t, iterator.Next())
|
||||
assert.False(t, iterator.HasNext())
|
||||
}
|
||||
|
||||
func TestExcelFileDataRowIterator_OnlyHeaderLine(t *testing.T) {
|
||||
testdata, err := os.ReadFile("../../../testdata/only_one_row_excel_file.xls")
|
||||
assert.Nil(t, err)
|
||||
|
||||
datatable, err := CreateNewExcelFileImportedDataTable(testdata)
|
||||
iterator := datatable.DataRowIterator()
|
||||
assert.False(t, iterator.HasNext())
|
||||
|
||||
// not existed data row 1
|
||||
assert.Nil(t, iterator.Next())
|
||||
assert.False(t, iterator.HasNext())
|
||||
|
||||
// not existed data row 2
|
||||
assert.Nil(t, iterator.Next())
|
||||
assert.False(t, iterator.HasNext())
|
||||
}
|
||||
|
||||
func TestExcelFileDataRowIterator_EmptyContent(t *testing.T) {
|
||||
testdata, err := os.ReadFile("../../../testdata/empty_excel_file.xls")
|
||||
assert.Nil(t, err)
|
||||
|
||||
datatable, err := CreateNewExcelFileImportedDataTable(testdata)
|
||||
iterator := datatable.DataRowIterator()
|
||||
assert.False(t, iterator.HasNext())
|
||||
|
||||
// not existed data row 1
|
||||
assert.Nil(t, iterator.Next())
|
||||
assert.False(t, iterator.HasNext())
|
||||
|
||||
// not existed data row 2
|
||||
assert.Nil(t, iterator.Next())
|
||||
assert.False(t, iterator.HasNext())
|
||||
}
|
||||
|
||||
func TestExcelFileDataRowColumnCount(t *testing.T) {
|
||||
testdata, err := os.ReadFile("../../../testdata/simple_excel_file.xls")
|
||||
assert.Nil(t, err)
|
||||
|
||||
datatable, err := CreateNewExcelFileImportedDataTable(testdata)
|
||||
iterator := datatable.DataRowIterator()
|
||||
|
||||
row1 := iterator.Next()
|
||||
assert.EqualValues(t, 4, row1.ColumnCount())
|
||||
|
||||
row2 := iterator.Next()
|
||||
assert.EqualValues(t, 4, row2.ColumnCount())
|
||||
}
|
||||
|
||||
func TestExcelFileDataRowGetData(t *testing.T) {
|
||||
testdata, err := os.ReadFile("../../../testdata/simple_excel_file.xls")
|
||||
assert.Nil(t, err)
|
||||
|
||||
datatable, err := CreateNewExcelFileImportedDataTable(testdata)
|
||||
iterator := datatable.DataRowIterator()
|
||||
|
||||
row1 := iterator.Next()
|
||||
assert.Equal(t, "A2", row1.GetData(0))
|
||||
assert.Equal(t, "B2", row1.GetData(1))
|
||||
assert.Equal(t, "C2", row1.GetData(2))
|
||||
|
||||
row2 := iterator.Next()
|
||||
assert.Equal(t, "A3", row2.GetData(0))
|
||||
assert.Equal(t, "B3", row2.GetData(1))
|
||||
assert.Equal(t, "C3", row2.GetData(2))
|
||||
}
|
||||
|
||||
func TestExcelFileDataRowGetData_GetNotExistedColumnData(t *testing.T) {
|
||||
testdata, err := os.ReadFile("../../../testdata/simple_excel_file.xls")
|
||||
assert.Nil(t, err)
|
||||
|
||||
datatable, err := CreateNewExcelFileImportedDataTable(testdata)
|
||||
iterator := datatable.DataRowIterator()
|
||||
|
||||
row1 := iterator.Next()
|
||||
assert.Equal(t, "", row1.GetData(3))
|
||||
}
|
||||
|
||||
func TestExcelFileDataRowGetData_MultipleSheets(t *testing.T) {
|
||||
testdata, err := os.ReadFile("../../../testdata/multiple_sheets_excel_file.xls")
|
||||
assert.Nil(t, err)
|
||||
|
||||
datatable, err := CreateNewExcelFileImportedDataTable(testdata)
|
||||
iterator := datatable.DataRowIterator()
|
||||
|
||||
sheet1Row1 := iterator.Next()
|
||||
assert.Equal(t, "1-A2", sheet1Row1.GetData(0))
|
||||
assert.Equal(t, "1-B2", sheet1Row1.GetData(1))
|
||||
assert.Equal(t, "1-C2", sheet1Row1.GetData(2))
|
||||
|
||||
sheet1Row2 := iterator.Next()
|
||||
assert.Equal(t, "1-A3", sheet1Row2.GetData(0))
|
||||
assert.Equal(t, "1-B3", sheet1Row2.GetData(1))
|
||||
assert.Equal(t, "1-C3", sheet1Row2.GetData(2))
|
||||
|
||||
// skip empty sheet2
|
||||
|
||||
sheet3Row1 := iterator.Next()
|
||||
assert.Equal(t, "3-A2", sheet3Row1.GetData(0))
|
||||
assert.Equal(t, "3-B2", sheet3Row1.GetData(1))
|
||||
assert.Equal(t, "", sheet3Row1.GetData(2))
|
||||
|
||||
// skip no data row sheet4
|
||||
|
||||
sheet5Row1 := iterator.Next()
|
||||
assert.Equal(t, "5-A2", sheet5Row1.GetData(0))
|
||||
assert.Equal(t, "5-B2", sheet5Row1.GetData(1))
|
||||
assert.Equal(t, "5-C2", sheet5Row1.GetData(2))
|
||||
|
||||
sheet5Row2 := iterator.Next()
|
||||
assert.Equal(t, "5-A3", sheet5Row2.GetData(0))
|
||||
assert.Equal(t, "5-B3", sheet5Row2.GetData(1))
|
||||
assert.Equal(t, "5-C3", sheet5Row2.GetData(2))
|
||||
}
|
||||
|
||||
func TestCreateNewExcelFileImportedDataTable_MultipleSheetsWithDifferentHeaders(t *testing.T) {
|
||||
testdata, err := os.ReadFile("../../../testdata/multiple_sheets_with_different_header_row_excel_file.xls")
|
||||
assert.Nil(t, err)
|
||||
|
||||
_, err = CreateNewExcelFileImportedDataTable(testdata)
|
||||
assert.EqualError(t, err, errs.ErrFieldsInMultiTableAreDifferent.Message)
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
package converters
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||
)
|
||||
|
||||
// EzBookKeepingCSVFileExporter defines the structure of csv file exporter
|
||||
type EzBookKeepingCSVFileExporter struct {
|
||||
DataConverter
|
||||
}
|
||||
|
||||
const csvHeaderLine = "Time,Timezone,Type,Category,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount,Tags,Comment\n"
|
||||
const csvDataLineFormat = "%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s\n"
|
||||
|
||||
// ToExportedContent returns the exported csv data
|
||||
func (e *EzBookKeepingCSVFileExporter) ToExportedContent(uid int64, timezone *time.Location, transactions []*models.Transaction, accountMap map[int64]*models.Account, categoryMap map[int64]*models.TransactionCategory, tagMap map[int64]*models.TransactionTag, allTagIndexs map[int64][]int64) ([]byte, error) {
|
||||
var ret strings.Builder
|
||||
|
||||
ret.Grow(len(transactions) * 100)
|
||||
ret.WriteString(csvHeaderLine)
|
||||
|
||||
for i := 0; i < len(transactions); i++ {
|
||||
transaction := transactions[i]
|
||||
|
||||
if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN {
|
||||
continue
|
||||
}
|
||||
|
||||
transactionTimeZone := time.FixedZone("Transaction Timezone", int(transaction.TimezoneUtcOffset)*60)
|
||||
transactionTime := utils.FormatUnixTimeToLongDateTimeWithoutSecond(utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime), transactionTimeZone)
|
||||
transactionTimezone := utils.FormatTimezoneOffset(transactionTimeZone)
|
||||
transactionType := e.getTransactionTypeName(transaction.Type)
|
||||
category := e.getTransactionCategoryName(transaction.CategoryId, categoryMap)
|
||||
subCategory := e.getTransactionSubCategoryName(transaction.CategoryId, categoryMap)
|
||||
account := e.getAccountName(transaction.AccountId, accountMap)
|
||||
accountCurrency := e.getAccountCurrency(transaction.AccountId, accountMap)
|
||||
amount := e.getDisplayAmount(transaction.Amount)
|
||||
account2 := ""
|
||||
account2Currency := ""
|
||||
account2Amount := ""
|
||||
|
||||
if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT {
|
||||
account2 = e.getAccountName(transaction.RelatedAccountId, accountMap)
|
||||
account2Currency = e.getAccountCurrency(transaction.RelatedAccountId, accountMap)
|
||||
account2Amount = e.getDisplayAmount(transaction.RelatedAccountAmount)
|
||||
}
|
||||
|
||||
tags := e.getTags(transaction.TransactionId, allTagIndexs, tagMap)
|
||||
comment := e.getComment(transaction.Comment)
|
||||
|
||||
ret.WriteString(fmt.Sprintf(csvDataLineFormat, transactionTime, transactionTimezone, transactionType, category, subCategory, account, accountCurrency, amount, account2, account2Currency, account2Amount, tags, comment))
|
||||
}
|
||||
|
||||
return []byte(ret.String()), nil
|
||||
}
|
||||
|
||||
func (e *EzBookKeepingCSVFileExporter) getTransactionTypeName(transactionDbType models.TransactionDbType) string {
|
||||
if transactionDbType == models.TRANSACTION_DB_TYPE_MODIFY_BALANCE {
|
||||
return "Balance Modification"
|
||||
} else if transactionDbType == models.TRANSACTION_DB_TYPE_INCOME {
|
||||
return "Income"
|
||||
} else if transactionDbType == models.TRANSACTION_DB_TYPE_EXPENSE {
|
||||
return "Expense"
|
||||
} else if transactionDbType == models.TRANSACTION_DB_TYPE_TRANSFER_OUT || transactionDbType == models.TRANSACTION_DB_TYPE_TRANSFER_IN {
|
||||
return "Transfer"
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (e *EzBookKeepingCSVFileExporter) getTransactionCategoryName(categoryId int64, categoryMap map[int64]*models.TransactionCategory) string {
|
||||
category, exists := categoryMap[categoryId]
|
||||
|
||||
if !exists {
|
||||
return ""
|
||||
}
|
||||
|
||||
if category.ParentCategoryId == 0 {
|
||||
return category.Name
|
||||
}
|
||||
|
||||
parentCategory, exists := categoryMap[category.ParentCategoryId]
|
||||
|
||||
if !exists {
|
||||
return ""
|
||||
}
|
||||
|
||||
return parentCategory.Name
|
||||
}
|
||||
|
||||
func (e *EzBookKeepingCSVFileExporter) getTransactionSubCategoryName(categoryId int64, categoryMap map[int64]*models.TransactionCategory) string {
|
||||
category, exists := categoryMap[categoryId]
|
||||
|
||||
if exists {
|
||||
return category.Name
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (e *EzBookKeepingCSVFileExporter) getAccountName(accountId int64, accountMap map[int64]*models.Account) string {
|
||||
account, exists := accountMap[accountId]
|
||||
|
||||
if exists {
|
||||
return account.Name
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (e *EzBookKeepingCSVFileExporter) getAccountCurrency(accountId int64, accountMap map[int64]*models.Account) string {
|
||||
account, exists := accountMap[accountId]
|
||||
|
||||
if exists {
|
||||
return account.Currency
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (e *EzBookKeepingCSVFileExporter) getDisplayAmount(amount int64) string {
|
||||
displayAmount := utils.Int64ToString(amount)
|
||||
integer := utils.SubString(displayAmount, 0, len(displayAmount)-2)
|
||||
decimals := utils.SubString(displayAmount, -2, 2)
|
||||
|
||||
if integer == "" {
|
||||
integer = "0"
|
||||
} else if integer == "-" {
|
||||
integer = "-0"
|
||||
}
|
||||
|
||||
if len(decimals) == 0 {
|
||||
decimals = "00"
|
||||
} else if len(decimals) == 1 {
|
||||
decimals = "0" + decimals
|
||||
}
|
||||
|
||||
return integer + "." + decimals
|
||||
}
|
||||
|
||||
func (e *EzBookKeepingCSVFileExporter) getTags(transactionId int64, allTagIndexs map[int64][]int64, tagMap map[int64]*models.TransactionTag) string {
|
||||
tagIndexs, exists := allTagIndexs[transactionId]
|
||||
|
||||
if !exists {
|
||||
return ""
|
||||
}
|
||||
|
||||
var ret strings.Builder
|
||||
|
||||
for i := 0; i < len(tagIndexs); i++ {
|
||||
if i > 0 {
|
||||
ret.WriteString(";")
|
||||
}
|
||||
|
||||
tagIndex := tagIndexs[i]
|
||||
tag, exists := tagMap[tagIndex]
|
||||
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
ret.WriteString(tag.Name)
|
||||
}
|
||||
|
||||
return ret.String()
|
||||
}
|
||||
|
||||
func (e *EzBookKeepingCSVFileExporter) getComment(comment string) string {
|
||||
comment = strings.Replace(comment, ",", " ", -1)
|
||||
comment = strings.Replace(comment, "\r\n", " ", -1)
|
||||
comment = strings.Replace(comment, "\n", " ", -1)
|
||||
|
||||
return comment
|
||||
}
|
||||
@@ -1,268 +0,0 @@
|
||||
package feidee
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/csv"
|
||||
"golang.org/x/text/encoding/unicode"
|
||||
"golang.org/x/text/transform"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
csvdatatable "github.com/mayswind/ezbookkeeping/pkg/converters/csv"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/log"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||
)
|
||||
|
||||
const feideeMymoneyAppTransactionDataCsvFileHeader = "随手记导出文件(headers:v5;"
|
||||
|
||||
const feideeMymoneyAppTransactionTimeColumnName = "日期"
|
||||
const feideeMymoneyAppTransactionTypeColumnName = "交易类型"
|
||||
const feideeMymoneyAppTransactionCategoryColumnName = "类别"
|
||||
const feideeMymoneyAppTransactionSubCategoryColumnName = "子类别"
|
||||
const feideeMymoneyAppTransactionAccountNameColumnName = "账户"
|
||||
const feideeMymoneyAppTransactionAccountCurrencyColumnName = "账户币种"
|
||||
const feideeMymoneyAppTransactionAmountColumnName = "金额"
|
||||
const feideeMymoneyAppTransactionDescriptionColumnName = "备注"
|
||||
const feideeMymoneyAppTransactionRelatedIdColumnName = "关联Id"
|
||||
|
||||
const feideeMymoneyAppTransactionTypeModifyBalanceText = "余额变更"
|
||||
const feideeMymoneyAppTransactionTypeIncomeText = "收入"
|
||||
const feideeMymoneyAppTransactionTypeExpenseText = "支出"
|
||||
const feideeMymoneyAppTransactionTypeTransferInText = "转入"
|
||||
const feideeMymoneyAppTransactionTypeTransferOutText = "转出"
|
||||
|
||||
var feideeMymoneyAppDataColumnNameMapping = map[datatable.TransactionDataTableColumn]string{
|
||||
datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME: feideeMymoneyAppTransactionTimeColumnName,
|
||||
datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE: feideeMymoneyAppTransactionTypeColumnName,
|
||||
datatable.TRANSACTION_DATA_TABLE_CATEGORY: feideeMymoneyAppTransactionCategoryColumnName,
|
||||
datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY: feideeMymoneyAppTransactionSubCategoryColumnName,
|
||||
datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME: feideeMymoneyAppTransactionAccountNameColumnName,
|
||||
datatable.TRANSACTION_DATA_TABLE_ACCOUNT_CURRENCY: feideeMymoneyAppTransactionAccountCurrencyColumnName,
|
||||
datatable.TRANSACTION_DATA_TABLE_AMOUNT: feideeMymoneyAppTransactionAmountColumnName,
|
||||
datatable.TRANSACTION_DATA_TABLE_DESCRIPTION: feideeMymoneyAppTransactionDescriptionColumnName,
|
||||
}
|
||||
|
||||
// feideeMymoneyAppTransactionDataCsvFileImporter defines the structure of feidee mymoney app csv importer for transaction data
|
||||
type feideeMymoneyAppTransactionDataCsvFileImporter struct{}
|
||||
|
||||
// Initialize a feidee mymoney app transaction data csv file importer singleton instance
|
||||
var (
|
||||
FeideeMymoneyAppTransactionDataCsvFileImporter = &feideeMymoneyAppTransactionDataCsvFileImporter{}
|
||||
)
|
||||
|
||||
// ParseImportedData returns the imported data by parsing the feidee mymoney app transaction csv data
|
||||
func (c *feideeMymoneyAppTransactionDataCsvFileImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezoneOffset int16, accountMap map[string]*models.Account, expenseCategoryMap map[string]*models.TransactionCategory, incomeCategoryMap map[string]*models.TransactionCategory, transferCategoryMap 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))
|
||||
|
||||
dataTable, err := c.createNewFeideeMymoneyAppImportedDataTable(ctx, reader)
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
commonDataTable := datatable.CreateNewImportedCommonDataTable(dataTable)
|
||||
|
||||
if !commonDataTable.HasColumn(feideeMymoneyAppTransactionTimeColumnName) ||
|
||||
!commonDataTable.HasColumn(feideeMymoneyAppTransactionTypeColumnName) ||
|
||||
!commonDataTable.HasColumn(feideeMymoneyAppTransactionSubCategoryColumnName) ||
|
||||
!commonDataTable.HasColumn(feideeMymoneyAppTransactionAccountNameColumnName) ||
|
||||
!commonDataTable.HasColumn(feideeMymoneyAppTransactionAmountColumnName) ||
|
||||
!commonDataTable.HasColumn(feideeMymoneyAppTransactionRelatedIdColumnName) {
|
||||
log.Errorf(ctx, "[feidee_mymoney_app_transaction_data_csv_file_importer.ParseImportedData] cannot parse import data, because missing essential columns in header row")
|
||||
return nil, nil, nil, nil, nil, nil, errs.ErrMissingRequiredFieldInHeaderRow
|
||||
}
|
||||
|
||||
transactionDataTable, err := c.createNewFeideeMymoneyAppTransactionDataTable(ctx, commonDataTable)
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
dataTableImporter := datatable.CreateNewSimpleImporter(feideeMymoneyTransactionTypeNameMapping)
|
||||
|
||||
return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezoneOffset, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
|
||||
}
|
||||
|
||||
func (c *feideeMymoneyAppTransactionDataCsvFileImporter) createNewFeideeMymoneyAppImportedDataTable(ctx core.Context, reader io.Reader) (datatable.ImportedDataTable, error) {
|
||||
csvReader := csv.NewReader(reader)
|
||||
csvReader.FieldsPerRecord = -1
|
||||
|
||||
allOriginalLines := make([][]string, 0)
|
||||
hasFileHeader := false
|
||||
|
||||
for {
|
||||
items, err := csvReader.Read()
|
||||
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(ctx, "[feidee_mymoney_app_transaction_data_csv_file_importer.createNewFeideeMymoneyAppTransactionDataTable] cannot parse feidee mymoney csv data, because %s", err.Error())
|
||||
return nil, errs.ErrInvalidCSVFile
|
||||
}
|
||||
|
||||
if !hasFileHeader {
|
||||
if len(items) <= 0 {
|
||||
continue
|
||||
} else if strings.Index(items[0], feideeMymoneyAppTransactionDataCsvFileHeader) == 0 {
|
||||
hasFileHeader = true
|
||||
continue
|
||||
} else {
|
||||
log.Warnf(ctx, "[feidee_mymoney_app_transaction_data_csv_file_importer.createNewFeideeMymoneyAppTransactionDataTable] read unexpected line before read file header, line content is %s", strings.Join(items, ","))
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
allOriginalLines = append(allOriginalLines, items)
|
||||
}
|
||||
|
||||
if !hasFileHeader {
|
||||
return nil, errs.ErrInvalidFileHeader
|
||||
}
|
||||
|
||||
if len(allOriginalLines) < 2 {
|
||||
log.Errorf(ctx, "[feidee_mymoney_app_transaction_data_csv_file_importer.createNewFeideeMymoneyAppTransactionDataTable] cannot parse import data, because data table row count is less 1")
|
||||
return nil, errs.ErrNotFoundTransactionDataInFile
|
||||
}
|
||||
|
||||
dataTable := csvdatatable.CreateNewCustomCsvImportedDataTable(allOriginalLines)
|
||||
|
||||
return dataTable, nil
|
||||
}
|
||||
|
||||
func (c *feideeMymoneyAppTransactionDataCsvFileImporter) createNewFeideeMymoneyAppTransactionDataTable(ctx core.Context, commonDataTable datatable.CommonDataTable) (datatable.TransactionDataTable, error) {
|
||||
newColumns := make([]datatable.TransactionDataTableColumn, 0, 11)
|
||||
newColumns = append(newColumns, datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE)
|
||||
newColumns = append(newColumns, datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME)
|
||||
|
||||
if commonDataTable.HasColumn(feideeMymoneyAppTransactionCategoryColumnName) {
|
||||
newColumns = append(newColumns, datatable.TRANSACTION_DATA_TABLE_CATEGORY)
|
||||
}
|
||||
|
||||
newColumns = append(newColumns, datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY)
|
||||
newColumns = append(newColumns, datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME)
|
||||
|
||||
if commonDataTable.HasColumn(feideeMymoneyAppTransactionAccountCurrencyColumnName) {
|
||||
newColumns = append(newColumns, datatable.TRANSACTION_DATA_TABLE_ACCOUNT_CURRENCY)
|
||||
}
|
||||
|
||||
newColumns = append(newColumns, datatable.TRANSACTION_DATA_TABLE_AMOUNT)
|
||||
newColumns = append(newColumns, datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME)
|
||||
|
||||
if commonDataTable.HasColumn(feideeMymoneyAppTransactionAccountCurrencyColumnName) {
|
||||
newColumns = append(newColumns, datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_CURRENCY)
|
||||
}
|
||||
|
||||
newColumns = append(newColumns, datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT)
|
||||
|
||||
if commonDataTable.HasColumn(feideeMymoneyAppTransactionDescriptionColumnName) {
|
||||
newColumns = append(newColumns, datatable.TRANSACTION_DATA_TABLE_DESCRIPTION)
|
||||
}
|
||||
|
||||
transactionRowParser := createFeideeMymoneyTransactionDataRowParser()
|
||||
transactionDataTable := datatable.CreateNewWritableTransactionDataTableWithRowParser(newColumns, transactionRowParser)
|
||||
transferTransactionsMap := make(map[string]map[datatable.TransactionDataTableColumn]string, 0)
|
||||
|
||||
commonDataTableIterator := commonDataTable.DataRowIterator()
|
||||
|
||||
for commonDataTableIterator.HasNext() {
|
||||
dataRow := commonDataTableIterator.Next()
|
||||
rowId := commonDataTableIterator.CurrentRowId()
|
||||
|
||||
if dataRow.ColumnCount() < commonDataTable.HeaderColumnCount() {
|
||||
log.Errorf(ctx, "[feidee_mymoney_app_transaction_data_csv_file_importer.createNewFeideeMymoneyAppTransactionDataTable] cannot parse row \"%s\", because may missing some columns (column count %d in data row is less than header column count %d)", rowId, dataRow.ColumnCount(), commonDataTable.HeaderColumnCount())
|
||||
return nil, errs.ErrFewerFieldsInDataRowThanInHeaderRow
|
||||
}
|
||||
|
||||
data := make(map[datatable.TransactionDataTableColumn]string, 11)
|
||||
|
||||
for columnType, columnName := range feideeMymoneyAppDataColumnNameMapping {
|
||||
if dataRow.HasData(columnName) {
|
||||
data[columnType] = dataRow.GetData(columnName)
|
||||
}
|
||||
}
|
||||
|
||||
transactionType := data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE]
|
||||
|
||||
if transactionType == feideeMymoneyAppTransactionTypeModifyBalanceText || transactionType == feideeMymoneyAppTransactionTypeIncomeText || transactionType == feideeMymoneyAppTransactionTypeExpenseText {
|
||||
if transactionType == feideeMymoneyAppTransactionTypeModifyBalanceText {
|
||||
data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = feideeMymoneyTransactionTypeNameMapping[models.TRANSACTION_TYPE_MODIFY_BALANCE]
|
||||
} else if transactionType == feideeMymoneyAppTransactionTypeIncomeText {
|
||||
data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = feideeMymoneyTransactionTypeNameMapping[models.TRANSACTION_TYPE_INCOME]
|
||||
} else if transactionType == feideeMymoneyAppTransactionTypeExpenseText {
|
||||
data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = feideeMymoneyTransactionTypeNameMapping[models.TRANSACTION_TYPE_EXPENSE]
|
||||
}
|
||||
|
||||
data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = ""
|
||||
data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_CURRENCY] = ""
|
||||
data[datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT] = ""
|
||||
transactionDataTable.Add(data)
|
||||
} else if transactionType == feideeMymoneyAppTransactionTypeTransferInText || transactionType == feideeMymoneyAppTransactionTypeTransferOutText {
|
||||
relatedId := ""
|
||||
|
||||
if dataRow.HasData(feideeMymoneyAppTransactionRelatedIdColumnName) {
|
||||
relatedId = dataRow.GetData(feideeMymoneyAppTransactionRelatedIdColumnName)
|
||||
}
|
||||
|
||||
if relatedId == "" {
|
||||
log.Errorf(ctx, "[feidee_mymoney_app_transaction_data_csv_file_importer.createNewFeideeMymoneyAppTransactionDataTable] transfer transaction has blank related id in row \"%s\"", rowId)
|
||||
return nil, errs.ErrRelatedIdCannotBeBlank
|
||||
}
|
||||
|
||||
relatedData, exists := transferTransactionsMap[relatedId]
|
||||
|
||||
if !exists {
|
||||
transferTransactionsMap[relatedId] = data
|
||||
continue
|
||||
}
|
||||
|
||||
if transactionType == feideeMymoneyAppTransactionTypeTransferInText && relatedData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] == feideeMymoneyAppTransactionTypeTransferOutText {
|
||||
relatedData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = feideeMymoneyTransactionTypeNameMapping[models.TRANSACTION_TYPE_TRANSFER]
|
||||
relatedData[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME]
|
||||
relatedData[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_CURRENCY] = data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_CURRENCY]
|
||||
relatedData[datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT] = data[datatable.TRANSACTION_DATA_TABLE_AMOUNT]
|
||||
transactionDataTable.Add(relatedData)
|
||||
delete(transferTransactionsMap, relatedId)
|
||||
} else if transactionType == feideeMymoneyAppTransactionTypeTransferOutText && relatedData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] == feideeMymoneyAppTransactionTypeTransferInText {
|
||||
data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = feideeMymoneyTransactionTypeNameMapping[models.TRANSACTION_TYPE_TRANSFER]
|
||||
data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = relatedData[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME]
|
||||
data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_CURRENCY] = relatedData[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_CURRENCY]
|
||||
data[datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT] = relatedData[datatable.TRANSACTION_DATA_TABLE_AMOUNT]
|
||||
transactionDataTable.Add(data)
|
||||
delete(transferTransactionsMap, relatedId)
|
||||
} else {
|
||||
log.Errorf(ctx, "[feidee_mymoney_app_transaction_data_csv_file_importer.createNewFeideeMymoneyAppTransactionDataTable] transfer transaction type \"%s\" is not expected in row \"%s\"", transactionType, rowId)
|
||||
return nil, errs.ErrTransactionTypeInvalid
|
||||
}
|
||||
} else {
|
||||
log.Errorf(ctx, "[feidee_mymoney_app_transaction_data_csv_file_importer.createNewFeideeMymoneyAppTransactionDataTable] cannot parse transaction type \"%s\" in row \"%s\"", transactionType, rowId)
|
||||
return nil, errs.ErrTransactionTypeInvalid
|
||||
}
|
||||
}
|
||||
|
||||
if len(transferTransactionsMap) > 0 {
|
||||
log.Errorf(ctx, "[feidee_mymoney_app_transaction_data_csv_file_importer.createNewFeideeMymoneyAppTransactionDataTable] there are %d transactions (related id is %s) which don't have related records", len(transferTransactionsMap), c.getFeideeMymoneyAppRelatedTransactionIds(transferTransactionsMap))
|
||||
return nil, errs.ErrFoundRecordNotHasRelatedRecord
|
||||
}
|
||||
|
||||
return transactionDataTable, nil
|
||||
}
|
||||
|
||||
func (c *feideeMymoneyAppTransactionDataCsvFileImporter) getFeideeMymoneyAppRelatedTransactionIds(transferTransactionsMap map[string]map[datatable.TransactionDataTableColumn]string) string {
|
||||
builder := strings.Builder{}
|
||||
|
||||
for relatedId := range transferTransactionsMap {
|
||||
if builder.Len() > 0 {
|
||||
builder.WriteRune(',')
|
||||
}
|
||||
|
||||
builder.WriteString(relatedId)
|
||||
}
|
||||
|
||||
return builder.String()
|
||||
}
|
||||
@@ -1,359 +0,0 @@
|
||||
package feidee
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"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 TestFeideeMymoneyCsvFileImporterParseImportedData_MinimumValidData(t *testing.T) {
|
||||
converter := FeideeMymoneyAppTransactionDataCsvFileImporter
|
||||
context := core.NewNullContext()
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1234567890,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
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"+
|
||||
"\"收入\",\"2024-09-01 01:23:45\",\"Test Category\",\"Test Account\",\"0.12\",\"\",\"\"\n"+
|
||||
"\"支出\",\"2024-09-01 12:34:56\",\"Test Category2\",\"Test Account\",\"1.00\",\"\",\"\"\n"+
|
||||
"\"转出\",\"2024-09-01 23:59:59\",\"Test Category3\",\"Test Account\",\"0.05\",\"\",\"00000000-0000-0000-0000-000000000001\"\n"+
|
||||
"\"转入\",\"2024-09-01 23:59:59\",\"Test Category3\",\"Test Account2\",\"0.05\",\"\",\"00000000-0000-0000-0000-000000000001\"\n"+
|
||||
"\"转入\",\"2024-09-02 23:59:59\",\"Test Category3\",\"Test Account\",\"0.5\",\"\",\"00000000-0000-0000-0000-000000000002\"\n"+
|
||||
"\"转出\",\"2024-09-02 23:59:59\",\"Test Category3\",\"Test Account2\",\"0.5\",\"\",\"00000000-0000-0000-0000-000000000002\""), 0, nil, nil, nil, nil, nil)
|
||||
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, 6, len(allNewTransactions))
|
||||
assert.Equal(t, 2, len(allNewAccounts))
|
||||
assert.Equal(t, 2, len(allNewSubExpenseCategories))
|
||||
assert.Equal(t, 2, len(allNewSubIncomeCategories))
|
||||
assert.Equal(t, 1, 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, "2024-09-01 00:00:00", utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime), time.UTC))
|
||||
assert.Equal(t, int64(12345), allNewTransactions[0].Amount)
|
||||
assert.Equal(t, "Test Account", allNewTransactions[0].OriginalSourceAccountName)
|
||||
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, "2024-09-01 01:00:00", utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(allNewTransactions[1].TransactionTime), time.UTC))
|
||||
assert.Equal(t, int64(12), allNewTransactions[1].Amount)
|
||||
assert.Equal(t, "Test Account2", allNewTransactions[1].OriginalSourceAccountName)
|
||||
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, "2024-09-01 01:23:45", utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(allNewTransactions[2].TransactionTime), time.UTC))
|
||||
assert.Equal(t, int64(12), allNewTransactions[2].Amount)
|
||||
assert.Equal(t, "Test Account", allNewTransactions[2].OriginalSourceAccountName)
|
||||
assert.Equal(t, "Test Category", allNewTransactions[2].OriginalCategoryName)
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewTransactions[3].Uid)
|
||||
assert.Equal(t, models.TRANSACTION_DB_TYPE_EXPENSE, allNewTransactions[3].Type)
|
||||
assert.Equal(t, "2024-09-01 12:34:56", utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(allNewTransactions[3].TransactionTime), time.UTC))
|
||||
assert.Equal(t, int64(100), allNewTransactions[3].Amount)
|
||||
assert.Equal(t, "Test Account", allNewTransactions[3].OriginalSourceAccountName)
|
||||
assert.Equal(t, "Test Category2", allNewTransactions[3].OriginalCategoryName)
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewTransactions[4].Uid)
|
||||
assert.Equal(t, models.TRANSACTION_DB_TYPE_TRANSFER_OUT, allNewTransactions[4].Type)
|
||||
assert.Equal(t, "2024-09-01 23:59:59", utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(allNewTransactions[4].TransactionTime), time.UTC))
|
||||
assert.Equal(t, int64(5), allNewTransactions[4].Amount)
|
||||
assert.Equal(t, "Test Account", allNewTransactions[4].OriginalSourceAccountName)
|
||||
assert.Equal(t, "Test Account2", allNewTransactions[4].OriginalDestinationAccountName)
|
||||
assert.Equal(t, "Test Category3", allNewTransactions[4].OriginalCategoryName)
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewTransactions[5].Uid)
|
||||
assert.Equal(t, models.TRANSACTION_DB_TYPE_TRANSFER_OUT, allNewTransactions[5].Type)
|
||||
assert.Equal(t, "2024-09-02 23:59:59", utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(allNewTransactions[5].TransactionTime), time.UTC))
|
||||
assert.Equal(t, int64(50), allNewTransactions[5].Amount)
|
||||
assert.Equal(t, "Test Account2", allNewTransactions[5].OriginalSourceAccountName)
|
||||
assert.Equal(t, "Test Account", allNewTransactions[5].OriginalDestinationAccountName)
|
||||
assert.Equal(t, "Test Category3", allNewTransactions[5].OriginalCategoryName)
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewAccounts[0].Uid)
|
||||
assert.Equal(t, "Test Account", allNewAccounts[0].Name)
|
||||
assert.Equal(t, "CNY", allNewAccounts[0].Currency)
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewAccounts[1].Uid)
|
||||
assert.Equal(t, "Test Account2", allNewAccounts[1].Name)
|
||||
assert.Equal(t, "CNY", allNewAccounts[1].Currency)
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewSubExpenseCategories[0].Uid)
|
||||
assert.Equal(t, "", allNewSubExpenseCategories[0].Name)
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewSubExpenseCategories[1].Uid)
|
||||
assert.Equal(t, "Test Category2", allNewSubExpenseCategories[1].Name)
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewSubIncomeCategories[0].Uid)
|
||||
assert.Equal(t, "", allNewSubIncomeCategories[0].Name)
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewSubIncomeCategories[1].Uid)
|
||||
assert.Equal(t, "Test Category", allNewSubIncomeCategories[1].Name)
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewSubTransferCategories[0].Uid)
|
||||
assert.Equal(t, "Test Category3", allNewSubTransferCategories[0].Name)
|
||||
}
|
||||
|
||||
func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseInvalidTime(t *testing.T) {
|
||||
converter := FeideeMymoneyAppTransactionDataCsvFileImporter
|
||||
context := core.NewNullContext()
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1234567890,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
|
||||
"\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+
|
||||
"\"收入\",\"2024-09-01T12:34:56\",\"Test Category\",\"Test Account\",\"0.12\",\"\",\"\""), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
|
||||
|
||||
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
|
||||
"\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+
|
||||
"\"收入\",\"09/01/2024 12:34:56\",\"Test Category\",\"Test Account\",\"0.12\",\"\",\"\""), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
|
||||
}
|
||||
|
||||
func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseInvalidType(t *testing.T) {
|
||||
converter := FeideeMymoneyAppTransactionDataCsvFileImporter
|
||||
context := core.NewNullContext()
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1234567890,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
_, _, _, _, _, _, 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\",\"\",\"\""), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrTransactionTypeInvalid.Message)
|
||||
}
|
||||
|
||||
func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseValidAccountCurrency(t *testing.T) {
|
||||
converter := FeideeMymoneyAppTransactionDataCsvFileImporter
|
||||
context := core.NewNullContext()
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1234567890,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
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\""), 0, nil, nil, nil, nil, nil)
|
||||
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, 2, len(allNewTransactions))
|
||||
assert.Equal(t, 2, len(allNewAccounts))
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewAccounts[0].Uid)
|
||||
assert.Equal(t, "Test Account", allNewAccounts[0].Name)
|
||||
assert.Equal(t, "USD", allNewAccounts[0].Currency)
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewAccounts[1].Uid)
|
||||
assert.Equal(t, "Test Account2", allNewAccounts[1].Name)
|
||||
assert.Equal(t, "EUR", allNewAccounts[1].Currency)
|
||||
}
|
||||
|
||||
func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseInvalidAccountCurrency(t *testing.T) {
|
||||
converter := FeideeMymoneyAppTransactionDataCsvFileImporter
|
||||
context := core.NewNullContext()
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1234567890,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
_, _, _, _, _, _, 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\""), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message)
|
||||
|
||||
_, _, _, _, _, _, 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\""), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message)
|
||||
}
|
||||
|
||||
func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseNotSupportedCurrency(t *testing.T) {
|
||||
converter := FeideeMymoneyAppTransactionDataCsvFileImporter
|
||||
context := core.NewNullContext()
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1234567890,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
|
||||
"\"交易类型\",\"日期\",\"子类别\",\"账户\",\"账户币种\",\"金额\",\"备注\",\"关联Id\"\n"+
|
||||
"\"余额变更\",\"2024-09-01 01:23:45\",\"\",\"Test Account\",\"XXX\",\"123.45\",\"\",\"\""), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message)
|
||||
|
||||
_, _, _, _, _, _, 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\""), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message)
|
||||
|
||||
_, _, _, _, _, _, 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\""), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message)
|
||||
}
|
||||
|
||||
func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseInvalidAmount(t *testing.T) {
|
||||
converter := FeideeMymoneyAppTransactionDataCsvFileImporter
|
||||
context := core.NewNullContext()
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1234567890,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
|
||||
"\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+
|
||||
"\"余额变更\",\"2024-09-01 01:23:45\",\"\",\"Test Account\",\"123 45\",\"\",\"\""), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
|
||||
|
||||
_, _, _, _, _, _, 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\""), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
|
||||
|
||||
_, _, _, _, _, _, 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\""), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
|
||||
}
|
||||
|
||||
func TestFeideeMymoneyCsvFileImporterParseImportedData_ParseDescription(t *testing.T) {
|
||||
converter := FeideeMymoneyAppTransactionDataCsvFileImporter
|
||||
context := core.NewNullContext()
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1234567890,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
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\",\"\""), 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_InvalidRelatedId(t *testing.T) {
|
||||
converter := FeideeMymoneyAppTransactionDataCsvFileImporter
|
||||
context := core.NewNullContext()
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1234567890,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
|
||||
"\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+
|
||||
"\"转出\",\"2024-09-01 23:59:59\",\"Test Category3\",\"Test Account\",\"0.05\",\"\",\"\""), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrRelatedIdCannotBeBlank.Message)
|
||||
|
||||
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
|
||||
"\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+
|
||||
"\"转入\",\"2024-09-01 23:59:59\",\"Test Category3\",\"Test Account\",\"0.05\",\"\",\"\""), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrRelatedIdCannotBeBlank.Message)
|
||||
|
||||
_, _, _, _, _, _, 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\""), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrFoundRecordNotHasRelatedRecord.Message)
|
||||
}
|
||||
|
||||
func TestFeideeMymoneyCsvFileImporterParseImportedData_MissingFileHeader(t *testing.T) {
|
||||
converter := FeideeMymoneyAppTransactionDataCsvFileImporter
|
||||
context := core.NewNullContext()
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+
|
||||
"\"余额变更\",\"2024-09-01 00:00:00\",\"\",\"Test Account\",\"123.45\",\"\",\"\"\n"), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrInvalidFileHeader.Message)
|
||||
|
||||
_, _, _, _, _, _, 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) {
|
||||
converter := FeideeMymoneyAppTransactionDataCsvFileImporter
|
||||
context := core.NewNullContext()
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
// Missing Time Column
|
||||
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
|
||||
"\"交易类型\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+
|
||||
"\"余额变更\",\"\",\"Test Account\",\"123.45\",\"\",\"\"\n"), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
|
||||
|
||||
// Missing Type Column
|
||||
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
|
||||
"\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+
|
||||
"\"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 = converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
|
||||
"\"交易类型\",\"日期\",\"账户\",\"金额\",\"备注\",\"关联Id\"\n"+
|
||||
"\"余额变更\",\"2024-09-01 00:00:00\",\"Test Account\",\"123.45\",\"\",\"\"\n"), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
|
||||
|
||||
// Missing Account Name Column
|
||||
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
|
||||
"\"交易类型\",\"日期\",\"子类别\",\"金额\",\"备注\",\"关联Id\"\n"+
|
||||
"\"余额变更\",\"2024-09-01 00:00:00\",\"\",\"123.45\",\"\",\"\"\n"), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
|
||||
|
||||
// Missing Amount Column
|
||||
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
|
||||
"\"交易类型\",\"日期\",\"子类别\",\"账户\",\"备注\",\"关联Id\"\n"+
|
||||
"\"余额变更\",\"2024-09-01 00:00:00\",\"\",\"Test Account\",\"\",\"\"\n"), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
|
||||
|
||||
// Missing Related ID Column
|
||||
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("随手记导出文件(headers:v5;xxxxx)\n"+
|
||||
"\"交易类型\",\"日期\",\"子类别\",\"账户\",\"金额\",\"备注\"\n"+
|
||||
"\"余额变更\",\"2024-09-01 00:00:00\",\"\",\"Test Account\",\"123.45\",\"\"\n"), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
package feidee
|
||||
|
||||
import (
|
||||
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||
)
|
||||
|
||||
var feideeMymoneyTransactionTypeNameMapping = map[models.TransactionType]string{
|
||||
models.TRANSACTION_TYPE_MODIFY_BALANCE: "余额变更",
|
||||
models.TRANSACTION_TYPE_INCOME: "收入",
|
||||
models.TRANSACTION_TYPE_EXPENSE: "支出",
|
||||
models.TRANSACTION_TYPE_TRANSFER: "转账",
|
||||
}
|
||||
|
||||
// feideeMymoneyTransactionDataRowParser defines the structure of feidee mymoney transaction data row parser
|
||||
type feideeMymoneyTransactionDataRowParser struct {
|
||||
}
|
||||
|
||||
// GetAddedColumns returns the added columns after converting the data row
|
||||
func (p *feideeMymoneyTransactionDataRowParser) GetAddedColumns() []datatable.TransactionDataTableColumn {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parse returns the converted transaction data row
|
||||
func (p *feideeMymoneyTransactionDataRowParser) Parse(data map[datatable.TransactionDataTableColumn]string) (rowData map[datatable.TransactionDataTableColumn]string, rowDataValid bool, err error) {
|
||||
rowData = make(map[datatable.TransactionDataTableColumn]string, len(data))
|
||||
|
||||
for column, value := range data {
|
||||
rowData[column] = value
|
||||
}
|
||||
|
||||
if rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME] != "" {
|
||||
rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME] = p.getLongDateTime(rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME])
|
||||
}
|
||||
|
||||
if rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] == feideeMymoneyTransactionTypeNameMapping[models.TRANSACTION_TYPE_MODIFY_BALANCE] {
|
||||
amount, err := utils.ParseAmount(rowData[datatable.TRANSACTION_DATA_TABLE_AMOUNT])
|
||||
|
||||
if err != nil {
|
||||
return nil, false, errs.ErrAmountInvalid
|
||||
}
|
||||
|
||||
// balance modification transaction in feidee mymoney app is not the opening balance transaction, it can be added many times
|
||||
if amount >= 0 {
|
||||
rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = feideeMymoneyTransactionTypeNameMapping[models.TRANSACTION_TYPE_INCOME]
|
||||
} else {
|
||||
rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = feideeMymoneyTransactionTypeNameMapping[models.TRANSACTION_TYPE_EXPENSE]
|
||||
rowData[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = utils.FormatAmount(-amount)
|
||||
}
|
||||
}
|
||||
|
||||
return rowData, true, nil
|
||||
}
|
||||
|
||||
// Parse returns the converted transaction data row
|
||||
func (p *feideeMymoneyTransactionDataRowParser) getLongDateTime(str string) string {
|
||||
if utils.IsValidLongDateTimeFormat(str) {
|
||||
return str
|
||||
}
|
||||
|
||||
if utils.IsValidLongDateTimeWithoutSecondFormat(str) {
|
||||
return str + ":00"
|
||||
}
|
||||
|
||||
if utils.IsValidLongDateFormat(str) {
|
||||
return str + " 00:00:00"
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
// createFeideeMymoneyTransactionDataRowParser returns feidee mymoney transaction data row parser
|
||||
func createFeideeMymoneyTransactionDataRowParser() datatable.TransactionDataRowParser {
|
||||
return &feideeMymoneyTransactionDataRowParser{}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
package feidee
|
||||
|
||||
import (
|
||||
"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/models"
|
||||
)
|
||||
|
||||
var feideeMymoneyWebDataColumnNameMapping = map[datatable.TransactionDataTableColumn]string{
|
||||
datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME: "日期",
|
||||
datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE: "交易类型",
|
||||
datatable.TRANSACTION_DATA_TABLE_CATEGORY: "分类",
|
||||
datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY: "子分类",
|
||||
datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME: "账户1",
|
||||
datatable.TRANSACTION_DATA_TABLE_AMOUNT: "金额",
|
||||
datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME: "账户2",
|
||||
datatable.TRANSACTION_DATA_TABLE_DESCRIPTION: "备注",
|
||||
}
|
||||
|
||||
// feideeMymoneyWebTransactionDataXlsFileImporter defines the structure of feidee mymoney (web) xls importer for transaction data
|
||||
type feideeMymoneyWebTransactionDataXlsFileImporter struct {
|
||||
datatable.DataTableTransactionDataImporter
|
||||
}
|
||||
|
||||
// Initialize a feidee mymoney (web) transaction data xls file importer singleton instance
|
||||
var (
|
||||
FeideeMymoneyWebTransactionDataXlsFileImporter = &feideeMymoneyWebTransactionDataXlsFileImporter{}
|
||||
)
|
||||
|
||||
// ParseImportedData returns the imported data by parsing the feidee mymoney (web) transaction xls data
|
||||
func (c *feideeMymoneyWebTransactionDataXlsFileImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezoneOffset int16, accountMap map[string]*models.Account, expenseCategoryMap map[string]*models.TransactionCategory, incomeCategoryMap map[string]*models.TransactionCategory, transferCategoryMap 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.CreateNewExcelFileImportedDataTable(data)
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
transactionRowParser := createFeideeMymoneyTransactionDataRowParser()
|
||||
transactionDataTable := datatable.CreateNewImportedTransactionDataTableWithRowParser(dataTable, feideeMymoneyWebDataColumnNameMapping, transactionRowParser)
|
||||
dataTableImporter := datatable.CreateNewSimpleImporter(feideeMymoneyTransactionTypeNameMapping)
|
||||
|
||||
return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezoneOffset, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
package feidee
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||
)
|
||||
|
||||
func TestFeideeMymoneyTransactionDataXlsImporterParseImportedData_MinimumValidData(t *testing.T) {
|
||||
converter := FeideeMymoneyWebTransactionDataXlsFileImporter
|
||||
context := core.NewNullContext()
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1234567890,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
testdata, err := os.ReadFile("../../../testdata/feidee_mymoney_test_file.xls")
|
||||
assert.Nil(t, err)
|
||||
|
||||
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := converter.ParseImportedData(context, user, testdata, 0, nil, nil, nil, nil, nil)
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, 7, len(allNewTransactions))
|
||||
assert.Equal(t, 2, len(allNewAccounts))
|
||||
assert.Equal(t, 3, len(allNewSubExpenseCategories))
|
||||
assert.Equal(t, 3, len(allNewSubIncomeCategories))
|
||||
assert.Equal(t, 1, 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, "2024-09-01 00:00:00", utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime), time.UTC))
|
||||
assert.Equal(t, int64(12345), allNewTransactions[0].Amount)
|
||||
assert.Equal(t, "Test Account", allNewTransactions[0].OriginalSourceAccountName)
|
||||
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, "2024-09-01 01:00:00", utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(allNewTransactions[1].TransactionTime), time.UTC))
|
||||
assert.Equal(t, int64(12), allNewTransactions[1].Amount)
|
||||
assert.Equal(t, "Test Account2", allNewTransactions[1].OriginalSourceAccountName)
|
||||
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, "2024-09-01 01:23:45", utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(allNewTransactions[2].TransactionTime), time.UTC))
|
||||
assert.Equal(t, int64(12), allNewTransactions[2].Amount)
|
||||
assert.Equal(t, "Test Account", allNewTransactions[2].OriginalSourceAccountName)
|
||||
assert.Equal(t, "Test Category", allNewTransactions[2].OriginalCategoryName)
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewTransactions[3].Uid)
|
||||
assert.Equal(t, models.TRANSACTION_DB_TYPE_EXPENSE, allNewTransactions[3].Type)
|
||||
assert.Equal(t, "2024-09-01 12:34:56", utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(allNewTransactions[3].TransactionTime), time.UTC))
|
||||
assert.Equal(t, int64(100), allNewTransactions[3].Amount)
|
||||
assert.Equal(t, "Test Account2", allNewTransactions[3].OriginalSourceAccountName)
|
||||
assert.Equal(t, "Test Category2", allNewTransactions[3].OriginalCategoryName)
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewTransactions[4].Uid)
|
||||
assert.Equal(t, models.TRANSACTION_DB_TYPE_TRANSFER_OUT, allNewTransactions[4].Type)
|
||||
assert.Equal(t, "2024-09-01 23:59:59", utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(allNewTransactions[4].TransactionTime), time.UTC))
|
||||
assert.Equal(t, int64(5), allNewTransactions[4].Amount)
|
||||
assert.Equal(t, "Test Comment5", allNewTransactions[4].Comment)
|
||||
assert.Equal(t, "Test Account", allNewTransactions[4].OriginalSourceAccountName)
|
||||
assert.Equal(t, "Test Account2", allNewTransactions[4].OriginalDestinationAccountName)
|
||||
assert.Equal(t, "Test Category3", allNewTransactions[4].OriginalCategoryName)
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewTransactions[5].Uid)
|
||||
assert.Equal(t, models.TRANSACTION_DB_TYPE_INCOME, allNewTransactions[5].Type)
|
||||
assert.Equal(t, "2024-09-10 00:00:00", utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(allNewTransactions[5].TransactionTime), time.UTC))
|
||||
assert.Equal(t, int64(-54300), allNewTransactions[5].Amount)
|
||||
assert.Equal(t, "Test Account2", allNewTransactions[5].OriginalSourceAccountName)
|
||||
assert.Equal(t, "Test Category5", allNewTransactions[5].OriginalCategoryName)
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewTransactions[6].Uid)
|
||||
assert.Equal(t, models.TRANSACTION_DB_TYPE_EXPENSE, allNewTransactions[6].Type)
|
||||
assert.Equal(t, "2024-09-11 05:06:00", utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(allNewTransactions[6].TransactionTime), time.UTC))
|
||||
assert.Equal(t, int64(-12340), allNewTransactions[6].Amount)
|
||||
assert.Equal(t, "Line1\nLine2", allNewTransactions[6].Comment)
|
||||
assert.Equal(t, "Test Account", allNewTransactions[6].OriginalSourceAccountName)
|
||||
assert.Equal(t, "Test Category4", allNewTransactions[6].OriginalCategoryName)
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewAccounts[0].Uid)
|
||||
assert.Equal(t, "Test Account", allNewAccounts[0].Name)
|
||||
assert.Equal(t, "CNY", allNewAccounts[0].Currency)
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewAccounts[1].Uid)
|
||||
assert.Equal(t, "Test Account2", allNewAccounts[1].Name)
|
||||
assert.Equal(t, "CNY", allNewAccounts[1].Currency)
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewSubExpenseCategories[0].Uid)
|
||||
assert.Equal(t, "", allNewSubExpenseCategories[0].Name)
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewSubExpenseCategories[1].Uid)
|
||||
assert.Equal(t, "Test Category2", allNewSubExpenseCategories[1].Name)
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewSubExpenseCategories[2].Uid)
|
||||
assert.Equal(t, "Test Category4", allNewSubExpenseCategories[2].Name)
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewSubIncomeCategories[0].Uid)
|
||||
assert.Equal(t, "", allNewSubIncomeCategories[0].Name)
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewSubIncomeCategories[1].Uid)
|
||||
assert.Equal(t, "Test Category", allNewSubIncomeCategories[1].Name)
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewSubIncomeCategories[2].Uid)
|
||||
assert.Equal(t, "Test Category5", allNewSubIncomeCategories[2].Name)
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewSubTransferCategories[0].Uid)
|
||||
assert.Equal(t, "Test Category3", allNewSubTransferCategories[0].Name)
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
package fireflyIII
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"github.com/mayswind/ezbookkeeping/pkg/converters/csv"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||
)
|
||||
|
||||
var fireflyIIITransactionDataColumnNameMapping = map[datatable.TransactionDataTableColumn]string{
|
||||
datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME: "date",
|
||||
datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE: "type",
|
||||
datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY: "category",
|
||||
datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME: "source_name",
|
||||
datatable.TRANSACTION_DATA_TABLE_ACCOUNT_CURRENCY: "currency_code",
|
||||
datatable.TRANSACTION_DATA_TABLE_AMOUNT: "amount",
|
||||
datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME: "destination_name",
|
||||
datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_CURRENCY: "foreign_currency_code",
|
||||
datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT: "foreign_amount",
|
||||
datatable.TRANSACTION_DATA_TABLE_TAGS: "tags",
|
||||
datatable.TRANSACTION_DATA_TABLE_DESCRIPTION: "description",
|
||||
}
|
||||
|
||||
var fireflyIIITransactionTypeNameMapping = map[models.TransactionType]string{
|
||||
models.TRANSACTION_TYPE_MODIFY_BALANCE: "Opening balance",
|
||||
models.TRANSACTION_TYPE_INCOME: "Deposit",
|
||||
models.TRANSACTION_TYPE_EXPENSE: "Withdrawal",
|
||||
models.TRANSACTION_TYPE_TRANSFER: "Transfer",
|
||||
}
|
||||
|
||||
// fireflyIIITransactionDataCsvFileImporter defines the structure of firefly III csv importer for transaction data
|
||||
type fireflyIIITransactionDataCsvFileImporter struct{}
|
||||
|
||||
// Initialize a firefly III transaction data csv file importer singleton instance
|
||||
var (
|
||||
FireflyIIITransactionDataCsvFileImporter = &fireflyIIITransactionDataCsvFileImporter{}
|
||||
)
|
||||
|
||||
// ParseImportedData returns the imported data by parsing the firefly III transaction csv data
|
||||
func (c *fireflyIIITransactionDataCsvFileImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezoneOffset int16, accountMap map[string]*models.Account, expenseCategoryMap map[string]*models.TransactionCategory, incomeCategoryMap map[string]*models.TransactionCategory, transferCategoryMap map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionTag, error) {
|
||||
reader := bytes.NewReader(data)
|
||||
dataTable, err := csv.CreateNewCsvImportedDataTable(ctx, reader)
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
transactionRowParser := createFireflyIIITransactionDataRowParser()
|
||||
transactionDataTable := datatable.CreateNewImportedTransactionDataTableWithRowParser(dataTable, fireflyIIITransactionDataColumnNameMapping, transactionRowParser)
|
||||
dataTableImporter := datatable.CreateNewImporter(fireflyIIITransactionTypeNameMapping, "", ",")
|
||||
|
||||
return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezoneOffset, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
|
||||
}
|
||||
@@ -1,296 +0,0 @@
|
||||
package fireflyIII
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||
)
|
||||
|
||||
func TestFireFlyIIICsvFileConverterParseImportedData_MinimumValidData(t *testing.T) {
|
||||
converter := FireflyIIITransactionDataCsvFileImporter
|
||||
context := core.NewNullContext()
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1234567890,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := converter.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name,category\n"+
|
||||
"\"Opening balance\",-123.45,2024-09-01T00:00:00+08:00,\"Initial balance for \"\"Test Account\"\"\",\"Test Account\",\n"+
|
||||
"Deposit,-0.12,2024-09-01T01:23:45+08:00,\"A revenue account\",\"Test Account\",\"Test Category\"\n"+
|
||||
"Withdrawal,-1.00,2024-09-01T12:34:56+08:00,\"Test Account\",\"A expense account\",\"Test Category2\"\n"+
|
||||
"Transfer,-0.05,2024-09-01T23:59:59+08:00,\"Test Account\",\"Test Account2\",\"Test Category3\""), 0, nil, nil, nil, nil, nil)
|
||||
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, 4, len(allNewTransactions))
|
||||
assert.Equal(t, 2, len(allNewAccounts))
|
||||
assert.Equal(t, 1, len(allNewSubExpenseCategories))
|
||||
assert.Equal(t, 1, len(allNewSubIncomeCategories))
|
||||
assert.Equal(t, 1, len(allNewSubTransferCategories))
|
||||
assert.Equal(t, 0, len(allNewTags))
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewTransactions[0].Uid)
|
||||
assert.Equal(t, models.TRANSACTION_DB_TYPE_MODIFY_BALANCE, allNewTransactions[0].Type)
|
||||
assert.Equal(t, int64(1725120000), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
|
||||
assert.Equal(t, int64(12345), allNewTransactions[0].Amount)
|
||||
assert.Equal(t, "Test Account", allNewTransactions[0].OriginalSourceAccountName)
|
||||
assert.Equal(t, "", allNewTransactions[0].OriginalCategoryName)
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewTransactions[1].Uid)
|
||||
assert.Equal(t, models.TRANSACTION_DB_TYPE_INCOME, allNewTransactions[1].Type)
|
||||
assert.Equal(t, int64(1725125025), utils.GetUnixTimeFromTransactionTime(allNewTransactions[1].TransactionTime))
|
||||
assert.Equal(t, int64(12), allNewTransactions[1].Amount)
|
||||
assert.Equal(t, "Test Account", allNewTransactions[1].OriginalSourceAccountName)
|
||||
assert.Equal(t, "Test Category", allNewTransactions[1].OriginalCategoryName)
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewTransactions[2].Uid)
|
||||
assert.Equal(t, models.TRANSACTION_DB_TYPE_EXPENSE, allNewTransactions[2].Type)
|
||||
assert.Equal(t, int64(1725165296), utils.GetUnixTimeFromTransactionTime(allNewTransactions[2].TransactionTime))
|
||||
assert.Equal(t, int64(100), allNewTransactions[2].Amount)
|
||||
assert.Equal(t, "Test Account", allNewTransactions[2].OriginalSourceAccountName)
|
||||
assert.Equal(t, "Test Category2", allNewTransactions[2].OriginalCategoryName)
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewTransactions[3].Uid)
|
||||
assert.Equal(t, models.TRANSACTION_DB_TYPE_TRANSFER_OUT, allNewTransactions[3].Type)
|
||||
assert.Equal(t, int64(1725206399), utils.GetUnixTimeFromTransactionTime(allNewTransactions[3].TransactionTime))
|
||||
assert.Equal(t, int64(5), allNewTransactions[3].Amount)
|
||||
assert.Equal(t, "Test Account", allNewTransactions[3].OriginalSourceAccountName)
|
||||
assert.Equal(t, "Test Account2", allNewTransactions[3].OriginalDestinationAccountName)
|
||||
assert.Equal(t, "Test Category3", allNewTransactions[3].OriginalCategoryName)
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewAccounts[0].Uid)
|
||||
assert.Equal(t, "Test Account", allNewAccounts[0].Name)
|
||||
assert.Equal(t, "CNY", allNewAccounts[0].Currency)
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewAccounts[1].Uid)
|
||||
assert.Equal(t, "Test Account2", allNewAccounts[1].Name)
|
||||
assert.Equal(t, "CNY", allNewAccounts[1].Currency)
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewSubExpenseCategories[0].Uid)
|
||||
assert.Equal(t, "Test Category2", allNewSubExpenseCategories[0].Name)
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewSubIncomeCategories[0].Uid)
|
||||
assert.Equal(t, "Test Category", allNewSubIncomeCategories[0].Name)
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewSubTransferCategories[0].Uid)
|
||||
assert.Equal(t, "Test Category3", allNewSubTransferCategories[0].Name)
|
||||
}
|
||||
|
||||
func TestFireFlyIIICsvFileConverterParseImportedData_ParseInvalidTime(t *testing.T) {
|
||||
converter := FireflyIIITransactionDataCsvFileImporter
|
||||
context := core.NewNullContext()
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1234567890,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name,category\n"+
|
||||
"Withdrawal,-1.00,2024-09-01T12:34:56,\"Test Account\",\"A expense account\",\"Test Category\""), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
|
||||
|
||||
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name,category\n"+
|
||||
"Withdrawal,-1.00,2024-09-01 12:34:56+08:00,\"Test Account\",\"A expense account\",\"Test Category\""), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
|
||||
}
|
||||
|
||||
func TestFireFlyIIICsvFileConverterParseImportedData_ParseInvalidType(t *testing.T) {
|
||||
converter := FireflyIIITransactionDataCsvFileImporter
|
||||
context := core.NewNullContext()
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1234567890,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name,category\n"+
|
||||
"Type,-123.45,2024-09-01T12:34:56+08:00,\"Test Account\",\"A expense account\",\"Test Category\""), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrTransactionTypeInvalid.Message)
|
||||
}
|
||||
|
||||
func TestFireFlyIIICsvFileConverterParseImportedData_ParseValidTimezone(t *testing.T) {
|
||||
converter := FireflyIIITransactionDataCsvFileImporter
|
||||
context := core.NewNullContext()
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1234567890,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name,category\n"+
|
||||
"Withdrawal,-1.00,2024-09-01T12:34:56-10:00,\"Test Account\",\"A expense account\",\"Test Category\""), 0, nil, nil, nil, nil, nil)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 1, len(allNewTransactions))
|
||||
assert.Equal(t, int64(1725230096), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
|
||||
|
||||
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name,category\n"+
|
||||
"Withdrawal,-1.00,2024-09-01T12:34:56+00:00,\"Test Account\",\"A expense account\",\"Test Category\""), 0, nil, nil, nil, nil, nil)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 1, len(allNewTransactions))
|
||||
assert.Equal(t, int64(1725194096), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
|
||||
|
||||
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name,category\n"+
|
||||
"Withdrawal,-1.00,2024-09-01T12:34:56+12:45,\"Test Account\",\"A expense account\",\"Test Category\""), 0, nil, nil, nil, nil, nil)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 1, len(allNewTransactions))
|
||||
assert.Equal(t, int64(1725148196), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
|
||||
}
|
||||
|
||||
func TestFireFlyIIICsvFileConverterParseImportedData_ParseValidAccountCurrency(t *testing.T) {
|
||||
converter := FireflyIIITransactionDataCsvFileImporter
|
||||
context := core.NewNullContext()
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1234567890,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
allNewTransactions, allNewAccounts, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("type,amount,foreign_amount,date,currency_code,foreign_currency_code,source_name,destination_name,category\n"+
|
||||
"\"Opening balance\",-123.45,,2024-09-01T00:00:00+08:00,USD,,\"Initial balance for \"\"Test Account\"\"\",\"Test Account\",\n"+
|
||||
"Transfer,-1.23,-1.10,2024-09-01T23:59:59+08:00,USD,EUR,\"Test Account\",\"Test Account2\",\"Test Category2\""), 0, nil, nil, nil, nil, nil)
|
||||
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, 2, len(allNewTransactions))
|
||||
assert.Equal(t, 2, len(allNewAccounts))
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewAccounts[0].Uid)
|
||||
assert.Equal(t, "Test Account", allNewAccounts[0].Name)
|
||||
assert.Equal(t, "USD", allNewAccounts[0].Currency)
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewAccounts[1].Uid)
|
||||
assert.Equal(t, "Test Account2", allNewAccounts[1].Name)
|
||||
assert.Equal(t, "EUR", allNewAccounts[1].Currency)
|
||||
}
|
||||
|
||||
func TestFireFlyIIICsvFileConverterParseImportedData_ParseInvalidAccountCurrency(t *testing.T) {
|
||||
converter := FireflyIIITransactionDataCsvFileImporter
|
||||
context := core.NewNullContext()
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1234567890,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("type,amount,foreign_amount,date,currency_code,foreign_currency_code,source_name,destination_name,category\n"+
|
||||
"\"Opening balance\",-123.45,,2024-09-01T00:00:00+08:00,USD,,\"Initial balance for \"\"Test Account\"\"\",\"Test Account\",\n"+
|
||||
"Transfer,-1.23,-1.10,2024-09-01T23:59:59+08:00,CNY,EUR,\"Test Account\",\"Test Account2\",\"Test Category3\""), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message)
|
||||
|
||||
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("type,amount,foreign_amount,date,currency_code,foreign_currency_code,source_name,destination_name,category\n"+
|
||||
"\"Opening balance\",-123.45,,2024-09-01T00:00:00+08:00,USD,,\"Initial balance for \"\"Test Account\"\"\",\"Test Account\",\n"+
|
||||
"Transfer,-1.23,-1.10,2024-09-01T23:59:59+08:00,CNY,EUR,\"Test Account2\",\"Test Account\",\"Test Category3\""), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message)
|
||||
}
|
||||
|
||||
func TestFireFlyIIICsvFileConverterParseImportedData_ParseNotSupportedCurrency(t *testing.T) {
|
||||
converter := FireflyIIITransactionDataCsvFileImporter
|
||||
context := core.NewNullContext()
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1234567890,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("type,amount,foreign_amount,date,currency_code,foreign_currency_code,source_name,destination_name,category\n"+
|
||||
"\"Opening balance\",-123.45,,2024-09-01T00:00:00+08:00,XXX,,\"Initial balance for \"\"Test Account\"\"\",\"Test Account\",\n"), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message)
|
||||
|
||||
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("type,amount,foreign_amount,date,currency_code,foreign_currency_code,source_name,destination_name,category\n"+
|
||||
"Transfer,-123.45,-123.45,2024-09-01T23:59:59+08:00,USD,XXX,\"Test Account\",\"Test Account2\",\"Test Category2\""), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message)
|
||||
}
|
||||
|
||||
func TestFireFlyIIICsvFileConverterParseImportedData_ParseInvalidAmount(t *testing.T) {
|
||||
converter := FireflyIIITransactionDataCsvFileImporter
|
||||
context := core.NewNullContext()
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1234567890,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name,category\n"+
|
||||
"Withdrawal,-123 45,2024-09-01T12:34:56+08:00,\"Test Account\",\"A expense account\",\"Test Category\"\n"), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
|
||||
|
||||
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("type,amount,foreign_amount,date,source_name,destination_name,category\n"+
|
||||
"Transfer,-123.45,-123 45,2024-09-01T23:59:59+08:00,\"Test Account\",\"Test Account2\",\"Test Category2\""), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
|
||||
}
|
||||
|
||||
func TestFireFlyIIICsvFileConverterParseImportedData_ParseDescription(t *testing.T) {
|
||||
converter := FireflyIIITransactionDataCsvFileImporter
|
||||
context := core.NewNullContext()
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1234567890,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("type,amount,description,date,source_name,destination_name,category\n"+
|
||||
"Withdrawal,-123.45,\"foo bar\t#test\",2024-09-01T12:34:56+08:00,\"Test Account\",\"A expense account\",\"Test Category\"\n"), 0, nil, nil, nil, nil, nil)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 1, len(allNewTransactions))
|
||||
assert.Equal(t, "foo bar\t#test", allNewTransactions[0].Comment)
|
||||
}
|
||||
|
||||
func TestFireFlyIIICsvFileConverterParseImportedData_MissingFileHeader(t *testing.T) {
|
||||
converter := FireflyIIITransactionDataCsvFileImporter
|
||||
context := core.NewNullContext()
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(""), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrNotFoundTransactionDataInFile.Message)
|
||||
}
|
||||
|
||||
func TestFireFlyIIICsvFileConverterParseImportedData_MissingRequiredColumn(t *testing.T) {
|
||||
converter := FireflyIIITransactionDataCsvFileImporter
|
||||
context := core.NewNullContext()
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
// Missing Time Column
|
||||
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("type,amount,source_name,destination_name,category\n"+
|
||||
"\"Opening balance\",-123.45,\"Initial balance for \"\"Test Account\"\"\",\"Test Account\",\n"), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
|
||||
|
||||
// Missing Type Column
|
||||
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("amount,date,source_name,destination_name,category\n"+
|
||||
"-123.45,2024-09-01T00:00:00+08:00,\"Initial balance for \"\"Test Account\"\"\",\"Test Account\",\n"), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
|
||||
|
||||
// Missing Sub Category Column
|
||||
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name\n"+
|
||||
"\"Opening balance\",-123.45,2024-09-01T00:00:00+08:00,\"Initial balance for \"\"Test Account\"\"\",\"Test Account\"\n"), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
|
||||
|
||||
// Missing Account Name Column
|
||||
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("type,amount,date,destination_name,category\n"+
|
||||
"\"Opening balance\",-123.45,2024-09-01T00:00:00+08:00,\"Test Account\",\n"), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
|
||||
|
||||
// Missing Amount Column
|
||||
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("type,date,source_name,destination_name,category\n"+
|
||||
"\"Opening balance\",2024-09-01T00:00:00+08:00,\"Initial balance for \"\"Test Account\"\"\",\"Test Account\",\n"), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
|
||||
|
||||
// Missing Account2 Name Column
|
||||
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("type,amount,date,source_name,category\n"+
|
||||
"\"Opening balance\",-123.45,2024-09-01T00:00:00+08:00,\"Initial balance for \"\"Test Account\"\"\",\n"), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
package fireflyIII
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||
)
|
||||
|
||||
// fireflyIIITransactionDataRowParser defines the structure of firefly III transaction data row parser
|
||||
type fireflyIIITransactionDataRowParser struct {
|
||||
}
|
||||
|
||||
// GetAddedColumns returns the added columns after converting the data row
|
||||
func (p *fireflyIIITransactionDataRowParser) GetAddedColumns() []datatable.TransactionDataTableColumn {
|
||||
return []datatable.TransactionDataTableColumn{
|
||||
datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE,
|
||||
}
|
||||
}
|
||||
|
||||
// Parse returns the converted transaction data row
|
||||
func (p *fireflyIIITransactionDataRowParser) Parse(data map[datatable.TransactionDataTableColumn]string) (rowData map[datatable.TransactionDataTableColumn]string, rowDataValid bool, err error) {
|
||||
rowData = make(map[datatable.TransactionDataTableColumn]string, len(data))
|
||||
|
||||
rowData[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY] = ""
|
||||
|
||||
for column, value := range data {
|
||||
rowData[column] = value
|
||||
}
|
||||
|
||||
// parse long date time and timezone
|
||||
if rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME] != "" {
|
||||
if strings.Index(rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME], "T") <= 0 {
|
||||
return nil, false, errs.ErrTransactionTimeInvalid
|
||||
}
|
||||
|
||||
dateTime, err := utils.ParseFromLongDateTimeWithTimezone(strings.ReplaceAll(rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME], "T", " "))
|
||||
|
||||
if err != nil {
|
||||
return nil, false, errs.ErrTransactionTimeInvalid
|
||||
}
|
||||
|
||||
rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME] = utils.FormatUnixTimeToLongDateTime(dateTime.Unix(), dateTime.Location())
|
||||
rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE] = utils.FormatTimezoneOffset(dateTime.Location())
|
||||
}
|
||||
|
||||
// trim trailing zero in decimal
|
||||
if rowData[datatable.TRANSACTION_DATA_TABLE_AMOUNT] != "" {
|
||||
rowData[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = utils.TrimTrailingZerosInDecimal(rowData[datatable.TRANSACTION_DATA_TABLE_AMOUNT])
|
||||
amount, err := utils.ParseAmount(rowData[datatable.TRANSACTION_DATA_TABLE_AMOUNT])
|
||||
|
||||
if err != nil {
|
||||
return nil, false, errs.ErrAmountInvalid
|
||||
}
|
||||
|
||||
rowData[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = utils.FormatAmount(-amount)
|
||||
}
|
||||
|
||||
if rowData[datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT] != "" {
|
||||
rowData[datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT] = utils.TrimTrailingZerosInDecimal(rowData[datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT])
|
||||
amount, err := utils.ParseAmount(rowData[datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT])
|
||||
|
||||
if err != nil {
|
||||
return nil, false, errs.ErrAmountInvalid
|
||||
}
|
||||
|
||||
rowData[datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT] = utils.FormatAmount(-amount)
|
||||
} else {
|
||||
rowData[datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT] = rowData[datatable.TRANSACTION_DATA_TABLE_AMOUNT]
|
||||
}
|
||||
|
||||
// the related account currency field is foreign currency in firefly III actually
|
||||
if rowData[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_CURRENCY] == "" {
|
||||
rowData[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_CURRENCY] = rowData[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_CURRENCY]
|
||||
}
|
||||
|
||||
// the destination account of modify balance transaction in firefly III is the asset account
|
||||
if rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] == fireflyIIITransactionTypeNameMapping[models.TRANSACTION_TYPE_MODIFY_BALANCE] {
|
||||
rowData[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = rowData[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME]
|
||||
}
|
||||
|
||||
// the destination account of income transaction in firefly III is the asset account
|
||||
if rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] == fireflyIIITransactionTypeNameMapping[models.TRANSACTION_TYPE_INCOME] {
|
||||
rowData[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = rowData[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME]
|
||||
}
|
||||
|
||||
return rowData, true, nil
|
||||
}
|
||||
|
||||
// createFireflyIIITransactionDataRowParser returns firefly III transaction data row parser
|
||||
func createFireflyIIITransactionDataRowParser() datatable.TransactionDataRowParser {
|
||||
return &fireflyIIITransactionDataRowParser{}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
package gnucash
|
||||
|
||||
import "encoding/xml"
|
||||
|
||||
const gnucashCommodityCurrencySpace = "CURRENCY"
|
||||
const gnucashRootAccountType = "ROOT"
|
||||
const gnucashEquityAccountType = "EQUITY"
|
||||
const gnucashIncomeAccountType = "INCOME"
|
||||
const gnucashExpenseAccountType = "EXPENSE"
|
||||
|
||||
const gnucashSlotEquityType = "equity-type"
|
||||
const gnucashSlotEquityTypeOpeningBalance = "opening-balance"
|
||||
|
||||
var gnucashAssetOrLiabilityAccountTypes = map[string]bool{
|
||||
"ASSET": true,
|
||||
"BANK": true,
|
||||
"CASH": true,
|
||||
"CREDIT": true,
|
||||
"LIABILITY": true,
|
||||
"MUTUAL": true,
|
||||
"PAYABLE": true,
|
||||
"RECEIVABLE": true,
|
||||
"STOCK": true,
|
||||
}
|
||||
|
||||
// gnucashDatabase represents the struct of gnucash database file
|
||||
type gnucashDatabase struct {
|
||||
XMLName xml.Name `xml:"gnc-v2"`
|
||||
Counts []*gnucashCountData `xml:"count-data"`
|
||||
Books []*gnucashBookData `xml:"book"`
|
||||
}
|
||||
|
||||
// gnucashCountData represents the struct of gnucash count data
|
||||
type gnucashCountData struct {
|
||||
Key string `xml:"type,attr"`
|
||||
Value string `xml:",chardata"`
|
||||
}
|
||||
|
||||
// gnucashBookData represents the struct of gnucash book data
|
||||
type gnucashBookData struct {
|
||||
Id string `xml:"id"`
|
||||
Counts []*gnucashCountData `xml:"count-data"`
|
||||
Accounts []*gnucashAccountData `xml:"account"`
|
||||
Transactions []*gnucashTransactionData `xml:"transaction"`
|
||||
}
|
||||
|
||||
// gnucashCommodityData represents the struct of gnucash commodity data
|
||||
type gnucashCommodityData struct {
|
||||
Space string `xml:"space"`
|
||||
Id string `xml:"id"`
|
||||
}
|
||||
|
||||
// gnucashSlotData represents the struct of gnucash slot data
|
||||
type gnucashSlotData struct {
|
||||
Key string `xml:"key"`
|
||||
Value string `xml:"value"`
|
||||
}
|
||||
|
||||
// gnucashAccountData represents the struct of gnucash account data
|
||||
type gnucashAccountData struct {
|
||||
Name string `xml:"name"`
|
||||
Id string `xml:"id"`
|
||||
AccountType string `xml:"type"`
|
||||
Description string `xml:"description"`
|
||||
ParentId string `xml:"parent"`
|
||||
Commodity *gnucashCommodityData `xml:"commodity"`
|
||||
Slots []*gnucashSlotData `xml:"slots>slot"`
|
||||
}
|
||||
|
||||
// gnucashTransactionData represents the struct of gnucash transaction data
|
||||
type gnucashTransactionData struct {
|
||||
Id string `xml:"id"`
|
||||
Currency *gnucashCommodityData `xml:"currency"`
|
||||
PostedDate string `xml:"date-posted>date"`
|
||||
EnteredDate string `xml:"date-entered>date"`
|
||||
Description string `xml:"description"`
|
||||
Splits []*gnucashTransactionSplitData `xml:"splits>split"`
|
||||
}
|
||||
|
||||
// gnucashTransactionSplitData represents the struct of gnucash transaction split data
|
||||
type gnucashTransactionSplitData struct {
|
||||
Id string `xml:"id"`
|
||||
ReconciledState string `xml:"reconciled-state"`
|
||||
Value string `xml:"value"`
|
||||
Quantity string `xml:"quantity"`
|
||||
Account string `xml:"account"`
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
package gnucash
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"encoding/xml"
|
||||
|
||||
"golang.org/x/net/html/charset"
|
||||
|
||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||
)
|
||||
|
||||
// gnucashDatabaseReader defines the structure of gnucash database reader
|
||||
type gnucashDatabaseReader struct {
|
||||
xmlDecoder *xml.Decoder
|
||||
}
|
||||
|
||||
// read returns the imported gnucash data
|
||||
func (r *gnucashDatabaseReader) read(ctx core.Context) (*gnucashDatabase, error) {
|
||||
database := &gnucashDatabase{}
|
||||
|
||||
err := r.xmlDecoder.Decode(&database)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return database, nil
|
||||
}
|
||||
|
||||
func createNewGnuCashDatabaseReader(data []byte) (*gnucashDatabaseReader, error) {
|
||||
if len(data) > 2 && data[0] == 0x1F && data[1] == 0x8B { // gzip magic number
|
||||
gzipReader, err := gzip.NewReader(bytes.NewReader(data))
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
xmlDecoder := xml.NewDecoder(gzipReader)
|
||||
xmlDecoder.CharsetReader = charset.NewReaderLabel
|
||||
|
||||
return &gnucashDatabaseReader{
|
||||
xmlDecoder: xmlDecoder,
|
||||
}, nil
|
||||
} else 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 &gnucashDatabaseReader{
|
||||
xmlDecoder: xmlDecoder,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, errs.ErrInvalidGnuCashFile
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package gnucash
|
||||
|
||||
import (
|
||||
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||
)
|
||||
|
||||
var gnucashTransactionTypeNameMapping = map[models.TransactionType]string{
|
||||
models.TRANSACTION_TYPE_MODIFY_BALANCE: utils.IntToString(int(models.TRANSACTION_TYPE_MODIFY_BALANCE)),
|
||||
models.TRANSACTION_TYPE_INCOME: utils.IntToString(int(models.TRANSACTION_TYPE_INCOME)),
|
||||
models.TRANSACTION_TYPE_EXPENSE: utils.IntToString(int(models.TRANSACTION_TYPE_EXPENSE)),
|
||||
models.TRANSACTION_TYPE_TRANSFER: utils.IntToString(int(models.TRANSACTION_TYPE_TRANSFER)),
|
||||
}
|
||||
|
||||
// gnucashTransactionDataImporter defines the structure of gnucash importer for transaction data
|
||||
type gnucashTransactionDataImporter struct {
|
||||
}
|
||||
|
||||
// Initialize a gnucash transaction data importer singleton instance
|
||||
var (
|
||||
GnuCashTransactionDataImporter = &gnucashTransactionDataImporter{}
|
||||
)
|
||||
|
||||
// ParseImportedData returns the imported data by parsing the gnucash transaction data
|
||||
func (c *gnucashTransactionDataImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezoneOffset int16, accountMap map[string]*models.Account, expenseCategoryMap map[string]*models.TransactionCategory, incomeCategoryMap map[string]*models.TransactionCategory, transferCategoryMap map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionTag, error) {
|
||||
gnucashDataReader, err := createNewGnuCashDatabaseReader(data)
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
gnucashData, err := gnucashDataReader.read(ctx)
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
transactionDataTable, err := createNewGnuCashTransactionDataTable(gnucashData)
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
dataTableImporter := datatable.CreateNewSimpleImporter(gnucashTransactionTypeNameMapping)
|
||||
|
||||
return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezoneOffset, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,371 +0,0 @@
|
||||
package gnucash
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
|
||||
"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/utils"
|
||||
)
|
||||
|
||||
var gnucashTransactionSupportedColumns = map[datatable.TransactionDataTableColumn]bool{
|
||||
datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME: true,
|
||||
datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE: true,
|
||||
datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE: true,
|
||||
datatable.TRANSACTION_DATA_TABLE_CATEGORY: true,
|
||||
datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY: true,
|
||||
datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME: true,
|
||||
datatable.TRANSACTION_DATA_TABLE_ACCOUNT_CURRENCY: true,
|
||||
datatable.TRANSACTION_DATA_TABLE_AMOUNT: true,
|
||||
datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME: true,
|
||||
datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_CURRENCY: true,
|
||||
datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT: true,
|
||||
datatable.TRANSACTION_DATA_TABLE_DESCRIPTION: true,
|
||||
}
|
||||
|
||||
// gnucashTransactionDataTable defines the structure of gnucash transaction data table
|
||||
type gnucashTransactionDataTable struct {
|
||||
allData []*gnucashTransactionData
|
||||
accountMap map[string]*gnucashAccountData
|
||||
}
|
||||
|
||||
// gnucashTransactionDataRow defines the structure of gnucash transaction data row
|
||||
type gnucashTransactionDataRow struct {
|
||||
dataTable *gnucashTransactionDataTable
|
||||
data *gnucashTransactionData
|
||||
finalItems map[datatable.TransactionDataTableColumn]string
|
||||
isValid bool
|
||||
}
|
||||
|
||||
// gnucashTransactionDataRowIterator defines the structure of gnucash transaction data row iterator
|
||||
type gnucashTransactionDataRowIterator struct {
|
||||
dataTable *gnucashTransactionDataTable
|
||||
currentIndex int
|
||||
}
|
||||
|
||||
// HasColumn returns whether the transaction data table has specified column
|
||||
func (t *gnucashTransactionDataTable) HasColumn(column datatable.TransactionDataTableColumn) bool {
|
||||
_, exists := gnucashTransactionSupportedColumns[column]
|
||||
return exists
|
||||
}
|
||||
|
||||
// TransactionRowCount returns the total count of transaction data row
|
||||
func (t *gnucashTransactionDataTable) TransactionRowCount() int {
|
||||
return len(t.allData)
|
||||
}
|
||||
|
||||
// TransactionRowIterator returns the iterator of transaction data row
|
||||
func (t *gnucashTransactionDataTable) TransactionRowIterator() datatable.TransactionDataRowIterator {
|
||||
return &gnucashTransactionDataRowIterator{
|
||||
dataTable: t,
|
||||
currentIndex: -1,
|
||||
}
|
||||
}
|
||||
|
||||
// IsValid returns whether this row is valid data for importing
|
||||
func (r *gnucashTransactionDataRow) IsValid() bool {
|
||||
return r.isValid
|
||||
}
|
||||
|
||||
// GetData returns the data in the specified column type
|
||||
func (r *gnucashTransactionDataRow) GetData(column datatable.TransactionDataTableColumn) string {
|
||||
_, exists := gnucashTransactionSupportedColumns[column]
|
||||
|
||||
if exists {
|
||||
return r.finalItems[column]
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// HasNext returns whether the iterator does not reach the end
|
||||
func (t *gnucashTransactionDataRowIterator) HasNext() bool {
|
||||
return t.currentIndex+1 < len(t.dataTable.allData)
|
||||
}
|
||||
|
||||
// Next returns the next imported data row
|
||||
func (t *gnucashTransactionDataRowIterator) Next(ctx core.Context, user *models.User) (daraRow datatable.TransactionDataRow, err error) {
|
||||
if t.currentIndex+1 >= len(t.dataTable.allData) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
t.currentIndex++
|
||||
|
||||
data := t.dataTable.allData[t.currentIndex]
|
||||
rowItems, isValid, err := t.parseTransaction(ctx, user, data)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(ctx, "[gnucash_transaction_table.Next] cannot parsing transaction in row#%d, because %s", t.currentIndex, err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &gnucashTransactionDataRow{
|
||||
dataTable: t.dataTable,
|
||||
data: data,
|
||||
finalItems: rowItems,
|
||||
isValid: isValid,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *gnucashTransactionDataRowIterator) parseTransaction(ctx core.Context, user *models.User, gnucashTransaction *gnucashTransactionData) (map[datatable.TransactionDataTableColumn]string, bool, error) {
|
||||
data := make(map[datatable.TransactionDataTableColumn]string, len(gnucashTransactionSupportedColumns))
|
||||
|
||||
if gnucashTransaction.PostedDate == "" {
|
||||
return nil, false, errs.ErrMissingTransactionTime
|
||||
}
|
||||
|
||||
dateTime, err := utils.ParseFromLongDateTimeWithTimezone2(gnucashTransaction.PostedDate)
|
||||
|
||||
if err != nil {
|
||||
return nil, false, errs.ErrTransactionTimeInvalid
|
||||
}
|
||||
|
||||
data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME] = utils.FormatUnixTimeToLongDateTime(dateTime.Unix(), dateTime.Location())
|
||||
data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE] = utils.FormatTimezoneOffset(dateTime.Location())
|
||||
|
||||
if len(gnucashTransaction.Splits) == 2 {
|
||||
splitData1 := gnucashTransaction.Splits[0]
|
||||
splitData2 := gnucashTransaction.Splits[1]
|
||||
|
||||
account1 := t.dataTable.accountMap[splitData1.Account]
|
||||
account2 := t.dataTable.accountMap[splitData2.Account]
|
||||
|
||||
if account1 == nil || account2 == nil {
|
||||
return nil, false, errs.ErrMissingAccountData
|
||||
}
|
||||
|
||||
if splitData1.Quantity == "" || splitData2.Quantity == "" {
|
||||
return nil, false, errs.ErrAmountInvalid
|
||||
}
|
||||
|
||||
amount1, err := t.parseAmount(splitData1.Quantity)
|
||||
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
amount2, err := t.parseAmount(splitData2.Quantity)
|
||||
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
if ((account1.AccountType == gnucashEquityAccountType || account1.AccountType == gnucashIncomeAccountType) && gnucashAssetOrLiabilityAccountTypes[account2.AccountType]) ||
|
||||
((account2.AccountType == gnucashEquityAccountType || account2.AccountType == gnucashIncomeAccountType) && gnucashAssetOrLiabilityAccountTypes[account1.AccountType]) { // income
|
||||
fromAccount := account1
|
||||
toAccount := account2
|
||||
toAmount := amount2
|
||||
|
||||
if (account2.AccountType == gnucashEquityAccountType || account2.AccountType == gnucashIncomeAccountType) && gnucashAssetOrLiabilityAccountTypes[account1.AccountType] {
|
||||
fromAccount = account2
|
||||
toAccount = account1
|
||||
toAmount = amount1
|
||||
}
|
||||
|
||||
if t.hasSpecifiedSlotKeyValue(fromAccount.Slots, gnucashSlotEquityType, gnucashSlotEquityTypeOpeningBalance) {
|
||||
data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = utils.IntToString(int(models.TRANSACTION_TYPE_MODIFY_BALANCE))
|
||||
} else {
|
||||
data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = utils.IntToString(int(models.TRANSACTION_TYPE_INCOME))
|
||||
}
|
||||
|
||||
data[datatable.TRANSACTION_DATA_TABLE_CATEGORY] = t.getCategoryName(fromAccount)
|
||||
data[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY] = fromAccount.Name
|
||||
data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = toAccount.Name
|
||||
|
||||
if toAccount.Commodity != nil && toAccount.Commodity.Space == gnucashCommodityCurrencySpace {
|
||||
data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_CURRENCY] = toAccount.Commodity.Id
|
||||
}
|
||||
|
||||
data[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = toAmount
|
||||
} else if (account1.AccountType == gnucashExpenseAccountType && gnucashAssetOrLiabilityAccountTypes[account2.AccountType]) ||
|
||||
(account2.AccountType == gnucashExpenseAccountType && gnucashAssetOrLiabilityAccountTypes[account1.AccountType]) { // expense
|
||||
fromAccount := account1
|
||||
fromAmount := amount1
|
||||
toAccount := account2
|
||||
|
||||
if account1.AccountType == gnucashExpenseAccountType && gnucashAssetOrLiabilityAccountTypes[account2.AccountType] {
|
||||
fromAccount = account2
|
||||
fromAmount = amount2
|
||||
toAccount = account1
|
||||
}
|
||||
|
||||
amount, err := utils.ParseAmount(fromAmount)
|
||||
|
||||
if err != nil {
|
||||
return nil, false, errs.ErrAmountInvalid
|
||||
}
|
||||
|
||||
fromAmount = utils.FormatAmount(-amount)
|
||||
|
||||
data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = utils.IntToString(int(models.TRANSACTION_TYPE_EXPENSE))
|
||||
data[datatable.TRANSACTION_DATA_TABLE_CATEGORY] = t.getCategoryName(toAccount)
|
||||
data[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY] = toAccount.Name
|
||||
data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = fromAccount.Name
|
||||
|
||||
if fromAccount.Commodity != nil && fromAccount.Commodity.Space == gnucashCommodityCurrencySpace {
|
||||
data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_CURRENCY] = fromAccount.Commodity.Id
|
||||
}
|
||||
|
||||
data[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = fromAmount
|
||||
} else if gnucashAssetOrLiabilityAccountTypes[account1.AccountType] && gnucashAssetOrLiabilityAccountTypes[account2.AccountType] {
|
||||
var fromAccount, toAccount *gnucashAccountData
|
||||
var fromAmount, toAmount string
|
||||
|
||||
if len(amount1) > 0 && amount1[0] == '-' {
|
||||
fromAccount = account1
|
||||
fromAmount = amount1[1:]
|
||||
toAccount = account2
|
||||
toAmount = amount2
|
||||
} else if len(amount2) > 0 && amount2[0] == '-' {
|
||||
fromAccount = account2
|
||||
fromAmount = amount2[1:]
|
||||
toAccount = account1
|
||||
toAmount = amount1
|
||||
} else {
|
||||
log.Errorf(ctx, "[gnucash_transaction_table.parseTransaction] cannot parse transfer transaction \"id:%s\", because unexcepted account amounts \"%s\" and \"%s\"", gnucashTransaction.Id, amount1, amount2)
|
||||
return nil, false, errs.ErrInvalidGnuCashFile
|
||||
}
|
||||
|
||||
data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = utils.IntToString(int(models.TRANSACTION_TYPE_TRANSFER))
|
||||
data[datatable.TRANSACTION_DATA_TABLE_CATEGORY] = ""
|
||||
data[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY] = ""
|
||||
data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = fromAccount.Name
|
||||
|
||||
if fromAccount.Commodity != nil && fromAccount.Commodity.Space == gnucashCommodityCurrencySpace {
|
||||
data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_CURRENCY] = fromAccount.Commodity.Id
|
||||
}
|
||||
|
||||
data[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = fromAmount
|
||||
data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = toAccount.Name
|
||||
|
||||
if toAccount.Commodity != nil && toAccount.Commodity.Space == gnucashCommodityCurrencySpace {
|
||||
data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_CURRENCY] = toAccount.Commodity.Id
|
||||
}
|
||||
|
||||
data[datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT] = toAmount
|
||||
} else {
|
||||
log.Errorf(ctx, "[gnucash_transaction_table.parseTransaction] cannot parse transaction \"id:%s\", because unexcepted account types \"%s\" and \"%s\"", gnucashTransaction.Id, account1.AccountType, account2.AccountType)
|
||||
return nil, false, errs.ErrThereAreNotSupportedTransactionType
|
||||
}
|
||||
} else if len(gnucashTransaction.Splits) == 1 {
|
||||
splitData := gnucashTransaction.Splits[0]
|
||||
account := t.dataTable.accountMap[splitData.Account]
|
||||
|
||||
if account == nil {
|
||||
return nil, false, errs.ErrMissingAccountData
|
||||
}
|
||||
|
||||
if splitData.Quantity == "" {
|
||||
return nil, false, errs.ErrAmountInvalid
|
||||
}
|
||||
|
||||
amount, err := t.parseAmount(splitData.Quantity)
|
||||
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
amountNum, err := utils.ParseAmount(amount)
|
||||
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
if amountNum == 0 {
|
||||
log.Warnf(ctx, "[gnucash_transaction_table.parseTransaction] skip parsing transaction \"id:%s\" with zero amount", gnucashTransaction.Id)
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
log.Errorf(ctx, "[gnucash_transaction_table.parseTransaction] cannot parse transaction \"id:%s\", because split count is %d", gnucashTransaction.Id, len(gnucashTransaction.Splits))
|
||||
return nil, false, errs.ErrThereAreNotSupportedTransactionType
|
||||
} else if len(gnucashTransaction.Splits) < 1 {
|
||||
log.Errorf(ctx, "[gnucash_transaction_table.parseTransaction] cannot parse transaction \"id:%s\", because split count is %d", gnucashTransaction.Id, len(gnucashTransaction.Splits))
|
||||
return nil, false, errs.ErrInvalidGnuCashFile
|
||||
} else {
|
||||
log.Errorf(ctx, "[gnucash_transaction_table.parseTransaction] cannot parse split transaction \"id:%s\", because split count is %d", gnucashTransaction.Id, len(gnucashTransaction.Splits))
|
||||
return nil, false, errs.ErrNotSupportedSplitTransactions
|
||||
}
|
||||
|
||||
data[datatable.TRANSACTION_DATA_TABLE_DESCRIPTION] = gnucashTransaction.Description
|
||||
|
||||
return data, true, nil
|
||||
}
|
||||
|
||||
func (t *gnucashTransactionDataRowIterator) parseAmount(quantity string) (string, error) {
|
||||
items := strings.Split(quantity, "/")
|
||||
|
||||
if len(items) != 2 {
|
||||
return "", errs.ErrAmountInvalid
|
||||
}
|
||||
|
||||
value, err := utils.StringToInt64(items[0])
|
||||
|
||||
if err != nil {
|
||||
return "", errs.ErrAmountInvalid
|
||||
}
|
||||
|
||||
if items[1] == "100" {
|
||||
return utils.FormatAmount(value), nil
|
||||
}
|
||||
|
||||
factor, err := utils.StringToInt64(items[1])
|
||||
|
||||
if err != nil {
|
||||
return "", errs.ErrAmountInvalid
|
||||
}
|
||||
|
||||
value = value * 100 / factor
|
||||
|
||||
return utils.FormatAmount(value), nil
|
||||
}
|
||||
|
||||
func (t *gnucashTransactionDataRowIterator) getCategoryName(accountData *gnucashAccountData) string {
|
||||
if accountData == nil || accountData.ParentId == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
parentAccount := t.dataTable.accountMap[accountData.ParentId]
|
||||
|
||||
if parentAccount == nil || parentAccount.AccountType == gnucashRootAccountType {
|
||||
return ""
|
||||
}
|
||||
|
||||
return parentAccount.Name
|
||||
}
|
||||
|
||||
func (t *gnucashTransactionDataRowIterator) hasSpecifiedSlotKeyValue(slots []*gnucashSlotData, key string, value string) bool {
|
||||
for i := 0; i < len(slots); i++ {
|
||||
if slots[i].Key == key && slots[i].Value == value {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func createNewGnuCashTransactionDataTable(database *gnucashDatabase) (*gnucashTransactionDataTable, error) {
|
||||
if database == nil || len(database.Books) < 1 {
|
||||
return nil, errs.ErrNotFoundTransactionDataInFile
|
||||
}
|
||||
|
||||
allData := make([]*gnucashTransactionData, 0)
|
||||
accountMap := make(map[string]*gnucashAccountData)
|
||||
|
||||
for i := 0; i < len(database.Books); i++ {
|
||||
book := database.Books[i]
|
||||
allData = append(allData, book.Transactions...)
|
||||
|
||||
for j := 0; j < len(book.Accounts); j++ {
|
||||
account := book.Accounts[j]
|
||||
accountMap[account.Id] = account
|
||||
}
|
||||
}
|
||||
|
||||
return &gnucashTransactionDataTable{
|
||||
allData: allData,
|
||||
accountMap: accountMap,
|
||||
}, nil
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user